mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +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:
@@ -209,6 +209,7 @@ export function run(argv: string[]): void {
|
||||
.option('--turn <n>', 'Turn number for cache (default: latest)')
|
||||
.option('--raw', 'Raw output only (no formatting)')
|
||||
.option('--final', 'Output final result only with usage hint')
|
||||
.option('--to-file <path>', 'Save output to file')
|
||||
.action((subcommand, args, options) => cliCommand(subcommand, args, options));
|
||||
|
||||
// Memory command
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -605,7 +605,7 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
// API: Execute CLI Tool
|
||||
if (pathname === '/api/cli/execute' && req.method === 'POST') {
|
||||
handlePostRequest(req, res, async (body) => {
|
||||
const { tool, prompt, mode, format, model, dir, includeDirs, timeout, smartContext, parentExecutionId, category } = body as any;
|
||||
const { tool, prompt, mode, format, model, dir, includeDirs, timeout, smartContext, parentExecutionId, category, toFile } = body as any;
|
||||
|
||||
if (!tool || !prompt) {
|
||||
return { error: 'tool and prompt are required', status: 400 };
|
||||
@@ -696,6 +696,21 @@ export async function handleCliRoutes(ctx: RouteContext): Promise<boolean> {
|
||||
console.log(`[ActiveExec] Direct execution ${executionId} marked as ${activeExec.status}, retained for ${EXECUTION_RETENTION_MS / 1000}s`);
|
||||
}
|
||||
|
||||
// Save output to file if --to-file is specified
|
||||
if (toFile && result.stdout) {
|
||||
try {
|
||||
const { writeFileSync, mkdirSync } = await import('fs');
|
||||
const { dirname, resolve } = await import('path');
|
||||
const filePath = resolve(dir || initialPath, toFile);
|
||||
const dirPath = dirname(filePath);
|
||||
mkdirSync(dirPath, { recursive: true });
|
||||
writeFileSync(filePath, result.stdout, 'utf8');
|
||||
console.log(`[API] Output saved to: ${filePath}`);
|
||||
} catch (err) {
|
||||
console.warn(`[API] Failed to save output to file: ${(err as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast completion
|
||||
broadcastToClients({
|
||||
type: 'CLI_EXECUTION_COMPLETED',
|
||||
|
||||
Reference in New Issue
Block a user