mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-11 02:33:51 +08:00
feat: Add --to-file option to ccw cli for saving output to files
Adds support for saving CLI execution output directly to files with the following features: - Support for relative paths: --to-file output.txt - Support for nested directories: --to-file results/analysis/output.txt (auto-creates directories) - Support for absolute paths: --to-file /tmp/output.txt or --to-file D:/results/output.txt - Works in both streaming and non-streaming modes - Automatically creates parent directories if they don't exist - Proper error handling with user-friendly messages - Shows file save location in completion feedback Implementation details: - Updated CLI option parser in ccw/src/cli.ts - Added toFile parameter to CliExecOptions interface - Implemented file saving logic in execAction() for both streaming and non-streaming modes - Updated HTTP API endpoint /api/cli/execute to support toFile parameter - All changes are backward compatible Testing: - Tested with relative paths (single and nested directories) - Tested with absolute paths (Windows and Unix style) - Tested with streaming mode - All tests passed successfully
This commit is contained in:
@@ -132,6 +132,8 @@ interface CliExecOptions {
|
||||
title?: string; // Optional title for review summary
|
||||
// Template/Rules options
|
||||
rule?: string; // Template name for auto-discovery (defines $PROTO and $TMPL env vars)
|
||||
// Output options
|
||||
toFile?: string; // Save output to file
|
||||
}
|
||||
|
||||
/** Cache configuration parsed from --cache */
|
||||
@@ -580,7 +582,7 @@ async function statusAction(debug?: boolean): Promise<void> {
|
||||
* @param {Object} options - CLI options
|
||||
*/
|
||||
async function execAction(positionalPrompt: string | undefined, options: CliExecOptions): Promise<void> {
|
||||
const { prompt: optionPrompt, file, tool: userTool, mode = 'analysis', model, cd, includeDirs, stream, resume, id, noNative, cache, injectMode, debug, uncommitted, base, commit, title, rule } = options;
|
||||
const { prompt: optionPrompt, file, tool: userTool, mode = 'analysis', model, cd, includeDirs, stream, resume, id, noNative, cache, injectMode, debug, uncommitted, base, commit, title, rule, toFile } = options;
|
||||
|
||||
// Determine the tool to use: explicit --tool option, or defaultTool from config
|
||||
let tool = userTool;
|
||||
@@ -919,6 +921,9 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
mode
|
||||
});
|
||||
|
||||
// Buffer to accumulate output when both --stream and --to-file are specified
|
||||
let streamBuffer = '';
|
||||
|
||||
// Streaming output handler - broadcasts to dashboard AND writes to stdout
|
||||
const onOutput = (unit: CliOutputUnit) => {
|
||||
// Always broadcast to dashboard for real-time viewing
|
||||
@@ -939,10 +944,14 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
case 'stdout':
|
||||
case 'code':
|
||||
case 'streaming_content': // Show streaming delta content in real-time
|
||||
process.stdout.write(typeof unit.content === 'string' ? unit.content : JSON.stringify(unit.content));
|
||||
const content1 = typeof unit.content === 'string' ? unit.content : JSON.stringify(unit.content);
|
||||
process.stdout.write(content1);
|
||||
if (toFile) streamBuffer += content1;
|
||||
break;
|
||||
case 'stderr':
|
||||
process.stderr.write(typeof unit.content === 'string' ? unit.content : JSON.stringify(unit.content));
|
||||
const content2 = typeof unit.content === 'string' ? unit.content : JSON.stringify(unit.content);
|
||||
process.stderr.write(content2);
|
||||
if (toFile) streamBuffer += content2;
|
||||
break;
|
||||
case 'thought':
|
||||
// Optional: display thinking process with different color
|
||||
@@ -955,7 +964,9 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
default:
|
||||
// Other types: output content if available
|
||||
if (unit.content) {
|
||||
process.stdout.write(typeof unit.content === 'string' ? unit.content : '');
|
||||
const content3 = typeof unit.content === 'string' ? unit.content : '';
|
||||
process.stdout.write(content3);
|
||||
if (toFile) streamBuffer += content3;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1006,12 +1017,43 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
const output = result.parsedOutput || result.stdout;
|
||||
if (output) {
|
||||
console.log(output);
|
||||
|
||||
// Save to file if --to-file is specified
|
||||
if (toFile) {
|
||||
try {
|
||||
const { writeFileSync, mkdirSync } = await import('fs');
|
||||
const { dirname, resolve } = await import('path');
|
||||
const filePath = resolve(cd || process.cwd(), toFile);
|
||||
const dir = dirname(filePath);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(filePath, output, 'utf8');
|
||||
if (debug) {
|
||||
console.log(chalk.gray(` Saved output to: ${filePath}`));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(chalk.red(` Error saving to file: ${(err as Error).message}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print summary with execution ID and turn info
|
||||
console.log();
|
||||
if (result.success) {
|
||||
// Save streaming output to file if needed
|
||||
if (stream && toFile && streamBuffer) {
|
||||
try {
|
||||
const { writeFileSync, mkdirSync } = await import('fs');
|
||||
const { dirname, resolve } = await import('path');
|
||||
const filePath = resolve(cd || process.cwd(), toFile);
|
||||
const dir = dirname(filePath);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(filePath, streamBuffer, 'utf8');
|
||||
} catch (err) {
|
||||
console.error(chalk.red(` Error saving to file: ${(err as Error).message}`));
|
||||
}
|
||||
}
|
||||
|
||||
if (!spinner) {
|
||||
const turnInfo = result.conversation.turn_count > 1
|
||||
? ` (turn ${result.conversation.turn_count})`
|
||||
@@ -1033,6 +1075,11 @@ async function execAction(positionalPrompt: string | undefined, options: CliExec
|
||||
if (!stream) {
|
||||
console.log(chalk.dim(` Output (optional): ccw cli output ${result.execution.id}`));
|
||||
}
|
||||
if (toFile) {
|
||||
const { resolve } = await import('path');
|
||||
const filePath = resolve(cd || process.cwd(), toFile);
|
||||
console.log(chalk.green(` Saved to: ${filePath}`));
|
||||
}
|
||||
|
||||
// Notify dashboard: execution completed (legacy)
|
||||
notifyDashboard({
|
||||
|
||||
Reference in New Issue
Block a user