/** * Commander adapter for the `dsh` command-line entry. The default command * boots a named profile (`--profile `), optionally with extra `--patch` * overlays and a positional task (one-shot mode for profiles mounting the * headless runner). `web` is a hardcoded alias for `--profile web` that adds * the Web flag family; `plugin` manages a profile's plugin dependencies by * forwarding to pnpm. Commander owns help, version, and parse errors. * @module @deepseek-ai/dsh/args */ import { Command, CommanderError } from 'commander' /** Boot a named profile. */ interface ProfileInvocation { mode: 'profile' profile: string /** Extra patch-list overlays applied after the profile's own layer, in argv order. */ patches: string[] /** Positional task text joined by spaces; non-empty only for one-shot runs. */ task?: string } /** Print a composed profile tree and exit without booting. */ interface DumpConfigInvocation { mode: 'dump-config' profile: string /** Omit the profile's user layer and --patch overlays; print bundle layers only. */ defaultOnly: boolean patches: string[] } /** * Browser UI: `dsh web` (alias of `--profile web`). Host and port remain * unvalidated pass-throughs to the webserver schema; absent values leave the * shipped web bundle values intact. */ interface WebInvocation { mode: 'web' patches: string[] host?: string port?: number dev: boolean workspaceRoot?: string /** Extra authorities for the /api browser-trust fence. */ trustedHosts?: string[] } /** Manage a profile's plugins: forward `args` to pnpm inside the profile directory. */ interface PluginInvocation { mode: 'plugin' profile: string /** Raw pnpm arguments, verbatim. */ args: string[] } /** The resolved `dsh` invocation. Help, version, and errors exit inside {@link parseDshArgs}. */ export type DshInvocation = ProfileInvocation | DumpConfigInvocation | WebInvocation | PluginInvocation /** Raw web-subcommand options straight from Commander. */ interface WebOptions { patch?: string[] host?: string port?: string dev?: boolean workspaceRoot?: string trustedHost?: string[] dumpConfig?: boolean dumpDefaultConfig?: boolean } /** * Repeatable single-value collector: `--patch a.yml --patch b.yml`. Never * variadic — a variadic `--patch` would swallow a following positional task. */ const collect = (value: string, previous: string[] = []): string[] => [...previous, value] /** * Resolve argv into one invocation, or print and exit for help, version, or an * error. * @param argv - arguments after the Node binary and script. * @param version - version string printed by `--version`. * @returns the resolved invocation. */ export function parseDshArgs(argv: readonly string[], version: string): DshInvocation { let resolved: DshInvocation | undefined const program = new Command() .name('dsh') .version(version, '-V, --version', 'output the version number') .description('dsh: boot a DeepSeek Harness profile — an ordered stack of plugin-bundle patch layers under your own overrides.') .addHelpText('after', ` Examples: dsh --profile web boot the web profile (same as: dsh web) dsh --profile headless "run the tests" answer one task, print the result, and exit dsh --profile tui --patch ./extra.yml boot a custom profile with one extra overlay dsh plugin --profile tui add install a plugin into the tui profile dsh web --port 8080 the web alias with its flag family `) .exitOverride() .enablePositionalOptions() .argument('[task...]', 'one-shot task text for profiles mounting the headless runner') .option('--profile ', 'the profile under $DSH_HOME/profiles to boot') .option('--patch ', 'extra patch-list overlay applied after the profile layer (repeatable)', collect) .option('--dump-config', 'print the composed profile tree and exit') .option('--dump-default-config', 'print the profile tree without its user layer or --patch overlays and exit') .action((task: string[], options: { profile?: string patch?: string[] dumpConfig?: boolean dumpDefaultConfig?: boolean }) => { const profile = options.profile ?? program.error('error: --profile is required') if (profile === '') program.error('error: --profile needs a name') const patches = options.patch ?? [] if (patches.includes('')) program.error('error: --patch needs a path') if (options.dumpConfig === true || options.dumpDefaultConfig === true) { if (options.dumpConfig === true && options.dumpDefaultConfig === true) { program.error('error: --dump-config and --dump-default-config are mutually exclusive') } if (task.length > 0) program.error('error: --dump-config/--dump-default-config take no task') const defaultOnly = options.dumpDefaultConfig === true if (defaultOnly && patches.length > 0) { program.error('error: --dump-default-config prints the bundle layers and takes no --patch') } resolved = { mode: 'dump-config', profile, defaultOnly, patches } return } resolved = { mode: 'profile', profile, patches, ...task.length > 0 ? { task: task.join(' ') } : {}, } }) /** Reject parent options that crossed a subcommand boundary. */ const rejectParentOptions = (command: string): void => { const parent = program.opts<{ profile?: string patch?: string[] dumpConfig?: boolean dumpDefaultConfig?: boolean }>() if (parent.profile !== undefined || parent.patch !== undefined || parent.dumpConfig !== undefined || parent.dumpDefaultConfig !== undefined) { program.error(`error: ${command} takes none of parent --profile, --patch, --dump-config, or --dump-default-config`) } } const web = program.command('web').description('serve the browser UI (alias of --profile web) on the configured host and port') web .option('--patch ', 'extra patch-list overlay applied after the profile layer (repeatable)', collect) .option('--host ', 'bind host; pass 0.0.0.0 to reach it from another machine') .option('--port ', 'listen port; pass 0 to let the OS pick a free one') .option('--dev', 'mount the client-plugin HMR receiver (run pnpm run dev:web separately to rebuild bundles)') .option('--workspace-root ', 'parent directory for workspaces created from the browser UI') .option('--trusted-host ', 'extra authority the /api browser-trust fence accepts (host or host:port; repeatable)') .option('--dump-config', 'print the composed web-profile tree (with the user layer and any --patch) and exit') .option('--dump-default-config', 'print the web profile\'s bundle layers (no user layer) and exit') .action((options: WebOptions) => { rejectParentOptions('web') const patches = options.patch ?? [] if (patches.includes('')) program.error('error: --patch needs a path') if (options.dumpConfig === true || options.dumpDefaultConfig === true) { if (options.dumpConfig === true && options.dumpDefaultConfig === true) { program.error('error: --dump-config and --dump-default-config are mutually exclusive') } const defaultOnly = options.dumpDefaultConfig === true if (defaultOnly && patches.length > 0) { program.error('error: --dump-default-config prints the bundle layers and takes no --patch') } // The dump is boot-free and does not derive flag patches; silently // dropping them would print a tree that differs from the same // invocation's boot. if (options.host !== undefined || options.port !== undefined || options.dev === true || options.workspaceRoot !== undefined || options.trustedHost !== undefined) { program.error('error: config dumps take no web flags (--host/--port/--dev/--workspace-root/--trusted-host)') } resolved = { mode: 'dump-config', profile: 'web', defaultOnly, patches } return } if (options.port !== undefined && !/^\d+$/.test(options.port)) { program.error(`error: --port must be a number, got ${JSON.stringify(options.port)}`) } resolved = { mode: 'web', patches, ...options.host !== undefined && { host: options.host }, ...options.port !== undefined && { port: Number(options.port) }, dev: options.dev === true, ...options.workspaceRoot !== undefined && { workspaceRoot: options.workspaceRoot }, ...options.trustedHost !== undefined && { trustedHosts: options.trustedHost }, } }) const plugin = program.command('plugin').description('manage a profile\'s plugins by forwarding the remaining arguments to pnpm in the profile directory') plugin .requiredOption('--profile ', 'the profile whose plugins to manage (initialized on first use)') .allowUnknownOption() .argument('[args...]', 'pnpm arguments, forwarded verbatim (add , remove , why , ...)') .action((args: string[], options: { profile: string }) => { rejectParentOptions('plugin') if (options.profile === '') program.error('error: --profile needs a name') if (args.length === 0) program.error('error: plugin needs pnpm arguments to forward (e.g. add )') resolved = { mode: 'plugin', profile: options.profile, args } }) try { program.parse(argv, { from: 'user' }) } catch (error) { return process.exit(error instanceof CommanderError ? error.exitCode : 1) } /* v8 ignore next -- an action resolves or Commander throws */ if (resolved === undefined) throw new Error('dsh: no invocation resolved') return resolved }