From 8179472e562001682b09d935a5a9551dc67424fd Mon Sep 17 00:00:00 2001 From: catlog22 Date: Fri, 23 Jan 2026 23:20:58 +0800 Subject: [PATCH] fix: auto-sync CLI tools availability on first config creation (Issue #95) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **问题描述**: 新安装 CCW 后,默认配置中所有 CLI 工具 enabled: true,但实际上用户可能没有安装这些工具,导致执行任务时尝试调用未安装的工具而失败。 **根本原因**: - DEFAULT_TOOLS_CONFIG 中所有工具默认 enabled: true - 首次创建配置时不检测工具实际可用性 - 现有的 syncBuiltinToolsAvailability() 只在用户手动触发时才执行 **修复内容**: 1. 新增 ensureClaudeCliToolsAsync() 异步版本 - 在创建默认配置后自动调用 syncBuiltinToolsAvailability() - 通过 which/where 命令检测工具实际可用性 - 根据检测结果自动调整 enabled 状态 2. 更新两个关键 API 端点使用新函数 - /api/cli/endpoints - 获取 API 端点列表 - /api/cli/tools-config - 获取 CLI 工具配置 **效果**: - 首次安装时自动检测并禁用未安装的工具 - 避免调用不可用工具导致的错误 - 用户可在 Dashboard 中看到准确的工具状态 Fixes #95 --- ccw/src/core/routes/cli-routes.ts | 55 +++++++++++++++++-------------- ccw/src/tools/claude-cli-tools.ts | 50 ++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 25 deletions(-) diff --git a/ccw/src/core/routes/cli-routes.ts b/ccw/src/core/routes/cli-routes.ts index 29a53a65..2c81b7cd 100644 --- a/ccw/src/core/routes/cli-routes.ts +++ b/ccw/src/core/routes/cli-routes.ts @@ -35,6 +35,7 @@ import { import { loadClaudeCliTools, ensureClaudeCliTools, + ensureClaudeCliToolsAsync, saveClaudeCliTools, loadClaudeCliSettings, saveClaudeCliSettings, @@ -329,16 +330,18 @@ export async function handleCliRoutes(ctx: RouteContext): Promise { // API: Get all API endpoints (for --tool custom --model ) if (pathname === '/api/cli/endpoints' && req.method === 'GET') { - try { - // Use ensureClaudeCliTools to auto-create config if missing - const config = ensureClaudeCliTools(initialPath); - const endpoints = getApiEndpointsFromTools(config); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ endpoints })); - } catch (err) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: (err as Error).message })); - } + (async () => { + try { + // Use ensureClaudeCliToolsAsync to auto-create config with availability sync + const config = await ensureClaudeCliToolsAsync(initialPath); + const endpoints = getApiEndpointsFromTools(config); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ endpoints })); + } catch (err) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: (err as Error).message })); + } + })(); return true; } @@ -820,21 +823,23 @@ export async function handleCliRoutes(ctx: RouteContext): Promise { // API: Get CLI Tools Config from .claude/cli-tools.json (with fallback to global) if (pathname === '/api/cli/tools-config' && req.method === 'GET') { - try { - // Use ensureClaudeCliTools to auto-create config if missing - const toolsConfig = ensureClaudeCliTools(initialPath); - const settingsConfig = loadClaudeCliSettings(initialPath); - const info = getClaudeCliToolsInfo(initialPath); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - tools: toolsConfig, - settings: settingsConfig, - _configInfo: info - })); - } catch (err) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: (err as Error).message })); - } + (async () => { + try { + // Use ensureClaudeCliToolsAsync to auto-create config with availability sync + const toolsConfig = await ensureClaudeCliToolsAsync(initialPath); + const settingsConfig = loadClaudeCliSettings(initialPath); + const info = getClaudeCliToolsInfo(initialPath); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + tools: toolsConfig, + settings: settingsConfig, + _configInfo: info + })); + } catch (err) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: (err as Error).message })); + } + })(); return true; } diff --git a/ccw/src/tools/claude-cli-tools.ts b/ccw/src/tools/claude-cli-tools.ts index 9aed6810..875c74af 100644 --- a/ccw/src/tools/claude-cli-tools.ts +++ b/ccw/src/tools/claude-cli-tools.ts @@ -418,6 +418,56 @@ export function ensureClaudeCliTools(projectDir: string, createInProject: boolea } } +/** + * Async version of ensureClaudeCliTools with automatic availability sync + * Creates default config in global ~/.claude directory and syncs with actual tool availability + * @param projectDir - Project directory path (used for reading existing project config) + * @param createInProject - DEPRECATED: Always creates in global dir. Kept for backward compatibility. + * @returns The config that was created/exists + */ +export async function ensureClaudeCliToolsAsync(projectDir: string, createInProject: boolean = false): Promise { + const resolved = resolveConfigPath(projectDir); + + if (resolved.source !== 'default') { + // Config exists, load and return it + return loadClaudeCliTools(projectDir); + } + + // Config doesn't exist - create in global directory only + debugLog('[claude-cli-tools] Config not found, creating default cli-tools.json in ~/.claude'); + + const defaultConfig: ClaudeCliToolsConfig = { ...DEFAULT_TOOLS_CONFIG }; + + // Always create in global directory (user-level config), respecting CCW_DATA_DIR + const claudeHome = process.env.CCW_DATA_DIR + ? path.join(process.env.CCW_DATA_DIR, '.claude') + : path.join(os.homedir(), '.claude'); + if (!fs.existsSync(claudeHome)) { + fs.mkdirSync(claudeHome, { recursive: true }); + } + const globalPath = getGlobalConfigPath(); + try { + fs.writeFileSync(globalPath, JSON.stringify(defaultConfig, null, 2), 'utf-8'); + debugLog(`[claude-cli-tools] Created default config at: ${globalPath}`); + + // Auto-sync with actual tool availability on first creation + try { + debugLog('[claude-cli-tools] Auto-syncing tool availability on first creation...'); + const syncResult = await syncBuiltinToolsAvailability(projectDir); + debugLog(`[claude-cli-tools] Auto-sync completed: enabled=[${syncResult.changes.enabled.join(', ')}], disabled=[${syncResult.changes.disabled.join(', ')}]`); + return { ...syncResult.config, _source: 'global' }; + } catch (syncErr) { + console.warn('[claude-cli-tools] Failed to auto-sync availability:', syncErr); + // Return default config if sync fails + return { ...defaultConfig, _source: 'global' }; + } + } catch (err) { + console.error('[claude-cli-tools] Failed to create global config:', err); + return { ...defaultConfig, _source: 'default' }; + } +} + + /** * Load CLI tools configuration from global ~/.claude/cli-tools.json * Falls back to default config if not found.