What plugin management does now lives in `dsh-plugin-manager`, a boot-group package beside `app-boot`: `PluginInstaller` runs pnpm, probes, and judges new packages over the profile on disk, and `PluginManager` adds every operation over the booted tree, each refusal a `PluginOperationError` with a `plugins/*` code and no Remote protocol attached. The manager reads the profile runtime, the preset roster, and the running-agent count through per-call readers, and the roster only as `PresetLayers`, so the package depends on neither the roster nor the agent registry. `dsh-host-plugin-manager` keeps `pluginManager` and the `plugins` Remote as a relay: one Remote method per manager method, and one exhaustive switch turning a failure into the Remote error of the same code (`plugins/bad-request` into `gateway/bad-request`). Its `./types` re-exports the manager's types, so `api-remotes` and the client import one vocabulary unchanged. `dsh plugin add` and `remove` now run the installer instead of a bare pnpm forwarder: every new package is probed, a package that declares neither a bundle nor a plugin module is removed again with its reason, and newly installed bundles are enabled as before. Other pnpm verbs still forward.
192 lines
9.0 KiB
TypeScript
192 lines
9.0 KiB
TypeScript
/**
|
|
* 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 <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')
|
|
}
|
|
// 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 <name> --help)')
|
|
.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')
|
|
.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')
|
|
}
|
|
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<BootOptions & { profile?: string }>()
|
|
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 <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')
|
|
.action((args: string[], options: BootOptions) => {
|
|
rejectParentOptions('web')
|
|
resolved = resolveBoot(web, 'web', options, args)
|
|
})
|
|
|
|
const plugin = program.command('plugin').description('manage a profile\'s plugins: add and remove install through the plugin manager, every other verb is forwarded to pnpm in the profile directory')
|
|
plugin
|
|
.requiredOption('--profile <name>', 'the profile whose plugins to manage (initialized on first use)')
|
|
.allowUnknownOption()
|
|
.argument('[args...]', 'add <pkg...>, remove <pkg...>, or any pnpm arguments forwarded verbatim (why <pkg>, update, ...)')
|
|
.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 }
|
|
})
|
|
|
|
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
|
|
}
|