refactor(cmdline): make command providers ordinary

This commit is contained in:
Turtle
2026-08-10 23:45:05 +08:00
parent 668bdb3d8e
commit 09e2d2ddc1
46 files changed
+400 -742

No files matched your search

+5 -5
View File
@@ -3,8 +3,8 @@
*
* 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 the booted app's startup row
* parses its own flag family and prints its own `--help` (see
* 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`,
@@ -23,7 +23,7 @@ interface ProfileInvocation {
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 the booted app's startup row. */
/** Everything after the launcher's own flags, verbatim, for injected app plugins. */
args: string[]
}
@@ -89,8 +89,8 @@ function resolveBoot(program: Command, profile: string, options: BootOptions, ar
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 the app's startup row, so it cannot
// show what that app's flags would decide, and printing a tree that differs
// 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(' ')}`)
+11 -19
View File
@@ -6,8 +6,8 @@
* live, and wire fail-loud plus bounded shutdown.
*
* App flags are not the launcher's business: the invocation's inner arguments
* are provided to the tree through `ctx.cmdlineArgs`, and the booted app's
* startup row parses them and configures its own rows.
* are provided to the tree through `ctx.cmdlineArgs`, where any injected app
* plugin may read the same immutable snapshot.
* @module @deepseek-ai/dsh/profile-boot
*/
@@ -37,7 +37,7 @@ const SHIPPED_PRESET_ROOT = fileURLToPath(new URL('../config/agent-presets/', im
/** Harness-home directory holding locally authored agent presets. */
const USER_PRESET_DIR = '.agent-presets'
import { DSH_ENVIRONMENT_KEY, type EnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
import { hasCmdlineConsumer, provideCmdline } from '@deepseek-ai/dsh-cmdline'
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
import type { HeadlessIo } from '@deepseek-ai/dsh-headless'
import { createProcessShutdown, type ProcessShutdown } from './process-shutdown.ts'
import { resolveWindowsShellLayer } from './windows-shell.ts'
@@ -206,12 +206,6 @@ function suppressSignalShutdownError(signal: AbortSignal, error: unknown): void
*/
export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Context; shutdown: ProcessShutdown }> {
const composed = composeProfile(options.profile, options.patchFiles)
if (!hasCmdlineConsumer([...composed.rows.values()]) && options.args.length > 0) {
throw new Error(
`${NAME}: profile ${JSON.stringify(options.profile)} takes no app arguments because no active row injects cmdlineArgs; `
+ `got ${options.args.map(argument => JSON.stringify(argument)).join(' ')}`,
)
}
// A one-shot composition ends by itself, which changes what a signal means
// and makes watching the user's patch layer pointless.
const headlessRow = composed.rows.get(HEADLESS_ROW_ID)
@@ -225,8 +219,7 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
shutdown.interrupt(code)
}
// Signals own teardown throughout the startup window, not only after boot()
// settles: an inserted startup row can publish readiness before sibling rows
// finish mounting.
// settles: an inserted provider can publish before sibling rows finish mounting.
process.on('SIGTERM', () => { interrupt(oneShot ? 143 : 0) })
process.on('SIGINT', () => { interrupt(130) })
installFailLoud(NAME, process, async () => {
@@ -235,9 +228,9 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
const rootConfig = join(composed.profile.dir, PROFILE_ROOT_FILENAME)
// Recomposition for the live user layers: bundle layers below, overlays
// above, so a user edit can never displace them. What an app's startup row
// resolved is not in here at all — it lives in that row's own service, which
// survives a recomposition. BOTH
// above, so a user edit can never displace them. Parsed app arguments are
// not in here at all — they live in app-provided services that survive a
// recomposition. BOTH
// user files are re-read per generation (the HMR watcher hands us only the
// changed file's patches, which one of the reads duplicates — fresh reads
// keep the two watchers from stitching in each other's stale copy).
@@ -263,9 +256,8 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
// Before any config-tree entry mounts, so plugins resolve all launch-time
// environment values from the same immutable provenance snapshot.
hostCtx.provide(DSH_ENVIRONMENT_KEY, options.environment)
// The command line is a launcher fact every app reads the same way: its
// own arguments, and the bounded exit its startup row requests after
// printing help or rejecting them.
// The command line and bounded exit request are launcher facts available
// to every app plugin that injects the argument snapshot.
provideCmdline(hostCtx, {
args: options.args,
exit: code => void shutdown.shutdown(code),
@@ -280,8 +272,8 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
}
})
app.current = ctx
// A surface can dispose the whole tree while startup or this post-boot
// watcher setup is still in flight. Loader presence and fiber state own
// A surface can dispose the whole tree while boot or this post-boot watcher
// setup is still in flight. Loader presence and fiber state own
// liveness; the local signal fact distinguishes that expected exit race
// from a real HMR error.
if (watchProfilePatch