|
@@ -2,19 +2,24 @@
|
|
|
* Commander adapter for the `dsh` command-line entry: the one place argv is
|
|
* Commander adapter for the `dsh` command-line entry: the one place argv is
|
|
|
* parsed and routed to a mode. `bin.ts` switches on the returned discriminant
|
|
* parsed and routed to a mode. `bin.ts` switches on the returned discriminant
|
|
|
* and dynamic-imports that mode's module. One program: the default (no
|
|
* and dynamic-imports that mode's module. One program: the default (no
|
|
|
- * subcommand) is the TUI/headless surface with option-only flags; `web` is a
|
|
|
|
|
- * real subcommand. Commander owns `--help`/`--version` and parse errors — it
|
|
|
|
|
- * prints and exits at the point of failure (a domain failure routes through
|
|
|
|
|
|
|
+ * subcommand) is the TUI/headless surface with option-only flags; `meta` and
|
|
|
|
|
+ * `web` are real subcommands. Commander owns `--help`/`--version` and parse
|
|
|
|
|
+ * errors — it prints and exits at the point of failure (a domain failure routes through
|
|
|
* `command.error`), so this returns only a resolved mode.
|
|
* `command.error`), so this returns only a resolved mode.
|
|
|
* @module @deepseek-ai/dsh/args
|
|
* @module @deepseek-ai/dsh/args
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
|
import { Command, CommanderError } from 'commander'
|
|
import { Command, CommanderError } from 'commander'
|
|
|
|
|
|
|
|
-/** Interactive TUI: the default mode. `--config` swaps the tree; `--resume <id>` rehydrates a session. */
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * Interactive TUI: the default mode. `--config` applies an overlay over the
|
|
|
|
|
+ * shipped composition in place of the personal one, `--config-replace` boots a
|
|
|
|
|
+ * file as the whole tree instead, and `--resume <id>` rehydrates a session.
|
|
|
|
|
+ */
|
|
|
interface TuiInvocation {
|
|
interface TuiInvocation {
|
|
|
mode: 'tui'
|
|
mode: 'tui'
|
|
|
config?: string
|
|
config?: string
|
|
|
|
|
+ configReplace?: string
|
|
|
resume?: string
|
|
resume?: string
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -24,18 +29,35 @@ interface HeadlessInvocation {
|
|
|
prompt: string
|
|
prompt: string
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** Interactive fresh TUI over this harness checkout; accepts no default-surface options. */
|
|
|
|
|
+interface MetaInvocation {
|
|
|
|
|
+ mode: 'meta'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Guided fresh-session entry: `dsh upgrade` seeds the first turn with the
|
|
|
|
|
+ * `dsh-upgrade` skill. It always mints a
|
|
|
|
|
+ * fresh session in the invoking directory and takes no options — `--resume`,
|
|
|
|
|
+ * `--config`, and `-p` are rejected as mistyped, so there is nothing to carry.
|
|
|
|
|
+ */
|
|
|
|
|
+interface SkillSessionInvocation {
|
|
|
|
|
+ mode: 'upgrade'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Browser UI: `dsh web`. `host`/`port` are present only when the flag was
|
|
* Browser UI: `dsh web`. `host`/`port` are present only when the flag was
|
|
|
* passed — pass-through overrides with no CLI default and no CLI validation:
|
|
* passed — pass-through overrides with no CLI default and no CLI validation:
|
|
|
* the `dsh-host-webserver` schema (`host` a loopback/all-interfaces literal,
|
|
* the `dsh-host-webserver` schema (`host` a loopback/all-interfaces literal,
|
|
|
* `port` a natural ≤ 65535) is the single source of both the default (the
|
|
* `port` a natural ≤ 65535) is the single source of both the default (the
|
|
|
- * shipped `cordis.yml` value stands when a flag is absent) and validity (a bad
|
|
|
|
|
|
|
+ * shipped Web overlay value stands when a flag is absent) and validity (a bad
|
|
|
* value fails loud at boot). `port` is `Number`-coerced only because the schema
|
|
* value fails loud at boot). `port` is `Number`-coerced only because the schema
|
|
|
* wants a number, not a string. `dev` mounts the client HMR driver;
|
|
* wants a number, not a string. `dev` mounts the client HMR driver;
|
|
|
* `workspaceRoot` is the parent directory for name-created workspaces.
|
|
* `workspaceRoot` is the parent directory for name-created workspaces.
|
|
|
*/
|
|
*/
|
|
|
interface WebInvocation {
|
|
interface WebInvocation {
|
|
|
mode: 'web'
|
|
mode: 'web'
|
|
|
|
|
+ /** Overlay of loader patches applied over the shipped web composition. */
|
|
|
|
|
+ config?: string
|
|
|
host?: string
|
|
host?: string
|
|
|
port?: number
|
|
port?: number
|
|
|
dev: boolean
|
|
dev: boolean
|
|
@@ -45,10 +67,16 @@ interface WebInvocation {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** The resolved `dsh` invocation: exactly one mode. `--help`/`--version`/errors exit inside {@link parseDshArgs}. */
|
|
/** The resolved `dsh` invocation: exactly one mode. `--help`/`--version`/errors exit inside {@link parseDshArgs}. */
|
|
|
-export type DshInvocation = TuiInvocation | HeadlessInvocation | WebInvocation
|
|
|
|
|
|
|
+export type DshInvocation =
|
|
|
|
|
+ | TuiInvocation
|
|
|
|
|
+ | HeadlessInvocation
|
|
|
|
|
+ | MetaInvocation
|
|
|
|
|
+ | SkillSessionInvocation
|
|
|
|
|
+ | WebInvocation
|
|
|
|
|
|
|
|
/** Raw web-subcommand options straight from Commander. */
|
|
/** Raw web-subcommand options straight from Commander. */
|
|
|
interface WebOptions {
|
|
interface WebOptions {
|
|
|
|
|
+ config?: string
|
|
|
host?: string
|
|
host?: string
|
|
|
port?: string
|
|
port?: string
|
|
|
dev?: boolean
|
|
dev?: boolean
|
|
@@ -65,6 +93,7 @@ interface WebOptions {
|
|
|
function resolveWeb(options: WebOptions): WebInvocation {
|
|
function resolveWeb(options: WebOptions): WebInvocation {
|
|
|
return {
|
|
return {
|
|
|
mode: 'web',
|
|
mode: 'web',
|
|
|
|
|
+ ...options.config !== undefined && { config: options.config },
|
|
|
...options.host !== undefined && { host: options.host },
|
|
...options.host !== undefined && { host: options.host },
|
|
|
...options.port !== undefined && { port: Number(options.port) },
|
|
...options.port !== undefined && { port: Number(options.port) },
|
|
|
dev: options.dev === true,
|
|
dev: options.dev === true,
|
|
@@ -86,21 +115,34 @@ export function parseDshArgs(argv: readonly string[], version: string): DshInvoc
|
|
|
const program = new Command()
|
|
const program = new Command()
|
|
|
.name('dsh')
|
|
.name('dsh')
|
|
|
.version(version, '-V, --version', 'output the version number')
|
|
.version(version, '-V, --version', 'output the version number')
|
|
|
- .description('dsh: interactive TUI (default), headless task, and browser UI')
|
|
|
|
|
|
|
+ .description('dsh: DeepSeek Harness — an interactive coding agent for your terminal.\nRun `dsh` with no arguments to start a session in the current directory.')
|
|
|
|
|
+ // The default surface takes no positional task, so `dsh "task"` fails
|
|
|
|
|
+ // commander's arity check with no hint; these examples are where a first
|
|
|
|
|
+ // reader learns the entry points and that a one-shot task rides `-p`.
|
|
|
|
|
+ .addHelpText('after', `
|
|
|
|
|
+Examples:
|
|
|
|
|
+ dsh start an interactive session in this directory
|
|
|
|
|
+ dsh -p "run the tests" answer one task, print the result, and exit
|
|
|
|
|
+ dsh --resume <id> continue a past session
|
|
|
|
|
+`)
|
|
|
.exitOverride()
|
|
.exitOverride()
|
|
|
|
|
+ // Stop parent options at a subcommand boundary so `web --config` belongs to
|
|
|
|
|
+ // Web while `--config ... web` remains a leaked default-surface option.
|
|
|
|
|
+ .enablePositionalOptions()
|
|
|
// Default surface: option-only (no positional), so `web` can be a real
|
|
// Default surface: option-only (no positional), so `web` can be a real
|
|
|
// subcommand without a positional collision.
|
|
// subcommand without a positional collision.
|
|
|
- .option('--config <path>', 'boot an alternate cordis.yml instead of the shipped tree (TUI mode)')
|
|
|
|
|
- .option('-p, --prompt <task>', 'run one headless turn for this task, print the result, and exit')
|
|
|
|
|
- .option('--resume <id>', 'resume the persisted session with this id (TUI mode)')
|
|
|
|
|
- .action((options: { config?: string; prompt?: string; resume?: string }) => {
|
|
|
|
|
|
|
+ .option('-p, --prompt <task>', 'answer this task without the interactive UI, then exit')
|
|
|
|
|
+ .option('--resume <id>', 'continue a past session by id')
|
|
|
|
|
+ .option('--config <path>', 'apply this overlay of loader patches instead of the personal one')
|
|
|
|
|
+ .option('--config-replace <path>', 'boot this file as the entire tree, ignoring the shipped and personal configuration')
|
|
|
|
|
+ .action((options: { config?: string; configReplace?: string; prompt?: string; resume?: string }) => {
|
|
|
if (options.prompt !== undefined) {
|
|
if (options.prompt !== undefined) {
|
|
|
// A headless prompt owns the invocation; an empty task has nothing to
|
|
// A headless prompt owns the invocation; an empty task has nothing to
|
|
|
// run, and --config/--resume are TUI inputs that must not silently
|
|
// run, and --config/--resume are TUI inputs that must not silently
|
|
|
// vanish from a headless run.
|
|
// vanish from a headless run.
|
|
|
if (options.prompt === '') program.error('error: --prompt needs a task')
|
|
if (options.prompt === '') program.error('error: --prompt needs a task')
|
|
|
- if (options.config !== undefined || options.resume !== undefined) {
|
|
|
|
|
- program.error('error: --prompt takes no --config or --resume')
|
|
|
|
|
|
|
+ if (options.config !== undefined || options.configReplace !== undefined || options.resume !== undefined) {
|
|
|
|
|
+ program.error('error: --prompt takes no --config, --config-replace, or --resume')
|
|
|
}
|
|
}
|
|
|
resolved = { mode: 'headless', prompt: options.prompt }
|
|
resolved = { mode: 'headless', prompt: options.prompt }
|
|
|
return
|
|
return
|
|
@@ -108,32 +150,67 @@ export function parseDshArgs(argv: readonly string[], version: string): DshInvoc
|
|
|
// An empty --resume= id would silently start a fresh session downstream
|
|
// An empty --resume= id would silently start a fresh session downstream
|
|
|
// (agent-loop treats '' as no-resume), so a mistyped resume must fail loud.
|
|
// (agent-loop treats '' as no-resume), so a mistyped resume must fail loud.
|
|
|
if (options.resume === '') program.error('error: --resume needs a session id')
|
|
if (options.resume === '') program.error('error: --resume needs a session id')
|
|
|
|
|
+ // The two config flags are mutually exclusive: one layers over the shipped
|
|
|
|
|
+ // tree, the other discards it, so accepting both would silently drop one.
|
|
|
|
|
+ if (options.config !== undefined && options.configReplace !== undefined) {
|
|
|
|
|
+ program.error('error: --config and --config-replace are mutually exclusive')
|
|
|
|
|
+ }
|
|
|
resolved = {
|
|
resolved = {
|
|
|
mode: 'tui',
|
|
mode: 'tui',
|
|
|
...options.config !== undefined && { config: options.config },
|
|
...options.config !== undefined && { config: options.config },
|
|
|
|
|
+ ...options.configReplace !== undefined && { configReplace: options.configReplace },
|
|
|
...options.resume !== undefined && { resume: options.resume },
|
|
...options.resume !== undefined && { resume: options.resume },
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- const web = program.command('web').description('serve the browser UI (host/port default to the shipped config)')
|
|
|
|
|
|
|
+ // Commander parses the parent (default-surface) options on either side of a
|
|
|
|
|
+ // subcommand into `program.opts()`. For a subcommand that shares none of them,
|
|
|
|
|
+ // a leaked config/prompt/resume option is a mistyped invocation that must fail
|
|
|
|
|
+ // loud rather than silently run and drop the input.
|
|
|
|
|
+ const rejectParentOptions = (command: string): void => {
|
|
|
|
|
+ const parent = program.opts<{ config?: string; configReplace?: string; prompt?: string; resume?: string }>()
|
|
|
|
|
+ if (parent.config !== undefined || parent.configReplace !== undefined
|
|
|
|
|
+ || parent.prompt !== undefined || parent.resume !== undefined) {
|
|
|
|
|
+ program.error(`error: ${command} takes none of --config, --config-replace, -p/--prompt, or --resume`)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Registration order is the rendered help order, so daily use comes first
|
|
|
|
|
+ // and the harness-development surfaces (`web --dev`, `meta`) come last.
|
|
|
|
|
+ // `upgrade` is a guided fresh-session entry: it takes no options and always
|
|
|
|
|
+ // mints a fresh session, so nothing is left to carry.
|
|
|
|
|
+ program
|
|
|
|
|
+ .command('upgrade')
|
|
|
|
|
+ .description('update this dsh installation to the latest version')
|
|
|
|
|
+ .action(() => {
|
|
|
|
|
+ rejectParentOptions('upgrade')
|
|
|
|
|
+ resolved = { mode: 'upgrade' }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ // Host and port name no default: the CLI passes neither through when the flag
|
|
|
|
|
+ // is absent, so the shipped Web overlay value stands and restating it here
|
|
|
|
|
+ // would duplicate a fact this file does not own.
|
|
|
|
|
+ const web = program.command('web').description('serve the browser UI on the configured host and port')
|
|
|
web
|
|
web
|
|
|
- .option('--host <host>', 'override the config bind host (127.0.0.1 or 0.0.0.0)')
|
|
|
|
|
- .option('--port <port>', 'override the config listen port (0 requests an OS-assigned port)')
|
|
|
|
|
- .option('--dev', 'mount the client HMR driver and watch plugin bundles for rebuilds')
|
|
|
|
|
- .option('--workspace-root <path>', 'parent directory for name-created workspaces')
|
|
|
|
|
|
|
+ .option('--config <path>', 'apply this overlay of loader patches over the shipped configuration')
|
|
|
|
|
+ .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')
|
|
|
|
|
+ .option('--dev', 'developer mode: hot-reload the browser client')
|
|
|
|
|
+ .option('--workspace-root <path>', 'parent directory for workspaces created from the browser UI')
|
|
|
.option('--trusted-host <authority...>', 'extra authority the /api browser-trust fence accepts (host or host:port; repeatable)')
|
|
.option('--trusted-host <authority...>', 'extra authority the /api browser-trust fence accepts (host or host:port; repeatable)')
|
|
|
.action((options: WebOptions) => {
|
|
.action((options: WebOptions) => {
|
|
|
- // Commander parses the parent (default-surface) options on either side of
|
|
|
|
|
- // the subcommand into `program.opts()`. `web` shares none of them, so a
|
|
|
|
|
- // leaked `--config`/`-p`/`--resume` is a mistyped invocation that must
|
|
|
|
|
- // fail loud rather than silently start the web server and drop it.
|
|
|
|
|
- const parent = program.opts<{ config?: string; prompt?: string; resume?: string }>()
|
|
|
|
|
- if (parent.config !== undefined || parent.prompt !== undefined || parent.resume !== undefined) {
|
|
|
|
|
- program.error('error: web takes none of --config, -p/--prompt, or --resume')
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ rejectParentOptions('web')
|
|
|
resolved = resolveWeb(options)
|
|
resolved = resolveWeb(options)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+ program
|
|
|
|
|
+ .command('meta')
|
|
|
|
|
+ .description('work on the dsh source that runs this command, from any directory')
|
|
|
|
|
+ .action(() => {
|
|
|
|
|
+ rejectParentOptions('meta')
|
|
|
|
|
+ resolved = { mode: 'meta' }
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
program.parse(argv, { from: 'user' })
|
|
program.parse(argv, { from: 'user' })
|
|
|
} catch (error) {
|
|
} catch (error) {
|