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-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. */
refactor(cli): unify the arg grammar — one program, --config flag, real web subcommand
Drop the bare `dsh <config>` positional in favor of a `--config <path>` flag.
Without a root positional, `web` can be a real Commander subcommand in one
program instead of the reserved-first-token dispatch to a second parser, so
`dsh --help` lists every mode natively (no hand-pasted command text) and the
second parser + reserved-token machinery are gone.
Grammar:
dsh TUI (shipped tree + ~/.dsh overlay)
dsh --config <path> TUI, alternate tree (demos/tests only)
dsh --resume <id> TUI, resume a session
dsh -p "task" headless one-shot
dsh web [--host --port --dev]
`dsh` is the product front door with no positional; `--config` exists only so
demo:cordis, demo:code-mode, and the keyless PTY smokes can point the shipped
bin at an example tree. Those three sites and the /resume re-exec argv move to
`--config <path>`. The `-p` + `--config`/`--resume` mode-mixing guard and the
cordis.yml-owns-host/port-default fix are preserved.
Agent Note + Chinese pair, README, tui.ts docs updated. All 13 PTY smokes
(including code-mode via --config and the exec-replace resume handoff) green.
2026-07-25 15:47:55 +08:00
interface WebOptions {
2026-08-06 04:40:32 +08:00
patch? : string [ ]
refactor(cli): unify the arg grammar — one program, --config flag, real web subcommand
Drop the bare `dsh <config>` positional in favor of a `--config <path>` flag.
Without a root positional, `web` can be a real Commander subcommand in one
program instead of the reserved-first-token dispatch to a second parser, so
`dsh --help` lists every mode natively (no hand-pasted command text) and the
second parser + reserved-token machinery are gone.
Grammar:
dsh TUI (shipped tree + ~/.dsh overlay)
dsh --config <path> TUI, alternate tree (demos/tests only)
dsh --resume <id> TUI, resume a session
dsh -p "task" headless one-shot
dsh web [--host --port --dev]
`dsh` is the product front door with no positional; `--config` exists only so
demo:cordis, demo:code-mode, and the keyless PTY smokes can point the shipped
bin at an example tree. Those three sites and the /resume re-exec argv move to
`--config <path>`. The `-p` + `--config`/`--resume` mode-mixing guard and the
cordis.yml-owns-host/port-default fix are preserved.
Agent Note + Chinese pair, README, tui.ts docs updated. All 13 PTY smokes
(including code-mode via --config and the exec-replace resume handoff) green.
2026-07-25 15:47:55 +08:00
host? : string
port? : string
dev? : boolean
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 {
refactor(cli): unify the arg grammar — one program, --config flag, real web subcommand
Drop the bare `dsh <config>` positional in favor of a `--config <path>` flag.
Without a root positional, `web` can be a real Commander subcommand in one
program instead of the reserved-first-token dispatch to a second parser, so
`dsh --help` lists every mode natively (no hand-pasted command text) and the
second parser + reserved-token machinery are gone.
Grammar:
dsh TUI (shipped tree + ~/.dsh overlay)
dsh --config <path> TUI, alternate tree (demos/tests only)
dsh --resume <id> TUI, resume a session
dsh -p "task" headless one-shot
dsh web [--host --port --dev]
`dsh` is the product front door with no positional; `--config` exists only so
demo:cordis, demo:code-mode, and the keyless PTY smokes can point the shipped
bin at an example tree. Those three sites and the /resume re-exec argv move to
`--config <path>`. The `-p` + `--config`/`--resume` mode-mixing guard and the
cordis.yml-owns-host/port-default fix are preserved.
Agent Note + Chinese pair, README, tui.ts docs updated. All 13 PTY smokes
(including code-mode via --config and the exec-replace resume handoff) green.
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
` )
refactor(cli): unify the arg grammar — one program, --config flag, real web subcommand
Drop the bare `dsh <config>` positional in favor of a `--config <path>` flag.
Without a root positional, `web` can be a real Commander subcommand in one
program instead of the reserved-first-token dispatch to a second parser, so
`dsh --help` lists every mode natively (no hand-pasted command text) and the
second parser + reserved-token machinery are gone.
Grammar:
dsh TUI (shipped tree + ~/.dsh overlay)
dsh --config <path> TUI, alternate tree (demos/tests only)
dsh --resume <id> TUI, resume a session
dsh -p "task" headless one-shot
dsh web [--host --port --dev]
`dsh` is the product front door with no positional; `--config` exists only so
demo:cordis, demo:code-mode, and the keyless PTY smokes can point the shipped
bin at an example tree. Those three sites and the /resume re-exec argv move to
`--config <path>`. The `-p` + `--config`/`--resume` mode-mixing guard and the
cordis.yml-owns-host/port-default fix are preserved.
Agent Note + Chinese pair, README, tui.ts docs updated. All 13 PTY smokes
(including code-mode via --config and the exec-replace resume handoff) green.
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 }
refactor(cli): unify the arg grammar — one program, --config flag, real web subcommand
Drop the bare `dsh <config>` positional in favor of a `--config <path>` flag.
Without a root positional, `web` can be a real Commander subcommand in one
program instead of the reserved-first-token dispatch to a second parser, so
`dsh --help` lists every mode natively (no hand-pasted command text) and the
second parser + reserved-token machinery are gone.
Grammar:
dsh TUI (shipped tree + ~/.dsh overlay)
dsh --config <path> TUI, alternate tree (demos/tests only)
dsh --resume <id> TUI, resume a session
dsh -p "task" headless one-shot
dsh web [--host --port --dev]
`dsh` is the product front door with no positional; `--config` exists only so
demo:cordis, demo:code-mode, and the keyless PTY smokes can point the shipped
bin at an example tree. Those three sites and the /resume re-exec argv move to
`--config <path>`. The `-p` + `--config`/`--resume` mode-mixing guard and the
cordis.yml-owns-host/port-default fix are preserved.
Agent Note + Chinese pair, README, tui.ts docs updated. All 13 PTY smokes
(including code-mode via --config and the exec-replace resume handoff) green.
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-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' )
refactor(cli): unify the arg grammar — one program, --config flag, real web subcommand
Drop the bare `dsh <config>` positional in favor of a `--config <path>` flag.
Without a root positional, `web` can be a real Commander subcommand in one
program instead of the reserved-first-token dispatch to a second parser, so
`dsh --help` lists every mode natively (no hand-pasted command text) and the
second parser + reserved-token machinery are gone.
Grammar:
dsh TUI (shipped tree + ~/.dsh overlay)
dsh --config <path> TUI, alternate tree (demos/tests only)
dsh --resume <id> TUI, resume a session
dsh -p "task" headless one-shot
dsh web [--host --port --dev]
`dsh` is the product front door with no positional; `--config` exists only so
demo:cordis, demo:code-mode, and the keyless PTY smokes can point the shipped
bin at an example tree. Those three sites and the /resume re-exec argv move to
`--config <path>`. The `-p` + `--config`/`--resume` mode-mixing guard and the
cordis.yml-owns-host/port-default fix are preserved.
Agent Note + Chinese pair, README, tui.ts docs updated. All 13 PTY smokes
(including code-mode via --config and the exec-replace resume handoff) green.
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-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
2026-08-07 14:26:50 +08:00
|| options . trustedHost !== undefined ) {
program . error ( 'error: config dumps take no web flags (--host/--port/--dev/--trusted-host)' )
2026-08-06 09:27:44 +08:00
}
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 . 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
} )
refactor(cli): unify the arg grammar — one program, --config flag, real web subcommand
Drop the bare `dsh <config>` positional in favor of a `--config <path>` flag.
Without a root positional, `web` can be a real Commander subcommand in one
program instead of the reserved-first-token dispatch to a second parser, so
`dsh --help` lists every mode natively (no hand-pasted command text) and the
second parser + reserved-token machinery are gone.
Grammar:
dsh TUI (shipped tree + ~/.dsh overlay)
dsh --config <path> TUI, alternate tree (demos/tests only)
dsh --resume <id> TUI, resume a session
dsh -p "task" headless one-shot
dsh web [--host --port --dev]
`dsh` is the product front door with no positional; `--config` exists only so
demo:cordis, demo:code-mode, and the keyless PTY smokes can point the shipped
bin at an example tree. Those three sites and the /resume re-exec argv move to
`--config <path>`. The `-p` + `--config`/`--resume` mode-mixing guard and the
cordis.yml-owns-host/port-default fix are preserved.
Agent Note + Chinese pair, README, tui.ts docs updated. All 13 PTY smokes
(including code-mode via --config and the exec-replace resume handoff) green.
2026-07-25 15:47:55 +08:00
2026-07-25 14:15:25 +08:00
try {
refactor(cli): unify the arg grammar — one program, --config flag, real web subcommand
Drop the bare `dsh <config>` positional in favor of a `--config <path>` flag.
Without a root positional, `web` can be a real Commander subcommand in one
program instead of the reserved-first-token dispatch to a second parser, so
`dsh --help` lists every mode natively (no hand-pasted command text) and the
second parser + reserved-token machinery are gone.
Grammar:
dsh TUI (shipped tree + ~/.dsh overlay)
dsh --config <path> TUI, alternate tree (demos/tests only)
dsh --resume <id> TUI, resume a session
dsh -p "task" headless one-shot
dsh web [--host --port --dev]
`dsh` is the product front door with no positional; `--config` exists only so
demo:cordis, demo:code-mode, and the keyless PTY smokes can point the shipped
bin at an example tree. Those three sites and the /resume re-exec argv move to
`--config <path>`. The `-p` + `--config`/`--resume` mode-mixing guard and the
cordis.yml-owns-host/port-default fix are preserved.
Agent Note + Chinese pair, README, tui.ts docs updated. All 13 PTY smokes
(including code-mode via --config and the exec-replace resume handoff) green.
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 */
refactor(cli): unify the arg grammar — one program, --config flag, real web subcommand
Drop the bare `dsh <config>` positional in favor of a `--config <path>` flag.
Without a root positional, `web` can be a real Commander subcommand in one
program instead of the reserved-first-token dispatch to a second parser, so
`dsh --help` lists every mode natively (no hand-pasted command text) and the
second parser + reserved-token machinery are gone.
Grammar:
dsh TUI (shipped tree + ~/.dsh overlay)
dsh --config <path> TUI, alternate tree (demos/tests only)
dsh --resume <id> TUI, resume a session
dsh -p "task" headless one-shot
dsh web [--host --port --dev]
`dsh` is the product front door with no positional; `--config` exists only so
demo:cordis, demo:code-mode, and the keyless PTY smokes can point the shipped
bin at an example tree. Those three sites and the /resume re-exec argv move to
`--config <path>`. The `-p` + `--config`/`--resume` mode-mixing guard and the
cordis.yml-owns-host/port-default fix are preserved.
Agent Note + Chinese pair, README, tui.ts docs updated. All 13 PTY smokes
(including code-mode via --config and the exec-replace resume handoff) green.
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
}