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:
catlog22
2026-01-29 09:48:30 +08:00
parent 4d93ffb06c
commit 11638facf7
3 changed files with 68 additions and 5 deletions

View File

@@ -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',