* fix(#3019): query --help reaches handler instead of short-circuiting to top-level usage The query argv parser in sdk/src/cli.ts harvested -h/--help as a global flag and main() short-circuited dispatch when args.help was true. Net effect: every `gsd-sdk query <anything> --help` printed top-level USAGE instead of contextual subcommand help. There was no path for users to discover what arguments a query subcommand accepts — they had to trigger "required" errors by trial and error. Two-layer fix: 1. sdk/src/cli.ts (parseCliArgsQueryPermissive) - Push -h / --help onto queryArgv instead of consuming them silently, so the registered handler / gsd-tools.cjs fallback gets to interpret the flag and render contextual help. - Only honor the global help flag when there is NO real subcommand to dispatch to (i.e. queryArgv contains only help flags). Preserves `gsd-sdk query --help` → top-level USAGE while letting `gsd-sdk query phase add --help` reach the handler. 2. get-shit-done/bin/gsd-tools.cjs - Render top-level usage on --help / -h / -? / --usage instead of erroring with "Unknown flag". The discovery hint in the usage text points users at the working method (run without args → error names required arguments) and references #3019 for tracking subcommand- level help printers. - --version remains rejected (no discovery use-case). #1818 anti-hallucination invariant preserved: the destructive command NEVER executes when --help is present. The new shape returns success:true + usage on stdout instead of the old success:false + error on stderr — both satisfy "destructive command did not run", and the new shape also restores discoverability. Tests: - sdk/src/cli.test.ts: 4 new vitest cases covering #3019 — query argv parser keeps --help with subcommand, parses -h short flag, preserves bare `query --help` top-level behavior, preserves --help position when intermixed with other query flags. - tests/bug-3019-help-passthrough.test.cjs: 5 node:test cases on the fallback — bare gsd-tools (no args) errors with usage; --help renders usage on stdout exit 0; -h same; subcommand --help renders usage; usage hint mentions discovery method (without prose substring matching — parses into typed sections). - tests/bug-1818-unknown-flags.test.cjs: rewritten to assert the new invariant ("destructive command did not run" + "usage was rendered") instead of the old shape ("--help is rejected with non-zero exit"). Each destructive test seeds a sentinel artifact (phase dir, slug output) and asserts it survives. Verification: - 47/47 vitest pass on sdk/src/cli.test.ts - 5/5 pass on tests/bug-3019-help-passthrough.test.cjs - 8/8 pass on tests/bug-1818-unknown-flags.test.cjs (rewritten) - 6763/6763 pass on full node:test suite - lint-no-source-grep clean (0 violations) Closes #3019 * fix(#3019): SDK fallback forwards plain-text help, broader usage list (CR) CodeRabbit on PR #3026 (4 findings — 1 Major outside-diff, 2 inline, 1 nitpick): 1. **Major outside-diff** — sdk/src/cli.ts:442-454. The fallback path that delegates to gsd-tools.cjs called parseCliQueryJsonOutput (JSON.parse) on stdout. Now that gsd-tools renders plain-text usage on --help, JSON.parse threw "Unexpected token 'U'". Wrapped the parse in try/catch — on parse failure, forward the plain stdout verbatim so subcommand help reaches the user. Regression test: tests/bug-3019-help-passthrough.test.cjs spawns the built SDK and asserts `gsd-sdk query phase --help` exits 0, stdout contains the gsd-tools usage, and stderr does NOT contain a JSON-parse error. 2. .changeset/help-passthrough.md:3 — `pr: TBD` → `pr: 3026`. 3. gsd-tools.cjs:346 (TOP_LEVEL_USAGE): - Removed self-referencing `#3019` link (immediately stale after this PR merges). - Expanded Commands list from 17 → all 47 dispatcher cases: agent-skills, audit-open, audit-uat, check-commit, commit, … phase, phases, roadmap, milestone, validate, progress, intel, graphify, learnings, etc. — the bulk of the surface that was previously unreachable via --help discovery. 4. Nitpick: `isUsageOutput` was duplicated in bug-1818 and bug-3019-help-passthrough tests. Moved to tests/helpers.cjs with structural-comment, removed both duplicates. Verification: 47/47 vitest pass, 14/14 regression tests pass, 6764/6764 full suite, lint clean. * test(#3019): use t.skip() instead of bare return when SDK not built (CR) CodeRabbit follow-up on PR #3026: The integration test guarded against missing sdk/dist/cli.js with a bare `return;` — node:test counts that as a passing test (0 assertions exercised, 0 failures). On a CI checkout that hasn't run the SDK build, the #3026 regression test silently green-lit and no signal ever surfaced that the integration check was skipped. Switched to `t.skip(...)` via the test context parameter so the omission shows up in the test report. The unit-level fix (sdk/src/cli.ts) is still covered by vitest, so the skip only affects the end-to-end spawn-built-SDK check. Verification: 6/6 pass when SDK is built; 5 pass + 1 skip when not.
749 lines
26 KiB
JavaScript
749 lines
26 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* CLI entry point for gsd-sdk.
|
|
*
|
|
* Usage: gsd-sdk run "<prompt>" [--project-dir <dir>] [--ws-port <port>]
|
|
* [--model <model>] [--max-budget <n>]
|
|
*/
|
|
|
|
import { parseArgs } from 'node:util';
|
|
import { execFile } from 'node:child_process';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { resolve, join, isAbsolute } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { GSD } from './index.js';
|
|
import { CLITransport } from './cli-transport.js';
|
|
import { WSTransport } from './ws-transport.js';
|
|
import { InitRunner } from './init-runner.js';
|
|
import { validateWorkstreamName } from './workstream-utils.js';
|
|
import { loadConfig } from './config.js';
|
|
import { assertRuntimeSupportsAutoMode } from './runtime-gate.js';
|
|
|
|
// ─── Parsed CLI args ─────────────────────────────────────────────────────────
|
|
|
|
export interface ParsedCliArgs {
|
|
command: string | undefined;
|
|
prompt: string | undefined;
|
|
/** For 'init' command: the raw input source (@file, text, or undefined for stdin). */
|
|
initInput: string | undefined;
|
|
/** For 'auto --init': bootstrap from a PRD before running the autonomous loop. */
|
|
init: string | undefined;
|
|
projectDir: string;
|
|
wsPort: number | undefined;
|
|
model: string | undefined;
|
|
maxBudget: number | undefined;
|
|
/** Workstream name for multi-workstream projects. Routes .planning/ to .planning/workstreams/<name>/. */
|
|
ws: string | undefined;
|
|
help: boolean;
|
|
version: boolean;
|
|
/**
|
|
* When `command === 'query'`, tokens after `query` with only known SDK flags removed.
|
|
* Extra flags are kept so handlers that share gsd-tools-style argv (e.g. `--pick`) still receive them.
|
|
*/
|
|
queryArgv?: string[];
|
|
}
|
|
|
|
/**
|
|
* Parse `gsd-sdk query …` without rejecting unknown flags (query argv is forwarded to the registry).
|
|
*/
|
|
function parseCliArgsQueryPermissive(argv: string[]): ParsedCliArgs {
|
|
let projectDir = process.cwd();
|
|
let ws: string | undefined;
|
|
let wsPort: number | undefined;
|
|
let model: string | undefined;
|
|
let maxBudget: number | undefined;
|
|
let help = false;
|
|
let version = false;
|
|
const queryArgv: string[] = [];
|
|
|
|
let i = 1;
|
|
while (i < argv.length) {
|
|
const a = argv[i];
|
|
if (a === '--project-dir' && argv[i + 1]) {
|
|
projectDir = argv[i + 1];
|
|
i += 2;
|
|
continue;
|
|
}
|
|
if (a === '--ws' && argv[i + 1]) {
|
|
ws = argv[i + 1];
|
|
i += 2;
|
|
continue;
|
|
}
|
|
if (a === '--ws-port' && argv[i + 1]) {
|
|
wsPort = Number(argv[i + 1]);
|
|
i += 2;
|
|
continue;
|
|
}
|
|
if (a === '--model' && argv[i + 1]) {
|
|
model = argv[i + 1];
|
|
i += 2;
|
|
continue;
|
|
}
|
|
if (a === '--max-budget' && argv[i + 1]) {
|
|
maxBudget = Number(argv[i + 1]);
|
|
i += 2;
|
|
continue;
|
|
}
|
|
// #3019: do NOT consume -h / --help here unconditionally. Pushing the
|
|
// flag onto queryArgv lets the registered handler (or the gsd-tools.cjs
|
|
// fallback) render contextual subcommand help. We still set the global
|
|
// `help` flag when the flag appears, but only short-circuit dispatch in
|
|
// main() when there is no real subcommand to dispatch to (i.e. the only
|
|
// tokens in queryArgv are the help flags themselves). That preserves
|
|
// `gsd-sdk query --help` → top-level USAGE while letting
|
|
// `gsd-sdk query phase add --help` reach the handler.
|
|
if (a === '-h' || a === '--help') {
|
|
help = true;
|
|
queryArgv.push(a);
|
|
i += 1;
|
|
continue;
|
|
}
|
|
if (a === '-v' || a === '--version') {
|
|
version = true;
|
|
i += 1;
|
|
continue;
|
|
}
|
|
queryArgv.push(a);
|
|
i += 1;
|
|
}
|
|
|
|
// If the user typed a real subcommand (anything other than help flags
|
|
// alone in queryArgv), do NOT short-circuit to top-level USAGE on help.
|
|
// The handler/fallback will render contextual help.
|
|
const nonHelpTokens = queryArgv.filter((t) => t !== '-h' && t !== '--help');
|
|
if (help && nonHelpTokens.length > 0) {
|
|
help = false;
|
|
}
|
|
|
|
return {
|
|
command: 'query',
|
|
prompt: undefined,
|
|
initInput: undefined,
|
|
init: undefined,
|
|
projectDir,
|
|
wsPort,
|
|
model,
|
|
maxBudget,
|
|
ws,
|
|
help,
|
|
version,
|
|
queryArgv,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Parse CLI arguments into a structured object.
|
|
* Exported for testing — the main() function uses this internally.
|
|
*/
|
|
export function parseCliArgs(argv: string[]): ParsedCliArgs {
|
|
if (argv[0] === 'query') {
|
|
return parseCliArgsQueryPermissive(argv);
|
|
}
|
|
|
|
const { values, positionals } = parseArgs({
|
|
args: argv,
|
|
options: {
|
|
'project-dir': { type: 'string', default: process.cwd() },
|
|
'ws-port': { type: 'string' },
|
|
ws: { type: 'string' },
|
|
model: { type: 'string' },
|
|
'max-budget': { type: 'string' },
|
|
init: { type: 'string' },
|
|
help: { type: 'boolean', short: 'h', default: false },
|
|
version: { type: 'boolean', short: 'v', default: false },
|
|
},
|
|
allowPositionals: true,
|
|
strict: true,
|
|
});
|
|
|
|
const command = positionals[0] as string | undefined;
|
|
const prompt = positionals.slice(1).join(' ') || undefined;
|
|
|
|
// For 'init' command, the positional after 'init' is the input source.
|
|
// For 'run' command, it's the prompt. Both use positionals[1+].
|
|
const initInput = command === 'init' ? prompt : undefined;
|
|
|
|
return {
|
|
command,
|
|
prompt,
|
|
initInput,
|
|
init: values.init as string | undefined,
|
|
projectDir: values['project-dir'] as string,
|
|
wsPort: values['ws-port'] ? Number(values['ws-port']) : undefined,
|
|
model: values.model as string | undefined,
|
|
maxBudget: values['max-budget'] ? Number(values['max-budget']) : undefined,
|
|
ws: values.ws as string | undefined,
|
|
help: values.help as boolean,
|
|
version: values.version as boolean,
|
|
};
|
|
}
|
|
|
|
// ─── Usage ───────────────────────────────────────────────────────────────────
|
|
|
|
export const USAGE = `
|
|
Usage: gsd-sdk <command> [args] [options]
|
|
|
|
Commands:
|
|
run <prompt> Run a full milestone from a text prompt
|
|
auto Run the full autonomous lifecycle (discover -> execute -> advance)
|
|
init [input] Bootstrap a new project from a PRD or description
|
|
input can be:
|
|
@path/to/prd.md Read input from a file
|
|
"description" Use text directly
|
|
(empty) Read from stdin
|
|
query <argv...> Registered query handlers only (longest-prefix argv match; see QUERY-HANDLERS.md)
|
|
Use --pick <field> to extract a specific field from JSON output
|
|
|
|
Options:
|
|
--init <input> Bootstrap from a PRD before running (auto only)
|
|
Accepts @path/to/prd.md or "description text"
|
|
--project-dir <dir> Project directory (default: cwd)
|
|
--ws <name> Route .planning/ to .planning/workstreams/<name>/
|
|
--ws-port <port> Enable WebSocket transport on <port>
|
|
--model <model> Override LLM model
|
|
--max-budget <n> Max budget per step in USD
|
|
-h, --help Show this help
|
|
-v, --version Show version
|
|
`.trim();
|
|
|
|
/**
|
|
* Read the package version from package.json.
|
|
*/
|
|
async function getVersion(): Promise<string> {
|
|
try {
|
|
const pkgPath = resolve(fileURLToPath(import.meta.url), '..', '..', 'package.json');
|
|
const raw = await readFile(pkgPath, 'utf-8');
|
|
const pkg = JSON.parse(raw) as { version?: string };
|
|
return pkg.version ?? 'unknown';
|
|
} catch {
|
|
return 'unknown';
|
|
}
|
|
}
|
|
|
|
// ─── Init input resolution ───────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Resolve the init command input to a string.
|
|
*
|
|
* - `@path/to/file.md` → reads the file contents
|
|
* - Raw text → returns as-is
|
|
* - No input → reads from stdin (with TTY detection)
|
|
*
|
|
* Exported for testing.
|
|
*/
|
|
export async function resolveInitInput(args: ParsedCliArgs): Promise<string> {
|
|
const input = args.initInput;
|
|
|
|
if (input && input.startsWith('@')) {
|
|
// File path: strip @ prefix, resolve relative to projectDir
|
|
const filePath = resolve(args.projectDir, input.slice(1));
|
|
try {
|
|
return await readFile(filePath, 'utf-8');
|
|
} catch (err) {
|
|
throw new Error(`Cannot read input file "${filePath}": ${(err as NodeJS.ErrnoException).code === 'ENOENT' ? 'file not found' : (err as Error).message}`);
|
|
}
|
|
}
|
|
|
|
if (input) {
|
|
// Raw text
|
|
return input;
|
|
}
|
|
|
|
// No input — read from stdin
|
|
return readStdin();
|
|
}
|
|
|
|
/**
|
|
* Read all data from stdin. Rejects if stdin is a TTY with no piped data.
|
|
*/
|
|
async function readStdin(): Promise<string> {
|
|
const { stdin } = process;
|
|
|
|
if (stdin.isTTY) {
|
|
throw new Error(
|
|
'No input provided. Usage:\n' +
|
|
' gsd-sdk init @path/to/prd.md\n' +
|
|
' gsd-sdk init "build a todo app"\n' +
|
|
' cat prd.md | gsd-sdk init'
|
|
);
|
|
}
|
|
|
|
return new Promise<string>((resolve, reject) => {
|
|
const chunks: Buffer[] = [];
|
|
stdin.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
stdin.on('error', reject);
|
|
});
|
|
}
|
|
|
|
/** When false, unknown `gsd-sdk query` commands error instead of shelling out to gsd-tools.cjs. */
|
|
function queryFallbackToCjsEnabled(): boolean {
|
|
const v = process.env.GSD_QUERY_FALLBACK?.toLowerCase();
|
|
if (v === 'off' || v === 'never' || v === 'false' || v === '0') return false;
|
|
return true;
|
|
}
|
|
|
|
async function parseCliQueryJsonOutput(raw: string, projectDir: string): Promise<unknown> {
|
|
const trimmed = raw.trim();
|
|
if (trimmed === '') return null;
|
|
let jsonStr = trimmed;
|
|
if (jsonStr.startsWith('@file:')) {
|
|
const rel = jsonStr.slice(6).trim();
|
|
const { resolvePathUnderProject } = await import('./query/helpers.js');
|
|
const filePath = await resolvePathUnderProject(projectDir, rel);
|
|
jsonStr = await readFile(filePath, 'utf-8');
|
|
}
|
|
return JSON.parse(jsonStr);
|
|
}
|
|
|
|
/** Map registry-style dotted command tokens to gsd-tools.cjs argv (space-separated subcommands). */
|
|
function dottedCommandToCjsArgv(normCmd: string, normArgs: string[]): string[] {
|
|
if (normCmd.includes('.')) {
|
|
return [...normCmd.split('.'), ...normArgs];
|
|
}
|
|
return [normCmd, ...normArgs];
|
|
}
|
|
|
|
function execGsdToolsCjsQuery(
|
|
projectDir: string,
|
|
gsdToolsPath: string,
|
|
normCmd: string,
|
|
normArgs: string[],
|
|
ws: string | undefined,
|
|
): Promise<{ stdout: string; stderr: string }> {
|
|
const cjsArgv = dottedCommandToCjsArgv(normCmd, normArgs);
|
|
const wsSuffix = ws ? ['--ws', ws] : [];
|
|
const fullArgv = [gsdToolsPath, ...cjsArgv, ...wsSuffix];
|
|
return new Promise((resolve, reject) => {
|
|
execFile(
|
|
process.execPath,
|
|
fullArgv,
|
|
{ cwd: projectDir, maxBuffer: 10 * 1024 * 1024, env: { ...process.env } },
|
|
(err, stdout, stderr) => {
|
|
if (err) reject(err);
|
|
else resolve({ stdout: stdout?.toString() ?? '', stderr: stderr?.toString() ?? '' });
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
|
|
export async function main(argv: string[] = process.argv.slice(2)): Promise<void> {
|
|
let args: ParsedCliArgs;
|
|
|
|
try {
|
|
args = parseCliArgs(argv);
|
|
} catch (err) {
|
|
console.error(`Error: ${(err as Error).message}`);
|
|
console.error(USAGE);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
if (args.help) {
|
|
console.log(USAGE);
|
|
return;
|
|
}
|
|
|
|
if (args.version) {
|
|
const ver = await getVersion();
|
|
console.log(`gsd-sdk v${ver}`);
|
|
return;
|
|
}
|
|
|
|
// Validate --ws flag if provided
|
|
if (args.ws !== undefined && !validateWorkstreamName(args.ws)) {
|
|
console.error(`Error: Invalid workstream name "${args.ws}". Use alphanumeric, hyphens, underscores, or dots only.`);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
// Fall back to GSD_WORKSTREAM env var when --ws is not supplied (#2791).
|
|
// gsd-tools.cjs resolves the active workstream via this env var; parity
|
|
// means gsd-sdk query commands see the same .planning/ path as gsd-tools.
|
|
if (args.ws === undefined && process.env.GSD_WORKSTREAM) {
|
|
const envWs = process.env.GSD_WORKSTREAM;
|
|
if (validateWorkstreamName(envWs)) {
|
|
args = { ...args, ws: envWs };
|
|
}
|
|
// If the env var contains an invalid name, silently ignore it (same as CJS).
|
|
}
|
|
|
|
// Multi-repo project-root resolution (issue #2623).
|
|
//
|
|
// When the user launches `gsd-sdk` from inside a `sub_repos`-listed child repo,
|
|
// `projectDir` defaults to `process.cwd()` which points at the child, not the
|
|
// parent workspace that owns `.planning/`. Mirror the legacy `gsd-tools.cjs`
|
|
// walk-up semantics so handlers see the correct project root.
|
|
//
|
|
// Idempotent: if `projectDir` already has its own `.planning/` (including an
|
|
// explicit `--project-dir` pointing at the workspace root), findProjectRoot
|
|
// returns it unchanged.
|
|
{
|
|
const { findProjectRoot } = await import('./query/helpers.js');
|
|
args = { ...args, projectDir: findProjectRoot(args.projectDir) };
|
|
}
|
|
|
|
// ─── Query command ──────────────────────────────────────────────────────
|
|
if (args.command === 'query') {
|
|
const { createRegistry } = await import('./query/index.js');
|
|
const { extractField, resolveQueryArgv } = await import('./query/registry.js');
|
|
const { GSDToolsError } = await import('./gsd-tools.js');
|
|
const { GSDError, exitCodeFor, ErrorClassification } = await import('./errors.js');
|
|
|
|
const queryArgs = args.queryArgv ?? [];
|
|
|
|
// Extract --pick before dispatch
|
|
const pickIdx = queryArgs.indexOf('--pick');
|
|
let pickField: string | undefined;
|
|
if (pickIdx !== -1) {
|
|
if (pickIdx + 1 >= queryArgs.length) {
|
|
console.error('Error: --pick requires a field name');
|
|
process.exitCode = 10;
|
|
return;
|
|
}
|
|
pickField = queryArgs[pickIdx + 1];
|
|
queryArgs.splice(pickIdx, 2);
|
|
}
|
|
|
|
if (queryArgs.length === 0 || !queryArgs[0]) {
|
|
console.error('Error: "gsd-sdk query" requires a command');
|
|
process.exitCode = 10;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const queryCommand = queryArgs[0];
|
|
const { normalizeQueryCommand } = await import('./query/normalize-query-command.js');
|
|
const [normCmd, normArgs] = normalizeQueryCommand(queryCommand, queryArgs.slice(1));
|
|
if (!normCmd || !String(normCmd).trim()) {
|
|
console.error('Error: "gsd-sdk query" requires a command');
|
|
process.exitCode = 10;
|
|
return;
|
|
}
|
|
const registry = createRegistry();
|
|
const tokens = [normCmd, ...normArgs];
|
|
const matched = resolveQueryArgv(tokens, registry);
|
|
if (!matched) {
|
|
if (!queryFallbackToCjsEnabled()) {
|
|
throw new GSDError(
|
|
`Unknown command: "${tokens.join(' ')}". Use a registered \`gsd-sdk query\` subcommand (see sdk/src/query/QUERY-HANDLERS.md) or invoke \`node …/gsd-tools.cjs\` for CJS-only operations. Set GSD_QUERY_FALLBACK=registered (default) to allow automatic fallback.`,
|
|
ErrorClassification.Validation,
|
|
);
|
|
}
|
|
const { resolveGsdToolsPath } = await import('./gsd-tools.js');
|
|
const gsdPath = resolveGsdToolsPath(args.projectDir);
|
|
console.error(
|
|
`[gsd-sdk] '${tokens.join(' ')}' not in native registry; falling back to gsd-tools.cjs.`,
|
|
);
|
|
console.error('[gsd-sdk] Transparent bridge — prefer adding a native handler when parity matters.');
|
|
const { stdout, stderr } = await execGsdToolsCjsQuery(
|
|
args.projectDir,
|
|
gsdPath,
|
|
normCmd,
|
|
normArgs,
|
|
args.ws,
|
|
);
|
|
if (stderr.trim()) console.error(stderr.trimEnd());
|
|
// #3026 CR (Major outside-diff): the gsd-tools.cjs fallback now
|
|
// emits plain-text usage on --help / -h with exit 0, instead of
|
|
// a JSON object. Wrap the JSON parse in a try/catch and forward
|
|
// non-JSON stdout verbatim so subcommand help reaches the user.
|
|
// (Previously this path JSON.parsed the help text and threw
|
|
// "Unexpected token 'U'" — exitCode=1 — a regression introduced
|
|
// alongside the --help passthrough fix.)
|
|
let output: unknown;
|
|
try {
|
|
output = await parseCliQueryJsonOutput(stdout, args.projectDir);
|
|
} catch {
|
|
if (stdout.trim()) {
|
|
process.stdout.write(stdout.endsWith('\n') ? stdout : stdout + '\n');
|
|
}
|
|
return;
|
|
}
|
|
if (pickField) {
|
|
output = extractField(output, pickField);
|
|
}
|
|
console.log(JSON.stringify(output, null, 2));
|
|
} else {
|
|
const result = await registry.dispatch(matched.cmd, matched.args, args.projectDir, args.ws);
|
|
let output: unknown = result.data;
|
|
|
|
if (pickField) {
|
|
output = extractField(output, pickField);
|
|
}
|
|
|
|
// Handlers can signal format:'text' to emit a raw string (e.g. agent-skills
|
|
// emits an <agent_skills> XML block workflows embed via $(...) substitution).
|
|
if (!pickField && result.format === 'text' && typeof output === 'string') {
|
|
process.stdout.write(output);
|
|
} else {
|
|
console.log(JSON.stringify(output, null, 2));
|
|
}
|
|
}
|
|
} catch (err) {
|
|
if (err instanceof GSDError) {
|
|
console.error(`Error: ${err.message}`);
|
|
process.exitCode = exitCodeFor(err.classification);
|
|
} else if (err instanceof GSDToolsError) {
|
|
console.error(`Error: ${err.message}`);
|
|
process.exitCode = err.exitCode ?? 1;
|
|
} else {
|
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (args.command !== 'run' && args.command !== 'init' && args.command !== 'auto') {
|
|
console.error('Error: Expected "gsd-sdk run <prompt>", "gsd-sdk auto", "gsd-sdk init [input]", or "gsd-sdk query <command>"');
|
|
console.error(USAGE);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
if (args.command === 'run' && !args.prompt) {
|
|
console.error('Error: "gsd-sdk run" requires a prompt');
|
|
console.error(USAGE);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
// ─── Init command ─────────────────────────────────────────────────────────
|
|
if (args.command === 'init') {
|
|
let input: string;
|
|
try {
|
|
input = await resolveInitInput(args);
|
|
} catch (err) {
|
|
console.error(`Error: ${(err as Error).message}`);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
console.log(`[init] Resolved input: ${input.length} chars`);
|
|
|
|
// Build GSD instance for tools and event stream
|
|
const gsd = new GSD({
|
|
projectDir: args.projectDir,
|
|
model: args.model,
|
|
maxBudgetUsd: args.maxBudget,
|
|
workstream: args.ws,
|
|
});
|
|
|
|
// Wire CLI transport
|
|
const cliTransport = new CLITransport();
|
|
gsd.addTransport(cliTransport);
|
|
|
|
// Optional WebSocket transport
|
|
let wsTransport: WSTransport | undefined;
|
|
if (args.wsPort !== undefined) {
|
|
wsTransport = new WSTransport({ port: args.wsPort });
|
|
await wsTransport.start();
|
|
gsd.addTransport(wsTransport);
|
|
console.log(`WebSocket transport listening on port ${args.wsPort}`);
|
|
}
|
|
|
|
try {
|
|
const tools = gsd.createTools();
|
|
const runner = new InitRunner({
|
|
projectDir: args.projectDir,
|
|
tools,
|
|
eventStream: gsd.eventStream,
|
|
config: {
|
|
maxBudgetPerSession: args.maxBudget,
|
|
orchestratorModel: args.model,
|
|
},
|
|
});
|
|
|
|
const result = await runner.run(input);
|
|
|
|
// Print completion summary
|
|
const status = result.success ? 'SUCCESS' : 'FAILED';
|
|
const stepCount = result.steps.length;
|
|
const passedSteps = result.steps.filter(s => s.success).length;
|
|
const cost = result.totalCostUsd.toFixed(2);
|
|
const duration = (result.totalDurationMs / 1000).toFixed(1);
|
|
const artifactList = result.artifacts.join(', ');
|
|
|
|
console.log(`\n[${status}] ${passedSteps}/${stepCount} steps, $${cost}, ${duration}s`);
|
|
if (result.artifacts.length > 0) {
|
|
console.log(`Artifacts: ${artifactList}`);
|
|
}
|
|
|
|
if (!result.success) {
|
|
// Log failed steps
|
|
for (const step of result.steps) {
|
|
if (!step.success && step.error) {
|
|
console.error(` ✗ ${step.step}: ${step.error}`);
|
|
}
|
|
}
|
|
process.exitCode = 1;
|
|
}
|
|
} catch (err) {
|
|
console.error(`Fatal error: ${(err as Error).message}`);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
cliTransport.close();
|
|
if (wsTransport) {
|
|
wsTransport.close();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// ─── Auto command ─────────────────────────────────────────────────────────
|
|
if (args.command === 'auto') {
|
|
// #2832: refuse to silently route non-Claude runtime projects through the
|
|
// Claude Agent SDK. Load project config (best effort — falls back to
|
|
// defaults when missing) and gate before constructing GSD/InitRunner.
|
|
try {
|
|
const cfg = await loadConfig(args.projectDir, args.ws);
|
|
assertRuntimeSupportsAutoMode(cfg);
|
|
} catch (err) {
|
|
console.error(`Fatal error: ${(err as Error).message}`);
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
|
|
const gsd = new GSD({
|
|
projectDir: args.projectDir,
|
|
model: args.model,
|
|
maxBudgetUsd: args.maxBudget,
|
|
autoMode: true,
|
|
workstream: args.ws,
|
|
});
|
|
|
|
// Wire CLI transport (always active)
|
|
const cliTransport = new CLITransport();
|
|
gsd.addTransport(cliTransport);
|
|
|
|
// Optional WebSocket transport
|
|
let wsTransport: WSTransport | undefined;
|
|
if (args.wsPort !== undefined) {
|
|
wsTransport = new WSTransport({ port: args.wsPort });
|
|
await wsTransport.start();
|
|
gsd.addTransport(wsTransport);
|
|
console.log(`WebSocket transport listening on port ${args.wsPort}`);
|
|
}
|
|
|
|
try {
|
|
// If --init provided, bootstrap project first
|
|
if (args.init) {
|
|
const initInput = await resolveInitInput({
|
|
...args,
|
|
command: 'init',
|
|
initInput: args.init,
|
|
});
|
|
|
|
console.log(`[auto] Bootstrapping project from --init (${initInput.length} chars)`);
|
|
|
|
const tools = gsd.createTools();
|
|
const runner = new InitRunner({
|
|
projectDir: args.projectDir,
|
|
tools,
|
|
eventStream: gsd.eventStream,
|
|
config: {
|
|
maxBudgetPerSession: args.maxBudget,
|
|
orchestratorModel: args.model,
|
|
},
|
|
});
|
|
|
|
const initResult = await runner.run(initInput);
|
|
|
|
const initStatus = initResult.success ? 'SUCCESS' : 'FAILED';
|
|
const stepCount = initResult.steps.length;
|
|
const passedSteps = initResult.steps.filter(s => s.success).length;
|
|
const initCost = initResult.totalCostUsd.toFixed(2);
|
|
const initDuration = (initResult.totalDurationMs / 1000).toFixed(1);
|
|
console.log(`[init ${initStatus}] ${passedSteps}/${stepCount} steps, $${initCost}, ${initDuration}s`);
|
|
|
|
if (!initResult.success) {
|
|
for (const step of initResult.steps) {
|
|
if (!step.success && step.error) {
|
|
console.error(` ✗ ${step.step}: ${step.error}`);
|
|
}
|
|
}
|
|
process.exitCode = 1;
|
|
return;
|
|
}
|
|
}
|
|
|
|
const result = await gsd.run('');
|
|
|
|
// Final summary
|
|
const status = result.success ? 'SUCCESS' : 'FAILED';
|
|
const phases = result.phases.length;
|
|
const cost = result.totalCostUsd.toFixed(2);
|
|
const duration = (result.totalDurationMs / 1000).toFixed(1);
|
|
console.log(`\n[${status}] ${phases} phase(s), $${cost}, ${duration}s`);
|
|
|
|
if (!result.success) {
|
|
process.exitCode = 1;
|
|
}
|
|
} catch (err) {
|
|
console.error(`Fatal error: ${(err as Error).message}`);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
cliTransport.close();
|
|
if (wsTransport) {
|
|
wsTransport.close();
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// ─── Run command ─────────────────────────────────────────────────────────
|
|
|
|
// Build GSD instance
|
|
const gsd = new GSD({
|
|
projectDir: args.projectDir,
|
|
model: args.model,
|
|
maxBudgetUsd: args.maxBudget,
|
|
workstream: args.ws,
|
|
});
|
|
|
|
// Wire CLI transport (always active)
|
|
const cliTransport = new CLITransport();
|
|
gsd.addTransport(cliTransport);
|
|
|
|
// Optional WebSocket transport
|
|
let wsTransport: WSTransport | undefined;
|
|
if (args.wsPort !== undefined) {
|
|
wsTransport = new WSTransport({ port: args.wsPort });
|
|
await wsTransport.start();
|
|
gsd.addTransport(wsTransport);
|
|
console.log(`WebSocket transport listening on port ${args.wsPort}`);
|
|
}
|
|
|
|
try {
|
|
const result = await gsd.run(args.prompt!);
|
|
|
|
// Final summary
|
|
const status = result.success ? 'SUCCESS' : 'FAILED';
|
|
const phases = result.phases.length;
|
|
const cost = result.totalCostUsd.toFixed(2);
|
|
const duration = (result.totalDurationMs / 1000).toFixed(1);
|
|
console.log(`\n[${status}] ${phases} phase(s), $${cost}, ${duration}s`);
|
|
|
|
if (!result.success) {
|
|
process.exitCode = 1;
|
|
}
|
|
} catch (err) {
|
|
console.error(`Fatal error: ${(err as Error).message}`);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
// Clean up transports
|
|
cliTransport.close();
|
|
if (wsTransport) {
|
|
wsTransport.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Auto-run when invoked directly ──────────────────────────────────────────
|
|
|
|
main();
|