2026-07-24 19:43:59 +08:00
/**
2026-08-06 20:52:26 +08:00
* 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
2026-08-10 21:49:11 +08:00
* its own flags** to the booted tree verbatim, where injected app plugins parse
* their own flag families and print their own `--help` (see
2026-08-06 20:52:26 +08:00
* `@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.
2026-07-24 19:43:59 +08:00
* @module @deepseek-ai/dsh/args
*/
2026-07-25 14:15:25 +08:00
import { Command , CommanderError } from 'commander'
2026-07-24 19:43:59 +08:00
2026-08-06 20:52:26 +08:00
/** Boot a named profile and hand it the invocation's inner arguments. */
2026-08-06 04:40:32 +08:00
interface ProfileInvocation {
mode : 'profile'
profile : string
/** Extra patch-list overlays applied after the profile's own layer, in argv order. */
patches : string [ ]
2026-08-10 21:49:11 +08:00
/** Everything after the launcher's own flags, verbatim, for injected app plugins. */
2026-08-06 20:52:26 +08:00
args : string [ ]
2026-07-24 19:43:59 +08:00
}
2026-08-06 04:40:32 +08:00
/** Print a composed profile tree and exit without booting. */
2026-07-31 02:00:01 +08:00
interface DumpConfigInvocation {
mode : 'dump-config'
2026-08-06 04:40:32 +08:00
profile : string
/** Omit the profile's user layer and --patch overlays; print bundle layers only. */
2026-07-31 02:00:01 +08:00
defaultOnly : boolean
2026-08-06 04:40:32 +08:00
patches : string [ ]
2026-07-24 19:43:59 +08:00
}
2026-08-06 04:40:32 +08:00
/** 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 [ ]
}
2026-08-04 10:07:17 +08:00
/** The resolved `dsh` invocation. Help, version, and errors exit inside {@link parseDshArgs}. */
2026-08-06 20:52:26 +08:00
export type DshInvocation = ProfileInvocation | DumpConfigInvocation | PluginInvocation
2026-07-24 19:43:59 +08:00
2026-08-06 20:52:26 +08:00
/** Launcher flags shared by the default command and the `web` alias. */
interface BootOptions {
2026-08-06 04:40:32 +08:00
patch? : string [ ]
2026-07-31 02:00:01 +08:00
dumpConfig? : boolean
dumpDefaultConfig? : boolean
}
2026-08-06 04:40:32 +08:00
/**
* Repeatable single-value collector: `--patch a.yml --patch b.yml`. Never
2026-08-06 20:52:26 +08:00
* variadic — a variadic `--patch` would swallow the inner arguments.
2026-08-06 04:40:32 +08:00
*/
const collect = ( value : string , previous : string [ ] = [ ] ) : string [ ] = > [ . . . previous , value ]
2026-07-24 19:43:59 +08:00
2026-08-06 20:52:26 +08:00
/** 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 <session> 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 <package> 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' )
}
2026-08-10 21:49:11 +08:00
// 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
2026-08-06 20:52:26 +08:00
// 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 }
}
2026-07-24 20:01:38 +08:00
/**
2026-08-04 10:07:17 +08:00
* 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.
2026-07-24 20:01:38 +08:00
*/
2026-08-04 10:07:17 +08:00
export function parseDshArgs ( argv : readonly string [ ] , version : string ) : DshInvocation {
2026-07-25 15:47:55 +08:00
let resolved : DshInvocation | undefined
2026-08-06 20:52:26 +08:00
// 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
2026-07-25 15:47:55 +08:00
. name ( 'dsh' )
. version ( version , '-V, --version' , 'output the version number' )
2026-08-06 04:40:32 +08:00
. description ( 'dsh: boot a DeepSeek Harness profile — an ordered stack of plugin-bundle patch layers under your own overrides.' )
2026-08-06 20:52:26 +08:00
. addHelpText ( 'after' , HELP_EXAMPLES )
2026-07-25 15:47:55 +08:00
. exitOverride ( )
2026-08-06 20:52:26 +08:00
// 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 ( )
2026-07-30 14:56:39 +08:00
. enablePositionalOptions ( )
2026-08-06 20:52:26 +08:00
. argument ( '[args...]' , 'arguments for the booted profile\'s app (see: dsh --profile <name> --help)' )
2026-08-06 04:40:32 +08:00
. option ( '--profile <name>' , 'the profile under $DSH_HOME/profiles to boot' )
. option ( '--patch <path>' , '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' )
2026-08-06 20:52:26 +08:00
. 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 <name> is required' )
2026-07-31 02:00:01 +08:00
}
2026-08-06 20:52:26 +08:00
const profile = options . profile
if ( profile === '' ) program . error ( 'error: --profile needs a name' )
resolved = resolveBoot ( program , profile , options , args )
2026-07-25 15:47:55 +08:00
} )
2026-08-09 15:27:21 +08:00
/** Reject parent options supplied before a subcommand. */
2026-07-29 15:28:38 +08:00
const rejectParentOptions = ( command : string ) : void = > {
2026-08-06 20:52:26 +08:00
const parent = program . opts < BootOptions & { profile ? : string } > ( )
2026-08-06 04:40:32 +08:00
if ( parent . profile !== undefined || parent . patch !== undefined
2026-07-31 02:00:01 +08:00
|| parent . dumpConfig !== undefined || parent . dumpDefaultConfig !== undefined ) {
2026-08-06 04:40:32 +08:00
program . error ( ` error: ${ command } takes none of parent --profile, --patch, --dump-config, or --dump-default-config ` )
2026-07-29 15:28:38 +08:00
}
}
2026-08-06 20:52:26 +08:00
const web = program . command ( 'web' ) . description ( 'boot the web profile (alias of --profile web); the web app\'s own flags follow' )
2026-07-25 15:47:55 +08:00
web
2026-08-06 20:52:26 +08:00
. helpOption ( false )
. allowUnknownOption ( )
. passThroughOptions ( )
. enablePositionalOptions ( )
. argument ( '[args...]' , 'arguments for the web app (see: dsh web --help)' )
2026-08-06 04:40:32 +08:00
. option ( '--patch <path>' , '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' )
2026-08-06 20:52:26 +08:00
. action ( ( args : string [ ] , options : BootOptions ) = > {
2026-07-29 15:28:38 +08:00
rejectParentOptions ( 'web' )
2026-08-06 20:52:26 +08:00
resolved = resolveBoot ( web , 'web' , options , args )
2026-08-06 04:40:32 +08:00
} )
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 <name>' , 'the profile whose plugins to manage (initialized on first use)' )
. allowUnknownOption ( )
. argument ( '[args...]' , 'pnpm arguments, forwarded verbatim (add <pkg>, remove <pkg>, why <pkg>, ...)' )
. 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 <package>)' )
resolved = { mode : 'plugin' , profile : options.profile , args }
2026-07-25 17:24:39 +08:00
} )
2026-07-25 15:47:55 +08:00
2026-07-25 14:15:25 +08:00
try {
2026-07-25 15:47:55 +08:00
program . parse ( argv , { from : 'user' } )
2026-07-25 14:15:25 +08:00
} catch ( error ) {
return process . exit ( error instanceof CommanderError ? error.exitCode : 1 )
}
2026-08-04 10:07:17 +08:00
/* v8 ignore next -- an action resolves or Commander throws */
2026-07-25 15:47:55 +08:00
if ( resolved === undefined ) throw new Error ( 'dsh: no invocation resolved' )
return resolved
2026-07-24 19:43:59 +08:00
}