/** * Commander adapter for the `dsh` command line. * * The launcher parses only what it owns — which profile to boot, which extra * patch overlays to apply, and the config dumps — and hands **everything after * its own flags** to the booted tree verbatim, where injected app plugins parse * their own flag families and print their own `--help` (see * `@deepseek-ai/dsh-cmdline`). Launcher flags therefore come first: the first * token this parser does not recognize starts the inner arguments, so * `dsh --profile tui --resume abc` boots the tui profile with `--resume abc`, * and `dsh --profile web -h` prints the web app's help, not this one's. * * `web` is a hardcoded alias for `--profile web`; `plugin` manages a profile's * plugin dependencies by forwarding to pnpm. * @module @deepseek-ai/dsh/args */ import { Command, CommanderError } from 'commander' /** Boot a named profile and hand it the invocation's inner arguments. */ interface ProfileInvocation { mode: 'profile' profile: string /** Extra patch-list overlays applied after the profile's own layer, in argv order. */ patches: string[] /** Everything after the launcher's own flags, verbatim, for injected app plugins. */ args: 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[] } /** 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 | PluginInvocation /** Launcher flags shared by the default command and the `web` alias. */ interface BootOptions { patch?: string[] dumpConfig?: boolean dumpDefaultConfig?: boolean } /** * Repeatable single-value collector: `--patch a.yml --patch b.yml`. Never * variadic — a variadic `--patch` would swallow the inner arguments. */ const collect = (value: string, previous: string[] = []): string[] => [...previous, value] /** The launcher's own help text; each app prints its own. */ const HELP_EXAMPLES = ` 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 --profile tui --resume arguments after the launcher flags reach the app dsh --profile web --help the web app's own flags and help dsh plugin --profile tui add install a plugin into the tui profile ` /** * Resolve a boot or dump invocation from the launcher flags and the leftover * inner arguments. * @param program - the command whose options were parsed (the root, or the `web` alias). * @param profile - the profile these flags boot. * @param options - the launcher flags commander collected. * @param args - the leftover arguments, in argv order. * @returns the resolved invocation. */ function resolveBoot(program: Command, profile: string, options: BootOptions, args: string[]): DshInvocation { const patches = options.patch ?? [] if (patches.includes('')) program.error('error: --patch needs a path') if (options.dumpConfig !== true && options.dumpDefaultConfig !== true) { return { mode: 'profile', profile, patches, args } } if (options.dumpConfig === true && options.dumpDefaultConfig === true) { program.error('error: --dump-config and --dump-default-config are mutually exclusive') } // The dump is boot-free: it never runs app command-line providers, so it // cannot show what those flags would decide, and printing a tree that differs // from the same invocation's boot would mislead. if (args.length > 0) { program.error(`error: config dumps take no app arguments, got ${args.map(argument => JSON.stringify(argument)).join(' ')}`) } const defaultOnly = options.dumpDefaultConfig === true if (defaultOnly && patches.length > 0) { program.error('error: --dump-default-config prints the bundle layers and takes no --patch') } return { mode: 'dump-config', profile, defaultOnly, patches } } /** * 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 // Annotated, not inferred: the actions below call back into `program`, and an // inferred type would be circular through its own chain. const program: Command = new Command() program .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', HELP_EXAMPLES) .exitOverride() // The launcher's flags come first and end at the first token it does not // know; everything from there on belongs to the booted app, including // its -h. `dsh -h` with no profile still prints this help, below. .helpOption(false) .allowUnknownOption() .passThroughOptions() .enablePositionalOptions() .argument('[args...]', 'arguments for the booted profile\'s app (see: dsh --profile --help)') .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((args: string[], options: BootOptions & { profile?: string }) => { // With the app owning -h, the launcher's own help is what a bare // `dsh -h` (no profile to hand it to) must print. if (options.profile === undefined) { if (args.some(argument => argument === '-h' || argument === '--help')) program.help() program.error('error: --profile is required') } const profile = options.profile if (profile === '') program.error('error: --profile needs a name') resolved = resolveBoot(program, profile, options, args) }) /** Reject parent options supplied before a subcommand. */ const rejectParentOptions = (command: string): void => { const parent = program.opts() 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('boot the web profile (alias of --profile web); the web app\'s own flags follow') web .helpOption(false) .allowUnknownOption() .passThroughOptions() .enablePositionalOptions() .argument('[args...]', 'arguments for the web app (see: dsh web --help)') .option('--patch ', 'extra patch-list overlay applied after the profile layer (repeatable)', collect) .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((args: string[], options: BootOptions) => { rejectParentOptions('web') resolved = resolveBoot(web, 'web', options, args) }) 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 }