From 6a6d1885d883d4fe0a23803f91cc3aa71e6b3fbd Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sun, 18 Jan 2026 11:53:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(install):=20=E6=B7=BB=E5=8A=A0=20Git=20Bas?= =?UTF-8?q?h=20=E5=A4=9A=E8=A1=8C=E6=8F=90=E7=A4=BA=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=B9=B6=E5=9C=A8=E5=8D=B8=E8=BD=BD=E6=97=B6?= =?UTF-8?q?=E8=AF=A2=E9=97=AE=E7=A7=BB=E9=99=A4=20refactor(cli):=20?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=B8=8D=E5=86=8D=E4=BD=BF=E7=94=A8=E7=9A=84?= =?UTF-8?q?=20CLI=20=E8=84=9A=E6=9C=AC=E5=92=8C=E6=B5=8B=E8=AF=95=E6=96=87?= =?UTF-8?q?=E4=BB=B6=20fix(cli):=20=E7=A7=BB=E9=99=A4=E5=A4=9A=E8=A1=8C?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E6=8F=90=E7=A4=BA=E7=9A=84=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ccw/bin/ccw-native.js | 32 ---- ccw/bin/ccw-prompt-test.js | 283 ----------------------------- ccw/scripts/simulate-cli-prompt.js | 214 ---------------------- ccw/scripts/test-shell-prompt.js | 159 ---------------- ccw/src/commands/cli.ts | 1 - ccw/src/commands/install.ts | 177 +++++++++++++++++- ccw/src/commands/uninstall.ts | 24 ++- 7 files changed, 198 insertions(+), 692 deletions(-) delete mode 100644 ccw/bin/ccw-native.js delete mode 100644 ccw/bin/ccw-prompt-test.js delete mode 100644 ccw/scripts/simulate-cli-prompt.js delete mode 100644 ccw/scripts/test-shell-prompt.js diff --git a/ccw/bin/ccw-native.js b/ccw/bin/ccw-native.js deleted file mode 100644 index 14ad139e..00000000 --- a/ccw/bin/ccw-native.js +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env node -/** - * CCW Native Wrapper - * - * 替代 npm 生成的 shell wrapper,直接用 Node.js 处理参数传递, - * 避免 Git Bash + Windows 的多行参数问题。 - */ - -import { spawn } from 'child_process'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// 目标脚本路径 -const targetScript = join(__dirname, 'ccw.js'); - -// 直接传递所有参数,不经过 shell -const child = spawn(process.execPath, [targetScript, ...process.argv.slice(2)], { - stdio: 'inherit', - shell: false, // 关键:不使用 shell,避免参数被 shell 解析 -}); - -child.on('close', (code) => { - process.exit(code || 0); -}); - -child.on('error', (err) => { - console.error('Failed to start ccw:', err.message); - process.exit(1); -}); diff --git a/ccw/bin/ccw-prompt-test.js b/ccw/bin/ccw-prompt-test.js deleted file mode 100644 index 59a2b146..00000000 --- a/ccw/bin/ccw-prompt-test.js +++ /dev/null @@ -1,283 +0,0 @@ -#!/usr/bin/env node -/** - * CCW Prompt Test Endpoint - * - * 独立的提示词解析测试工具,用于调试不同格式的提示词传递。 - * - * Usage: - * node ccw/bin/ccw-prompt-test.js -p "prompt" # 测试 -p 参数 - * node ccw/bin/ccw-prompt-test.js -f file.txt # 测试文件读取 - * echo "prompt" | node ccw/bin/ccw-prompt-test.js # 测试 stdin - * node ccw/bin/ccw-prompt-test.js --raw # 原始 argv 输出 - */ - -import { readFileSync, existsSync } from 'fs'; -import { resolve } from 'path'; - -// ANSI colors -const colors = { - reset: '\x1b[0m', - bold: '\x1b[1m', - dim: '\x1b[2m', - red: '\x1b[31m', - green: '\x1b[32m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - magenta: '\x1b[35m', - cyan: '\x1b[36m', - white: '\x1b[37m', - gray: '\x1b[90m', -}; - -const c = (color, text) => `${colors[color]}${text}${colors.reset}`; - -/** - * Parse command line arguments manually (no dependencies) - */ -function parseArgs(argv) { - const args = argv.slice(2); - const result = { - prompt: undefined, - file: undefined, - tool: 'gemini', - mode: 'analysis', - raw: false, - help: false, - positional: [], - }; - - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - - if (arg === '--help' || arg === '-h') { - result.help = true; - } else if (arg === '--raw') { - result.raw = true; - } else if (arg === '-p' || arg === '--prompt') { - result.prompt = args[++i]; - } else if (arg === '-f' || arg === '--file') { - result.file = args[++i]; - } else if (arg === '--tool') { - result.tool = args[++i]; - } else if (arg === '--mode') { - result.mode = args[++i]; - } else if (!arg.startsWith('-')) { - result.positional.push(arg); - } - } - - return result; -} - -/** - * Read from stdin if available (non-TTY) - */ -function readStdin() { - if (process.stdin.isTTY) { - return null; - } - try { - return readFileSync(0, 'utf8').trim(); - } catch { - return null; - } -} - -/** - * Analyze prompt content - */ -function analyzePrompt(prompt) { - if (!prompt) return null; - - const analysis = { - length: prompt.length, - lines: prompt.split('\n').length, - hasNewlines: prompt.includes('\n'), - hasAtPatterns: /@[^\s]+/.test(prompt), - atPatterns: [], - hasBullets: /[•●○■□▪▫]/.test(prompt), - hasMemory: /Memory:/i.test(prompt), - sections: [], - }; - - // Extract @ patterns - const atMatches = prompt.matchAll(/@[^\s|]+/g); - analysis.atPatterns = Array.from(atMatches).map(m => m[0]); - - // Detect sections - const sectionPatterns = ['PURPOSE', 'TASK', 'MODE', 'CONTEXT', 'EXPECTED', 'CONSTRAINTS']; - for (const section of sectionPatterns) { - if (new RegExp(`${section}:`, 'i').test(prompt)) { - analysis.sections.push(section); - } - } - - return analysis; -} - -/** - * Print box - */ -function printBox(title, content, color = 'cyan') { - const width = 70; - const line = '─'.repeat(width); - console.log(c(color, `┌${line}┐`)); - console.log(c(color, '│') + c('bold', ` ${title}`.padEnd(width)) + c(color, '│')); - console.log(c(color, `├${line}┤`)); - - const lines = content.split('\n'); - for (const l of lines) { - const truncated = l.length > width - 2 ? l.substring(0, width - 5) + '...' : l; - console.log(c(color, '│') + ` ${truncated}`.padEnd(width) + c(color, '│')); - } - console.log(c(color, `└${line}┘`)); -} - -/** - * Main - */ -function main() { - const parsed = parseArgs(process.argv); - - // Help - if (parsed.help) { - console.log(` -${c('bold', 'CCW Prompt Test Endpoint')} - -${c('cyan', 'Usage:')} - node ccw/bin/ccw-prompt-test.js -p "prompt" Test -p argument - node ccw/bin/ccw-prompt-test.js -f file.txt Test file input - echo "prompt" | node ccw/bin/ccw-prompt-test.js Test stdin pipe - node ccw/bin/ccw-prompt-test.js --raw Show raw argv only - -${c('cyan', 'Options:')} - -p, --prompt Prompt text - -f, --file Read prompt from file - --tool Tool name (default: gemini) - --mode Mode (default: analysis) - --raw Show only raw process.argv - -h, --help Show this help - -${c('cyan', 'Multi-line prompt methods:')} - ${c('green', '1. Stdin pipe (recommended):')} - echo "PURPOSE: Test - TASK: Step 1" | node ccw/bin/ccw-prompt-test.js - - ${c('green', '2. File input:')} - node ccw/bin/ccw-prompt-test.js -f prompt.txt - - ${c('green', '3. Heredoc:')} - node ccw/bin/ccw-prompt-test.js << 'EOF' - PURPOSE: Test - TASK: Step 1 - EOF -`); - return; - } - - // Raw mode - if (parsed.raw) { - console.log(c('bold', '\nRaw process.argv:')); - process.argv.forEach((arg, i) => { - console.log(` [${i}]: ${JSON.stringify(arg)}`); - }); - return; - } - - console.log(c('bold', '\n═══════════════════════════════════════════════════════════════════════')); - console.log(c('bold', ' CCW PROMPT TEST ENDPOINT')); - console.log(c('bold', '═══════════════════════════════════════════════════════════════════════\n')); - - // 1. Raw argv - console.log(c('yellow', '📦 1. RAW PROCESS.ARGV:')); - console.log(c('gray', ` Total: ${process.argv.length} arguments`)); - process.argv.forEach((arg, i) => { - const display = arg.length > 60 ? arg.substring(0, 57) + '...' : arg; - const hasNewline = arg.includes('\n'); - console.log(c('gray', ` [${i}]: `) + c(hasNewline ? 'green' : 'white', JSON.stringify(display))); - if (hasNewline) { - console.log(c('green', ` ↳ Contains ${arg.split('\n').length} lines (newlines preserved!)`)); - } - }); - console.log(); - - // 2. Parsed options - console.log(c('yellow', '📋 2. PARSED OPTIONS:')); - console.log(c('gray', ' --prompt: ') + (parsed.prompt ? c('green', JSON.stringify(parsed.prompt.substring(0, 50) + (parsed.prompt.length > 50 ? '...' : ''))) : c('dim', '(not set)'))); - console.log(c('gray', ' --file: ') + (parsed.file ? c('cyan', parsed.file) : c('dim', '(not set)'))); - console.log(c('gray', ' --tool: ') + c('white', parsed.tool)); - console.log(c('gray', ' --mode: ') + c('white', parsed.mode)); - console.log(c('gray', ' stdin.isTTY: ') + c(process.stdin.isTTY ? 'yellow' : 'green', String(process.stdin.isTTY))); - console.log(); - - // 3. Resolve final prompt - console.log(c('yellow', '🎯 3. PROMPT RESOLUTION:')); - let finalPrompt = null; - let source = null; - - // Priority: file > stdin > -p > positional - if (parsed.file) { - source = 'file'; - const filePath = resolve(parsed.file); - if (existsSync(filePath)) { - finalPrompt = readFileSync(filePath, 'utf8').trim(); - console.log(c('gray', ' Source: ') + c('magenta', `--file (${filePath})`)); - } else { - console.log(c('red', ` Error: File not found: ${filePath}`)); - } - } else { - const stdinContent = readStdin(); - if (stdinContent) { - source = 'stdin'; - finalPrompt = stdinContent; - console.log(c('gray', ' Source: ') + c('green', 'stdin (piped input)')); - } else if (parsed.prompt) { - source = '-p option'; - finalPrompt = parsed.prompt; - console.log(c('gray', ' Source: ') + c('cyan', '--prompt/-p option')); - } else if (parsed.positional.length > 0) { - source = 'positional'; - finalPrompt = parsed.positional.join(' '); - console.log(c('gray', ' Source: ') + c('yellow', 'positional argument')); - } else { - console.log(c('red', ' No prompt found!')); - } - } - console.log(); - - // 4. Prompt analysis - if (finalPrompt) { - const analysis = analyzePrompt(finalPrompt); - - console.log(c('yellow', '📊 4. PROMPT ANALYSIS:')); - console.log(c('gray', ' Length: ') + c('white', `${analysis.length} chars`)); - console.log(c('gray', ' Lines: ') + c('white', String(analysis.lines))); - console.log(c('gray', ' Has newlines: ') + c(analysis.hasNewlines ? 'green' : 'yellow', analysis.hasNewlines ? '✓ Yes' : '✗ No')); - console.log(c('gray', ' Has @ patterns: ') + c(analysis.hasAtPatterns ? 'green' : 'dim', analysis.hasAtPatterns ? '✓ Yes' : '✗ No')); - if (analysis.atPatterns.length > 0) { - console.log(c('gray', ' @ patterns:')); - analysis.atPatterns.forEach(p => console.log(c('blue', ` • ${p}`))); - } - console.log(c('gray', ' Has bullets: ') + c(analysis.hasBullets ? 'green' : 'dim', analysis.hasBullets ? '✓ Yes' : '✗ No')); - console.log(c('gray', ' Has Memory: ') + c(analysis.hasMemory ? 'green' : 'dim', analysis.hasMemory ? '✓ Yes' : '✗ No')); - if (analysis.sections.length > 0) { - console.log(c('gray', ' Sections: ') + c('cyan', analysis.sections.join(', '))); - } - console.log(); - - // 5. Final prompt content - console.log(c('yellow', '📄 5. FINAL PROMPT CONTENT:')); - printBox(`${source} → ${analysis.length} chars, ${analysis.lines} lines`, finalPrompt, 'green'); - console.log(); - - // 6. Simulated CLI command - console.log(c('yellow', '🚀 6. SIMULATED CLI EXECUTION:')); - console.log(c('gray', ' Would execute:')); - console.log(c('cyan', ` ${parsed.tool} cli --mode ${parsed.mode}`)); - console.log(c('gray', ' With prompt of ') + c('green', `${analysis.length} chars, ${analysis.lines} lines`)); - } - - console.log(c('bold', '\n═══════════════════════════════════════════════════════════════════════\n')); -} - -main(); diff --git a/ccw/scripts/simulate-cli-prompt.js b/ccw/scripts/simulate-cli-prompt.js deleted file mode 100644 index a15aac8a..00000000 --- a/ccw/scripts/simulate-cli-prompt.js +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env node -/** - * CLI Prompt Simulation Script - * - * Simulates different prompt formats and outputs the final content passed to CLI. - * Usage: node ccw/scripts/simulate-cli-prompt.js - */ - -import chalk from 'chalk'; - -// Test cases for different prompt formats -const testCases = [ - { - name: 'Single-line prompt', - input: { - prompt: 'Analyze the authentication module for security issues', - tool: 'gemini', - mode: 'analysis', - }, - }, - { - name: 'Single-line with quotes', - input: { - prompt: 'Fix the error: "Cannot read property \'id\' of undefined"', - tool: 'gemini', - mode: 'analysis', - }, - }, - { - name: 'Multi-line structured prompt', - input: { - prompt: `PURPOSE: Identify security vulnerabilities -TASK: • Scan injection flaws • Check auth bypass -MODE: analysis -CONTEXT: @src/auth/**/* -EXPECTED: Security report`, - tool: 'gemini', - mode: 'analysis', - }, - }, - { - name: '@ patterns with glob wildcards', - input: { - prompt: `CONTEXT: @src/**/*.{ts,tsx} @!node_modules/** @!dist/** -TASK: Analyze TypeScript files`, - tool: 'gemini', - mode: 'analysis', - }, - }, - { - name: '@ patterns with Memory section', - input: { - prompt: `PURPOSE: Security audit -CONTEXT: @src/auth/**/* @src/middleware/auth.ts | Memory: Using bcrypt for passwords, JWT for sessions -EXPECTED: Vulnerability report`, - tool: 'gemini', - mode: 'analysis', - }, - }, - { - name: 'Full template format (from cli-tools-usage.md)', - input: { - prompt: `PURPOSE: Identify OWASP Top 10 vulnerabilities in authentication module to pass security audit; success = all critical/high issues documented with remediation -TASK: • Scan for injection flaws (SQL, command, LDAP) • Check authentication bypass vectors • Evaluate session management • Assess sensitive data exposure -MODE: analysis -CONTEXT: @src/auth/**/* @src/middleware/auth.ts | Memory: Using bcrypt for passwords, JWT for sessions -EXPECTED: Security report with: severity matrix, file:line references, CVE mappings where applicable, remediation code snippets prioritized by risk -CONSTRAINTS: Focus on authentication | Ignore test files`, - tool: 'gemini', - mode: 'analysis', - }, - }, - { - name: 'Special characters and Unicode', - input: { - prompt: `TASK: • 分析代码 • Check → errors • Fix ✗ issues -EXPECTED: Report with ✓ checkmarks`, - tool: 'gemini', - mode: 'analysis', - }, - }, - { - name: 'Code-like content', - input: { - prompt: `Fix: const result = arr.filter(x => x > 0).map(x => x * 2); -Error at line 42: TypeError: Cannot read property 'length' of null`, - tool: 'gemini', - mode: 'write', - }, - }, - { - name: 'Shell-like patterns', - input: { - prompt: `Run: npm run build && npm test | grep "passed" -Expected output: All tests passed`, - tool: 'gemini', - mode: 'analysis', - }, - }, -]; - -/** - * Simulate prompt processing (mirrors cli.ts logic) - */ -function simulatePromptProcessing(input) { - const { prompt, tool, mode, rule = 'universal-rigorous-style' } = input; - - // Step 1: Get base prompt - let finalPrompt = prompt; - - // Step 2: Extract @ patterns from CONTEXT (for cache simulation) - const contextMatch = prompt.match(/CONTEXT:\s*([^\n]+)/i); - let extractedPatterns = []; - if (contextMatch) { - const contextLine = contextMatch[1]; - const patternMatches = contextLine.matchAll(/@[^\s|]+/g); - extractedPatterns = Array.from(patternMatches).map(m => m[0]); - } - - // Step 3: Simulate template concatenation - const mockSystemRules = `[SYSTEM RULES - ${mode} mode protocol loaded]`; - const mockRoles = `[ROLES - ${rule} template loaded]`; - - const parts = [finalPrompt]; - parts.push(`\n=== SYSTEM RULES ===\n${mockSystemRules}`); - parts.push(`\n=== ROLES ===\n${mockRoles}`); - - return { - originalPrompt: prompt, - finalPrompt: parts.join('\n'), - extractedPatterns, - metadata: { - tool, - mode, - rule, - originalLength: prompt.length, - finalLength: parts.join('\n').length, - lineCount: parts.join('\n').split('\n').length, - hasMultiline: prompt.includes('\n'), - hasAtPatterns: extractedPatterns.length > 0, - }, - }; -} - -/** - * Display test result - */ -function displayResult(testCase, result) { - console.log(chalk.bold.cyan('\n' + '═'.repeat(70))); - console.log(chalk.bold.white(`📋 Test: ${testCase.name}`)); - console.log(chalk.cyan('═'.repeat(70))); - - // Input section - console.log(chalk.bold.yellow('\n📥 INPUT:')); - console.log(chalk.gray(' Tool: ') + chalk.green(testCase.input.tool)); - console.log(chalk.gray(' Mode: ') + chalk.green(testCase.input.mode)); - console.log(chalk.gray(' Prompt:')); - console.log(chalk.white(' ┌' + '─'.repeat(66) + '┐')); - testCase.input.prompt.split('\n').forEach(line => { - const truncated = line.length > 64 ? line.substring(0, 61) + '...' : line; - console.log(chalk.white(' │ ') + chalk.cyan(truncated.padEnd(64)) + chalk.white(' │')); - }); - console.log(chalk.white(' └' + '─'.repeat(66) + '┘')); - - // Metadata section - console.log(chalk.bold.yellow('\n📊 METADATA:')); - console.log(chalk.gray(' Original length: ') + chalk.magenta(result.metadata.originalLength + ' chars')); - console.log(chalk.gray(' Final length: ') + chalk.magenta(result.metadata.finalLength + ' chars')); - console.log(chalk.gray(' Line count: ') + chalk.magenta(result.metadata.lineCount)); - console.log(chalk.gray(' Has multiline: ') + (result.metadata.hasMultiline ? chalk.green('✓') : chalk.red('✗'))); - console.log(chalk.gray(' Has @ patterns: ') + (result.metadata.hasAtPatterns ? chalk.green('✓') : chalk.red('✗'))); - - if (result.extractedPatterns.length > 0) { - console.log(chalk.gray(' Extracted patterns:')); - result.extractedPatterns.forEach(p => { - console.log(chalk.gray(' • ') + chalk.blue(p)); - }); - } - - // Final prompt section - console.log(chalk.bold.yellow('\n📤 FINAL PROMPT (passed to CLI):')); - console.log(chalk.white(' ┌' + '─'.repeat(66) + '┐')); - result.finalPrompt.split('\n').slice(0, 15).forEach(line => { - const truncated = line.length > 64 ? line.substring(0, 61) + '...' : line; - console.log(chalk.white(' │ ') + chalk.green(truncated.padEnd(64)) + chalk.white(' │')); - }); - if (result.finalPrompt.split('\n').length > 15) { - console.log(chalk.white(' │ ') + chalk.dim(`... (${result.finalPrompt.split('\n').length - 15} more lines)`.padEnd(64)) + chalk.white(' │')); - } - console.log(chalk.white(' └' + '─'.repeat(66) + '┘')); -} - -/** - * Main execution - */ -function main() { - console.log(chalk.bold.magenta('\n' + '█'.repeat(70))); - console.log(chalk.bold.white(' CLI PROMPT SIMULATION - Testing Different Prompt Formats')); - console.log(chalk.bold.magenta('█'.repeat(70))); - - console.log(chalk.gray('\nThis script simulates how different prompt formats are processed')); - console.log(chalk.gray('and shows the final content passed to the CLI executor.\n')); - - for (const testCase of testCases) { - const result = simulatePromptProcessing(testCase.input); - displayResult(testCase, result); - } - - console.log(chalk.bold.magenta('\n' + '█'.repeat(70))); - console.log(chalk.bold.white(` Completed ${testCases.length} simulations`)); - console.log(chalk.bold.magenta('█'.repeat(70) + '\n')); -} - -main(); diff --git a/ccw/scripts/test-shell-prompt.js b/ccw/scripts/test-shell-prompt.js deleted file mode 100644 index 44854800..00000000 --- a/ccw/scripts/test-shell-prompt.js +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env node -/** - * CLI Shell Prompt Test Script - * - * Tests actual shell execution of different prompt formats. - * Demonstrates correct vs incorrect multi-line prompt handling. - */ - -import { execSync, exec } from 'child_process'; -import { writeFileSync, unlinkSync, mkdtempSync } from 'fs'; -import { join } from 'path'; -import { tmpdir } from 'os'; -import chalk from 'chalk'; - -const tmpDir = mkdtempSync(join(tmpdir(), 'ccw-shell-test-')); - -/** - * Execute ccw cli test-parse and capture output - */ -function testParse(command, description) { - console.log(chalk.bold.cyan(`\n${'─'.repeat(70)}`)); - console.log(chalk.bold.white(`📋 ${description}`)); - console.log(chalk.gray(`Command: ${command.substring(0, 100)}${command.length > 100 ? '...' : ''}`)); - console.log(chalk.cyan('─'.repeat(70))); - - try { - const result = execSync(command, { - encoding: 'utf8', - cwd: process.cwd(), - shell: true, - timeout: 10000, - }); - - // Extract key info from output - const promptMatch = result.match(/Value: "([^"]+)"/); - const sourceMatch = result.match(/Source: ([^\n]+)/); - - if (promptMatch) { - console.log(chalk.green('✓ Parsed prompt: ') + chalk.yellow(promptMatch[1].substring(0, 60))); - } - if (sourceMatch) { - console.log(chalk.gray(' Source: ') + sourceMatch[1]); - } - - // Check if prompt was truncated or split - if (result.includes('Positional Arguments') && result.includes('[0]:')) { - const posMatch = result.match(/\[0\]: "([^"]+)"/); - if (posMatch) { - console.log(chalk.red('⚠ WARNING: Part of prompt leaked to positional args: ') + chalk.yellow(posMatch[1])); - } - } - - return { success: true, output: result }; - } catch (error) { - console.log(chalk.red('✗ Error: ') + error.message); - return { success: false, error }; - } -} - -/** - * Main test suite - */ -function main() { - console.log(chalk.bold.magenta('\n' + '█'.repeat(70))); - console.log(chalk.bold.white(' SHELL PROMPT FORMAT TESTS')); - console.log(chalk.bold.magenta('█'.repeat(70))); - - // ============================================ - // INCORRECT METHODS (will fail or parse wrong) - // ============================================ - console.log(chalk.bold.red('\n\n⛔ INCORRECT METHODS (will fail):')); - - // Method 1: Direct multi-line in double quotes (WRONG) - testParse( - `ccw cli test-parse -p "PURPOSE: Test -TASK: Step 1" --tool gemini`, - '❌ Direct multi-line in double quotes (WRONG)' - ); - - // ============================================ - // CORRECT METHODS - // ============================================ - console.log(chalk.bold.green('\n\n✅ CORRECT METHODS:')); - - // Method 1: Single line (works) - testParse( - `ccw cli test-parse -p "PURPOSE: Test | TASK: Step 1" --tool gemini`, - '✓ Single line with pipe separator' - ); - - // Method 2: File-based (-f option) - RECOMMENDED - const promptFile = join(tmpDir, 'prompt.txt'); - writeFileSync(promptFile, `PURPOSE: Identify vulnerabilities -TASK: • Scan injection flaws • Check auth bypass -MODE: analysis -CONTEXT: @src/auth/**/* -EXPECTED: Security report`); - - testParse( - `ccw cli test-parse -f "${promptFile}" --tool gemini`, - '✓ File-based prompt (-f option) - RECOMMENDED' - ); - - // Method 3: $'...' syntax with literal \n (bash only) - testParse( - `ccw cli test-parse -p $'PURPOSE: Test\\nTASK: Step 1\\nMODE: analysis' --tool gemini`, - "✓ $'...' syntax with \\n (bash only)" - ); - - // Method 4: Heredoc via stdin (if supported) - // Note: ccw cli currently doesn't support stdin, but showing the pattern - console.log(chalk.bold.cyan(`\n${'─'.repeat(70)}`)); - console.log(chalk.bold.white(`📋 ✓ Heredoc pattern (for reference)`)); - console.log(chalk.gray(`Command: cat <<'EOF' > prompt.txt && ccw cli -f prompt.txt ...`)); - console.log(chalk.cyan('─'.repeat(70))); - console.log(chalk.green('✓ Create file with heredoc, then use -f option')); - - // Method 5: Single line with escaped newlines in content - testParse( - `ccw cli test-parse -p "PURPOSE: Test | TASK: Step 1 | MODE: analysis | CONTEXT: @src/**/*" --tool gemini`, - '✓ Single line with | as logical separator' - ); - - // Cleanup - try { - unlinkSync(promptFile); - } catch {} - - // Summary - console.log(chalk.bold.magenta('\n\n' + '█'.repeat(70))); - console.log(chalk.bold.white(' SUMMARY: Recommended Approaches')); - console.log(chalk.bold.magenta('█'.repeat(70))); - - console.log(` -${chalk.bold.green('For multi-line prompts, use ONE of these methods:')} - -${chalk.bold('1. File-based (RECOMMENDED):')} - ${chalk.cyan('ccw cli -f prompt.txt --tool gemini --mode analysis')} - -${chalk.bold("2. $'...' syntax (bash only):")} - ${chalk.cyan("ccw cli -p $'PURPOSE: ...\\nTASK: ...\\nMODE: ...' --tool gemini")} - -${chalk.bold('3. Heredoc + file:')} - ${chalk.cyan(`cat <<'EOF' > /tmp/prompt.txt -PURPOSE: ... -TASK: ... -EOF -ccw cli -f /tmp/prompt.txt --tool gemini`)} - -${chalk.bold('4. Single line with separators:')} - ${chalk.cyan('ccw cli -p "PURPOSE: ... | TASK: ... | MODE: ..." --tool gemini')} - -${chalk.bold.red('AVOID:')} - ${chalk.red('ccw cli -p "PURPOSE: ...')} - ${chalk.red('TASK: ..." # Multi-line in quotes - WILL BREAK')} -`); -} - -main(); diff --git a/ccw/src/commands/cli.ts b/ccw/src/commands/cli.ts index 3ad82870..65c9b4b6 100644 --- a/ccw/src/commands/cli.ts +++ b/ccw/src/commands/cli.ts @@ -454,7 +454,6 @@ function testParseAction(args: string[], options: CliExecOptions): void { } else { args.forEach((arg, i) => { console.log(chalk.gray(` [${i}]: `) + chalk.green(`"${arg}"`)); - // Show if multiline if (arg.includes('\n')) { console.log(chalk.yellow(` ↳ Contains ${arg.split('\n').length} lines`)); } diff --git a/ccw/src/commands/install.ts b/ccw/src/commands/install.ts index 5706cf8a..6b711f8d 100644 --- a/ccw/src/commands/install.ts +++ b/ccw/src/commands/install.ts @@ -1,7 +1,8 @@ -import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync, unlinkSync, rmdirSync } from 'fs'; +import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync, unlinkSync, rmdirSync, appendFileSync } from 'fs'; import { join, dirname, basename } from 'path'; -import { homedir } from 'os'; +import { homedir, platform } from 'os'; import { fileURLToPath } from 'url'; +import { execSync } from 'child_process'; import inquirer from 'inquirer'; import chalk from 'chalk'; import { showHeader, createSpinner, info, warning, error, summaryBox, divider } from '../utils/ui.js'; @@ -9,6 +10,10 @@ import { createManifest, addFileEntry, addDirectoryEntry, saveManifest, findMani import { validatePath } from '../utils/path-resolver.js'; import type { Ora } from 'ora'; +// Git Bash fix markers +const GITBASH_FIX_START = '# >>> ccw gitbash fix'; +const GITBASH_FIX_END = '# <<< ccw gitbash fix'; + const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -272,6 +277,27 @@ export async function installCommand(options: InstallOptions): Promise { borderColor: 'green' }); + // Install Git Bash fix on Windows + if (platform() === 'win32') { + console.log(''); + const { installFix } = await inquirer.prompt([{ + type: 'confirm', + name: 'installFix', + message: 'Install Git Bash multi-line prompt fix? (recommended for Git Bash users)', + default: true + }]); + + if (installFix) { + const fixResult = await installGitBashFix(); + if (fixResult.installed) { + info(`Git Bash fix: ${fixResult.message}`); + info(' Run: source ~/.bashrc (to apply immediately)'); + } else { + warning(`Git Bash fix skipped: ${fixResult.message}`); + } + } + } + // Show next steps console.log(''); info('Next steps:'); @@ -497,6 +523,153 @@ async function copyDirectory( return { files, directories }; } +/** + * Check if running in Git Bash on Windows + */ +function isGitBashOnWindows(): boolean { + if (platform() !== 'win32') return false; + + // Check for MSYSTEM env var (set by Git Bash) + const msystem = process.env.MSYSTEM; + if (msystem && ['MINGW64', 'MINGW32', 'MSYS'].includes(msystem)) { + return true; + } + + // Check for typical Git Bash shell path + const shell = process.env.SHELL || ''; + if (shell.includes('bash') || shell.includes('sh')) { + return true; + } + + return false; +} + +/** + * Get the ccw.js path for Git Bash fix + */ +function getCcwJsPath(): string | null { + try { + const npmPrefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim(); + const ccwJsPath = join(npmPrefix, 'node_modules', 'claude-code-workflow', 'ccw', 'bin', 'ccw.js'); + + if (existsSync(ccwJsPath)) { + return ccwJsPath; + } + return null; + } catch { + return null; + } +} + +/** + * Generate Git Bash fix content for .bashrc + */ +function generateGitBashFix(ccwJsPath: string): string { + // Use Windows path format for node (works in Git Bash) + const windowsPath = ccwJsPath.replace(/\//g, '/'); + + return ` +${GITBASH_FIX_START} +# Fix for multi-line prompt arguments in Git Bash + Windows +# npm's shell wrapper cannot handle multi-line quoted strings correctly +ccw() { + node "${windowsPath}" "$@" +} +${GITBASH_FIX_END} +`; +} + +/** + * Install Git Bash fix to user's shell config + * @returns true if fix was installed, false otherwise + */ +export async function installGitBashFix(): Promise<{ installed: boolean; message: string }> { + // Only applicable on Windows + if (platform() !== 'win32') { + return { installed: false, message: 'Not Windows platform' }; + } + + const ccwJsPath = getCcwJsPath(); + if (!ccwJsPath) { + return { installed: false, message: 'ccw not found in npm global modules' }; + } + + // Find shell config file + const home = homedir(); + const configFiles = [ + join(home, '.bashrc'), + join(home, '.bash_profile'), + join(home, '.profile') + ]; + + let targetConfig: string | null = null; + for (const configFile of configFiles) { + if (existsSync(configFile)) { + targetConfig = configFile; + break; + } + } + + // If no config exists, create .bashrc + if (!targetConfig) { + targetConfig = join(home, '.bashrc'); + } + + // Check if fix already exists + if (existsSync(targetConfig)) { + const content = readFileSync(targetConfig, 'utf8'); + if (content.includes(GITBASH_FIX_START)) { + // Update existing fix + const fixContent = generateGitBashFix(ccwJsPath); + const regex = new RegExp(`${GITBASH_FIX_START}[\\s\\S]*?${GITBASH_FIX_END}`, 'g'); + const newContent = content.replace(regex, fixContent.trim()); + writeFileSync(targetConfig, newContent, 'utf8'); + return { installed: true, message: `Updated in ${basename(targetConfig)}` }; + } + } + + // Append fix to config file + const fixContent = generateGitBashFix(ccwJsPath); + appendFileSync(targetConfig, fixContent, 'utf8'); + + return { installed: true, message: `Added to ${basename(targetConfig)}` }; +} + +/** + * Remove Git Bash fix from user's shell config + * @returns true if fix was removed, false otherwise + */ +export function removeGitBashFix(): { removed: boolean; message: string } { + const home = homedir(); + const configFiles = [ + join(home, '.bashrc'), + join(home, '.bash_profile'), + join(home, '.profile') + ]; + + let removed = false; + let targetFile = ''; + + for (const configFile of configFiles) { + if (!existsSync(configFile)) continue; + + const content = readFileSync(configFile, 'utf8'); + if (content.includes(GITBASH_FIX_START)) { + // Remove the fix block + const regex = new RegExp(`\\n?${GITBASH_FIX_START}[\\s\\S]*?${GITBASH_FIX_END}\\n?`, 'g'); + const newContent = content.replace(regex, '\n'); + writeFileSync(configFile, newContent, 'utf8'); + removed = true; + targetFile = basename(configFile); + } + } + + if (removed) { + return { removed: true, message: `Removed from ${targetFile}` }; + } + return { removed: false, message: 'No fix found to remove' }; +} + /** * Get package version * @returns {string} - Version string diff --git a/ccw/src/commands/uninstall.ts b/ccw/src/commands/uninstall.ts index cd15f10b..2cd0c37d 100644 --- a/ccw/src/commands/uninstall.ts +++ b/ccw/src/commands/uninstall.ts @@ -1,10 +1,11 @@ import { existsSync, unlinkSync, rmdirSync, readdirSync, statSync } from 'fs'; import { join, dirname, basename } from 'path'; -import { homedir } from 'os'; +import { homedir, platform } from 'os'; import inquirer from 'inquirer'; import chalk from 'chalk'; import { showBanner, createSpinner, success, info, warning, error, summaryBox, divider } from '../utils/ui.js'; import { getAllManifests, deleteManifest } from '../core/manifest.js'; +import { removeGitBashFix } from './install.js'; // Global subdirectories that should be protected when Global installation exists const GLOBAL_SUBDIRS = ['workflows', 'scripts', 'templates']; @@ -256,6 +257,27 @@ export async function uninstallCommand(options: UninstallOptions): Promise }); } + // Ask to remove Git Bash fix on Windows if this is the last installation + const remainingManifests = getAllManifests(); + if (platform() === 'win32' && remainingManifests.length === 0) { + console.log(''); + const { removeFix } = await inquirer.prompt([{ + type: 'confirm', + name: 'removeFix', + message: 'Remove Git Bash multi-line prompt fix from shell config?', + default: true + }]); + + if (removeFix) { + const fixResult = removeGitBashFix(); + if (fixResult.removed) { + info(`Git Bash fix: ${fixResult.message}`); + } else { + info(`Git Bash fix: ${fixResult.message}`); + } + } + } + console.log(''); }