description: "App-owned command lines for dsh app bins: your app parses its own flags, --help, and exit behavior from the launcher's remaining arguments."
English | 中文
dsh-cmdline lets your app own its command line: the launcher keeps only its own flags (--profile, --patch, the config dumps) and passes everything after them to your app verbatim, so your app decides its flags, its --help text, and its parse errors. Values you parse from those arguments win over any default written in the config, without writing anything back. Your app also gets a bounded way to ask for process exit, wired to the launcher's shutdown. Use it when you write an app bin that accepts its own flags; it adds no prompt, schema, or model-facing surface of its own.
Your app reads the invocation's inner arguments at startup, and any number of its plugins can use them. The common path: a startup plugin reads the arguments, parses them, and publishes the parsed values; other rows configure themselves from those values.
The launcher makes three things available to your app:
ctx.cmdlineArgs — the inner arguments of your invocation. Reading them returns an immutable snapshot and never consumes or changes them: dsh --profile tui --resume abc gives your app ['--resume', 'abc'].ctx.appExit — a way to ask the process to exit once the tree has shut down, wired to the launcher's shutdown controller.ctx.appReady — the successful-startup signal, committed only after the Loader tree and launcher-owned setup succeed.An app launched with no arguments sees an empty list — that is the honest answer, not a missing value.
exitOnStdinEnd(ctx, label) binds a successfully started stdio application's EOF to ctx.appExit(0). It never reads or resumes stdin, so a protocol transport receives bytes buffered before it mounts; startup rejection wins over a racing EOF, and the owning fiber removes both pending listeners.
You bring your own commander program: declare your flags and your actions, and the package runs it against the inner arguments. Your action is the only place validation happens, and it publishes whatever your rows need. The plugin's Loader row carries no special marker:
- id: web-startup
name: '@deepseek-ai/dsh-web-app/startup'
Rows configured from the parsed values inject the published service and read it directly in their config:
- id: webserver
name: '@deepseek-ai/dsh-host-webserver'
inject: [webStartup]
config:
host: !!js ctx.webStartup.host ?? '127.0.0.1'
port: !!js ctx.webStartup.port ?? 3080
The outcomes: dsh --profile web --port 8080 starts the server on port 8080 even when the config says 3080, because the flag wins. --help prints your app's help and exits 0 without starting anything; a rejected value (for example a non-numeric port) prints your error and exits nonzero, and no row that depends on the parsed values ever starts.
The value written beside a !!js expression is the fallback: the flag wins when present, the written value is used otherwise. Resolution happens once at startup, after your parser ran, so a flag is never silently reset by a later config reload.
Any number of plugins can read the same arguments — reading never consumes them — and each can parse what it needs and publish its own values. The launcher does not decide who owns the command line: an app with no reader ignores its arguments.
Apps built outside this repository behave the same way: their --help prints and exits instead of crashing, even though they carry their own commander copy.
Read these pages when the package-level contract is not enough. They move from the handoff mechanism to the apps that consume it.
None, as this package resolves the process command line before any session exists; configured rows own every model-visible consequence.
None; this package neither assembles nor sends a provider request.
These limits describe where app-owned command lines are a poor fit or need special care. They are current package constraints, not a task backlog.
--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 -- --.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.