Files
deepseek-harness/packages/boot/cmdline
Turtle 1f0a0440f3 refactor(cmdline)!: an app's entrypoint provides values its rows read
Replaces the patch round trip. An app's entrypoint resolves the command
line into a service, and the rows it configures read that service from
their own config — port: !!js ctx.get('webStartup')?.port ?? 3080 — so the
resolved value beats the value written beside it and nothing is written
back into a row or handed to the launcher.

A bundle names the entrypoint row in its manifest (dsh.bundle.entrypoint),
which is what lets the boot mount in two passes: entrypoints alone, then
the whole composition. That ordering is required, not cosmetic — a row's
config expressions are evaluated when the include applies the row, and a
strict ctx.get only answers for a service whose providing fiber is already
active.

What this removes: ctx.appPatches and the launcher-owned patch layer, the
disable/re-enable recycle and its in-flight-mount barrier, overrideConfig,
and the reload hazard they existed for. A live config edit now re-applies
the second pass against services that are still up, so a served port
survives by construction.

What it adds: ctx.appReady, because Loader settlement no longer means the
app is up — a row mounted in the second pass can observe a settled tree
while that pass is still running, or already rolling back. The web URL line
waits for it, so a boot that fails in the second pass announces nothing.
2026-08-10 23:45:04 +08:00
..

@deepseek-ai/dsh-cmdline

English | 中文

The command line a dsh launcher hands to the app it boots. The launcher parses only its own flags (--profile, --patch, the config dumps) and hands everything after them to the tree verbatim, so an app owns its flag family, its --help text, and its parse errors instead of the launcher knowing them.

The launcher values

A launcher calls provideCmdline(ctx, host) before any tree entry mounts, which provides:

  • ctx.cmdlineArgs — the invocation's inner arguments. get() is the whole interface, and it returns a snapshot: dsh --profile tui --resume abc yields ['--resume', 'abc'].
  • ctx.appExit — a bounded process-exit request, wired to the launcher's shutdown controller.
  • ctx.appReady — settles when the launcher has finished mounting, for a row that publishes readiness (a URL line a supervisor waits for).

An embedding host with no command line provides an empty list; that is the honest answer, not a missing value.

Entrypoints, and the service their app reads

An app reads those arguments from its entrypoint row — a plugin that injects cmdlineArgs and calls runStartup(ctx, service, program, plan):

export const name = 'web-startup'
export const inject = ['cmdlineArgs']

export function apply(ctx: Context): void {
  runStartup(ctx, 'webStartup', webCommand(), planWebStartup)
}

The bundle's package.json names that row, which is what makes the boot mount it before everything else:

{ "dsh": { "bundle": { "patch": "./cordis.patch.yml", "entrypoint": "web-startup" } } }

Every row the app configures from flags then reads what the entrypoint resolved, naming the key it takes and the value it falls back to:

- id: webserver
  name: '@deepseek-ai/dsh-host-webserver'
  inject: [webStartup]
  config:
    host: !!js ctx.get('webStartup')?.host ?? '127.0.0.1'
    port: !!js ctx.get('webStartup')?.port ?? 3080

runStartup parses the arguments, asks plan for the values, and provides them as the service. On --help, --version, a parse error, or a program.error(...) from the plan, it writes commander's text and requests exit — nothing is provided, and the rest of the composition never mounts.

plan receives the options of every row that injects the service, for a value that has to take the composition into account: the /api fence authorities are the shipped example, since a bind the composition configured decides whether LAN literals are derived at all.

Why the boot has phases

A row's config expressions are evaluated when the include applies it, and a strict ctx.get only answers for a service whose providing fiber is already active. A composition therefore mounts in two passes: the entrypoints alone, then everything else — which is exactly what the manifest declaration buys. The rows of a later pass read live values, a --help exits before the second pass exists, and a user editing a live patch file re-runs that pass against services that are still up, so a flag cannot be silently reset.

enableRow(ctx, id) turns on a row a bundle ships disabled because only some invocations want it (dsh web --dev and its client-plugin reload chain). Call it from a row that mounts beside the one being enabled, not from an entrypoint: a row enabled in the first pass would wait for services the second pass has yet to mount.

One command line, one owner

A composition has exactly one command-line owner. An app that layers over another one disables the underlying entrypoint row and names both services, so the rows it absorbed start on the values their own fallbacks name — dsh-headless does this over dsh-web-app.

An out-of-tree plugin brings its own commander copy, so commander's control-flow errors are detected structurally rather than by class identity; an identity check would rethrow a printed help as a fatal load failure.

Model Experience

None, as this package resolves the process's own command line before any session exists.

KV Cache effect

None; this package neither assembles nor sends a provider request.

Known Limitations and Deferred Work

  • Launcher flags must precede app arguments. The split is positional: the first token the launcher does not recognize starts the inner arguments, so --patch placed after an app flag belongs to the app. The launcher's parser consumes one --, so an app argument that must survive as a literal -- needs -- --.
  • A startup service has no declared owner. The rows name it and an entrypoint provides it; nothing links the two statically, so a bundle that ships reading rows without its entrypoint fails at settlement (pending entries naming the service) rather than at load.
  • A user patch that replaces a row's whole config drops its expressions. A flag beats the value written beside it, not a literal a user wrote in place of the expression; keeping the expression is what keeps the flag winning.