From 275d2cb0af33b7da0410469e9334ccb34ec238a4 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Tue, 13 Jan 2026 21:31:46 +0800 Subject: [PATCH] feat: Add environment file support for CLI tools - Introduced a new input group for environment file configuration in the dashboard CSS. - Updated hook manager to queue CLAUDE.md updates with configurable threshold and timeout. - Enhanced CLI manager view to include environment file input for built-in tools (gemini, qwen). - Implemented environment file loading mechanism in cli-executor-core, allowing custom environment variables. - Added unit tests for environment file parsing and loading functionalities. - Updated memory update queue to support dynamic configuration of threshold and timeout settings. --- .../dashboard-css/21-cli-toolmgmt.css | 45 +++ .../dashboard-js/components/hook-manager.js | 117 +++----- ccw/src/templates/dashboard-js/i18n.js | 14 + .../dashboard-js/views/cli-manager.js | 74 ++++- ccw/src/tools/claude-cli-tools.ts | 18 +- ccw/src/tools/cli-executor-core.ts | 92 ++++++- ccw/src/tools/memory-update-queue.js | 126 +++++++-- ccw/tests/cli-env-file.test.ts | 258 ++++++++++++++++++ 8 files changed, 639 insertions(+), 105 deletions(-) create mode 100644 ccw/tests/cli-env-file.test.ts diff --git a/ccw/src/templates/dashboard-css/21-cli-toolmgmt.css b/ccw/src/templates/dashboard-css/21-cli-toolmgmt.css index 8b3ab2d0..c1dbbe30 100644 --- a/ccw/src/templates/dashboard-css/21-cli-toolmgmt.css +++ b/ccw/src/templates/dashboard-css/21-cli-toolmgmt.css @@ -304,6 +304,51 @@ margin-top: 0; } +/* Environment File Input Group */ +.env-file-input-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.env-file-input-row { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.env-file-input-row .tool-config-input { + flex: 1; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 0.8125rem; + margin-top: 0; +} + +.env-file-input-row .btn-sm { + flex-shrink: 0; + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.5rem 0.75rem; + font-size: 0.8125rem; + white-space: nowrap; +} + +.env-file-hint { + display: flex; + align-items: center; + gap: 0.375rem; + font-size: 0.75rem; + color: hsl(var(--muted-foreground)); + margin: 0; + padding: 0; +} + +.env-file-hint i { + flex-shrink: 0; + opacity: 0.7; +} + .btn-ghost.text-destructive:hover { background: hsl(var(--destructive) / 0.1); } diff --git a/ccw/src/templates/dashboard-js/components/hook-manager.js b/ccw/src/templates/dashboard-js/components/hook-manager.js index bdde2f1f..be2a2cbb 100644 --- a/ccw/src/templates/dashboard-js/components/hook-manager.js +++ b/ccw/src/templates/dashboard-js/components/hook-manager.js @@ -49,43 +49,17 @@ const HOOK_TEMPLATES = { description: 'Auto-update code index when files are written or edited', category: 'indexing' }, - 'memory-update-related': { + 'memory-update-queue': { event: 'Stop', matcher: '', command: 'bash', - args: ['-c', 'ccw tool exec memory_queue "{\\"action\\":\\"add\\",\\"path\\":\\"$CLAUDE_PROJECT_DIR\\",\\"tool\\":\\"gemini\\",\\"strategy\\":\\"single-layer\\"}"'], - description: 'Queue CLAUDE.md update for changed modules when session ends', + args: ['-c', 'ccw tool exec memory_queue "{\\"action\\":\\"add\\",\\"path\\":\\"$CLAUDE_PROJECT_DIR\\"}"'], + description: 'Queue CLAUDE.md update when session ends (batched by threshold/timeout)', category: 'memory', configurable: true, config: { - tool: { type: 'select', options: ['gemini', 'qwen', 'codex'], default: 'gemini', label: 'CLI Tool' }, - strategy: { type: 'select', options: ['single-layer', 'multi-layer'], default: 'single-layer', label: 'Strategy' } - } - }, - 'memory-update-periodic': { - event: 'PostToolUse', - matcher: 'Write|Edit', - command: 'bash', - args: ['-c', 'ccw tool exec memory_queue "{\\"action\\":\\"add\\",\\"path\\":\\"$CLAUDE_PROJECT_DIR\\",\\"tool\\":\\"gemini\\",\\"strategy\\":\\"single-layer\\"}"'], - description: 'Queue CLAUDE.md update on file changes (batched with threshold/timeout)', - category: 'memory', - configurable: true, - config: { - tool: { type: 'select', options: ['gemini', 'qwen', 'codex'], default: 'gemini', label: 'CLI Tool' }, - strategy: { type: 'select', options: ['single-layer', 'multi-layer'], default: 'single-layer', label: 'Strategy' } - } - }, - 'memory-update-count-based': { - event: 'PostToolUse', - matcher: 'Write|Edit', - command: 'bash', - args: ['-c', 'ccw tool exec memory_queue "{\\"action\\":\\"add\\",\\"path\\":\\"$CLAUDE_PROJECT_DIR\\",\\"tool\\":\\"gemini\\",\\"strategy\\":\\"single-layer\\"}"'], - description: 'Queue CLAUDE.md update on file changes (auto-flush at 5 paths or 5min timeout)', - category: 'memory', - configurable: true, - config: { - tool: { type: 'select', options: ['gemini', 'qwen', 'codex'], default: 'gemini', label: 'CLI Tool' }, - strategy: { type: 'select', options: ['single-layer', 'multi-layer'], default: 'single-layer', label: 'Strategy' } + threshold: { type: 'number', default: 5, min: 1, max: 20, label: 'Threshold (paths)', step: 1 }, + timeout: { type: 'number', default: 300, min: 60, max: 1800, label: 'Timeout (seconds)', step: 60 } } }, // SKILL Context Loader templates @@ -210,33 +184,19 @@ const HOOK_TEMPLATES = { const WIZARD_TEMPLATES = { 'memory-update': { name: 'Memory Update Hook', - description: 'Automatically update CLAUDE.md documentation based on code changes', + description: 'Queue-based CLAUDE.md updates with configurable threshold and timeout', icon: 'brain', options: [ { - id: 'on-stop', - name: 'On Session End', - description: 'Update documentation when Claude session ends', - templateId: 'memory-update-related' - }, - { - id: 'periodic', - name: 'Periodic Update', - description: 'Update documentation at regular intervals during session', - templateId: 'memory-update-periodic' - }, - { - id: 'count-based', - name: 'Count-Based Update', - description: 'Update documentation when file changes reach threshold', - templateId: 'memory-update-count-based' + id: 'queue', + name: 'Queue-Based Update', + description: 'Batch updates when threshold reached or timeout expires', + templateId: 'memory-update-queue' } ], configFields: [ - { key: 'tool', type: 'select', label: 'CLI Tool', options: ['gemini', 'qwen', 'codex'], default: 'gemini', description: 'Tool for documentation generation' }, - { key: 'interval', type: 'number', label: 'Interval (seconds)', default: 300, min: 60, max: 3600, step: 60, showFor: ['periodic'], description: 'Time between updates' }, - { key: 'threshold', type: 'number', label: 'File Count Threshold', default: 10, min: 3, max: 50, step: 1, showFor: ['count-based'], description: 'Number of file changes to trigger update' }, - { key: 'strategy', type: 'select', label: 'Update Strategy', options: ['related', 'single-layer'], default: 'related', description: 'Related: changed modules, Single-layer: current directory' } + { key: 'threshold', type: 'number', label: 'Threshold (paths)', default: 5, min: 1, max: 20, step: 1, description: 'Number of paths to trigger batch update' }, + { key: 'timeout', type: 'number', label: 'Timeout (seconds)', default: 300, min: 60, max: 1800, step: 60, description: 'Auto-flush queue after this time' } ] }, 'skill-context': { @@ -730,9 +690,7 @@ function renderWizardModalContent() { // Helper to get translated option names const getOptionName = (optId) => { if (wizardId === 'memory-update') { - if (optId === 'on-stop') return t('hook.wizard.onSessionEnd'); - if (optId === 'periodic') return t('hook.wizard.periodicUpdate'); - if (optId === 'count-based') return t('hook.wizard.countBasedUpdate'); + if (optId === 'queue') return t('hook.wizard.queueBasedUpdate') || 'Queue-Based Update'; } if (wizardId === 'memory-setup') { if (optId === 'file-read') return t('hook.wizard.fileReadTracker'); @@ -748,9 +706,7 @@ function renderWizardModalContent() { const getOptionDesc = (optId) => { if (wizardId === 'memory-update') { - if (optId === 'on-stop') return t('hook.wizard.onSessionEndDesc'); - if (optId === 'periodic') return t('hook.wizard.periodicUpdateDesc'); - if (optId === 'count-based') return t('hook.wizard.countBasedUpdateDesc'); + if (optId === 'queue') return t('hook.wizard.queueBasedUpdateDesc') || 'Batch updates when threshold reached or timeout expires'; } if (wizardId === 'memory-setup') { if (optId === 'file-read') return t('hook.wizard.fileReadTrackerDesc'); @@ -767,20 +723,16 @@ function renderWizardModalContent() { // Helper to get translated field labels const getFieldLabel = (fieldKey) => { const labels = { - 'tool': t('hook.wizard.cliTool'), - 'interval': t('hook.wizard.intervalSeconds'), - 'threshold': t('hook.wizard.fileCountThreshold'), - 'strategy': t('hook.wizard.updateStrategy') + 'threshold': t('hook.wizard.thresholdPaths') || 'Threshold (paths)', + 'timeout': t('hook.wizard.timeoutSeconds') || 'Timeout (seconds)' }; return labels[fieldKey] || wizard.configFields.find(f => f.key === fieldKey)?.label || fieldKey; }; const getFieldDesc = (fieldKey) => { const descs = { - 'tool': t('hook.wizard.toolForDocGen'), - 'interval': t('hook.wizard.timeBetweenUpdates'), - 'threshold': t('hook.wizard.fileCountThresholdDesc'), - 'strategy': t('hook.wizard.relatedStrategy') + 'threshold': t('hook.wizard.thresholdPathsDesc') || 'Number of paths to trigger batch update', + 'timeout': t('hook.wizard.timeoutSecondsDesc') || 'Auto-flush queue after this time' }; return descs[fieldKey] || wizard.configFields.find(f => f.key === fieldKey)?.description || ''; }; @@ -1154,16 +1106,9 @@ function generateWizardCommand() { } // Handle memory-update wizard (default) - // Now uses memory_queue for batched updates - const tool = wizardConfig.tool || 'gemini'; - const strategy = wizardConfig.strategy || 'single-layer'; - - // Build the ccw tool command using memory_queue - // Use double quotes to allow bash $CLAUDE_PROJECT_DIR expansion - const params = `"{\\"action\\":\\"add\\",\\"path\\":\\"$CLAUDE_PROJECT_DIR\\",\\"tool\\":\\"${tool}\\",\\"strategy\\":\\"${strategy}\\"}"`; - - // All trigger types now use the same queue-based command - // The queue handles batching (threshold: 5 paths, timeout: 5 min) + // Now uses memory_queue for batched updates with configurable threshold/timeout + // The command adds to queue, configuration is applied separately via submitHookWizard + const params = `"{\\"action\\":\\"add\\",\\"path\\":\\"$CLAUDE_PROJECT_DIR\\"}"`; return `ccw tool exec memory_queue ${params}`; } @@ -1259,6 +1204,26 @@ async function submitHookWizard() { } await saveHook(scope, baseTemplate.event, hookData); + + // For memory-update wizard, also configure queue settings + if (wizard.id === 'memory-update') { + const threshold = wizardConfig.threshold || 5; + const timeout = wizardConfig.timeout || 300; + try { + const configParams = JSON.stringify({ action: 'configure', threshold, timeout }); + const response = await fetch('/api/tools/execute', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tool: 'memory_queue', params: configParams }) + }); + if (response.ok) { + showRefreshToast(`Queue configured: threshold=${threshold}, timeout=${timeout}s`, 'success'); + } + } catch (e) { + console.warn('Failed to configure memory queue:', e); + } + } + closeHookWizardModal(); } diff --git a/ccw/src/templates/dashboard-js/i18n.js b/ccw/src/templates/dashboard-js/i18n.js index f10e2aea..6b81c907 100644 --- a/ccw/src/templates/dashboard-js/i18n.js +++ b/ccw/src/templates/dashboard-js/i18n.js @@ -261,6 +261,13 @@ const i18n = { 'cli.wrapper': 'Wrapper', 'cli.customClaudeSettings': 'Custom Claude CLI settings', 'cli.updateFailed': 'Failed to update', + + // CLI Tool Config - Environment File + 'cli.envFile': 'Environment File', + 'cli.envFileOptional': '(optional)', + 'cli.envFilePlaceholder': 'Path to .env file (e.g., ~/.gemini-env or C:/Users/xxx/.env)', + 'cli.envFileHint': 'Load environment variables (e.g., API keys) before CLI execution. Supports ~ for home directory.', + 'cli.envFileBrowse': 'Browse', // CodexLens Configuration 'codexlens.config': 'CodexLens Configuration', @@ -2421,6 +2428,13 @@ const i18n = { 'cli.wrapper': '封装', 'cli.customClaudeSettings': '自定义 Claude CLI 配置', 'cli.updateFailed': '更新失败', + + // CLI 工具配置 - 环境文件 + 'cli.envFile': '环境文件', + 'cli.envFileOptional': '(可选)', + 'cli.envFilePlaceholder': '.env 文件路径(如 ~/.gemini-env 或 C:/Users/xxx/.env)', + 'cli.envFileHint': '在 CLI 执行前加载环境变量(如 API 密钥)。支持 ~ 表示用户目录。', + 'cli.envFileBrowse': '浏览', // CodexLens 配置 'codexlens.config': 'CodexLens 配置', diff --git a/ccw/src/templates/dashboard-js/views/cli-manager.js b/ccw/src/templates/dashboard-js/views/cli-manager.js index 37c31587..2b084237 100644 --- a/ccw/src/templates/dashboard-js/views/cli-manager.js +++ b/ccw/src/templates/dashboard-js/views/cli-manager.js @@ -523,6 +523,27 @@ function buildToolConfigModalContent(tool, config, models, status) { '' + '' + + // Environment File Section (only for builtin tools: gemini, qwen) + (tool === 'gemini' || tool === 'qwen' ? ( + '
' + + '

' + t('cli.envFile') + ' ' + t('cli.envFileOptional') + '

' + + '
' + + '
' + + '' + + '' + + '
' + + '

' + + ' ' + + t('cli.envFileHint') + + '

' + + '
' + + '
' + ) : '') + + // Footer '