spawn/cli/src/index.ts
A 4cec25c6b7
fix: pass spawn name through cmdRun and headless flows (#1674)
cmdRun (spawn <agent> <cloud>) was not collecting or passing the spawn
name, so SPAWN_NAME was never set in the script environment and the
history record lacked a name. cmdRunHeadless had the same gap.

- Add promptSpawnName() call to cmdRun and pass result to execScript
- Wire spawnName through HeadlessOptions to runBashHeadless
- Add --name CLI flag to set SPAWN_NAME from the command line
- Skip interactive name prompt when SPAWN_NAME is already set
- Bump CLI to 0.5.33

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 21:52:52 -08:00

631 lines
24 KiB
TypeScript

#!/usr/bin/env bun
import {
cmdInteractive,
cmdAgentInteractive,
cmdRun,
cmdRunHeadless,
cmdList,
cmdListClear,
cmdLast,
cmdDelete,
cmdMatrix,
cmdAgents,
cmdClouds,
cmdAgentInfo,
cmdCloudInfo,
cmdUpdate,
cmdHelp,
cmdPick,
findClosestKeyByNameOrKey,
resolveAgentKey,
resolveCloudKey,
loadManifestWithSpinner,
isInteractiveTTY,
} from "./commands.js";
import pc from "picocolors";
import pkg from "../package.json" with { type: "json" };
import { checkForUpdates } from "./update-check.js";
import { loadManifest, agentKeys, cloudKeys, getCacheAge } from "./manifest.js";
const VERSION = pkg.version;
function handleError(err: unknown): never {
// Use duck typing instead of instanceof to avoid prototype chain issues
const msg = err && typeof err === "object" && "message" in err ? String(err.message) : String(err);
console.error(pc.red(`Error: ${msg}`));
console.error(`\nRun ${pc.cyan("spawn help")} for usage information.`);
process.exit(1);
}
/** Extract a flag and its value from args, returning [value, remainingArgs] */
function extractFlagValue(
args: string[],
flags: string[],
flagLabel: string,
usageHint: string
): [string | undefined, string[]] {
const idx = args.findIndex(arg => flags.includes(arg));
if (idx === -1) return [undefined, args];
if (!args[idx + 1] || args[idx + 1].startsWith("-")) {
console.error(pc.red(`Error: ${pc.bold(args[idx])} requires a value`));
console.error(`\nUsage: ${pc.cyan(usageHint)}`);
process.exit(1);
}
const value = args[idx + 1];
const remaining = [...args];
remaining.splice(idx, 2);
return [value, remaining];
}
const HELP_FLAGS = ["--help", "-h", "help"];
const KNOWN_FLAGS = new Set([
"--help", "-h",
"--version", "-v", "-V",
"--prompt", "-p", "--prompt-file", "-f",
"--dry-run", "-n",
"--debug",
"--headless",
"--output",
"--name",
"--default",
"-a", "-c", "--agent", "--cloud",
"--clear",
]);
/** Expand --flag=value into --flag value so all flag parsing works uniformly */
export function expandEqualsFlags(args: string[]): string[] {
const result: string[] = [];
for (const arg of args) {
if (arg.startsWith("--") && arg.includes("=")) {
const eqIdx = arg.indexOf("=");
result.push(arg.slice(0, eqIdx), arg.slice(eqIdx + 1));
} else {
result.push(arg);
}
}
return result;
}
/** Check for unknown flags and show an actionable error */
function checkUnknownFlags(args: string[]): void {
for (const arg of args) {
if ((arg.startsWith("--") || (arg.startsWith("-") && arg.length > 1 && !/^-\d/.test(arg))) && !KNOWN_FLAGS.has(arg)) {
console.error(pc.red(`Unknown flag: ${pc.bold(arg)}`));
console.error();
console.error(` Supported flags:`);
console.error(` ${pc.cyan("--prompt, -p")} Provide a prompt for non-interactive execution`);
console.error(` ${pc.cyan("--prompt-file, -f")} Read prompt from a file`);
console.error(` ${pc.cyan("--dry-run, -n")} Preview what would be provisioned`);
console.error(` ${pc.cyan("--debug")} Show all commands being executed`);
console.error(` ${pc.cyan("--headless")} Non-interactive mode (no prompts, no SSH session)`);
console.error(` ${pc.cyan("--output json")} Output structured JSON to stdout`);
console.error(` ${pc.cyan("--name")} Set the spawn/resource name`);
console.error(` ${pc.cyan("--help, -h")} Show help information`);
console.error(` ${pc.cyan("--version, -v")} Show version`);
console.error();
console.error(` For ${pc.cyan("spawn pick")}:`);
console.error(` ${pc.cyan("--default")} Pre-selected value in the picker`);
console.error();
console.error(` For ${pc.cyan("spawn list")}:`);
console.error(` ${pc.cyan("-a, --agent")} Filter history by agent`);
console.error(` ${pc.cyan("-c, --cloud")} Filter history by cloud`);
console.error(` ${pc.cyan("--clear")} Clear all spawn history`);
console.error();
console.error(` Run ${pc.cyan("spawn help")} for full usage information.`);
process.exit(1);
}
}
}
/** Show info for a name that could be an agent or cloud, or show an error with suggestions */
function showUnknownCommandError(name: string, manifest: { agents: Record<string, { name: string }>; clouds: Record<string, { name: string }> }): never {
const agentMatch = findClosestKeyByNameOrKey(name, agentKeys(manifest), (k) => manifest.agents[k].name);
const cloudMatch = findClosestKeyByNameOrKey(name, cloudKeys(manifest), (k) => manifest.clouds[k].name);
console.error(pc.red(`Unknown agent or cloud: ${pc.bold(name)}`));
console.error();
if (agentMatch || cloudMatch) {
const suggestions: string[] = [];
if (agentMatch) suggestions.push(`${pc.cyan(agentMatch)} (agent: ${manifest.agents[agentMatch].name})`);
if (cloudMatch) suggestions.push(`${pc.cyan(cloudMatch)} (cloud: ${manifest.clouds[cloudMatch].name})`);
console.error(` Did you mean ${suggestions.join(" or ")}?`);
}
console.error();
console.error(` Run ${pc.cyan("spawn agents")} to see available agents.`);
console.error(` Run ${pc.cyan("spawn clouds")} to see available clouds.`);
console.error(` Run ${pc.cyan("spawn help")} for usage information.`);
process.exit(1);
}
async function showInfoOrError(name: string): Promise<void> {
const manifest = await loadManifestWithSpinner();
// Direct key match — pass pre-loaded manifest to avoid a redundant spinner
if (manifest.agents[name]) { await cmdAgentInfo(name, manifest); return; }
if (manifest.clouds[name]) { await cmdCloudInfo(name, manifest); return; }
// Try resolving display names and case-insensitive matches
const resolvedAgent = resolveAgentKey(manifest, name);
if (resolvedAgent) { await cmdAgentInfo(resolvedAgent, manifest); return; }
const resolvedCloud = resolveCloudKey(manifest, name);
if (resolvedCloud) { await cmdCloudInfo(resolvedCloud, manifest); return; }
showUnknownCommandError(name, manifest);
}
async function handleDefaultCommand(agent: string, cloud: string | undefined, prompt?: string, dryRun?: boolean, debug?: boolean, headless?: boolean, outputFormat?: string): Promise<void> {
if (cloud && HELP_FLAGS.includes(cloud)) {
await showInfoOrError(agent);
return;
}
if (headless) {
if (!cloud) {
if (outputFormat === "json") {
console.log(JSON.stringify({ status: "error", error_code: "VALIDATION_ERROR", error_message: "--headless requires both <agent> and <cloud>" }));
} else {
console.error(pc.red("Error: --headless requires both <agent> and <cloud>"));
console.error(`\nUsage: ${pc.cyan("spawn <agent> <cloud> --headless --output json")}`);
}
process.exit(3);
}
await cmdRunHeadless(agent, cloud, { prompt, debug, outputFormat, spawnName: process.env.SPAWN_NAME });
return;
}
if (cloud) {
await cmdRun(agent, cloud, prompt, dryRun, debug);
return;
}
if (dryRun) {
console.error(pc.red("Error: --dry-run requires both <agent> and <cloud>"));
console.error(`\nUsage: ${pc.cyan(`spawn <agent> <cloud> --dry-run`)}`);
process.exit(1);
}
if (prompt) {
await suggestCloudsForPrompt(agent);
process.exit(1);
}
// Interactive cloud selection when agent is provided without cloud
if (isInteractiveTTY()) {
await cmdAgentInteractive(agent, prompt, dryRun);
return;
}
await showInfoOrError(agent);
}
/** Show "prompt requires cloud" error and suggest available clouds for the agent */
async function suggestCloudsForPrompt(agent: string): Promise<void> {
console.error(pc.red("Error: --prompt requires both <agent> and <cloud>"));
console.error(`\nUsage: ${pc.cyan(`spawn ${agent} <cloud> --prompt "your prompt here"`)}`);
try {
const manifest = await loadManifest();
const resolvedAgent = resolveAgentKey(manifest, agent);
if (!resolvedAgent) return;
const clouds = cloudKeys(manifest).filter(
(c: string) => manifest.matrix[`${c}/${resolvedAgent}`] === "implemented"
);
if (clouds.length === 0) return;
const agentName = manifest.agents[resolvedAgent].name;
console.error(`\nAvailable clouds for ${pc.bold(agentName)}:`);
for (const c of clouds.slice(0, 5)) {
console.error(` ${pc.cyan(`spawn ${resolvedAgent} ${c} --prompt "..."`)}`);
}
if (clouds.length > 5) {
console.error(` Run ${pc.cyan(`spawn ${resolvedAgent}`)} to see all ${clouds.length} clouds.`);
}
} catch (err) {
// Manifest unavailable — skip cloud suggestions
}
}
/** Print a descriptive error for a failed prompt file read and exit */
function handlePromptFileError(promptFile: string, err: unknown): never {
const code = err && typeof err === "object" && "code" in err ? err.code : "";
if (code === "ENOENT") {
console.error(pc.red(`Prompt file not found: ${pc.bold(promptFile)}`));
console.error(`\nCheck the path and try again.`);
} else if (code === "EACCES") {
console.error(pc.red(`Permission denied reading prompt file: ${pc.bold(promptFile)}`));
console.error(`\nCheck file permissions: ${pc.cyan(`ls -la ${promptFile}`)}`);
} else if (code === "EISDIR") {
console.error(pc.red(`'${promptFile}' is a directory, not a file.`));
console.error(`\nProvide a path to a text file containing your prompt.`);
} else {
const msg = err && typeof err === "object" && "message" in err ? String(err.message) : String(err);
console.error(pc.red(`Error reading prompt file '${promptFile}': ${msg}`));
}
process.exit(1);
}
/** Read and validate a prompt file, exiting on any error */
async function readPromptFile(promptFile: string): Promise<string> {
const { validatePromptFilePath, validatePromptFileStats } = await import("./security.js");
const { readFileSync, statSync } = await import("fs");
try {
validatePromptFilePath(promptFile);
} catch (err) {
const msg = err && typeof err === "object" && "message" in err ? String(err.message) : String(err);
console.error(pc.red(msg));
process.exit(1);
}
try {
const stats = statSync(promptFile);
validatePromptFileStats(promptFile, stats);
} catch (err) {
const code = err && typeof err === "object" && "code" in err ? err.code : "";
if (code === "ENOENT" || code === "EACCES" || code === "EISDIR") {
handlePromptFileError(promptFile, err);
}
const msg = err && typeof err === "object" && "message" in err ? String(err.message) : String(err);
console.error(pc.red(msg));
process.exit(1);
}
try {
return readFileSync(promptFile, "utf-8");
} catch (err) {
handlePromptFileError(promptFile, err);
}
}
/** Parse --prompt / -p and --prompt-file flags, returning the resolved prompt text and remaining args */
async function resolvePrompt(args: string[]): Promise<[string | undefined, string[]]> {
let [prompt, filteredArgs] = extractFlagValue(
args,
["--prompt", "-p"],
"prompt",
'spawn <agent> <cloud> --prompt "your prompt here"'
);
const [promptFile, finalArgs] = extractFlagValue(
filteredArgs,
["--prompt-file", "-f"],
"prompt file",
"spawn <agent> <cloud> --prompt-file instructions.txt"
);
filteredArgs = finalArgs;
if (prompt && promptFile) {
console.error(pc.red("Error: --prompt and --prompt-file cannot be used together"));
console.error(`\nUse one or the other:`);
console.error(` ${pc.cyan('spawn <agent> <cloud> --prompt "your prompt here"')}`);
console.error(` ${pc.cyan("spawn <agent> <cloud> --prompt-file instructions.txt")}`);
process.exit(1);
}
if (promptFile) {
prompt = await readPromptFile(promptFile);
}
return [prompt, filteredArgs];
}
/** Handle the case when no command is given (interactive mode or help) */
async function handleNoCommand(prompt: string | undefined, dryRun?: boolean): Promise<void> {
if (dryRun) {
console.error(pc.red("Error: --dry-run requires both <agent> and <cloud>"));
console.error(`\nUsage: ${pc.cyan("spawn <agent> <cloud> --dry-run")}`);
process.exit(1);
}
if (prompt) {
console.error(pc.red("Error: --prompt requires both <agent> and <cloud>"));
console.error(`\nUsage: ${pc.cyan('spawn <agent> <cloud> --prompt "your prompt here"')}`);
process.exit(1);
}
if (isInteractiveTTY()) {
await cmdInteractive();
} else {
console.error(pc.yellow("Cannot run interactive picker: not a terminal"));
console.error(pc.dim(" (stdin/stdout is piped or redirected)"));
console.error();
console.error(` Launch directly: ${pc.cyan("spawn <agent> <cloud>")}`);
console.error(` Rerun previous: ${pc.cyan("spawn list")}`);
console.error(` Browse agents: ${pc.cyan("spawn agents")}`);
console.error(` Browse clouds: ${pc.cyan("spawn clouds")}`);
console.error(` Full help: ${pc.cyan("spawn help")}`);
console.error();
process.exit(1);
}
}
function formatCacheAge(seconds: number): string {
if (!isFinite(seconds)) return "no cache";
if (seconds < 60) return "just now";
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
return `${Math.floor(seconds / 86400)}d ago`;
}
function showVersion(): void {
console.log(`spawn v${VERSION}`);
const binPath = process.argv[1];
if (binPath) {
console.log(pc.dim(` ${binPath}`));
}
console.log(pc.dim(` ${process.versions.bun ? "bun" : "node"} ${process.versions.bun ?? process.versions.node} ${process.platform} ${process.arch}`));
const age = getCacheAge();
console.log(pc.dim(` manifest cache: ${formatCacheAge(age)}`));
console.log(pc.dim(` https://github.com/OpenRouterTeam/spawn`));
console.log(pc.dim(` Run ${pc.cyan("spawn update")} to check for updates.`));
}
const IMMEDIATE_COMMANDS: Record<string, () => void> = {
"help": cmdHelp, "--help": cmdHelp, "-h": cmdHelp,
"version": showVersion,
"--version": showVersion,
"-v": showVersion,
"-V": showVersion,
};
const SUBCOMMANDS: Record<string, () => Promise<void>> = {
"matrix": cmdMatrix, "m": cmdMatrix,
"agents": cmdAgents,
"clouds": cmdClouds,
"update": cmdUpdate,
"last": cmdLast, "rerun": cmdLast,
};
// list/ls/history handled separately for -a/-c flag parsing
const LIST_COMMANDS = new Set(["list", "ls", "history"]);
// delete/rm/destroy handled separately for -a/-c flag parsing
const DELETE_COMMANDS = new Set(["delete", "rm", "destroy"]);
// Common verb prefixes that users naturally try (e.g. "spawn run claude sprite")
// These are not real subcommands -- we strip them and forward to the default handler
const VERB_ALIASES = new Set(["run", "launch", "start", "deploy", "exec"]);
/** Warn when extra positional arguments are silently ignored */
function warnExtraArgs(filteredArgs: string[], maxExpected: number): void {
const extra = filteredArgs.slice(maxExpected);
if (extra.length > 0) {
console.error(pc.yellow(`Extra argument${extra.length > 1 ? "s" : ""} ignored: ${extra.join(", ")}`));
console.error(pc.dim(` Usage: spawn <agent> <cloud> [--prompt "..."]`));
console.error();
}
}
/** Parse -a/--agent <agent> and -c/--cloud <cloud> filter flags from args.
* Also accepts a bare positional arg as a filter (e.g. "spawn list claude"). */
function parseListFilters(args: string[]): { agentFilter?: string; cloudFilter?: string } {
let agentFilter: string | undefined;
let cloudFilter: string | undefined;
const positional: string[] = [];
for (let i = 0; i < args.length; i++) {
if (args[i] === "-a" || args[i] === "--agent") {
if (!args[i + 1] || args[i + 1].startsWith("-")) {
console.error(pc.red(`Error: ${pc.bold(args[i])} requires an agent name`));
console.error(`\nUsage: ${pc.cyan("spawn list -a <agent>")}`);
process.exit(1);
}
agentFilter = args[i + 1];
i++;
} else if (args[i] === "-c" || args[i] === "--cloud") {
if (!args[i + 1] || args[i + 1].startsWith("-")) {
console.error(pc.red(`Error: ${pc.bold(args[i])} requires a cloud name`));
console.error(`\nUsage: ${pc.cyan("spawn list -c <cloud>")}`);
process.exit(1);
}
cloudFilter = args[i + 1];
i++;
} else if (!args[i].startsWith("-")) {
positional.push(args[i]);
}
}
// Support bare positional filter: "spawn list claude" or "spawn list hetzner"
if (!agentFilter && !cloudFilter && positional.length > 0) {
agentFilter = positional[0];
}
return { agentFilter, cloudFilter };
}
/** Check if trailing args contain a help flag */
function hasTrailingHelpFlag(args: string[]): boolean {
return args.slice(1).some(a => HELP_FLAGS.includes(a));
}
/** Handle list/ls/history commands with filters and --clear */
async function dispatchListCommand(filteredArgs: string[]): Promise<void> {
if (hasTrailingHelpFlag(filteredArgs)) { cmdHelp(); return; }
if (filteredArgs.slice(1).includes("--clear")) {
await cmdListClear();
return;
}
const { agentFilter, cloudFilter } = parseListFilters(filteredArgs.slice(1));
await cmdList(agentFilter, cloudFilter);
}
/** Handle delete/rm/destroy commands with filters */
async function dispatchDeleteCommand(filteredArgs: string[]): Promise<void> {
if (hasTrailingHelpFlag(filteredArgs)) { cmdHelp(); return; }
const { agentFilter, cloudFilter } = parseListFilters(filteredArgs.slice(1));
await cmdDelete(agentFilter, cloudFilter);
}
/** Handle named subcommands (agents, clouds, matrix, etc.) */
async function dispatchSubcommand(cmd: string, filteredArgs: string[]): Promise<void> {
if (hasTrailingHelpFlag(filteredArgs)) { cmdHelp(); return; }
// "spawn agents <name>" or "spawn clouds <name>" -> show info for that name
if ((cmd === "agents" || cmd === "clouds") && filteredArgs.length > 1 && !filteredArgs[1].startsWith("-")) {
const name = filteredArgs[1];
warnExtraArgs(filteredArgs, 2);
console.error(pc.dim(`Tip: next time you can just run ${pc.cyan(`spawn ${name}`)}`));
console.error();
await showInfoOrError(name);
return;
}
warnExtraArgs(filteredArgs, 1);
await SUBCOMMANDS[cmd]();
}
/** Handle verb aliases like "spawn run claude sprite" -> "spawn claude sprite" */
async function dispatchVerbAlias(cmd: string, filteredArgs: string[], prompt: string | undefined, dryRun: boolean, debug: boolean, headless: boolean, outputFormat?: string): Promise<void> {
if (filteredArgs.length > 1) {
const remaining = filteredArgs.slice(1);
warnExtraArgs(remaining, 2);
await handleDefaultCommand(remaining[0], remaining[1], prompt, dryRun, debug, headless, outputFormat);
return;
}
console.error(pc.red(`Error: ${pc.bold(cmd)} requires an agent and cloud`));
console.error(`\nUsage: ${pc.cyan("spawn <agent> <cloud>")}`);
console.error(pc.dim(` The "${cmd}" keyword is optional -- just use ${pc.cyan("spawn <agent> <cloud>")} directly.`));
process.exit(1);
}
/** Handle slash notation: "spawn claude/hetzner" -> "spawn claude hetzner" */
async function dispatchSlashNotation(cmd: string, prompt: string | undefined, dryRun: boolean, debug: boolean, headless: boolean, outputFormat?: string): Promise<boolean> {
const parts = cmd.split("/");
if (parts.length === 2 && parts[0] && parts[1]) {
if (!headless) {
console.error(pc.dim(`Tip: use a space instead of slash: ${pc.cyan(`spawn ${parts[0]} ${parts[1]}`)}`));
console.error();
}
await handleDefaultCommand(parts[0], parts[1], prompt, dryRun, debug, headless, outputFormat);
return true;
}
return false;
}
/** Dispatch a named command or fall through to agent/cloud handling */
async function dispatchCommand(cmd: string, filteredArgs: string[], prompt: string | undefined, dryRun: boolean, debug: boolean, headless: boolean, outputFormat?: string): Promise<void> {
if (IMMEDIATE_COMMANDS[cmd]) {
warnExtraArgs(filteredArgs, 1);
IMMEDIATE_COMMANDS[cmd]();
return;
}
if (LIST_COMMANDS.has(cmd)) { await dispatchListCommand(filteredArgs); return; }
if (DELETE_COMMANDS.has(cmd)) { await dispatchDeleteCommand(filteredArgs); return; }
if (SUBCOMMANDS[cmd]) { await dispatchSubcommand(cmd, filteredArgs); return; }
if (VERB_ALIASES.has(cmd)) { await dispatchVerbAlias(cmd, filteredArgs, prompt, dryRun, debug, headless, outputFormat); return; }
if (filteredArgs.length === 1 && cmd.includes("/")) {
if (await dispatchSlashNotation(cmd, prompt, dryRun, debug, headless, outputFormat)) return;
}
warnExtraArgs(filteredArgs, 2);
await handleDefaultCommand(filteredArgs[0], filteredArgs[1], prompt, dryRun, debug, headless, outputFormat);
}
async function main(): Promise<void> {
const rawArgs = process.argv.slice(2);
// ── `spawn pick` — bypass all flag parsing; used by bash scripts ──────────
// Must be handled before expandEqualsFlags / resolvePrompt so that pick's
// own --prompt flag is not mistakenly consumed by the top-level prompt logic.
if (rawArgs[0] === "pick") {
try {
await cmdPick(expandEqualsFlags(rawArgs.slice(1)));
} catch (err) {
handleError(err);
}
return;
}
const args = expandEqualsFlags(rawArgs);
await checkForUpdates();
const [prompt, filteredArgs] = await resolvePrompt(args);
// Extract --dry-run / -n boolean flag
const dryRunIdx = filteredArgs.findIndex(a => a === "--dry-run" || a === "-n");
const dryRun = dryRunIdx !== -1;
if (dryRun) filteredArgs.splice(dryRunIdx, 1);
// Extract --debug boolean flag
const debugIdx = filteredArgs.findIndex(a => a === "--debug");
const debug = debugIdx !== -1;
if (debug) filteredArgs.splice(debugIdx, 1);
// Extract --headless boolean flag
const headlessIdx = filteredArgs.findIndex(a => a === "--headless");
const headless = headlessIdx !== -1;
if (headless) filteredArgs.splice(headlessIdx, 1);
// Extract --output <format> flag
let [outputFormat, outputFilteredArgs] = extractFlagValue(
filteredArgs,
["--output"],
"output format",
"spawn <agent> <cloud> --headless --output json"
);
// Replace filteredArgs contents in-place (splice + push to maintain reference)
filteredArgs.splice(0, filteredArgs.length, ...outputFilteredArgs);
// Validate --output value
if (outputFormat && outputFormat !== "json") {
console.error(pc.red(`Error: --output only supports "json" (got "${outputFormat}")`));
console.error(`\nUsage: ${pc.cyan("spawn <agent> <cloud> --headless --output json")}`);
process.exit(1);
}
// Extract --name <value> flag
const [nameFlag, nameFilteredArgs] = extractFlagValue(
filteredArgs,
["--name"],
"spawn name",
'spawn <agent> <cloud> --name "my-dev-box"'
);
filteredArgs.splice(0, filteredArgs.length, ...nameFilteredArgs);
if (nameFlag) {
process.env.SPAWN_NAME = nameFlag;
}
// --output implies --headless
const effectiveHeadless = headless || !!outputFormat;
// Validate headless-incompatible flags
if (effectiveHeadless && dryRun) {
if (outputFormat === "json") {
console.log(JSON.stringify({ status: "error", error_code: "VALIDATION_ERROR", error_message: "--headless and --dry-run cannot be used together" }));
} else {
console.error(pc.red("Error: --headless and --dry-run cannot be used together"));
console.error(`\nUse ${pc.cyan("--dry-run")} for previewing, or ${pc.cyan("--headless")} for execution.`);
}
process.exit(3);
}
checkUnknownFlags(filteredArgs);
const cmd = filteredArgs[0];
try {
if (!cmd) {
if (effectiveHeadless) {
if (outputFormat === "json") {
console.log(JSON.stringify({ status: "error", error_code: "VALIDATION_ERROR", error_message: "--headless requires both <agent> and <cloud>" }));
} else {
console.error(pc.red("Error: --headless requires both <agent> and <cloud>"));
console.error(`\nUsage: ${pc.cyan("spawn <agent> <cloud> --headless --output json")}`);
}
process.exit(3);
}
await handleNoCommand(prompt, dryRun);
} else {
await dispatchCommand(cmd, filteredArgs, prompt, dryRun, debug, effectiveHeadless, outputFormat);
}
} catch (err) {
if (effectiveHeadless && outputFormat === "json") {
const msg = err && typeof err === "object" && "message" in err ? String(err.message) : String(err);
console.log(JSON.stringify({ status: "error", error_code: "UNEXPECTED_ERROR", error_message: msg }));
process.exit(1);
}
handleError(err);
}
}
main();