diff --git a/docs/quickstart.md b/docs/quickstart.md index 8de1f3c..bf571ad 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -74,6 +74,19 @@ Deep Code 会在当前目录中启动交互式界面。你可以直接输入任 deepcode -p "总结这个项目" ``` +### 非交互模式(Headless) + +如果你需要在脚本、CI/CD 流水线或 Docker 容器中自动执行任务,可以使用 `--headless` 参数。该模式下 deepcode 不会启动交互界面,执行完 prompt 后自动退出并将结果输出到 stdout: + +```bash +deepcode --headless -p "总结这个项目" + +# 可与管道组合 +deepcode --headless -p "列出所有 API 接口" | grep "auth" +``` + +`--headless` 必须与 `-p` 搭配使用,且在该模式下所有权限操作均自动批准。 + ## 第一次可以这样问 可以先从只读任务开始: diff --git a/docs/quickstart_en.md b/docs/quickstart_en.md index 44f6269..d923504 100644 --- a/docs/quickstart_en.md +++ b/docs/quickstart_en.md @@ -74,6 +74,19 @@ To start with an initial prompt: deepcode -p "Summarize this project" ``` +### Non-Interactive Mode (Headless) + +For scripts, CI/CD pipelines, or Docker containers where you need automatic execution, use the `--headless` flag. In this mode, deepcode skips the interactive TUI, runs the prompt, prints the result to stdout, and exits: + +```bash +deepcode --headless -p "Summarize this project" + +# Pipe with other tools +deepcode --headless -p "List all API endpoints" | grep "auth" +``` + +`--headless` requires `-p`, and all permission operations are auto-approved in this mode. + ## Try These First Start with a read-only task: diff --git a/packages/cli/src/cli-args.ts b/packages/cli/src/cli-args.ts index b86eda4..2977be9 100644 --- a/packages/cli/src/cli-args.ts +++ b/packages/cli/src/cli-args.ts @@ -33,6 +33,8 @@ export interface ParsedCliArgs { version: boolean; /** True when --help / -h was passed */ help: boolean; + /** True when --headless was passed (non-interactive, run-and-exit mode) */ + headless: boolean; } const EPILOG = [ @@ -73,7 +75,7 @@ async function configureYargs(argv?: string[]) { .locale("en") .scriptName("deepcode") .usage( - "Usage: $0 [options] [command]\n\nDeep Code - Launch an interactive CLI, use -p/--prompt for non-interactive mode" + "Usage: $0 [options] [command]\n\nDeep Code - Launch an interactive CLI, use -p/--prompt with --headless for non-interactive mode" ) .command("$0 [query..]", "Launch Deep Code CLI", (yargsInstance: Argv) => yargsInstance @@ -82,6 +84,10 @@ async function configureYargs(argv?: string[]) { type: "string", describe: "Submit a prompt on launch", }) + .option("headless", { + type: "boolean", + describe: "Run in non-interactive (headless) mode. Use with --prompt to execute and exit automatically.", + }) .option("resume", { alias: "r", type: "string", @@ -106,6 +112,10 @@ async function configureYargs(argv?: string[]) { if (argv["prompt"] === "") { return "--prompt / -p requires a non-empty value."; } + // headless mode requires --prompt + if (argv["headless"] && !argv["prompt"]) { + return "--headless mode requires --prompt / -p with a non-empty value."; + } return true; }) ) @@ -156,5 +166,6 @@ export async function parseArguments(argv?: string[]): Promise { resume, version: parsed.version === true, help: parsed.help === true, + headless: parsed.headless === true, }; } diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 80b11f0..362472a 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -3,7 +3,14 @@ import { render } from "ink"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { homedir } from "node:os"; -import { setShellIfWindows, getProjectCode } from "@vegamo/deepcode-core"; +import { + setShellIfWindows, + getProjectCode, + SessionManager, + createOpenAIClient, + resolveCurrentSettings, +} from "@vegamo/deepcode-core"; +import type { SessionMessage, LlmStreamProgress } from "@vegamo/deepcode-core"; import { checkForNpmUpdate, promptForPendingUpdate } from "./common/update-check"; import { AppContainer } from "./ui"; import { parseArguments } from "./cli-args"; @@ -32,6 +39,11 @@ async function main(): Promise { let resumeSessionId = parsed.resume; const projectRoot = process.cwd(); + if (parsed.headless && parsed.prompt) { + await runHeadless(parsed.prompt, projectRoot); + return; + } + if (!process.stdin.isTTY) { writeStderrLine("deepcode requires an interactive terminal (TTY). Re-run from a real terminal session.\n"); process.exit(1); @@ -115,3 +127,65 @@ function configureWindowsShell(): void { process.exit(1); } } + +/** + * Run in headless (non-interactive) mode. + * Creates a SessionManager, submits the prompt, outputs the final response, and exits. + */ +async function runHeadless(promptText: string, projectRoot: string): Promise { + process.stderr.write("[headless] Starting non-interactive mode...\n"); + + const assistantMessages: SessionMessage[] = []; + + const sessionManager = new SessionManager({ + projectRoot, + createOpenAIClient: () => createOpenAIClient(projectRoot), + getResolvedSettings: () => { + const settings = resolveCurrentSettings(projectRoot); + // Headless = no user to ask → auto-approve all permissions + return { + ...settings, + permissions: { + allow: [], + deny: [], + ask: [], + defaultMode: "allowAll" as const, + }, + }; + }, + renderMarkdown: (text: string) => text, + onAssistantMessage: (message: SessionMessage) => { + assistantMessages.push(message); + // Stream content to stdout as it arrives + if (message.content) { + process.stdout.write(message.content as string); + } + }, + onLlmStreamProgress: (progress: LlmStreamProgress) => { + if (progress.phase === "start") { + process.stderr.write("[headless] Model is thinking...\n"); + } else if (progress.phase === "end") { + process.stderr.write("[headless] Response complete.\n"); + } + }, + }); + + try { + // Initialize MCP servers from settings + const settings = resolveCurrentSettings(projectRoot); + await sessionManager.initMcpServers(settings.mcpServers); + + // Submit the prompt + await sessionManager.handleUserPrompt({ text: promptText }); + + process.stdout.write("\n"); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`[headless] Error: ${message}\n`); + process.exit(1); + } finally { + sessionManager.dispose(); + } + + process.exit(0); +}