refactor(cmdline): make command providers ordinary

This commit is contained in:
Turtle
2026-08-10 21:49:11 +08:00
parent 668bdb3d8e
commit 09e2d2ddc1
46 changed files with 400 additions and 742 deletions
+10 -14
View File
@@ -13,30 +13,28 @@ A launcher calls `provideCmdline(ctx, host)` before any tree entry mounts, which
An embedding host with no command line provides an empty list; that is the honest answer, not a missing value.
## Startup rows, and the service their app reads
## Ordinary providers and injected config
An app reads those arguments from its **startup row** — a Loader row and plugin that inject `cmdlineArgs` and calls `runStartup(ctx, service, program, plan)`:
Any app plugin may inject `cmdlineArgs`, parse it, and publish an ordinary app-owned service. `parseCmdline(ctx, program, plan)` is only a commander adapter; the caller owns the returned value and service:
```ts ignore
export const name = 'web-startup'
export const inject = ['cmdlineArgs']
export function apply(ctx: Context): void {
runStartup(ctx, 'webStartup', webCommand(), planWebStartup)
const values = parseCmdline(ctx, webCommand(), planWebStartup)
if (values !== undefined) ctx.provide('webStartup', values)
}
```
The Loader-row injection is also its discovery declaration, so no bundle manifest field is needed:
Its Loader row carries no launcher marker or special kind:
```yaml
- id: web-startup
name: '@deepseek-ai/dsh-web-app/startup'
inject: [cmdlineArgs]
```
The launcher uses that injection only to reject arguments for a composition with no command-line owner, and to reject a composition with multiple owners. Loader mounts the composition once and holds each row until its own injections are active.
Every row the app configures from flags then reads what the startup row resolved, naming the key it takes and the value it falls back to:
Every row configured from those values uses ordinary service injection and direct lazy config access:
```yaml
- id: webserver
@@ -47,9 +45,7 @@ Every row the app configures from flags then reads what the startup row resolved
port: !!js ctx.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, so rows that depend on the startup service never activate.
`plan` receives the startup context and the options of every row that injects the service, for a value that has to take the composition into account. Include still holds nested expressions raw at this point, so a plan that needs a composed fallback can interpolate the relevant row config against the pre-service startup context; the `/api` fence authorities are the shipped example.
`parseCmdline` parses the immutable arguments and asks `plan` for the app-owned value. On `--help`, `--version`, a parse error, or a `program.error(...)` from the plan, it writes commander's text, requests exit, and returns `undefined`; the provider publishes nothing, so dependent rows never activate.
### How injection orders config
@@ -57,9 +53,9 @@ Loader defers a row's `!!js` interpolation until that row's declared injections
`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). The activation is an in-memory override: it does not rewrite the row's configured `disabled` value and survives config reapplication for that mounted entry. Loader applies the enabled row's ordinary injection ordering.
### One command line, one owner
### Shared immutable arguments
A composition has exactly one command-line owner. An app that layers over another one disables the underlying startup row and provides every startup service its retained rows inject.
`get()` does not consume or mutate argv. Multiple plugins can parse the same snapshot and independently provide services. The launcher does not inspect the composition for a command-line owner; a profile with no reader simply ignores its app arguments.
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.
@@ -74,5 +70,5 @@ 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.** Reading rows name it and a `cmdlineArgs` consumer provides it; nothing links those two injections statically, so a bundle that ships reading rows without its startup row fails at settlement (pending entries naming the service) rather than at load.
- **An app-owned service has no statically declared provider.** Consumer rows name it through ordinary injection; a bundle that omits its provider fails at settlement with 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.