2026-07-24 19:43:59 +08:00
/**
2026-08-04 10:07:17 +08:00
* Commander adapter for the `dsh` command-line entry. The default command
2026-08-06 04:40:32 +08:00
* boots a named profile (`--profile <name>`), optionally with extra `--patch`
2026-08-08 02:07:11 +08:00
* overlays. `run` owns one-shot task execution, defaulting to the headless
* profile; `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.
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 04:40:32 +08:00
/** 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 [ ]
2026-08-08 02:07:11 +08:00
}
/** Run one task through a profile mounting the headless runner. */
interface RunInvocation {
mode : 'run'
profile : string
/** Extra patch-list overlays applied after the profile's own layer, in argv order. */
patches : string [ ]
/** Non-blank task text joined from the variadic positional arguments. */
task : 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-07-29 15:28:38 +08:00
/**
2026-08-06 04:40:32 +08:00
* 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.
2026-07-25 15:03:17 +08:00
*/
2026-07-24 19:43:59 +08:00
interface WebInvocation {
mode : 'web'
2026-08-06 04:40:32 +08:00
patches : string [ ]
2026-07-25 15:03:17 +08:00
host? : string
port? : number
2026-07-25 12:02:28 +08:00
dev : boolean
2026-07-25 18:05:39 +08:00
workspaceRoot? : string
2026-08-04 10:07:17 +08:00
/** Extra authorities for the /api browser-trust fence. */
2026-07-28 15:40:02 +08:00
trustedHosts? : 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-08 02:07:11 +08:00
export type DshInvocation = ProfileInvocation | RunInvocation | DumpConfigInvocation | WebInvocation | PluginInvocation
2026-07-24 19:43:59 +08:00
2026-07-25 16:19:02 +08:00
/** Raw web-subcommand options straight from Commander. */
2026-07-25 15:47:55 +08:00
interface WebOptions {
2026-08-06 04:40:32 +08:00
patch? : string [ ]
2026-07-25 15:47:55 +08:00
host? : string
port? : string
dev? : boolean
2026-07-25 18:05:39 +08:00
workspaceRoot? : string
2026-07-28 15:40:02 +08:00
trustedHost? : string [ ]
2026-07-31 02:00:01 +08:00
dumpConfig? : boolean
dumpDefaultConfig? : boolean
}
2026-08-08 02:07:11 +08:00
/** Raw run-subcommand options straight from Commander. */
interface RunOptions {
2026-08-08 15:05:50 +08:00
profile : string
2026-08-08 02:07:11 +08:00
patch? : string [ ]
}
2026-08-06 04:40:32 +08:00
/**
* 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 ]
2026-07-24 19:43:59 +08:00
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
const program = new Command ( )
. 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-07-29 15:28:38 +08:00
. addHelpText ( 'after' , `
Examples:
2026-08-06 04:40:32 +08:00
dsh --profile web boot the web profile (same as: dsh web)
2026-08-08 02:07:11 +08:00
dsh run "run the tests" answer one task, print the result, and exit
dsh run --profile custom "run the tests" run one task through a custom one-shot profile
2026-08-06 04:40:32 +08:00
dsh --profile tui --patch ./extra.yml boot a custom profile with one extra overlay
dsh plugin --profile tui add <package> install a plugin into the tui profile
dsh web --port 8080 the web alias with its flag family
2026-07-29 15:28:38 +08:00
` )
2026-07-25 15:47:55 +08:00
. exitOverride ( )
2026-07-30 14:56:39 +08:00
. enablePositionalOptions ( )
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-08 02:07:11 +08:00
. action ( ( options : {
2026-08-06 04:40:32 +08:00
profile? : string
patch? : string [ ]
2026-07-31 02:00:01 +08:00
dumpConfig? : boolean
dumpDefaultConfig? : boolean
} ) = > {
2026-08-06 04:40:32 +08:00
const profile = options . profile ? ? program . error ( 'error: --profile <name> 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' )
2026-07-31 02:00:01 +08:00
}
2026-08-06 04:40:32 +08:00
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 }
2026-07-31 02:00:01 +08:00
return
}
2026-08-08 02:07:11 +08:00
resolved = { mode : 'profile' , profile , patches }
2026-07-25 15:47:55 +08:00
} )
2026-08-04 10:07:17 +08:00
/** Reject parent options that crossed a subcommand boundary. */
2026-07-29 15:28:38 +08:00
const rejectParentOptions = ( command : string ) : void = > {
2026-07-31 02:00:01 +08:00
const parent = program . opts < {
2026-08-06 04:40:32 +08:00
profile? : string
patch? : string [ ]
2026-07-31 02:00:01 +08:00
dumpConfig? : boolean
dumpDefaultConfig? : boolean
} > ( )
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-08 02:07:11 +08:00
const run = program . command ( 'run' ) . description ( 'run one task through a profile mounting the headless runner' )
run
. option ( '--profile <name>' , 'one-shot profile under $DSH_HOME/profiles' , 'headless' )
. option ( '--patch <path>' , 'extra patch-list overlay applied after the profile layer (repeatable)' , collect )
. argument ( '<task...>' , 'task text' )
. action ( ( task : string [ ] , options : RunOptions ) = > {
rejectParentOptions ( 'run' )
2026-08-08 15:05:50 +08:00
const profile = options . profile
2026-08-08 02:07:11 +08:00
if ( profile === '' ) program . error ( 'error: --profile needs a name' )
const patches = options . patch ? ? [ ]
if ( patches . includes ( '' ) ) program . error ( 'error: --patch needs a path' )
const joined = task . join ( ' ' )
if ( joined . trim ( ) === '' ) program . error ( 'error: run needs a non-blank task' )
resolved = { mode : 'run' , profile , patches , task : joined }
} )
2026-08-06 04:40:32 +08:00
const web = program . command ( 'web' ) . description ( 'serve the browser UI (alias of --profile web) on the configured host and port' )
2026-07-25 15:47:55 +08:00
web
2026-08-06 04:40:32 +08:00
. option ( '--patch <path>' , 'extra patch-list overlay applied after the profile layer (repeatable)' , collect )
2026-07-29 15:28:38 +08:00
. option ( '--host <host>' , 'bind host; pass 0.0.0.0 to reach it from another machine' )
. option ( '--port <port>' , 'listen port; pass 0 to let the OS pick a free one' )
2026-07-29 11:22:48 +08:00
. option ( '--dev' , 'mount the client-plugin HMR receiver (run pnpm run dev:web separately to rebuild bundles)' )
2026-07-29 15:28:38 +08:00
. option ( '--workspace-root <path>' , 'parent directory for workspaces created from the browser UI' )
2026-07-28 15:40:02 +08:00
. option ( '--trusted-host <authority...>' , 'extra authority the /api browser-trust fence accepts (host or host:port; repeatable)' )
2026-08-06 04:40:32 +08:00
. 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-07-25 17:24:39 +08:00
. action ( ( options : WebOptions ) = > {
2026-07-29 15:28:38 +08:00
rejectParentOptions ( 'web' )
2026-08-06 04:40:32 +08:00
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' )
}
2026-08-06 09:27:44 +08:00
// 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)' )
}
2026-08-06 04:40:32 +08:00
resolved = { mode : 'dump-config' , profile : 'web' , defaultOnly , patches }
2026-07-31 02:00:01 +08:00
return
2026-07-25 17:24:39 +08:00
}
2026-08-06 09:27:44 +08:00
if ( options . port !== undefined && ! /^\d+$/ . test ( options . port ) ) {
program . error ( ` error: --port must be a number, got ${ JSON . stringify ( options . port ) } ` )
}
2026-08-06 04:40:32 +08:00
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 <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
}