feat(install): 添加 Git Bash 多行提示修复功能并在卸载时询问移除

refactor(cli): 删除不再使用的 CLI 脚本和测试文件
fix(cli): 移除多行参数提示的输出
This commit is contained in:
catlog22
2026-01-18 11:53:49 +08:00
parent a34eeb63bf
commit 6a6d1885d8
7 changed files with 198 additions and 692 deletions

View File

@@ -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);
});

View File

@@ -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 <text> Prompt text
-f, --file <path> Read prompt from file
--tool <tool> Tool name (default: gemini)
--mode <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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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`));
}

View File

@@ -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<void> {
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

View File

@@ -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<void>
});
}
// 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('');
}