File size: 10,264 Bytes
58a9693 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | /**
* 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
/** Shipped template used once to initialize a missing profile. */
fromDefaultProfile?: string | undefined
/** 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
/** Shipped template used once to initialize a missing profile. */
fromDefaultProfile?: string | undefined
/** 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
fromDefaultProfile?: string
}
/**
* 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]
function rejectElectronProfile(program: Command, profile: string): void {
if (profile.toLowerCase() === 'desktop') {
program.error('error: profile "desktop" is managed exclusively by the Electron application')
}
}
/** 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 rescue --from-default-profile web
create rescue from the shipped web template, then boot it
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.fromDefaultProfile === '') program.error('error: --from-default-profile needs a name')
if (options.dumpConfig !== true && options.dumpDefaultConfig !== true) {
return { mode: 'profile', profile, fromDefaultProfile: options.fromDefaultProfile, 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, fromDefaultProfile: options.fromDefaultProfile, 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('--from-default-profile <name>', 'initialize a new custom profile from a shipped profile template')
.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')
rejectElectronProfile(program, profile)
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
|| parent.fromDefaultProfile !== undefined) {
program.error(
`error: ${command} takes none of parent --profile, --from-default-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 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')
rejectElectronProfile(plugin, options.profile)
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
}
|