refactor: deduplicate installer-mcp + decompose installWorkflows

installer-mcp.ts (678→462):
- Extract configureMcpInClaude() shared pipeline, eliminates 3x duplication
- Extract mirrorCcgServers() + formatSyncMessage() for sync dedup
- installAceTool/installAceToolRs/installFastContext now thin wrappers

installer.ts — decompose installWorkflows (290-line monolith → 6 sub-functions):
- installCommandFiles, installAgentFiles, installPromptFiles
- installSkillFiles, installRuleFiles, installBinaryFile
- Shared InstallContext interface passes state between steps

Total: 1926→1724 lines (-202), dist 215→212 kB (-3 kB)
This commit is contained in:
li committed 2026-03-12 13:16:14 +08:00
1 parent 1d31302a45
commit 92b9c40897
2 files changed
+489 -691

No files matched your search

+167 -383
View File
@@ -6,6 +6,67 @@ import { parse as parseToml, stringify as stringifyToml } from 'smol-toml'
import { backupClaudeCodeConfig, buildMcpServerConfig, fixWindowsMcpConfig, mergeMcpServers, readClaudeCodeConfig, writeClaudeCodeConfig } from './mcp'
import { isWindows } from './platform'
// ═══════════════════════════════════════════════════════
// Shared types & helpers
// ═══════════════════════════════════════════════════════
type McpInstallResult = { success: boolean, message: string, configPath?: string }
/**
* Common pipeline for installing an MCP server into ~/.claude.json:
* read → backup → merge → Windows fix → write.
*
* All MCP installers funnel through this to avoid duplication.
*/
async function configureMcpInClaude(
serverId: string,
serverConfig: Record<string, any>,
label: string,
): Promise<McpInstallResult> {
try {
let existingConfig = await readClaudeCodeConfig()
if (!existingConfig) {
existingConfig = { mcpServers: {} }
}
// Backup before modifying (if config exists)
if (existingConfig.mcpServers && Object.keys(existingConfig.mcpServers).length > 0) {
const backupPath = await backupClaudeCodeConfig()
if (backupPath) {
console.log(` ✓ Backup created: ${backupPath}`)
}
}
// Merge new server into existing config
let mergedConfig = mergeMcpServers(existingConfig, {
[serverId]: serverConfig,
})
// Apply Windows fixes if needed
if (isWindows()) {
mergedConfig = fixWindowsMcpConfig(mergedConfig)
console.log(' ✓ Applied Windows MCP configuration fixes')
}
// Write config back (preserve all other fields)
await writeClaudeCodeConfig(mergedConfig)
return {
success: true,
message: isWindows()
? `${label} configured successfully with Windows compatibility`
: `${label} configured successfully`,
configPath: join(homedir(), '.claude.json'),
}
}
catch (error) {
return {
success: false,
message: `Failed to configure ${label}: ${error}`,
}
}
}
// ═══════════════════════════════════════════════════════
// ace-tool MCP
// ═══════════════════════════════════════════════════════
@@ -18,181 +79,56 @@ export async function uninstallAceTool(): Promise<{ success: boolean, message: s
const existingConfig = await readClaudeCodeConfig()
if (!existingConfig) {
return {
success: true,
message: 'No ~/.claude.json found, nothing to remove',
}
return { success: true, message: 'No ~/.claude.json found, nothing to remove' }
}
// Check if ace-tool exists
if (!existingConfig.mcpServers || !existingConfig.mcpServers['ace-tool']) {
return {
success: true,
message: 'ace-tool MCP not found in config',
}
return { success: true, message: 'ace-tool MCP not found in config' }
}
// Backup before modifying
await backupClaudeCodeConfig()
// Remove ace-tool from mcpServers
delete existingConfig.mcpServers['ace-tool']
// Write back
await writeClaudeCodeConfig(existingConfig)
return {
success: true,
message: 'ace-tool MCP removed from ~/.claude.json',
}
return { success: true, message: 'ace-tool MCP removed from ~/.claude.json' }
}
catch (error) {
return {
success: false,
message: `Failed to uninstall ace-tool: ${error}`,
}
return { success: false, message: `Failed to uninstall ace-tool: ${error}` }
}
}
/**
* Install and configure ace-tool MCP for Claude Code.
* Writes to ~/.claude.json (the correct config file for Claude Code CLI).
*/
export async function installAceTool(config: AceToolConfig): Promise<{ success: boolean, message: string, configPath?: string }> {
export async function installAceTool(config: AceToolConfig): Promise<McpInstallResult> {
const { baseUrl, token } = config
try {
// Read existing config or create new one
let existingConfig = await readClaudeCodeConfig()
const args = ['-y', 'ace-tool@latest']
if (baseUrl) args.push('--base-url', baseUrl)
if (token) args.push('--token', token)
if (!existingConfig) {
existingConfig = { mcpServers: {} }
}
// Backup before modifying (if config exists)
if (existingConfig.mcpServers && Object.keys(existingConfig.mcpServers).length > 0) {
const backupPath = await backupClaudeCodeConfig()
if (backupPath) {
console.log(` ✓ Backup created: ${backupPath}`)
}
}
// Build args array (with -y flag for npx auto-confirm)
const args = ['-y', 'ace-tool@latest']
if (baseUrl) {
args.push('--base-url', baseUrl)
}
if (token) {
args.push('--token', token)
}
// Create base ace-tool MCP server config
const aceToolConfig = buildMcpServerConfig({
type: 'stdio' as const,
command: 'npx',
args,
})
// Merge new server into existing config
let mergedConfig = mergeMcpServers(existingConfig, {
'ace-tool': aceToolConfig,
})
// Apply Windows fixes if needed
if (isWindows()) {
mergedConfig = fixWindowsMcpConfig(mergedConfig)
console.log(' ✓ Applied Windows MCP configuration fixes')
}
// Write config back (preserve all other fields)
await writeClaudeCodeConfig(mergedConfig)
return {
success: true,
message: isWindows()
? 'ace-tool MCP configured successfully with Windows compatibility'
: 'ace-tool MCP configured successfully',
configPath: join(homedir(), '.claude.json'),
}
}
catch (error) {
return {
success: false,
message: `Failed to configure ace-tool: ${error}`,
}
}
const serverConfig = buildMcpServerConfig({ type: 'stdio', command: 'npx', args })
return configureMcpInClaude('ace-tool', serverConfig, 'ace-tool MCP')
}
/**
* Install and configure ace-tool-rs MCP for Claude Code.
* ace-tool-rs is a Rust implementation of ace-tool, more lightweight and faster.
* ace-tool-rs is a Rust implementation — more lightweight and faster.
*/
export async function installAceToolRs(config: AceToolConfig): Promise<{ success: boolean, message: string, configPath?: string }> {
export async function installAceToolRs(config: AceToolConfig): Promise<McpInstallResult> {
const { baseUrl, token } = config
try {
// Read existing config or create new one
let existingConfig = await readClaudeCodeConfig()
const args = ['ace-tool-rs']
if (baseUrl) args.push('--base-url', baseUrl)
if (token) args.push('--token', token)
if (!existingConfig) {
existingConfig = { mcpServers: {} }
}
// Backup before modifying (if config exists)
if (existingConfig.mcpServers && Object.keys(existingConfig.mcpServers).length > 0) {
const backupPath = await backupClaudeCodeConfig()
if (backupPath) {
console.log(` ✓ Backup created: ${backupPath}`)
}
}
// Build args array for ace-tool-rs
const args = ['ace-tool-rs']
if (baseUrl) {
args.push('--base-url', baseUrl)
}
if (token) {
args.push('--token', token)
}
// Create base ace-tool-rs MCP server config
const aceToolRsConfig = buildMcpServerConfig({
type: 'stdio' as const,
command: 'npx',
args,
env: {
RUST_LOG: 'info',
},
})
// Merge new server into existing config
let mergedConfig = mergeMcpServers(existingConfig, {
'ace-tool': aceToolRsConfig,
})
// Apply Windows fixes if needed
if (isWindows()) {
mergedConfig = fixWindowsMcpConfig(mergedConfig)
console.log(' ✓ Applied Windows MCP configuration fixes')
}
// Write config back (preserve all other fields)
await writeClaudeCodeConfig(mergedConfig)
return {
success: true,
message: isWindows()
? 'ace-tool-rs MCP configured successfully with Windows compatibility'
: 'ace-tool-rs MCP configured successfully',
configPath: join(homedir(), '.claude.json'),
}
}
catch (error) {
return {
success: false,
message: `Failed to configure ace-tool-rs: ${error}`,
}
}
const serverConfig = buildMcpServerConfig({
type: 'stdio',
command: 'npx',
args,
env: { RUST_LOG: 'info' },
})
return configureMcpInClaude('ace-tool', serverConfig, 'ace-tool-rs MCP')
}
// ═══════════════════════════════════════════════════════
@@ -210,7 +146,7 @@ export interface ContextWeaverConfig {
* Install and configure ContextWeaver MCP for Claude Code.
* ContextWeaver is a local-first semantic code search engine with hybrid search + rerank.
*/
export async function installContextWeaver(config: ContextWeaverConfig): Promise<{ success: boolean, message: string, configPath?: string }> {
export async function installContextWeaver(config: ContextWeaverConfig): Promise<McpInstallResult> {
const { siliconflowApiKey } = config
try {
@@ -222,7 +158,6 @@ export async function installContextWeaver(config: ContextWeaverConfig): Promise
console.log(' ✓ ContextWeaver CLI 安装成功')
}
catch {
// Try with sudo on Unix systems
if (process.platform !== 'win32') {
try {
execSync('sudo npm install -g @hsingjui/contextweaver', { stdio: 'pipe' })
@@ -258,51 +193,16 @@ RERANK_TOP_N=20
`
await fs.writeFile(join(contextWeaverDir, '.env'), envContent, 'utf-8')
// 2. Read existing Claude Code config
let existingConfig = await readClaudeCodeConfig()
if (!existingConfig) {
existingConfig = { mcpServers: {} }
}
// Backup before modifying
if (existingConfig.mcpServers && Object.keys(existingConfig.mcpServers).length > 0) {
const backupPath = await backupClaudeCodeConfig()
if (backupPath) {
console.log(` ✓ Backup created: ${backupPath}`)
}
}
// 3. Build ContextWeaver MCP server config
const contextWeaverMcpConfig = buildMcpServerConfig({
type: 'stdio' as const,
// 2. Configure MCP via shared pipeline
const serverConfig = buildMcpServerConfig({
type: 'stdio',
command: 'contextweaver',
args: ['mcp'],
})
// 4. Merge into existing config
let mergedConfig = mergeMcpServers(existingConfig, {
contextweaver: contextWeaverMcpConfig,
})
// Apply Windows fixes if needed
if (isWindows()) {
mergedConfig = fixWindowsMcpConfig(mergedConfig)
}
// 5. Write config back
await writeClaudeCodeConfig(mergedConfig)
return {
success: true,
message: 'ContextWeaver MCP configured successfully',
configPath: join(homedir(), '.claude.json'),
}
return await configureMcpInClaude('contextweaver', serverConfig, 'ContextWeaver MCP')
}
catch (error) {
return {
success: false,
message: `Failed to configure ContextWeaver: ${error}`,
}
return { success: false, message: `Failed to configure ContextWeaver: ${error}` }
}
}
@@ -311,27 +211,15 @@ RERANK_TOP_N=20
*/
export async function uninstallContextWeaver(): Promise<{ success: boolean, message: string }> {
try {
// 1. Remove from claude.json
const existingConfig = await readClaudeCodeConfig()
if (existingConfig?.mcpServers?.contextweaver) {
delete existingConfig.mcpServers.contextweaver
await writeClaudeCodeConfig(existingConfig)
}
// 2. Optionally remove ~/.contextweaver directory (keep it for now, user might want to keep config)
// const contextWeaverDir = join(homedir(), '.contextweaver')
// await fs.remove(contextWeaverDir)
return {
success: true,
message: 'ContextWeaver MCP uninstalled successfully',
}
return { success: true, message: 'ContextWeaver MCP uninstalled successfully' }
}
catch (error) {
return {
success: false,
message: `Failed to uninstall ContextWeaver: ${error}`,
}
return { success: false, message: `Failed to uninstall ContextWeaver: ${error}` }
}
}
@@ -341,68 +229,21 @@ export async function uninstallContextWeaver(): Promise<{ success: boolean, mess
/**
* Install and configure Fast Context (Windsurf) MCP for Claude Code.
* Fast Context uses Windsurf's AI-powered code search — no full-repo indexing needed.
*/
export async function installFastContext(config: FastContextConfig): Promise<{ success: boolean, message: string, configPath?: string }> {
export async function installFastContext(config: FastContextConfig): Promise<McpInstallResult> {
const { apiKey, includeSnippets } = config
try {
let existingConfig = await readClaudeCodeConfig()
if (!existingConfig) {
existingConfig = { mcpServers: {} }
}
const env: Record<string, string> = {}
if (apiKey) env.WINDSURF_API_KEY = apiKey
if (includeSnippets) env.FC_INCLUDE_SNIPPETS = 'true'
// Backup before modifying
if (existingConfig.mcpServers && Object.keys(existingConfig.mcpServers).length > 0) {
const backupPath = await backupClaudeCodeConfig()
if (backupPath) {
console.log(` ✓ Backup created: ${backupPath}`)
}
}
// Build env
const env: Record<string, string> = {}
if (apiKey) {
env.WINDSURF_API_KEY = apiKey
}
if (includeSnippets) {
env.FC_INCLUDE_SNIPPETS = 'true'
}
// Build fast-context MCP server config
const fastContextConfig = buildMcpServerConfig({
type: 'stdio' as const,
command: 'npx',
args: ['-y', '--prefer-online', 'fast-context-mcp@latest'],
...(Object.keys(env).length > 0 ? { env } : {}),
})
// Merge into existing config
let mergedConfig = mergeMcpServers(existingConfig, {
'fast-context': fastContextConfig,
})
if (isWindows()) {
mergedConfig = fixWindowsMcpConfig(mergedConfig)
console.log(' ✓ Applied Windows MCP configuration fixes')
}
await writeClaudeCodeConfig(mergedConfig)
return {
success: true,
message: isWindows()
? 'fast-context MCP configured successfully with Windows compatibility'
: 'fast-context MCP configured successfully',
configPath: join(homedir(), '.claude.json'),
}
}
catch (error) {
return {
success: false,
message: `Failed to configure fast-context: ${error}`,
}
}
const serverConfig = buildMcpServerConfig({
type: 'stdio',
command: 'npx',
args: ['-y', '--prefer-online', 'fast-context-mcp@latest'],
...(Object.keys(env).length > 0 ? { env } : {}),
})
return configureMcpInClaude('fast-context', serverConfig, 'fast-context MCP')
}
/**
@@ -415,17 +256,10 @@ export async function uninstallFastContext(): Promise<{ success: boolean, messag
delete existingConfig.mcpServers['fast-context']
await writeClaudeCodeConfig(existingConfig)
}
return {
success: true,
message: 'fast-context MCP uninstalled successfully',
}
return { success: true, message: 'fast-context MCP uninstalled successfully' }
}
catch (error) {
return {
success: false,
message: `Failed to uninstall fast-context: ${error}`,
}
return { success: false, message: `Failed to uninstall fast-context: ${error}` }
}
}
@@ -442,24 +276,8 @@ export async function installMcpServer(
args: string[],
env: Record<string, string> = {},
): Promise<{ success: boolean, message: string }> {
try {
await backupClaudeCodeConfig()
const existingConfig = await readClaudeCodeConfig()
const serverConfig = buildMcpServerConfig({ type: 'stdio', command, args, env })
let mergedConfig = mergeMcpServers(existingConfig, { [id]: serverConfig })
if (isWindows()) {
mergedConfig = fixWindowsMcpConfig(mergedConfig)
}
await writeClaudeCodeConfig(mergedConfig)
return { success: true, message: `${id} MCP installed successfully` }
}
catch (error) {
return { success: false, message: `Failed to install ${id}: ${error}` }
}
const serverConfig = buildMcpServerConfig({ type: 'stdio', command, args, env })
return configureMcpInClaude(id, serverConfig, id)
}
/**
@@ -472,7 +290,6 @@ export async function uninstallMcpServer(id: string): Promise<{ success: boolean
delete existingConfig.mcpServers[id]
await writeClaudeCodeConfig(existingConfig)
}
return { success: true, message: `${id} MCP uninstalled successfully` }
}
catch (error) {
@@ -495,9 +312,10 @@ const CCG_MCP_IDS = new Set([
'fast-context',
])
type SyncResult = { success: boolean, message: string, synced: string[], removed: string[] }
/**
* Read Claude's MCP config and filter to CCG-managed servers.
* Shared helper for syncMcpToCodex and syncMcpToGemini.
*/
async function getCcgMcpServersFromClaude(): Promise<Record<string, any>> {
const claudeConfig = await readClaudeCodeConfig()
@@ -513,29 +331,55 @@ async function getCcgMcpServersFromClaude(): Promise<Record<string, any>> {
}
/**
* Sync (mirror) CCG-managed MCP servers from Claude's ~/.claude.json
* to Codex's ~/.codex/config.toml
*
* - Only touches servers in CCG_MCP_IDS — user's custom MCP servers are untouched.
* - Servers present in Claude are added/updated with ALL fields (not just command/args/env).
* - Servers absent from Claude but present in Codex (within CCG_MCP_IDS) are REMOVED.
* - Preserves all existing Codex config (model_provider, model, etc.)
* - Uses atomic write (temp file + rename) to prevent corruption.
* Apply mirror logic: add/update servers from Claude, remove stale CCG servers.
* Returns { synced, removed } arrays. Mutates targetServers in place.
*/
export async function syncMcpToCodex(): Promise<{
success: boolean
message: string
synced: string[]
removed: string[]
}> {
function mirrorCcgServers(
serversToSync: Record<string, any>,
targetServers: Record<string, any>,
): { synced: string[], removed: string[] } {
const synced: string[] = []
const removed: string[] = []
// Add/update CCG servers
for (const [id, claudeServer] of Object.entries(serversToSync)) {
targetServers[id] = claudeServer
synced.push(id)
}
// Remove CCG servers that no longer exist in Claude
for (const id of CCG_MCP_IDS) {
if (!serversToSync[id] && targetServers[id]) {
delete targetServers[id]
removed.push(id)
}
}
return { synced, removed }
}
/**
* Format sync result message
*/
function formatSyncMessage(target: string, synced: string[], removed: string[]): string {
const parts: string[] = []
if (synced.length > 0) parts.push(`synced: ${synced.join(', ')}`)
if (removed.length > 0) parts.push(`removed: ${removed.join(', ')}`)
return `${target} MCP mirror complete (${parts.join('; ')})`
}
/**
* Sync (mirror) CCG-managed MCP servers from Claude's ~/.claude.json
* to Codex's ~/.codex/config.toml
*
* - Only touches servers in CCG_MCP_IDS — user's custom servers untouched.
* - Uses atomic write (temp file + rename) to prevent corruption.
*/
export async function syncMcpToCodex(): Promise<SyncResult> {
try {
// 1. Read Claude's CCG-managed MCP config
const serversToSync = await getCcgMcpServersFromClaude()
// 2. Read or create Codex config
// Read or create Codex config
const codexConfigDir = join(homedir(), '.codex')
const codexConfigPath = join(codexConfigDir, 'config.toml')
await fs.ensureDir(codexConfigDir)
@@ -546,91 +390,49 @@ export async function syncMcpToCodex(): Promise<{
codexConfig = parseToml(content) as Record<string, any>
}
// 3. Ensure mcp_servers table exists
if (!codexConfig.mcp_servers) {
codexConfig.mcp_servers = {}
}
// 4. Mirror: add/update CCG servers (pass through ALL fields)
for (const [id, claudeServer] of Object.entries(serversToSync)) {
const server = claudeServer as Record<string, any>
const codexEntry: Record<string, any> = {}
// Pass through all TOML-compatible fields from Claude config
for (const [key, value] of Object.entries(server)) {
// Codex needs field-level copy (TOML compatibility: filter null/undefined)
const codexServersToSync: Record<string, any> = {}
for (const [id, server] of Object.entries(serversToSync)) {
const entry: Record<string, any> = {}
for (const [key, value] of Object.entries(server as Record<string, any>)) {
if (value !== null && value !== undefined) {
codexEntry[key] = value
entry[key] = value
}
}
codexConfig.mcp_servers[id] = codexEntry
synced.push(id)
codexServersToSync[id] = entry
}
// 5. Mirror: remove CCG servers that no longer exist in Claude
for (const id of CCG_MCP_IDS) {
if (!serversToSync[id] && codexConfig.mcp_servers[id]) {
delete codexConfig.mcp_servers[id]
removed.push(id)
}
}
const { synced, removed } = mirrorCcgServers(codexServersToSync, codexConfig.mcp_servers)
// Skip write if no changes needed
if (synced.length === 0 && removed.length === 0) {
return {
success: true,
message: 'No CCG MCP servers to sync or remove',
synced: [],
removed: [],
}
return { success: true, message: 'No CCG MCP servers to sync or remove', synced: [], removed: [] }
}
// 6. Atomic write: temp file + rename to prevent corruption
// Atomic write: temp file + rename
const tmpPath = `${codexConfigPath}.tmp`
await fs.writeFile(tmpPath, stringifyToml(codexConfig), 'utf-8')
await fs.rename(tmpPath, codexConfigPath)
const parts: string[] = []
if (synced.length > 0) parts.push(`synced: ${synced.join(', ')}`)
if (removed.length > 0) parts.push(`removed: ${removed.join(', ')}`)
return {
success: true,
message: `Codex MCP mirror complete (${parts.join('; ')})`,
synced,
removed,
}
return { success: true, message: formatSyncMessage('Codex', synced, removed), synced, removed }
}
catch (error) {
return {
success: false,
message: `Failed to sync MCP to Codex: ${error}`,
synced,
removed,
}
return { success: false, message: `Failed to sync MCP to Codex: ${error}`, synced: [], removed: [] }
}
}
/**
* Sync (mirror) CCG-managed MCP servers from Claude's ~/.claude.json
* to Gemini CLI's ~/.gemini/settings.json
*
* Same logic as syncMcpToCodex but targets Gemini's JSON config format.
*/
export async function syncMcpToGemini(): Promise<{
success: boolean
message: string
synced: string[]
removed: string[]
}> {
const synced: string[] = []
const removed: string[] = []
export async function syncMcpToGemini(): Promise<SyncResult> {
try {
// 1. Read Claude's CCG-managed MCP config
const serversToSync = await getCcgMcpServersFromClaude()
// 2. Read or create Gemini settings
// Read or create Gemini settings
const geminiDir = join(homedir(), '.gemini')
const geminiSettingsPath = join(geminiDir, 'settings.json')
await fs.ensureDir(geminiDir)
@@ -640,39 +442,21 @@ export async function syncMcpToGemini(): Promise<{
geminiSettings = await fs.readJSON(geminiSettingsPath)
}
// 3. Ensure mcpServers exists
if (!geminiSettings.mcpServers) {
geminiSettings.mcpServers = {}
}
// 4. Mirror: add/update CCG servers
for (const [id, claudeServer] of Object.entries(serversToSync)) {
geminiSettings.mcpServers[id] = claudeServer
synced.push(id)
}
// 5. Mirror: remove CCG servers that no longer exist in Claude
for (const id of CCG_MCP_IDS) {
if (!serversToSync[id] && geminiSettings.mcpServers[id]) {
delete geminiSettings.mcpServers[id]
removed.push(id)
}
}
const { synced, removed } = mirrorCcgServers(serversToSync, geminiSettings.mcpServers)
if (synced.length === 0 && removed.length === 0) {
return { success: true, message: 'No CCG MCP servers to sync to Gemini', synced: [], removed: [] }
}
// 6. Write back (preserve all other Gemini settings)
await fs.writeJSON(geminiSettingsPath, geminiSettings, { spaces: 2 })
const parts: string[] = []
if (synced.length > 0) parts.push(`synced: ${synced.join(', ')}`)
if (removed.length > 0) parts.push(`removed: ${removed.join(', ')}`)
return { success: true, message: `Gemini MCP mirror complete (${parts.join('; ')})`, synced, removed }
return { success: true, message: formatSyncMessage('Gemini', synced, removed), synced, removed }
}
catch (error) {
return { success: false, message: `Failed to sync MCP to Gemini: ${error}`, synced, removed }
return { success: false, message: `Failed to sync MCP to Gemini: ${error}`, synced: [], removed: [] }
}
}
+322 -308
View File
@@ -3,7 +3,6 @@ import fs from 'fs-extra'
import { join } from 'pathe'
import { getWorkflowById } from './installer-data'
import { PACKAGE_ROOT, injectConfigVariables, replaceHomePathsInTemplate } from './installer-template'
import { isWindows } from './platform'
// ═══════════════════════════════════════════════════════
// Re-exports — all consumers import from './installer'
@@ -41,27 +40,45 @@ export {
writeFastContextPrompt,
} from './installer-prompt'
// ═══════════════════════════════════════════════════════
// Install context — shared across sub-functions
// ═══════════════════════════════════════════════════════
interface InstallConfig {
routing: {
mode: string
frontend: { models: string[], primary: string }
backend: { models: string[], primary: string }
review: { models: string[] }
}
liteMode: boolean
mcpProvider: string
}
interface InstallContext {
installDir: string
force: boolean
config: InstallConfig
templateDir: string
result: InstallResult
}
// ═══════════════════════════════════════════════════════
// Binary download
// ═══════════════════════════════════════════════════════
// GitHub Release download config
const GITHUB_REPO = 'fengshao1227/ccg-workflow'
const RELEASE_TAG = 'preset'
const BINARY_DOWNLOAD_URL = `https://github.com/${GITHUB_REPO}/releases/download/${RELEASE_TAG}`
/**
* Download codeagent-wrapper binary from GitHub Release.
* Uses the fixed `preset` tag release for pre-compiled binaries.
* Supports redirect-following (GitHub releases redirect to CDN).
*
* Retry: 3 attempts with exponential backoff (2s, 4s, 6s).
* Timeout: 60s per attempt via AbortController.
* Retry: 3 attempts with exponential backoff. Timeout: 60s per attempt.
*/
async function downloadBinaryFromRelease(binaryName: string, destPath: string): Promise<boolean> {
const url = `${BINARY_DOWNLOAD_URL}/${binaryName}`
const MAX_ATTEMPTS = 3
const TIMEOUT_MS = 60_000 // 60s per attempt
const TIMEOUT_MS = 60_000
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
try {
@@ -82,17 +99,13 @@ async function downloadBinaryFromRelease(binaryName: string, destPath: string):
clearTimeout(timer)
await fs.writeFile(destPath, buffer)
// Set executable permission on Unix-like systems
if (process.platform !== 'win32') {
await fs.chmod(destPath, 0o755)
}
return true
}
catch {
if (attempt < MAX_ATTEMPTS) {
// Exponential backoff before retry
await new Promise(resolve => setTimeout(resolve, attempt * 2000))
continue
}
@@ -104,12 +117,129 @@ async function downloadBinaryFromRelease(binaryName: string, destPath: string):
}
// ═══════════════════════════════════════════════════════
// Skills counter
// Install sub-steps
// ═══════════════════════════════════════════════════════
/**
* Count installed SKILL.md files in the skills directory (recursive).
* Excludes root-level SKILL.md (which is the index/manifest, not a skill itself).
* Install slash command .md files from templates/commands/
*/
async function installCommandFiles(ctx: InstallContext, workflowIds: string[]): Promise<void> {
const commandsDir = join(ctx.installDir, 'commands', 'ccg')
for (const workflowId of workflowIds) {
const workflow = getWorkflowById(workflowId)
if (!workflow) {
ctx.result.errors.push(`Unknown workflow: ${workflowId}`)
continue
}
for (const cmd of workflow.commands) {
const srcFile = join(ctx.templateDir, 'commands', `${cmd}.md`)
const destFile = join(commandsDir, `${cmd}.md`)
try {
if (await fs.pathExists(srcFile)) {
if (ctx.force || !(await fs.pathExists(destFile))) {
let content = await fs.readFile(srcFile, 'utf-8')
content = injectConfigVariables(content, ctx.config)
content = replaceHomePathsInTemplate(content, ctx.installDir)
await fs.writeFile(destFile, content, 'utf-8')
ctx.result.installedCommands.push(cmd)
}
}
else {
const placeholder = `---
description: "${workflow.descriptionEn}"
---
# /ccg:${cmd}
${workflow.description}
> This command is part of CCG multi-model collaboration system.
`
await fs.writeFile(destFile, placeholder, 'utf-8')
ctx.result.installedCommands.push(cmd)
}
}
catch (error) {
ctx.result.errors.push(`Failed to install ${cmd}: ${error}`)
ctx.result.success = false
}
}
}
}
/**
* Install agent .md files from templates/commands/agents/
*/
async function installAgentFiles(ctx: InstallContext): Promise<void> {
const agentsSrcDir = join(ctx.templateDir, 'commands', 'agents')
const agentsDestDir = join(ctx.installDir, 'agents', 'ccg')
if (!(await fs.pathExists(agentsSrcDir))) return
try {
await fs.ensureDir(agentsDestDir)
const agentFiles = await fs.readdir(agentsSrcDir)
for (const file of agentFiles) {
if (file.endsWith('.md')) {
const srcFile = join(agentsSrcDir, file)
const destFile = join(agentsDestDir, file)
if (ctx.force || !(await fs.pathExists(destFile))) {
let content = await fs.readFile(srcFile, 'utf-8')
content = injectConfigVariables(content, ctx.config)
content = replaceHomePathsInTemplate(content, ctx.installDir)
await fs.writeFile(destFile, content, 'utf-8')
}
}
}
}
catch (error) {
ctx.result.errors.push(`Failed to install agents: ${error}`)
ctx.result.success = false
}
}
/**
* Install expert prompt .md files from templates/prompts/{codex,gemini,claude}/
*/
async function installPromptFiles(ctx: InstallContext): Promise<void> {
const promptsTemplateDir = join(ctx.templateDir, 'prompts')
const promptsDir = join(ctx.installDir, '.ccg', 'prompts')
if (!(await fs.pathExists(promptsTemplateDir))) return
const modelDirs = ['codex', 'gemini', 'claude']
for (const model of modelDirs) {
const srcModelDir = join(promptsTemplateDir, model)
const destModelDir = join(promptsDir, model)
if (await fs.pathExists(srcModelDir)) {
try {
await fs.ensureDir(destModelDir)
const files = await fs.readdir(srcModelDir)
for (const file of files) {
if (file.endsWith('.md')) {
const srcFile = join(srcModelDir, file)
const destFile = join(destModelDir, file)
if (ctx.force || !(await fs.pathExists(destFile))) {
const content = await fs.readFile(srcFile, 'utf-8')
const processed = replaceHomePathsInTemplate(content, ctx.installDir)
await fs.writeFile(destFile, processed, 'utf-8')
ctx.result.installedPrompts.push(`${model}/${file.replace('.md', '')}`)
}
}
}
}
catch (error) {
ctx.result.errors.push(`Failed to install ${model} prompts: ${error}`)
ctx.result.success = false
}
}
}
}
/**
* Count installed SKILL.md files (recursive, excludes root-level).
*/
async function countInstalledSkills(skillsDir: string, depth = 0): Promise<number> {
let count = 0
@@ -125,14 +255,150 @@ async function countInstalledSkills(skillsDir: string, depth = 0): Promise<numbe
}
}
}
catch {
// Directory doesn't exist or can't be read
}
catch { /* Directory doesn't exist or can't be read */ }
return count
}
/**
* Install skill files from templates/skills/ → ~/.claude/skills/ccg/
* Includes v1.7.73 legacy layout migration.
*/
async function installSkillFiles(ctx: InstallContext): Promise<void> {
const skillsTemplateDir = join(ctx.templateDir, 'skills')
const skillsDestDir = join(ctx.installDir, 'skills', 'ccg')
if (!(await fs.pathExists(skillsTemplateDir))) return
try {
// Migration: move old v1.7.73 layout into skills/ccg/ namespace
const oldSkillsRoot = join(ctx.installDir, 'skills')
const ccgLegacyItems = ['tools', 'orchestration', 'SKILL.md', 'run_skill.js']
const needsMigration = !await fs.pathExists(skillsDestDir)
&& await fs.pathExists(join(oldSkillsRoot, 'tools'))
if (needsMigration) {
await fs.ensureDir(skillsDestDir)
for (const item of ccgLegacyItems) {
const oldPath = join(oldSkillsRoot, item)
const newPath = join(skillsDestDir, item)
if (await fs.pathExists(oldPath)) {
await fs.move(oldPath, newPath, { overwrite: true })
}
}
}
// Recursive copy: preserves full directory tree
await fs.copy(skillsTemplateDir, skillsDestDir, {
overwrite: ctx.force,
errorOnExist: false,
})
// Post-copy: apply template variable replacement to .md files
const replacePathsInDir = async (dir: string): Promise<void> => {
const entries = await fs.readdir(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = join(dir, entry.name)
if (entry.isDirectory()) {
await replacePathsInDir(fullPath)
}
else if (entry.name.endsWith('.md')) {
const content = await fs.readFile(fullPath, 'utf-8')
const processed = replaceHomePathsInTemplate(content, ctx.installDir)
if (processed !== content) {
await fs.writeFile(fullPath, processed, 'utf-8')
}
}
}
}
await replacePathsInDir(skillsDestDir)
ctx.result.installedSkills = await countInstalledSkills(skillsDestDir)
}
catch (error) {
ctx.result.errors.push(`Failed to install skills: ${error}`)
ctx.result.success = false
}
}
/**
* Install rule .md files from templates/rules/ → ~/.claude/rules/
*/
async function installRuleFiles(ctx: InstallContext): Promise<void> {
const rulesTemplateDir = join(ctx.templateDir, 'rules')
const rulesDestDir = join(ctx.installDir, 'rules')
if (!(await fs.pathExists(rulesTemplateDir))) return
try {
await fs.ensureDir(rulesDestDir)
const rulesFiles = await fs.readdir(rulesTemplateDir)
for (const file of rulesFiles) {
if (file.endsWith('.md')) {
const srcFile = join(rulesTemplateDir, file)
const destFile = join(rulesDestDir, file)
if (ctx.force || !(await fs.pathExists(destFile))) {
const content = await fs.readFile(srcFile, 'utf-8')
const processed = replaceHomePathsInTemplate(content, ctx.installDir)
await fs.writeFile(destFile, processed, 'utf-8')
}
}
}
ctx.result.installedRules = true
}
catch (error) {
ctx.result.errors.push(`Failed to install rules: ${error}`)
}
}
/**
* Download and install codeagent-wrapper binary for current platform.
*/
async function installBinaryFile(ctx: InstallContext): Promise<void> {
try {
const binDir = join(ctx.installDir, 'bin')
await fs.ensureDir(binDir)
const platform = process.platform
const arch = process.arch
let binaryName: string
if (platform === 'darwin') {
binaryName = arch === 'arm64' ? 'codeagent-wrapper-darwin-arm64' : 'codeagent-wrapper-darwin-amd64'
}
else if (platform === 'linux') {
binaryName = arch === 'arm64' ? 'codeagent-wrapper-linux-arm64' : 'codeagent-wrapper-linux-amd64'
}
else if (platform === 'win32') {
binaryName = arch === 'arm64' ? 'codeagent-wrapper-windows-arm64.exe' : 'codeagent-wrapper-windows-amd64.exe'
}
else {
ctx.result.errors.push(`Unsupported platform: ${platform}`)
ctx.result.success = false
return
}
const destBinary = join(binDir, platform === 'win32' ? 'codeagent-wrapper.exe' : 'codeagent-wrapper')
const installed = await downloadBinaryFromRelease(binaryName, destBinary)
if (installed) {
try {
const { execSync } = await import('node:child_process')
execSync(`"${destBinary}" --version`, { stdio: 'pipe' })
ctx.result.binPath = binDir
ctx.result.binInstalled = true
}
catch (verifyError) {
ctx.result.errors.push(`Binary verification failed (non-blocking): ${verifyError}`)
}
}
else {
ctx.result.errors.push(`Failed to download binary: ${binaryName} from GitHub Release (after 3 attempts). Check network or visit https://github.com/${GITHUB_REPO}/releases/tag/${RELEASE_TAG}`)
}
}
catch (error) {
ctx.result.errors.push(`Failed to install codeagent-wrapper (non-blocking): ${error}`)
}
}
// ═══════════════════════════════════════════════════════
// Core install / uninstall
// Public API: install / uninstall
// ═══════════════════════════════════════════════════════
export async function installWorkflows(
@@ -150,281 +416,44 @@ export async function installWorkflows(
mcpProvider?: string
},
): Promise<InstallResult> {
// Default config
const installConfig = {
routing: config?.routing || {
mode: 'smart',
frontend: { models: ['gemini'], primary: 'gemini' },
backend: { models: ['codex'], primary: 'codex' },
review: { models: ['codex', 'gemini'] },
const ctx: InstallContext = {
installDir,
force,
config: {
routing: config?.routing as InstallConfig['routing'] || {
mode: 'smart',
frontend: { models: ['gemini'], primary: 'gemini' },
backend: { models: ['codex'], primary: 'codex' },
review: { models: ['codex', 'gemini'] },
},
liteMode: config?.liteMode || false,
mcpProvider: config?.mcpProvider || 'ace-tool',
},
templateDir: join(PACKAGE_ROOT, 'templates'),
result: {
success: true,
installedCommands: [],
installedPrompts: [],
errors: [],
configPath: '',
},
liteMode: config?.liteMode || false,
mcpProvider: config?.mcpProvider || 'ace-tool',
}
const result: InstallResult = {
success: true,
installedCommands: [],
installedPrompts: [],
errors: [],
configPath: '',
}
const commandsDir = join(installDir, 'commands', 'ccg')
const ccgConfigDir = join(installDir, '.ccg') // v1.4.0: 配置目录
const promptsDir = join(ccgConfigDir, 'prompts') // v1.4.0: prompts 移到配置目录
// Ensure base directories
await fs.ensureDir(join(installDir, 'commands', 'ccg'))
await fs.ensureDir(join(installDir, '.ccg'))
await fs.ensureDir(join(installDir, '.ccg', 'prompts'))
await fs.ensureDir(commandsDir)
await fs.ensureDir(ccgConfigDir)
await fs.ensureDir(promptsDir)
// Execute each install step
await installCommandFiles(ctx, workflowIds)
await installAgentFiles(ctx)
await installPromptFiles(ctx)
await installSkillFiles(ctx)
await installRuleFiles(ctx)
await installBinaryFile(ctx)
// Get template source directory (relative to this package)
const templateDir = join(PACKAGE_ROOT, 'templates')
// Install commands
for (const workflowId of workflowIds) {
const workflow = getWorkflowById(workflowId)
if (!workflow) {
result.errors.push(`Unknown workflow: ${workflowId}`)
continue
}
for (const cmd of workflow.commands) {
const srcFile = join(templateDir, 'commands', `${cmd}.md`)
const destFile = join(commandsDir, `${cmd}.md`)
try {
if (await fs.pathExists(srcFile)) {
if (force || !(await fs.pathExists(destFile))) {
// Read template content, inject config variables, replace ~ paths, then write
let templateContent = await fs.readFile(srcFile, 'utf-8')
templateContent = injectConfigVariables(templateContent, installConfig)
const processedContent = replaceHomePathsInTemplate(templateContent, installDir)
await fs.writeFile(destFile, processedContent, 'utf-8')
result.installedCommands.push(cmd)
}
}
else {
// If template doesn't exist, create placeholder
const placeholder = `---
description: "${workflow.descriptionEn}"
---
# /ccg:${cmd}
${workflow.description}
> This command is part of CCG multi-model collaboration system.
`
await fs.writeFile(destFile, placeholder, 'utf-8')
result.installedCommands.push(cmd)
}
}
catch (error) {
result.errors.push(`Failed to install ${cmd}: ${error}`)
result.success = false
}
}
}
// Install agents directory (subagents - should go to ~/.claude/agents/ccg/)
const agentsSrcDir = join(templateDir, 'commands', 'agents')
const agentsDestDir = join(installDir, 'agents', 'ccg')
if (await fs.pathExists(agentsSrcDir)) {
try {
await fs.ensureDir(agentsDestDir)
const agentFiles = await fs.readdir(agentsSrcDir)
for (const file of agentFiles) {
if (file.endsWith('.md')) {
const srcFile = join(agentsSrcDir, file)
const destFile = join(agentsDestDir, file)
if (force || !(await fs.pathExists(destFile))) {
// Read template content, inject config variables, replace ~ paths, then write
let templateContent = await fs.readFile(srcFile, 'utf-8')
templateContent = injectConfigVariables(templateContent, installConfig)
const processedContent = replaceHomePathsInTemplate(templateContent, installDir)
await fs.writeFile(destFile, processedContent, 'utf-8')
}
}
}
}
catch (error) {
result.errors.push(`Failed to install agents: ${error}`)
result.success = false
}
}
// Install prompts (codex, gemini, claude role definitions)
const promptsTemplateDir = join(templateDir, 'prompts')
if (await fs.pathExists(promptsTemplateDir)) {
const modelDirs = ['codex', 'gemini', 'claude']
for (const model of modelDirs) {
const srcModelDir = join(promptsTemplateDir, model)
const destModelDir = join(promptsDir, model)
if (await fs.pathExists(srcModelDir)) {
try {
await fs.ensureDir(destModelDir)
const files = await fs.readdir(srcModelDir)
for (const file of files) {
if (file.endsWith('.md')) {
const srcFile = join(srcModelDir, file)
const destFile = join(destModelDir, file)
if (force || !(await fs.pathExists(destFile))) {
// Read template content, replace ~ paths, then write
const templateContent = await fs.readFile(srcFile, 'utf-8')
const processedContent = replaceHomePathsInTemplate(templateContent, installDir)
await fs.writeFile(destFile, processedContent, 'utf-8')
result.installedPrompts.push(`${model}/${file.replace('.md', '')}`)
}
}
}
}
catch (error) {
result.errors.push(`Failed to install ${model} prompts: ${error}`)
result.success = false
}
}
}
}
// Install skills (quality gates, multi-agent orchestration, etc. → ~/.claude/skills/ccg/)
// Uses 'ccg' namespace to avoid clobbering user's own skills on uninstall
const skillsTemplateDir = join(templateDir, 'skills')
const skillsDestDir = join(installDir, 'skills', 'ccg')
if (await fs.pathExists(skillsTemplateDir)) {
try {
// Migration: move old v1.7.73 layout (skills/{tools,orchestration,SKILL.md,run_skill.js})
// into skills/ccg/ namespace, preserving user's own skills in skills/
const oldSkillsRoot = join(installDir, 'skills')
const ccgLegacyItems = ['tools', 'orchestration', 'SKILL.md', 'run_skill.js']
const needsMigration = !await fs.pathExists(skillsDestDir)
&& await fs.pathExists(join(oldSkillsRoot, 'tools'))
if (needsMigration) {
await fs.ensureDir(skillsDestDir)
for (const item of ccgLegacyItems) {
const oldPath = join(oldSkillsRoot, item)
const newPath = join(skillsDestDir, item)
if (await fs.pathExists(oldPath)) {
await fs.move(oldPath, newPath, { overwrite: true })
}
}
}
// Recursive copy: preserves full directory tree (tools/lib, tools/*/scripts/, orchestration/*)
await fs.copy(skillsTemplateDir, skillsDestDir, {
overwrite: force,
errorOnExist: false,
})
// Post-copy: apply template variable replacement to .md files
// (handles custom installDir where ~/.claude paths need substitution)
const replacePathsInDir = async (dir: string): Promise<void> => {
const entries = await fs.readdir(dir, { withFileTypes: true })
for (const entry of entries) {
const fullPath = join(dir, entry.name)
if (entry.isDirectory()) {
await replacePathsInDir(fullPath)
}
else if (entry.name.endsWith('.md')) {
const content = await fs.readFile(fullPath, 'utf-8')
const processed = replaceHomePathsInTemplate(content, installDir)
if (processed !== content) {
await fs.writeFile(fullPath, processed, 'utf-8')
}
}
}
}
await replacePathsInDir(skillsDestDir)
result.installedSkills = await countInstalledSkills(skillsDestDir)
}
catch (error) {
result.errors.push(`Failed to install skills: ${error}`)
result.success = false
}
}
// Install rules (quality gate auto-trigger → ~/.claude/rules/ccg-skills.md)
const rulesTemplateDir = join(templateDir, 'rules')
const rulesDestDir = join(installDir, 'rules')
if (await fs.pathExists(rulesTemplateDir)) {
try {
await fs.ensureDir(rulesDestDir)
const rulesFiles = await fs.readdir(rulesTemplateDir)
for (const file of rulesFiles) {
if (file.endsWith('.md')) {
const srcFile = join(rulesTemplateDir, file)
const destFile = join(rulesDestDir, file)
if (force || !(await fs.pathExists(destFile))) {
const templateContent = await fs.readFile(srcFile, 'utf-8')
const processedContent = replaceHomePathsInTemplate(templateContent, installDir)
await fs.writeFile(destFile, processedContent, 'utf-8')
}
}
}
result.installedRules = true
}
catch (error) {
result.errors.push(`Failed to install rules: ${error}`)
}
}
// Install codeagent-wrapper binary
try {
const binDir = join(installDir, 'bin')
await fs.ensureDir(binDir)
// Detect platform and select appropriate binary
const platform = process.platform
const arch = process.arch
let binaryName: string
if (platform === 'darwin') {
binaryName = arch === 'arm64' ? 'codeagent-wrapper-darwin-arm64' : 'codeagent-wrapper-darwin-amd64'
}
else if (platform === 'linux') {
binaryName = arch === 'arm64' ? 'codeagent-wrapper-linux-arm64' : 'codeagent-wrapper-linux-amd64'
}
else if (platform === 'win32') {
binaryName = arch === 'arm64' ? 'codeagent-wrapper-windows-arm64.exe' : 'codeagent-wrapper-windows-amd64.exe'
}
else {
result.errors.push(`Unsupported platform: ${platform}`)
result.success = false
result.configPath = commandsDir
return result
}
const destBinary = join(binDir, platform === 'win32' ? 'codeagent-wrapper.exe' : 'codeagent-wrapper')
// Download from GitHub Release (3 attempts with retry)
const installed = await downloadBinaryFromRelease(binaryName, destBinary)
if (installed) {
// Verify installation by running --version
try {
const { execSync } = await import('node:child_process')
execSync(`"${destBinary}" --version`, { stdio: 'pipe' })
result.binPath = binDir
result.binInstalled = true
}
catch (verifyError) {
// Binary downloaded but verification failed — warning, not blocking
result.errors.push(`Binary verification failed (non-blocking): ${verifyError}`)
}
}
else {
// Binary download failed — warning, not blocking (commands + skills still work)
result.errors.push(`Failed to download binary: ${binaryName} from GitHub Release (after 3 attempts). Check network or visit https://github.com/${GITHUB_REPO}/releases/tag/${RELEASE_TAG}`)
}
}
catch (error) {
// Binary install exception — warning, not blocking
result.errors.push(`Failed to install codeagent-wrapper (non-blocking): ${error}`)
}
result.configPath = commandsDir
return result
ctx.result.configPath = join(installDir, 'commands', 'ccg')
return ctx.result
}
// ═══════════════════════════════════════════════════════
@@ -458,7 +487,6 @@ export async function uninstallWorkflows(installDir: string): Promise<UninstallR
}
const commandsDir = join(installDir, 'commands', 'ccg')
const promptsDir = join(installDir, '.ccg', 'prompts')
const agentsDir = join(installDir, 'agents', 'ccg')
const skillsDir = join(installDir, 'skills', 'ccg')
const rulesDir = join(installDir, 'rules')
@@ -468,15 +496,12 @@ export async function uninstallWorkflows(installDir: string): Promise<UninstallR
// Remove CCG commands directory
if (await fs.pathExists(commandsDir)) {
try {
// Get list for reporting
const files = await fs.readdir(commandsDir)
for (const file of files) {
if (file.endsWith('.md')) {
result.removedCommands.push(file.replace('.md', ''))
}
}
// Force remove the entire directory
await fs.remove(commandsDir)
}
catch (error) {
@@ -488,13 +513,10 @@ export async function uninstallWorkflows(installDir: string): Promise<UninstallR
// Remove CCG agents directory
if (await fs.pathExists(agentsDir)) {
try {
// Get list for reporting
const files = await fs.readdir(agentsDir)
for (const file of files) {
result.removedAgents.push(file.replace('.md', ''))
}
// Force remove the entire directory
await fs.remove(agentsDir)
}
catch (error) {
@@ -503,10 +525,9 @@ export async function uninstallWorkflows(installDir: string): Promise<UninstallR
}
}
// Remove CCG skills directory only (skills/ccg/) — preserves user's own skills in skills/
// Remove CCG skills directory only (skills/ccg/) — preserves user's own skills
if (await fs.pathExists(skillsDir)) {
try {
// Collect skill names for reporting (SKILL.md files, excluding root)
const collectSkillNames = async (dir: string, depth: number): Promise<string[]> => {
const names: string[] = []
const entries = await fs.readdir(dir, { withFileTypes: true })
@@ -515,7 +536,6 @@ export async function uninstallWorkflows(installDir: string): Promise<UninstallR
names.push(...await collectSkillNames(join(dir, entry.name), depth + 1))
}
else if (entry.name === 'SKILL.md' && depth > 0) {
// Use parent directory name as skill name
const parts = dir.split('/')
names.push(parts[parts.length - 1])
}
@@ -523,8 +543,6 @@ export async function uninstallWorkflows(installDir: string): Promise<UninstallR
return names
}
result.removedSkills = await collectSkillNames(skillsDir, 0)
// Remove entire skills directory
await fs.remove(skillsDir)
}
catch (error) {
@@ -533,11 +551,10 @@ export async function uninstallWorkflows(installDir: string): Promise<UninstallR
}
}
// Remove CCG rules files (rules/ccg-skills.md, rules/ccg-grok-search.md)
// Remove CCG rules files
if (await fs.pathExists(rulesDir)) {
try {
const ccgRuleFiles = ['ccg-skills.md', 'ccg-grok-search.md']
for (const ruleFile of ccgRuleFiles) {
for (const ruleFile of ['ccg-skills.md', 'ccg-grok-search.md']) {
const rulePath = join(rulesDir, ruleFile)
if (await fs.pathExists(rulePath)) {
await fs.remove(rulePath)
@@ -550,8 +567,6 @@ export async function uninstallWorkflows(installDir: string): Promise<UninstallR
}
}
// Remove CCG prompts directory (in .ccg/) - handled by removing .ccg dir below
// Remove codeagent-wrapper binary
if (await fs.pathExists(binDir)) {
try {
@@ -568,7 +583,7 @@ export async function uninstallWorkflows(installDir: string): Promise<UninstallR
}
}
// Remove .ccg config directory (Force remove)
// Remove .ccg config directory
if (await fs.pathExists(ccgConfigDir)) {
try {
await fs.remove(ccgConfigDir)
@@ -576,7 +591,6 @@ export async function uninstallWorkflows(installDir: string): Promise<UninstallR
}
catch (error) {
result.errors.push(`Failed to remove .ccg directory: ${error}`)
// Don't mark as failure just for config, but good to know
}
}