mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-14 02:42:04 +08:00
feat: 优化 CLI 工具配置管理,动态加载工具并简化配置路径
This commit is contained in:
@@ -15,7 +15,6 @@ Available CLI endpoints are dynamically defined by the config file:
|
|||||||
- Managed through the CCW Dashboard Status page
|
- Managed through the CCW Dashboard Status page
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Tool Execution
|
## Tool Execution
|
||||||
|
|
||||||
- **Context Requirements**: @~/.claude/workflows/context-tools.md
|
- **Context Requirements**: @~/.claude/workflows/context-tools.md
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Displays CLI tool availability status and allows setting default tool
|
// Displays CLI tool availability status and allows setting default tool
|
||||||
|
|
||||||
// ========== CLI State ==========
|
// ========== CLI State ==========
|
||||||
let cliToolStatus = { gemini: {}, qwen: {}, codex: {}, claude: {} };
|
let cliToolStatus = {}; // Dynamically populated from config
|
||||||
let codexLensStatus = { ready: false };
|
let codexLensStatus = { ready: false };
|
||||||
let semanticStatus = { available: false };
|
let semanticStatus = { available: false };
|
||||||
let ccwInstallStatus = { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
|
let ccwInstallStatus = { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
|
||||||
@@ -38,8 +38,8 @@ async function loadAllStatuses() {
|
|||||||
if (!response.ok) throw new Error('Failed to load status');
|
if (!response.ok) throw new Error('Failed to load status');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// Update all status data
|
// Update all status data - merge with config tools to ensure all tools are tracked
|
||||||
cliToolStatus = data.cli || { gemini: {}, qwen: {}, codex: {}, claude: {} };
|
cliToolStatus = data.cli || {};
|
||||||
codexLensStatus = data.codexLens || { ready: false };
|
codexLensStatus = data.codexLens || { ready: false };
|
||||||
semanticStatus = data.semantic || { available: false };
|
semanticStatus = data.semantic || { available: false };
|
||||||
ccwInstallStatus = data.ccwInstall || { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
|
ccwInstallStatus = data.ccwInstall || { installed: true, workflowsInstalled: true, missingFiles: [], installPath: '' };
|
||||||
@@ -70,6 +70,7 @@ async function loadAllStatuses() {
|
|||||||
async function loadAllStatusesFallback() {
|
async function loadAllStatusesFallback() {
|
||||||
console.warn('[CLI Status] Using fallback individual API calls');
|
console.warn('[CLI Status] Using fallback individual API calls');
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
loadCliToolsConfig(), // Ensure config is loaded (auto-creates if missing)
|
||||||
loadCliToolStatus(),
|
loadCliToolStatus(),
|
||||||
loadCodexLensStatus()
|
loadCodexLensStatus()
|
||||||
]);
|
]);
|
||||||
@@ -307,12 +308,49 @@ async function loadCliSettingsEndpoints() {
|
|||||||
function updateCliBadge() {
|
function updateCliBadge() {
|
||||||
const badge = document.getElementById('badgeCliTools');
|
const badge = document.getElementById('badgeCliTools');
|
||||||
if (badge) {
|
if (badge) {
|
||||||
const available = Object.values(cliToolStatus).filter(t => t.available).length;
|
// Merge tools from both status and config to get complete list
|
||||||
const total = Object.keys(cliToolStatus).length;
|
const allTools = new Set([
|
||||||
badge.textContent = `${available}/${total}`;
|
...Object.keys(cliToolStatus),
|
||||||
badge.classList.toggle('text-success', available === total);
|
...Object.keys(cliToolsConfig)
|
||||||
badge.classList.toggle('text-warning', available > 0 && available < total);
|
]);
|
||||||
badge.classList.toggle('text-destructive', available === 0);
|
|
||||||
|
// Count available and enabled CLI tools
|
||||||
|
let available = 0;
|
||||||
|
allTools.forEach(tool => {
|
||||||
|
const status = cliToolStatus[tool] || {};
|
||||||
|
const config = cliToolsConfig[tool] || { enabled: true };
|
||||||
|
if (status.available && config.enabled !== false) {
|
||||||
|
available++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also count CodexLens and Semantic Search
|
||||||
|
let totalExtras = 0;
|
||||||
|
let availableExtras = 0;
|
||||||
|
|
||||||
|
// CodexLens counts if ready
|
||||||
|
if (codexLensStatus.ready) {
|
||||||
|
totalExtras++;
|
||||||
|
availableExtras++;
|
||||||
|
} else if (codexLensStatus.ready === false) {
|
||||||
|
// Only count as total if we have status info (not just initial state)
|
||||||
|
totalExtras++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Semantic Search counts if CodexLens is ready (it's a feature of CodexLens)
|
||||||
|
if (codexLensStatus.ready) {
|
||||||
|
totalExtras++;
|
||||||
|
if (semanticStatus.available) {
|
||||||
|
availableExtras++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = allTools.size + totalExtras;
|
||||||
|
const totalAvailable = available + availableExtras;
|
||||||
|
badge.textContent = `${totalAvailable}/${total}`;
|
||||||
|
badge.classList.toggle('text-success', totalAvailable === total && total > 0);
|
||||||
|
badge.classList.toggle('text-warning', totalAvailable > 0 && totalAvailable < total);
|
||||||
|
badge.classList.toggle('text-destructive', totalAvailable === 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,17 +391,33 @@ function renderCliStatus() {
|
|||||||
gemini: 'Google AI for code analysis',
|
gemini: 'Google AI for code analysis',
|
||||||
qwen: 'Alibaba AI assistant',
|
qwen: 'Alibaba AI assistant',
|
||||||
codex: 'OpenAI code generation',
|
codex: 'OpenAI code generation',
|
||||||
claude: 'Anthropic AI assistant'
|
claude: 'Anthropic AI assistant',
|
||||||
|
opencode: 'OpenCode multi-model API'
|
||||||
};
|
};
|
||||||
|
|
||||||
const toolIcons = {
|
const toolIcons = {
|
||||||
gemini: 'sparkle',
|
gemini: 'sparkle',
|
||||||
qwen: 'bot',
|
qwen: 'bot',
|
||||||
codex: 'code-2',
|
codex: 'code-2',
|
||||||
claude: 'brain'
|
claude: 'brain',
|
||||||
|
opencode: 'globe' // Default icon for new tools
|
||||||
};
|
};
|
||||||
|
|
||||||
const tools = ['gemini', 'qwen', 'codex', 'claude'];
|
// Helper to get description for any tool (with fallback)
|
||||||
|
const getToolDescription = (tool) => {
|
||||||
|
return toolDescriptions[tool] || `${tool.charAt(0).toUpperCase() + tool.slice(1)} CLI tool`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to get icon for any tool (with fallback)
|
||||||
|
const getToolIcon = (tool) => {
|
||||||
|
return toolIcons[tool] || 'terminal';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get tools dynamically from config, merging with status for complete list
|
||||||
|
const tools = [...new Set([
|
||||||
|
...Object.keys(cliToolsConfig),
|
||||||
|
...Object.keys(cliToolStatus)
|
||||||
|
])].filter(t => t && t !== '_configInfo'); // Filter out metadata keys
|
||||||
|
|
||||||
const toolsHtml = tools.map(tool => {
|
const toolsHtml = tools.map(tool => {
|
||||||
const status = cliToolStatus[tool] || {};
|
const status = cliToolStatus[tool] || {};
|
||||||
@@ -429,7 +483,7 @@ function renderCliStatus() {
|
|||||||
${cliSettingsBadge}
|
${cliSettingsBadge}
|
||||||
</div>
|
</div>
|
||||||
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
|
<div class="cli-tool-desc text-xs text-muted-foreground mt-1">
|
||||||
${toolDescriptions[tool]}
|
${getToolDescription(tool)}
|
||||||
</div>
|
</div>
|
||||||
<div class="cli-tool-info mt-2 flex items-center justify-between">
|
<div class="cli-tool-info mt-2 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -810,7 +864,8 @@ async function refreshAllCliStatus() {
|
|||||||
async function toggleCliTool(tool, enabled) {
|
async function toggleCliTool(tool, enabled) {
|
||||||
// If disabling the current default tool, switch to another available+enabled tool
|
// If disabling the current default tool, switch to another available+enabled tool
|
||||||
if (!enabled && defaultCliTool === tool) {
|
if (!enabled && defaultCliTool === tool) {
|
||||||
const tools = ['gemini', 'qwen', 'codex', 'claude'];
|
// Get tools dynamically from config
|
||||||
|
const tools = Object.keys(cliToolsConfig).filter(t => t && t !== '_configInfo');
|
||||||
const newDefault = tools.find(t => {
|
const newDefault = tools.find(t => {
|
||||||
if (t === tool) return false;
|
if (t === tool) return false;
|
||||||
const status = cliToolStatus[t] || {};
|
const status = cliToolStatus[t] || {};
|
||||||
|
|||||||
@@ -620,7 +620,9 @@ async function renderCliManager() {
|
|||||||
if (searchInput) searchInput.parentElement.style.display = 'none';
|
if (searchInput) searchInput.parentElement.style.display = 'none';
|
||||||
|
|
||||||
// Load data (including CodexLens status for tools section)
|
// Load data (including CodexLens status for tools section)
|
||||||
|
// loadCliToolsConfig() ensures cli-tools.json is auto-created if missing
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
loadCliToolsConfig(),
|
||||||
loadCliToolStatus(),
|
loadCliToolStatus(),
|
||||||
loadCodexLensStatus(),
|
loadCodexLensStatus(),
|
||||||
loadCcwInstallations(),
|
loadCcwInstallations(),
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* Claude CLI Tools Configuration Manager
|
* Claude CLI Tools Configuration Manager
|
||||||
* Manages .claude/cli-tools.json (tools) and .claude/cli-settings.json (settings)
|
* Manages cli-tools.json (tools) and cli-settings.json (settings)
|
||||||
*
|
*
|
||||||
* Configuration Strategy:
|
* Configuration Strategy (GLOBAL ONLY):
|
||||||
* - READ: Project → Global → Default (fallback chain)
|
* - READ: Global → Default (no project-level configs)
|
||||||
* - CREATE: Always in ~/.claude/ (global user-level config)
|
* - CREATE/SAVE: Always in ~/.claude/ (global user-level config)
|
||||||
* - SAVE: Based on source (project config saves to project, others to global)
|
|
||||||
*
|
*
|
||||||
* Read priority:
|
* Config location: ~/.claude/cli-tools.json
|
||||||
* 1. Project workspace: {projectDir}/.claude/ (if exists)
|
* Settings location: ~/.claude/cli-settings.json
|
||||||
* 2. Global: ~/.claude/ (fallback)
|
*
|
||||||
|
* Note: Project-level configs are NOT used - all config is user-level.
|
||||||
*/
|
*/
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
@@ -177,50 +177,35 @@ function getGlobalSettingsPath(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve config path with fallback:
|
* Resolve config path - GLOBAL ONLY
|
||||||
* 1. Project: {projectDir}/.claude/cli-tools.json
|
* Config is user-level, stored only in ~/.claude/cli-tools.json
|
||||||
* 2. Global: ~/.claude/cli-tools.json
|
* Returns { path, source } where source is 'global' | 'default'
|
||||||
* Returns { path, source } where source is 'project' | 'global' | 'default'
|
|
||||||
*/
|
*/
|
||||||
function resolveConfigPath(projectDir: string): { path: string; source: 'project' | 'global' | 'default' } {
|
function resolveConfigPath(projectDir: string): { path: string; source: 'project' | 'global' | 'default' } {
|
||||||
const projectPath = getProjectConfigPath(projectDir);
|
|
||||||
if (fs.existsSync(projectPath)) {
|
|
||||||
return { path: projectPath, source: 'project' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const globalPath = getGlobalConfigPath();
|
const globalPath = getGlobalConfigPath();
|
||||||
if (fs.existsSync(globalPath)) {
|
if (fs.existsSync(globalPath)) {
|
||||||
return { path: globalPath, source: 'global' };
|
return { path: globalPath, source: 'global' };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { path: projectPath, source: 'default' };
|
// Return global path for default (will be created there)
|
||||||
|
return { path: globalPath, source: 'default' };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve settings path with fallback:
|
* Resolve settings path - GLOBAL ONLY
|
||||||
* 1. Project: {projectDir}/.claude/cli-settings.json
|
* Settings are user-level, stored only in ~/.claude/cli-settings.json
|
||||||
* 2. Global: ~/.claude/cli-settings.json
|
|
||||||
*/
|
*/
|
||||||
function resolveSettingsPath(projectDir: string): { path: string; source: 'project' | 'global' | 'default' } {
|
function resolveSettingsPath(projectDir: string): { path: string; source: 'project' | 'global' | 'default' } {
|
||||||
const projectPath = getProjectSettingsPath(projectDir);
|
|
||||||
if (fs.existsSync(projectPath)) {
|
|
||||||
return { path: projectPath, source: 'project' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const globalPath = getGlobalSettingsPath();
|
const globalPath = getGlobalSettingsPath();
|
||||||
if (fs.existsSync(globalPath)) {
|
if (fs.existsSync(globalPath)) {
|
||||||
return { path: globalPath, source: 'global' };
|
return { path: globalPath, source: 'global' };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { path: projectPath, source: 'default' };
|
// Return global path for default (will be created there)
|
||||||
|
return { path: globalPath, source: 'default' };
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureClaudeDir(projectDir: string): void {
|
// NOTE: ensureClaudeDir removed - config should only be in ~/.claude/, not project directory
|
||||||
const claudeDir = path.join(projectDir, '.claude');
|
|
||||||
if (!fs.existsSync(claudeDir)) {
|
|
||||||
fs.mkdirSync(claudeDir, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========== Main Functions ==========
|
// ========== Main Functions ==========
|
||||||
|
|
||||||
@@ -336,10 +321,8 @@ export function ensureClaudeCliTools(projectDir: string, createInProject: boolea
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load CLI tools configuration with fallback:
|
* Load CLI tools configuration from global ~/.claude/cli-tools.json
|
||||||
* 1. Project: {projectDir}/.claude/cli-tools.json
|
* Falls back to default config if not found.
|
||||||
* 2. Global: ~/.claude/cli-tools.json
|
|
||||||
* 3. Default config
|
|
||||||
*
|
*
|
||||||
* Automatically migrates older config versions to v3.0.0
|
* Automatically migrates older config versions to v3.0.0
|
||||||
*/
|
*/
|
||||||
@@ -398,27 +381,18 @@ export function loadClaudeCliTools(projectDir: string): ClaudeCliToolsConfig & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save CLI tools configuration
|
* Save CLI tools configuration to global ~/.claude/cli-tools.json
|
||||||
* - If config was loaded from project, saves to project
|
* Always saves to global directory (user-level config)
|
||||||
* - Otherwise saves to global ~/.claude/cli-tools.json
|
|
||||||
*/
|
*/
|
||||||
export function saveClaudeCliTools(projectDir: string, config: ClaudeCliToolsConfig & { _source?: string }): void {
|
export function saveClaudeCliTools(projectDir: string, config: ClaudeCliToolsConfig & { _source?: string }): void {
|
||||||
const { _source, ...configToSave } = config;
|
const { _source, ...configToSave } = config;
|
||||||
|
|
||||||
// Determine save location based on source
|
// Always save to global directory
|
||||||
let configPath: string;
|
const globalDir = path.join(os.homedir(), '.claude');
|
||||||
if (_source === 'project') {
|
if (!fs.existsSync(globalDir)) {
|
||||||
// Config was loaded from project, save back to project
|
fs.mkdirSync(globalDir, { recursive: true });
|
||||||
ensureClaudeDir(projectDir);
|
|
||||||
configPath = getProjectConfigPath(projectDir);
|
|
||||||
} else {
|
|
||||||
// Default: save to global directory
|
|
||||||
const globalDir = path.join(os.homedir(), '.claude');
|
|
||||||
if (!fs.existsSync(globalDir)) {
|
|
||||||
fs.mkdirSync(globalDir, { recursive: true });
|
|
||||||
}
|
|
||||||
configPath = getGlobalConfigPath();
|
|
||||||
}
|
}
|
||||||
|
const configPath = getGlobalConfigPath();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(configPath, JSON.stringify(configToSave, null, 2), 'utf-8');
|
fs.writeFileSync(configPath, JSON.stringify(configToSave, null, 2), 'utf-8');
|
||||||
@@ -430,10 +404,8 @@ export function saveClaudeCliTools(projectDir: string, config: ClaudeCliToolsCon
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load CLI settings configuration with fallback:
|
* Load CLI settings configuration from global ~/.claude/cli-settings.json
|
||||||
* 1. Project: {projectDir}/.claude/cli-settings.json
|
* Falls back to default settings if not found.
|
||||||
* 2. Global: ~/.claude/cli-settings.json
|
|
||||||
* 3. Default settings
|
|
||||||
*/
|
*/
|
||||||
export function loadClaudeCliSettings(projectDir: string): ClaudeCliSettingsConfig & { _source?: string } {
|
export function loadClaudeCliSettings(projectDir: string): ClaudeCliSettingsConfig & { _source?: string } {
|
||||||
const resolved = resolveSettingsPath(projectDir);
|
const resolved = resolveSettingsPath(projectDir);
|
||||||
@@ -469,14 +441,19 @@ export function loadClaudeCliSettings(projectDir: string): ClaudeCliSettingsConf
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save CLI settings configuration to project .claude/cli-settings.json
|
* Save CLI settings configuration to global ~/.claude/cli-settings.json
|
||||||
|
* Always saves to global directory (user-level config)
|
||||||
*/
|
*/
|
||||||
export function saveClaudeCliSettings(projectDir: string, config: ClaudeCliSettingsConfig & { _source?: string }): void {
|
export function saveClaudeCliSettings(projectDir: string, config: ClaudeCliSettingsConfig & { _source?: string }): void {
|
||||||
ensureClaudeDir(projectDir);
|
|
||||||
const settingsPath = getProjectSettingsPath(projectDir);
|
|
||||||
|
|
||||||
const { _source, ...configToSave } = config;
|
const { _source, ...configToSave } = config;
|
||||||
|
|
||||||
|
// Always save to global directory
|
||||||
|
const globalDir = path.join(os.homedir(), '.claude');
|
||||||
|
if (!fs.existsSync(globalDir)) {
|
||||||
|
fs.mkdirSync(globalDir, { recursive: true });
|
||||||
|
}
|
||||||
|
const settingsPath = getGlobalSettingsPath();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(settingsPath, JSON.stringify(configToSave, null, 2), 'utf-8');
|
fs.writeFileSync(settingsPath, JSON.stringify(configToSave, null, 2), 'utf-8');
|
||||||
console.log(`[claude-cli-tools] Saved settings to: ${settingsPath}`);
|
console.log(`[claude-cli-tools] Saved settings to: ${settingsPath}`);
|
||||||
|
|||||||
@@ -859,9 +859,28 @@ export {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get status of all CLI tools
|
* Get status of all CLI tools
|
||||||
|
* Dynamically reads tools from config file
|
||||||
*/
|
*/
|
||||||
export async function getCliToolsStatus(): Promise<Record<string, ToolAvailability>> {
|
export async function getCliToolsStatus(): Promise<Record<string, ToolAvailability>> {
|
||||||
const tools = ['gemini', 'qwen', 'codex', 'claude', 'opencode'];
|
// Default built-in tools
|
||||||
|
const builtInTools = ['gemini', 'qwen', 'codex', 'claude', 'opencode'];
|
||||||
|
|
||||||
|
// Try to get tools from config
|
||||||
|
let tools = builtInTools;
|
||||||
|
try {
|
||||||
|
// Dynamic import to avoid circular dependencies
|
||||||
|
const { loadClaudeCliTools } = await import('./claude-cli-tools.js');
|
||||||
|
const config = loadClaudeCliTools(configBaseDir);
|
||||||
|
if (config.tools && typeof config.tools === 'object') {
|
||||||
|
// Merge built-in tools with config tools to ensure all are checked
|
||||||
|
const configTools = Object.keys(config.tools);
|
||||||
|
tools = [...new Set([...builtInTools, ...configTools])];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback to built-in tools if config load fails
|
||||||
|
debugLog('cli-executor', `Using built-in tools (config load failed: ${(e as Error).message})`);
|
||||||
|
}
|
||||||
|
|
||||||
const results: Record<string, ToolAvailability> = {};
|
const results: Record<string, ToolAvailability> = {};
|
||||||
|
|
||||||
await Promise.all(tools.map(async (tool) => {
|
await Promise.all(tools.map(async (tool) => {
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ const WINDOWS_METACHARS = /[&|<>()%!"]/g;
|
|||||||
export function escapeWindowsArg(arg: string): string {
|
export function escapeWindowsArg(arg: string): string {
|
||||||
if (arg === '') return '""';
|
if (arg === '') return '""';
|
||||||
|
|
||||||
|
// Normalize newlines to spaces to prevent cmd.exe from
|
||||||
|
// misinterpreting multiline arguments (breaks argument parsing)
|
||||||
|
let sanitizedArg = arg.replace(/\r?\n/g, ' ');
|
||||||
|
|
||||||
// Escape caret first to avoid double-escaping when prefixing other metachars.
|
// Escape caret first to avoid double-escaping when prefixing other metachars.
|
||||||
let escaped = arg.replace(/\^/g, '^^');
|
let escaped = sanitizedArg.replace(/\^/g, '^^');
|
||||||
|
|
||||||
// Escape cmd.exe metacharacters with caret.
|
// Escape cmd.exe metacharacters with caret.
|
||||||
escaped = escaped.replace(WINDOWS_METACHARS, '^$&');
|
escaped = escaped.replace(WINDOWS_METACHARS, '^$&');
|
||||||
|
|||||||
Reference in New Issue
Block a user