import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { createRequire } from "node:module"; import { ensureProjectContext } from "../context/project-context.js"; export interface KcodeCliResult { exitCode: number; output: string; } export interface KcodeCliOptions { stdinIsTTY?: boolean; } const packageRoot = dirname(dirname(dirname(fileURLToPath(import.meta.url)))); const require = createRequire(import.meta.url); const packageMetadata = readPackageMetadata(packageRoot); const packageName = packageMetadata.name ?? "kcode-pi"; const packageVersion = packageMetadata.version ?? "unknown"; export interface PiCliCommand { command: string; args: string[]; source: "bundled" | "global"; displayPath: string; } export function runKcodeCli(args: string[], cwd = process.cwd(), options: KcodeCliOptions = {}): KcodeCliResult { // 唯一子命令:显示版本 if (args[0] === "--version" || args[0] === "-v") { const piCli = resolvePiCliCommand(["--version"]); const pi = piCli ? spawnSync(piCli.command, piCli.args, { encoding: "utf8" }) : undefined; const piVersion = pi?.status === 0 ? pi.stdout?.toString().trim() || pi.stderr?.toString().trim() || "可用" : "未找到"; return { exitCode: 0, output: [`${packageName}@${packageVersion}`, `Pi CLI: ${piVersion}`, `Node: ${process.version}`].join("\n"), }; } // 默认:自动启动,并将 arguments 传递给 Pi CLI return autoStart(args, cwd, options); } function autoStart(args: string[], cwd: string, options: KcodeCliOptions): KcodeCliResult { // 1. 自动初始化项目配置(静默) const settingsPath = join(cwd, ".pi", "settings.json"); if (!existsSync(settingsPath)) { mkdirSync(dirname(settingsPath), { recursive: true }); const packages = [normalizePath(packageRoot)]; writeFileSync(settingsPath, `${JSON.stringify({ packages }, null, 2)}\n`, "utf8"); ensureProjectContext(cwd); } // 2. 检查 Node 版本 if (!isSupportedNodeVersion(process.version)) { return { exitCode: 1, output: `错误:需要 Node >=22.19.0,当前 ${process.version}` }; } // 3. 检查 Pi CLI const piArgs = normalizeHeadlessArgs(args, options); const piCli = resolvePiCliCommand(piArgs); if (!piCli) { return { exitCode: 1, output: "错误:未找到 Pi CLI。请运行 npm install -g kcode-pi 重新安装。" }; } // 4. 启动 Pi(继承 stdio) const pi = spawnSync(piCli.command, piCli.args, { cwd, stdio: "inherit", shell: false }); if (pi.error || pi.status === null) { return { exitCode: 1, output: `Pi CLI 启动失败:${pi.error?.message ?? piCli.displayPath}` }; } return { exitCode: pi.status, output: "" }; } export function normalizeHeadlessArgs(args: string[], options: KcodeCliOptions = {}): string[] { if (hasArg(args, "--help", "-h") || hasArg(args, "--version", "-v")) return args; if (hasArg(args, "--print", "-p") || optionValue(args, "--mode") === "rpc") return args; if (optionValue(args, "--mode") === "json") return args; const stdinIsTTY = options.stdinIsTTY ?? process.stdin.isTTY ?? true; if (args.length === 0) return stdinIsTTY ? args : ["--print"]; if (!stdinIsTTY || hasInitialPromptInput(args)) { return ["--print", ...args]; } return args; } export function resolvePiCliCommand(piArgs: string[] = []): PiCliCommand | undefined { const bundledPi = bundledPiCliPath(); if (bundledPi) { return { command: process.execPath, args: [bundledPi, ...piArgs], source: "bundled", displayPath: bundledPi, }; } return { command: "pi", args: piArgs, source: "global", displayPath: "pi", }; } function isSupportedNodeVersion(version: string): boolean { const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version.trim()); if (!match) return false; const major = Number(match[1]); const minor = Number(match[2]); const patch = Number(match[3]); if (major > 22) return true; if (major < 22) return false; if (minor > 19) return true; if (minor < 19) return false; return patch >= 0; } function bundledPiCliPath(): string | undefined { const directPath = join(packageRoot, "node_modules", "@earendil-works", "pi-coding-agent", "dist", "cli.js"); if (existsSync(directPath)) return directPath; try { const mainPath = require.resolve("@earendil-works/pi-coding-agent"); const packageDir = dirname(dirname(mainPath)); const cliPath = join(packageDir, "dist", "cli.js"); return existsSync(cliPath) ? cliPath : undefined; } catch { return undefined; } } function normalizePath(path: string): string { return resolve(path); } function readPackageMetadata(packagePath: string): { name?: string; version?: string } { try { return JSON.parse(readFileSync(join(packagePath, "package.json"), "utf8")) as { name?: string; version?: string }; } catch { return {}; } } function hasArg(args: string[], ...names: string[]): boolean { return args.some((arg) => names.includes(arg)); } function optionValue(args: string[], name: string): string | undefined { for (let index = 0; index < args.length; index++) { const arg = args[index]; if (arg === name) return args[index + 1]; if (arg.startsWith(`${name}=`)) return arg.slice(name.length + 1); } return undefined; } function hasInitialPromptInput(args: string[]): boolean { for (let index = 0; index < args.length; index++) { const arg = args[index]; if (arg.startsWith("@")) return true; if (arg === "--") return args.slice(index + 1).some((item) => item.trim()); if (!arg.startsWith("-")) return true; if (optionConsumesValue(arg)) index++; } return false; } function optionConsumesValue(arg: string): boolean { if (arg.includes("=")) return false; return new Set([ "--provider", "--model", "--api-key", "--system-prompt", "--append-system-prompt", "--name", "-n", "--session", "--session-id", "--fork", "--session-dir", "--models", "--tools", "-t", "--exclude-tools", "-xt", "--thinking", "--extension", "-e", "--skill", "--prompt-template", "--theme", "--export", "--list-models", "--mode", ]).has(arg); }