From c66b542f71c70d95bac74844e51b6622857d404e Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 24 May 2026 22:17:20 -0400 Subject: [PATCH] refactor(#185): migrate hub, errors, and manifest to canonical src seams --- sdk/src/dispatch/hub.ts | 246 +++++++++++++++++++++++++++++ sdk/src/errors.ts | 73 +-------- sdk/src/errors/index.ts | 72 +++++++++ sdk/src/manifest/index.ts | 17 ++ sdk/src/query/command-manifest.ts | 18 +-- sdk/src/query/query-dispatch.ts | 247 +----------------------------- 6 files changed, 338 insertions(+), 335 deletions(-) create mode 100644 sdk/src/dispatch/hub.ts create mode 100644 sdk/src/errors/index.ts create mode 100644 sdk/src/manifest/index.ts diff --git a/sdk/src/dispatch/hub.ts b/sdk/src/dispatch/hub.ts new file mode 100644 index 000000000..bebffbd99 --- /dev/null +++ b/sdk/src/dispatch/hub.ts @@ -0,0 +1,246 @@ +import type { QueryRegistry } from '../query/registry.js'; +import { extractField } from '../query/registry.js'; +import { normalizeQueryCommand } from '../query/query-command-resolution-strategy.js'; +import { runCjsFallbackDispatch } from '../query/query-fallback-executor.js'; +import type { QueryDispatchError, QueryDispatchResult } from '../query/query-dispatch-contract.js'; +import type { QueryResult } from '../query/utils.js'; +import type { QueryNativeDispatchAdapter } from '../query/query-native-dispatch-adapter.js'; +import type { CommandTopology, CommandTopologyMatch } from '../query/command-topology.js'; +import { unknownCommandError, validationError, fallbackDispatchErrorFromSignal, nativeDispatchErrorFromSignal } from '../query/query-error-taxonomy.js'; +import { canUseCjsFallback } from '../query/query-fallback-policy.js'; +import { toFailureSignal } from '../query-failure-classification.js'; + +export interface QueryDispatchDeps { + registry: QueryRegistry; + projectDir: string; + ws?: string; + cjsFallbackEnabled: boolean; + resolveGsdToolsPath: (projectDir: string) => string; + /** @deprecated use topology */ + dispatchNative?: (cmd: string, args: string[]) => Promise; + /** @deprecated use topology */ + nativeAdapter?: QueryNativeDispatchAdapter; + topology: CommandTopology; +} + +export type DispatchMode = 'native' | 'cjs' | 'error'; + +export interface DispatchPlan { + mode: DispatchMode; + normalized: { command: string; args: string[]; tokens: string[] }; + matched: CommandTopologyMatch | null; + noMatchMessage?: string; + noMatchNormalized?: string; + noMatchAttempted?: string[]; + noMatchHints?: string[]; +} + +export type DispatchSuccessFormat = 'json' | 'text' | undefined; + +export interface DispatchInputValidationResult { + queryArgs: string[]; + pickField?: string; + error?: QueryDispatchResult; +} + +export function dispatchFailure(error: QueryDispatchError, stderr: string[] = []): QueryDispatchResult { + return { + ok: false, + error, + stderr, + exit_code: error.code, + }; +} + +export function dispatchSuccess(stdout: string, stderr: string[] = []): QueryDispatchResult { + return { + ok: true, + stdout, + stderr, + exit_code: 0, + }; +} + +export function toDispatchFailure(error: QueryDispatchError, stderr: string[] = []): QueryDispatchResult { + return dispatchFailure(error, stderr); +} + +export function mapNativeDispatchError(error: unknown, command: string, args: string[]): QueryDispatchError { + return nativeDispatchErrorFromSignal(toFailureSignal(error), command, args); +} + +export function mapFallbackDispatchError(error: unknown, command: string, args: string[]): QueryDispatchError { + return fallbackDispatchErrorFromSignal(toFailureSignal(error), command, args); +} + +export function formatPick(data: unknown, pickField?: string): unknown { + if (!pickField) return data; + return extractField(data, pickField); +} + +export function formatSuccess(data: unknown, format: DispatchSuccessFormat, pickField?: string): string { + if (format === 'text' && typeof data === 'string') { + if (pickField) { + throw new Error('--pick is not supported for text output'); + } + return data.endsWith('\n') ? data : `${data}\n`; + } + const output = formatPick(data, pickField); + if (pickField && typeof output === 'string') { + return output.endsWith('\n') ? output : `${output}\n`; + } + return `${JSON.stringify(output === undefined ? null : output, null, 2)}\n`; +} + +export function validateQueryDispatchInput(queryArgv: string[]): DispatchInputValidationResult { + const queryArgs = [...queryArgv]; + const pickIdx = queryArgs.indexOf('--pick'); + if (pickIdx !== -1) { + if (pickIdx + 1 >= queryArgs.length) { + return { + queryArgs, + error: dispatchFailure(validationError({ + message: 'Error: --pick requires a field name', + details: { field: '--pick', reason: 'missing_value' }, + })), + }; + } + const pickField = queryArgs[pickIdx + 1]; + queryArgs.splice(pickIdx, 2); + if (queryArgs.length === 0 || !queryArgs[0]) { + return { + queryArgs, + error: dispatchFailure(validationError({ + message: 'Error: "gsd-sdk query" requires a command', + details: { reason: 'missing_command' }, + })), + }; + } + return { queryArgs, pickField }; + } + + if (queryArgs.length === 0 || !queryArgs[0]) { + return { + queryArgs, + error: dispatchFailure(validationError({ + message: 'Error: "gsd-sdk query" requires a command', + details: { reason: 'missing_command' }, + })), + }; + } + + return { queryArgs }; +} + +export function planQueryDispatch( + queryArgv: string[], + topology: CommandTopology, + cjsFallbackEnabled: boolean, +): DispatchPlan { + const queryCommand = queryArgv[0]; + if (!queryCommand) { + return { mode: 'error', normalized: { command: '', args: [], tokens: [] }, matched: null }; + } + + const [normCmd, normArgs] = normalizeQueryCommand(queryCommand, queryArgv.slice(1)); + const normalizedTokens = [normCmd, ...normArgs]; + const resolved = topology.resolve(queryArgv, !cjsFallbackEnabled); + + if (resolved.kind === 'match') { + return { mode: 'native', normalized: { command: normCmd, args: normArgs, tokens: normalizedTokens }, matched: resolved }; + } + + if (cjsFallbackEnabled) { + return { mode: 'cjs', normalized: { command: normCmd, args: normArgs, tokens: normalizedTokens }, matched: null }; + } + + return { + mode: 'error', + normalized: { command: normCmd, args: normArgs, tokens: normalizedTokens }, + matched: null, + noMatchMessage: resolved.message, + noMatchNormalized: resolved.normalized, + noMatchAttempted: resolved.attempted, + noMatchHints: resolved.hints, + }; +} + +function fail(error: ReturnType | ReturnType, stderr: string[] = []): QueryDispatchResult { + return toDispatchFailure(error, stderr); +} + +export async function runQueryDispatch(deps: QueryDispatchDeps, queryArgv: string[]): Promise { + const validated = validateQueryDispatchInput(queryArgv); + if (validated.error) return validated.error; + + const { queryArgs, pickField } = validated; + + const plan = planQueryDispatch(queryArgs, deps.topology, deps.cjsFallbackEnabled); + const normCmd = plan.normalized.command; + const normArgs = plan.normalized.args; + + if (!normCmd || !String(normCmd).trim()) { + return fail(validationError({ message: 'Error: "gsd-sdk query" requires a command', details: { reason: 'empty_normalized_command' } })); + } + + if (plan.mode === 'error') { + return fail(unknownCommandError({ + message: plan.noMatchMessage ?? `Error: Unknown command: "${queryArgs[0] ?? normCmd}"`, + normalized: plan.noMatchNormalized ?? [normCmd, ...normArgs].join(' ').trim(), + attempted: plan.noMatchAttempted ?? [], + hints: plan.noMatchHints ?? [], + })); + } + + if (plan.mode === 'cjs') { + if (canUseCjsFallback({ cjsFallbackEnabled: deps.cjsFallbackEnabled })) { + try { + const gsdPath = deps.resolveGsdToolsPath(deps.projectDir); + return await runCjsFallbackDispatch({ + projectDir: deps.projectDir, + gsdToolsPath: gsdPath, + normCmd, + normArgs, + ws: deps.ws, + pickField, + }); + } catch (e) { + return toDispatchFailure(mapFallbackDispatchError(e, normCmd, normArgs)); + } + } + return toDispatchFailure(mapFallbackDispatchError(new Error('CJS fallback denied by policy'), normCmd, normArgs)); + } + + const matched = plan.matched; + if (!matched) { + return toDispatchFailure(mapFallbackDispatchError(new Error('No native match in dispatch plan'), normCmd, normArgs)); + } + + // #3259: guard — if the invocation contains --help / -h AND the matched + // handler is a mutating command (mutation: true in the command manifest), + // short-circuit to a non-mutating stub. Mutating handlers are not help-aware + // by default (fail-closed). This prevents e.g. `milestone.complete --help` + // from writing milestone artifacts to disk. + const helpFlagPresent = matched.args.some((a) => a === '--help' || a === '-h'); + if (helpFlagPresent && matched.mutation) { + return dispatchSuccess( + formatSuccess( + { help: `Usage: gsd-sdk query ${matched.canonical} [args...]` }, + undefined, + ), + ); + } + + const dispatchNative = deps.nativeAdapter + ? (cmd: string, args: string[]) => deps.nativeAdapter!.dispatch(cmd, args) + : deps.dispatchNative; + + try { + const result = dispatchNative + ? await dispatchNative(matched.canonical, matched.args) + : await matched.adapter(matched.args, deps.projectDir, deps.ws); + return dispatchSuccess(formatSuccess(result.data, result.format, pickField)); + } catch (e) { + return toDispatchFailure(mapNativeDispatchError(e, matched.canonical, matched.args)); + } +} diff --git a/sdk/src/errors.ts b/sdk/src/errors.ts index caa17e0be..e5bbb4f67 100644 --- a/sdk/src/errors.ts +++ b/sdk/src/errors.ts @@ -1,72 +1 @@ -/** - * Error classification system for the GSD SDK. - * - * Provides a taxonomy of error types with semantic exit codes, - * enabling CLI consumers and agents to distinguish between - * validation failures, execution errors, blocked states, and - * interruptions. - * - * @example - * ```typescript - * import { GSDError, ErrorClassification, exitCodeFor } from './errors.js'; - * - * throw new GSDError('missing required arg', ErrorClassification.Validation); - * // CLI catch handler: process.exitCode = exitCodeFor(err.classification); // 10 - * ``` - */ - -// ─── Error Classification ─────────────────────────────────────────────────── - -/** Classifies SDK errors into semantic categories for exit code mapping. */ -export enum ErrorClassification { - /** Bad input, missing args, schema violations. Exit code 10. */ - Validation = 'validation', - - /** Runtime failure, file I/O, parse errors. Exit code 1. */ - Execution = 'execution', - - /** Dependency missing, phase not found. Exit code 11. */ - Blocked = 'blocked', - - /** Timeout, signal, user cancel. Exit code 1. */ - Interruption = 'interruption', -} - -// ─── GSDError ─────────────────────────────────────────────────────────────── - -/** - * Base error class for the GSD SDK with classification support. - * - * @param message - Human-readable error description - * @param classification - Error category for exit code mapping - */ -export class GSDError extends Error { - readonly name = 'GSDError'; - readonly classification: ErrorClassification; - - constructor(message: string, classification: ErrorClassification) { - super(message); - this.classification = classification; - } -} - -// ─── Exit code mapping ────────────────────────────────────────────────────── - -/** - * Maps an error classification to a semantic exit code. - * - * @param classification - The error classification to map - * @returns Numeric exit code: 10 (validation), 11 (blocked), 1 (execution/interruption) - */ -export function exitCodeFor(classification: ErrorClassification): number { - switch (classification) { - case ErrorClassification.Validation: - return 10; - case ErrorClassification.Blocked: - return 11; - case ErrorClassification.Execution: - case ErrorClassification.Interruption: - default: - return 1; - } -} +export * from './errors/index.js'; diff --git a/sdk/src/errors/index.ts b/sdk/src/errors/index.ts new file mode 100644 index 000000000..caa17e0be --- /dev/null +++ b/sdk/src/errors/index.ts @@ -0,0 +1,72 @@ +/** + * Error classification system for the GSD SDK. + * + * Provides a taxonomy of error types with semantic exit codes, + * enabling CLI consumers and agents to distinguish between + * validation failures, execution errors, blocked states, and + * interruptions. + * + * @example + * ```typescript + * import { GSDError, ErrorClassification, exitCodeFor } from './errors.js'; + * + * throw new GSDError('missing required arg', ErrorClassification.Validation); + * // CLI catch handler: process.exitCode = exitCodeFor(err.classification); // 10 + * ``` + */ + +// ─── Error Classification ─────────────────────────────────────────────────── + +/** Classifies SDK errors into semantic categories for exit code mapping. */ +export enum ErrorClassification { + /** Bad input, missing args, schema violations. Exit code 10. */ + Validation = 'validation', + + /** Runtime failure, file I/O, parse errors. Exit code 1. */ + Execution = 'execution', + + /** Dependency missing, phase not found. Exit code 11. */ + Blocked = 'blocked', + + /** Timeout, signal, user cancel. Exit code 1. */ + Interruption = 'interruption', +} + +// ─── GSDError ─────────────────────────────────────────────────────────────── + +/** + * Base error class for the GSD SDK with classification support. + * + * @param message - Human-readable error description + * @param classification - Error category for exit code mapping + */ +export class GSDError extends Error { + readonly name = 'GSDError'; + readonly classification: ErrorClassification; + + constructor(message: string, classification: ErrorClassification) { + super(message); + this.classification = classification; + } +} + +// ─── Exit code mapping ────────────────────────────────────────────────────── + +/** + * Maps an error classification to a semantic exit code. + * + * @param classification - The error classification to map + * @returns Numeric exit code: 10 (validation), 11 (blocked), 1 (execution/interruption) + */ +export function exitCodeFor(classification: ErrorClassification): number { + switch (classification) { + case ErrorClassification.Validation: + return 10; + case ErrorClassification.Blocked: + return 11; + case ErrorClassification.Execution: + case ErrorClassification.Interruption: + default: + return 1; + } +} diff --git a/sdk/src/manifest/index.ts b/sdk/src/manifest/index.ts new file mode 100644 index 000000000..85c873f34 --- /dev/null +++ b/sdk/src/manifest/index.ts @@ -0,0 +1,17 @@ +import { STATE_COMMAND_MANIFEST } from '../query/command-manifest.state.js'; +import { VERIFY_COMMAND_MANIFEST } from '../query/command-manifest.verify.js'; +import { INIT_COMMAND_MANIFEST } from '../query/command-manifest.init.js'; +import { PHASE_COMMAND_MANIFEST } from '../query/command-manifest.phase.js'; +import { PHASES_COMMAND_MANIFEST } from '../query/command-manifest.phases.js'; +import { VALIDATE_COMMAND_MANIFEST } from '../query/command-manifest.validate.js'; +import { ROADMAP_COMMAND_MANIFEST } from '../query/command-manifest.roadmap.js'; + +export const COMMAND_MANIFEST = [ + ...STATE_COMMAND_MANIFEST, + ...VERIFY_COMMAND_MANIFEST, + ...INIT_COMMAND_MANIFEST, + ...PHASE_COMMAND_MANIFEST, + ...PHASES_COMMAND_MANIFEST, + ...VALIDATE_COMMAND_MANIFEST, + ...ROADMAP_COMMAND_MANIFEST, +] as const; diff --git a/sdk/src/query/command-manifest.ts b/sdk/src/query/command-manifest.ts index 250b372de..0437960e9 100644 --- a/sdk/src/query/command-manifest.ts +++ b/sdk/src/query/command-manifest.ts @@ -1,17 +1 @@ -import { STATE_COMMAND_MANIFEST } from './command-manifest.state.js'; -import { VERIFY_COMMAND_MANIFEST } from './command-manifest.verify.js'; -import { INIT_COMMAND_MANIFEST } from './command-manifest.init.js'; -import { PHASE_COMMAND_MANIFEST } from './command-manifest.phase.js'; -import { PHASES_COMMAND_MANIFEST } from './command-manifest.phases.js'; -import { VALIDATE_COMMAND_MANIFEST } from './command-manifest.validate.js'; -import { ROADMAP_COMMAND_MANIFEST } from './command-manifest.roadmap.js'; - -export const COMMAND_MANIFEST = [ - ...STATE_COMMAND_MANIFEST, - ...VERIFY_COMMAND_MANIFEST, - ...INIT_COMMAND_MANIFEST, - ...PHASE_COMMAND_MANIFEST, - ...PHASES_COMMAND_MANIFEST, - ...VALIDATE_COMMAND_MANIFEST, - ...ROADMAP_COMMAND_MANIFEST, -] as const; +export * from '../manifest/index.js'; diff --git a/sdk/src/query/query-dispatch.ts b/sdk/src/query/query-dispatch.ts index b58889285..809896c97 100644 --- a/sdk/src/query/query-dispatch.ts +++ b/sdk/src/query/query-dispatch.ts @@ -1,246 +1 @@ -import type { QueryRegistry } from './registry.js'; -import { extractField } from './registry.js'; -import { normalizeQueryCommand } from './query-command-resolution-strategy.js'; -import { runCjsFallbackDispatch } from './query-fallback-executor.js'; -import type { QueryDispatchError, QueryDispatchResult } from './query-dispatch-contract.js'; -import type { QueryResult } from './utils.js'; -import type { QueryNativeDispatchAdapter } from './query-native-dispatch-adapter.js'; -import type { CommandTopology, CommandTopologyMatch } from './command-topology.js'; -import { unknownCommandError, validationError, fallbackDispatchErrorFromSignal, nativeDispatchErrorFromSignal } from './query-error-taxonomy.js'; -import { canUseCjsFallback } from './query-fallback-policy.js'; -import { toFailureSignal } from '../query-failure-classification.js'; - -export interface QueryDispatchDeps { - registry: QueryRegistry; - projectDir: string; - ws?: string; - cjsFallbackEnabled: boolean; - resolveGsdToolsPath: (projectDir: string) => string; - /** @deprecated use topology */ - dispatchNative?: (cmd: string, args: string[]) => Promise; - /** @deprecated use topology */ - nativeAdapter?: QueryNativeDispatchAdapter; - topology: CommandTopology; -} - -export type DispatchMode = 'native' | 'cjs' | 'error'; - -export interface DispatchPlan { - mode: DispatchMode; - normalized: { command: string; args: string[]; tokens: string[] }; - matched: CommandTopologyMatch | null; - noMatchMessage?: string; - noMatchNormalized?: string; - noMatchAttempted?: string[]; - noMatchHints?: string[]; -} - -export type DispatchSuccessFormat = 'json' | 'text' | undefined; - -export interface DispatchInputValidationResult { - queryArgs: string[]; - pickField?: string; - error?: QueryDispatchResult; -} - -export function dispatchFailure(error: QueryDispatchError, stderr: string[] = []): QueryDispatchResult { - return { - ok: false, - error, - stderr, - exit_code: error.code, - }; -} - -export function dispatchSuccess(stdout: string, stderr: string[] = []): QueryDispatchResult { - return { - ok: true, - stdout, - stderr, - exit_code: 0, - }; -} - -export function toDispatchFailure(error: QueryDispatchError, stderr: string[] = []): QueryDispatchResult { - return dispatchFailure(error, stderr); -} - -export function mapNativeDispatchError(error: unknown, command: string, args: string[]): QueryDispatchError { - return nativeDispatchErrorFromSignal(toFailureSignal(error), command, args); -} - -export function mapFallbackDispatchError(error: unknown, command: string, args: string[]): QueryDispatchError { - return fallbackDispatchErrorFromSignal(toFailureSignal(error), command, args); -} - -export function formatPick(data: unknown, pickField?: string): unknown { - if (!pickField) return data; - return extractField(data, pickField); -} - -export function formatSuccess(data: unknown, format: DispatchSuccessFormat, pickField?: string): string { - if (format === 'text' && typeof data === 'string') { - if (pickField) { - throw new Error('--pick is not supported for text output'); - } - return data.endsWith('\n') ? data : `${data}\n`; - } - const output = formatPick(data, pickField); - if (pickField && typeof output === 'string') { - return output.endsWith('\n') ? output : `${output}\n`; - } - return `${JSON.stringify(output === undefined ? null : output, null, 2)}\n`; -} - -export function validateQueryDispatchInput(queryArgv: string[]): DispatchInputValidationResult { - const queryArgs = [...queryArgv]; - const pickIdx = queryArgs.indexOf('--pick'); - if (pickIdx !== -1) { - if (pickIdx + 1 >= queryArgs.length) { - return { - queryArgs, - error: dispatchFailure(validationError({ - message: 'Error: --pick requires a field name', - details: { field: '--pick', reason: 'missing_value' }, - })), - }; - } - const pickField = queryArgs[pickIdx + 1]; - queryArgs.splice(pickIdx, 2); - if (queryArgs.length === 0 || !queryArgs[0]) { - return { - queryArgs, - error: dispatchFailure(validationError({ - message: 'Error: "gsd-sdk query" requires a command', - details: { reason: 'missing_command' }, - })), - }; - } - return { queryArgs, pickField }; - } - - if (queryArgs.length === 0 || !queryArgs[0]) { - return { - queryArgs, - error: dispatchFailure(validationError({ - message: 'Error: "gsd-sdk query" requires a command', - details: { reason: 'missing_command' }, - })), - }; - } - - return { queryArgs }; -} - -export function planQueryDispatch( - queryArgv: string[], - topology: CommandTopology, - cjsFallbackEnabled: boolean, -): DispatchPlan { - const queryCommand = queryArgv[0]; - if (!queryCommand) { - return { mode: 'error', normalized: { command: '', args: [], tokens: [] }, matched: null }; - } - - const [normCmd, normArgs] = normalizeQueryCommand(queryCommand, queryArgv.slice(1)); - const normalizedTokens = [normCmd, ...normArgs]; - const resolved = topology.resolve(queryArgv, !cjsFallbackEnabled); - - if (resolved.kind === 'match') { - return { mode: 'native', normalized: { command: normCmd, args: normArgs, tokens: normalizedTokens }, matched: resolved }; - } - - if (cjsFallbackEnabled) { - return { mode: 'cjs', normalized: { command: normCmd, args: normArgs, tokens: normalizedTokens }, matched: null }; - } - - return { - mode: 'error', - normalized: { command: normCmd, args: normArgs, tokens: normalizedTokens }, - matched: null, - noMatchMessage: resolved.message, - noMatchNormalized: resolved.normalized, - noMatchAttempted: resolved.attempted, - noMatchHints: resolved.hints, - }; -} - -function fail(error: ReturnType | ReturnType, stderr: string[] = []): QueryDispatchResult { - return toDispatchFailure(error, stderr); -} - -export async function runQueryDispatch(deps: QueryDispatchDeps, queryArgv: string[]): Promise { - const validated = validateQueryDispatchInput(queryArgv); - if (validated.error) return validated.error; - - const { queryArgs, pickField } = validated; - - const plan = planQueryDispatch(queryArgs, deps.topology, deps.cjsFallbackEnabled); - const normCmd = plan.normalized.command; - const normArgs = plan.normalized.args; - - if (!normCmd || !String(normCmd).trim()) { - return fail(validationError({ message: 'Error: "gsd-sdk query" requires a command', details: { reason: 'empty_normalized_command' } })); - } - - if (plan.mode === 'error') { - return fail(unknownCommandError({ - message: plan.noMatchMessage ?? `Error: Unknown command: "${queryArgs[0] ?? normCmd}"`, - normalized: plan.noMatchNormalized ?? [normCmd, ...normArgs].join(' ').trim(), - attempted: plan.noMatchAttempted ?? [], - hints: plan.noMatchHints ?? [], - })); - } - - if (plan.mode === 'cjs') { - if (canUseCjsFallback({ cjsFallbackEnabled: deps.cjsFallbackEnabled })) { - try { - const gsdPath = deps.resolveGsdToolsPath(deps.projectDir); - return await runCjsFallbackDispatch({ - projectDir: deps.projectDir, - gsdToolsPath: gsdPath, - normCmd, - normArgs, - ws: deps.ws, - pickField, - }); - } catch (e) { - return toDispatchFailure(mapFallbackDispatchError(e, normCmd, normArgs)); - } - } - return toDispatchFailure(mapFallbackDispatchError(new Error('CJS fallback denied by policy'), normCmd, normArgs)); - } - - const matched = plan.matched; - if (!matched) { - return toDispatchFailure(mapFallbackDispatchError(new Error('No native match in dispatch plan'), normCmd, normArgs)); - } - - // #3259: guard — if the invocation contains --help / -h AND the matched - // handler is a mutating command (mutation: true in the command manifest), - // short-circuit to a non-mutating stub. Mutating handlers are not help-aware - // by default (fail-closed). This prevents e.g. `milestone.complete --help` - // from writing milestone artifacts to disk. - const helpFlagPresent = matched.args.some((a) => a === '--help' || a === '-h'); - if (helpFlagPresent && matched.mutation) { - return dispatchSuccess( - formatSuccess( - { help: `Usage: gsd-sdk query ${matched.canonical} [args...]` }, - undefined, - ), - ); - } - - const dispatchNative = deps.nativeAdapter - ? (cmd: string, args: string[]) => deps.nativeAdapter!.dispatch(cmd, args) - : deps.dispatchNative; - - try { - const result = dispatchNative - ? await dispatchNative(matched.canonical, matched.args) - : await matched.adapter(matched.args, deps.projectDir, deps.ws); - return dispatchSuccess(formatSuccess(result.data, result.format, pickField)); - } catch (e) { - return toDispatchFailure(mapNativeDispatchError(e, matched.canonical, matched.args)); - } -} +export * from '../dispatch/hub.js';