@deepseek-ai/dsh-cmdline 0.1.1-rc.2 → 0.1.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/boot/cmdline/README.md
5
- README.md: 33125014539e801dbd2952a3b4513cafc80bdcee
6
- README.zh.md: 7ef49a1027d3c17817c9171e1166ed6feecd8559
5
+ README.md: 7ea825d8ef309cc295e3334bf5ae220e3e0524e5
6
+ README.zh.md: 9f5f876ec6d1b408e2156dc3075499885032a141
package/README.md CHANGED
@@ -1,41 +1,54 @@
1
- # `@deepseek-ai/dsh-cmdline`
1
+ ---
2
+ 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."
3
+ kind: "package-library"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-cmdline
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- 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.
10
+ ## Summary
6
11
 
7
- ## The launcher values
12
+ `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.
8
13
 
9
- A launcher calls `provideCmdline(ctx, host)` before any tree entry mounts, which provides:
14
+ ## Table of Contents
10
15
 
11
- - `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']`.
12
- - `ctx.appExit` — a bounded process-exit request, wired to the launcher's shutdown controller.
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
13
22
 
14
- An embedding host with no command line provides an empty list; that is the honest answer, not a missing value.
23
+ -----
15
24
 
16
- ## Ordinary providers and injected config
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
17
27
 
18
- Any app plugin may inject `cmdlineArgs`, parse it, and publish an ordinary app-owned service. `parseCmdline(ctx, program)` is only a commander adapter; the program's own action owns validation and the published service:
28
+ 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.
19
29
 
20
- ```ts ignore
21
- export const name = 'web-startup'
22
- export const inject = ['cmdlineArgs']
30
+ ### The launcher values
23
31
 
24
- export function apply(ctx: Context): void {
25
- const program = webCommand()
26
- program.action(() => ctx.provide('webStartup', webValuesFrom(program)))
27
- parseCmdline(ctx, program)
28
- }
29
- ```
32
+ The launcher makes three things available to your app:
33
+
34
+ - `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']`.
35
+ - `ctx.appExit` — a way to ask the process to exit once the tree has shut down, wired to the launcher's shutdown controller.
36
+ - `ctx.appReady` — the successful-startup signal, committed only after the Loader tree and launcher-owned setup succeed.
37
+
38
+ An app launched with no arguments sees an empty list — that is the honest answer, not a missing value.
30
39
 
31
- Its Loader row carries no launcher marker or special kind:
40
+ `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.
41
+
42
+ ### Parsing your flags
43
+
44
+ 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:
32
45
 
33
46
  ```yaml
34
47
  - id: web-startup
35
48
  name: '@deepseek-ai/dsh-web-app/startup'
36
49
  ```
37
50
 
38
- Every row configured from those values uses ordinary service injection and direct lazy config access:
51
+ Rows configured from the parsed values inject the published service and read it directly in their config:
39
52
 
40
53
  ```yaml
41
54
  - id: webserver
@@ -46,21 +59,67 @@ Every row configured from those values uses ordinary service injection and direc
46
59
  port: !!js ctx.webStartup.port ?? 3080
47
60
  ```
48
61
 
49
- `parseCmdline` refuses at load a program in which no command declares an action, routes every command's exit and output through the launcher (commander copies those settings into subcommands only at registration), and parses the immutable arguments; commander runs the invoked command's synchronous action on success. An action rejects an invalid invocation with `program.error(...)` — before publishing, since statements ahead of the rejection have already run. On `--help`, `--version`, a parse error, or that rejection, the helper writes commander's text and requests exit; the provider publishes nothing, so dependent rows never activate.
62
+ 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.
63
+
64
+ ### How flags beat config values
65
+
66
+ 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.
67
+
68
+ ### Reading the same arguments from several plugins
69
+
70
+ 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.
71
+
72
+ 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.
73
+
74
+ -----
75
+
76
+ <a id="understand-the-implementation"></a>
77
+ ## Understand the implementation
78
+
79
+ <details>
80
+ <summary>Implementation internals — click to expand</summary>
81
+
82
+ This section explains how the outcomes above are realized and points at the code that realizes them; everything here is developer-facing and not needed to use the package.
83
+
84
+ ### Design notes
50
85
 
51
- ### How injection orders config
86
+ - **Launcher facts, not config.** `cmdlineArgs` and `appExit` are provided on the host context before the tree mounts; they are not Loader rows, so no composition owns or overrides them.
87
+ - **Positional split.** The launcher recognizes no app row: the first token after its own flags starts the app's arguments, so the app owns its flag family, its `--help` text, and its parse errors.
88
+ - **Structural error detection.** `isCommanderError` reads commander's error code prefix instead of using `instanceof`, because an out-of-tree plugin brings its own commander copy whose `CommanderError` identity differs; `configureExitAndOutput` walks every subcommand because commander copies exit and output settings only at registration.
89
+ - **Injectable output streams.** `internals` holds the output streams so tests can capture commander's text without touching the process.
52
90
 
53
- Loader defers a row's `!!js` interpolation until that row's declared injections are active, then evaluates against the row's plugin context. The example above can therefore read `ctx.webStartup` directly: Cordis has already populated that injected service before Loader asks for `webserver`'s config. Include trees preserve nested expression nodes until each target row reaches this point. Provider replacement and live patch reload repeat interpolation against the current injected services, so a launch flag cannot be silently reset.
91
+ ### Parsing contract
54
92
 
55
- ### Shared immutable arguments
93
+ The parse path is one small family with two owners: `provideCmdline` freezes the host arguments and provides `cmdlineArgs` and `appExit` before any tree entry mounts, and `parseCmdline` runs your commander program against the immutable arguments, routing every command's help, version, and error output through the launcher. A rejected value, `--help`, or `--version` prints commander's text and requests `ctx.appExit` without publishing anything, so dependent rows never activate; Loader defers each row's `!!js` interpolation until its declared injections are active. Per-export contracts live in the code, not this README — see [`src/index.ts`](src/index.ts).
56
94
 
57
- `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.
95
+ ### Source map
58
96
 
59
- 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.
97
+ | File | Role |
98
+ |---|---|
99
+ | [`src/index.ts`](src/index.ts) | `CmdlineArgs`/`AppExit` types, `provideCmdline`, `parseCmdline`, commander exit/output routing |
100
+ | [`src/invariant.ts`](src/invariant.ts) | Invariant companion (no runtime invariant; Loader settlement reports missing services) |
60
101
 
102
+ </details>
103
+
104
+ -----
105
+
106
+ <a id="further-exploration"></a>
107
+ ## Further Exploration
108
+
109
+ Read these pages when the package-level contract is not enough. They move from the handoff mechanism to the apps that consume it and the decisions behind it.
110
+
111
+ - [App-owned command-line decision](../../../.agents/notes/implemented/architecture/2026-08-06-app-owned-command-line.md) — why apps own their flag family and how the handoff works.
112
+ - [Command-line seam trim](../../../.agents/notes/implemented/architecture/2026-08-11-cmdline-seam-trim.md) — the seams reduced to existing interfaces.
113
+ - [dsh-app-boot](../app-boot/README.md) — the boot sequence that provides these launcher values.
114
+ - [dsh-web-app bundle](../../bundle/web-app/README.md) — an app that owns the Web flag family through this package.
115
+ - [dsh-headless bundle](../../bundle/headless/README.md) — the one-shot runner that reads its task from the command line.
116
+
117
+ -----
118
+
119
+ <a id="model-experience"></a>
61
120
  ## Model Experience
62
121
 
63
- None, as this package resolves the process's own command line before any session exists.
122
+ None, as this package resolves the process command line before any session exists; configured rows own every model-visible consequence.
64
123
 
65
124
  #### KV Cache effect
66
125
 
@@ -68,6 +127,25 @@ None; this package neither assembles nor sends a provider request.
68
127
 
69
128
  ## Known Limitations and Deferred Work
70
129
 
71
- - **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 `-- --`.
72
- - **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.
73
- - **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.
130
+ <a id="known-limitations-and-deferred-work"></a>
131
+
132
+
133
+ 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.
134
+
135
+ - **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 `-- --`.
136
+ - **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.
137
+ - **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.
138
+
139
+ <a id="dev-note"></a>
140
+ ### Dev Note
141
+
142
+ <details>
143
+ <summary>Working context for maintainers — click to expand</summary>
144
+
145
+ This Dev Note is working context for maintainers: open design questions and directions that are not decided. It is explicitly non-authoritative — shipped behavior, limits, and accepted rationale live in the sections above, the package code, and the linked Agent Notes.
146
+
147
+ #### Open: parser surface
148
+
149
+ `parseCmdline` is a commander adapter, not a command-line framework: help, version, and error output follow commander's formatting, and the exit/output routing assumes commander's control-flow model. A different parser would need its own routing and error handling; nothing in the `cmdlineArgs` service contract depends on commander.
150
+
151
+ </details>
package/README.zh.md CHANGED
@@ -1,41 +1,54 @@
1
- # `@deepseek-ai/dsh-cmdline`
1
+ ---
2
+ description: "dsh app bin 的应用自有命令行:应用从启动器剩余参数中解析自己的 flag、--help 与退出行为。"
3
+ kind: "package-library"
4
+ ---
5
+
6
+ # @deepseek-ai/dsh-cmdline
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- dsh 启动器交给它所引导应用的那条命令行。启动器只解析属于自己的 flag(`--profile`、`--patch`、配置 dump),并把**其后的一切**原样交给配置树,因此 flag 家族、`--help` 文本和解析错误都由应用自己持有,启动器不必知道它们。
10
+ ## 概述
6
11
 
7
- ## 启动器提供的值
12
+ `dsh-cmdline` 让你的应用持有自己的命令行:启动器只保留属于自己的 flag(`--profile`、`--patch`、配置 dump),并把**其后的一切**原样交给你的应用,因此 flag、`--help` 文本与解析错误都由你的应用决定。你从这些参数解析出的值会胜过配置中写下的任何默认值,且无需写回任何内容。你的应用还获得一个有边界的进程退出请求,接到启动器的关停上。当你编写接受自有 flag 的应用 bin 时使用它;它本身不增加任何提示词、schema 或面向模型的表面。
8
13
 
9
- 启动器在任何配置树条目挂载之前调用 `provideCmdline(ctx, host)`,它提供:
14
+ ## 目录
10
15
 
11
- - `ctx.cmdlineArgs`:本次调用的内层参数。`get()` 就是它的全部接口,返回一份快照:`dsh --profile tui --resume abc` 得到 `['--resume', 'abc']`。
12
- - `ctx.appExit`:一个有边界的进程退出请求,接到启动器的关停控制器上。
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
13
22
 
14
- 没有命令行的嵌入宿主提供空列表;这是诚实的答案,而不是缺失的值。
23
+ -----
15
24
 
16
- ## 普通提供方与注入配置
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
17
27
 
18
- 任何应用插件都可以注入 `cmdlineArgs`、解析它,再发布一个普通的应用自有服务。`parseCmdline(ctx, program)` 只适配 commander;校验与发布的服务都归 program 自己的 action 持有:
28
+ 你的应用在启动时读取本次调用的内层参数,任意数量的插件都可以使用它们。常用路径是:启动插件读取参数、解析它们,再发布解析后的值;其他行由这些值配置自身。
19
29
 
20
- ```ts ignore
21
- export const name = 'web-startup'
22
- export const inject = ['cmdlineArgs']
30
+ ### 启动器提供的值
23
31
 
24
- export function apply(ctx: Context): void {
25
- const program = webCommand()
26
- program.action(() => ctx.provide('webStartup', webValuesFrom(program)))
27
- parseCmdline(ctx, program)
28
- }
29
- ```
32
+ 启动器向你的应用提供三样东西:
33
+
34
+ - `ctx.cmdlineArgs`——本次调用的内层参数。读取它返回一份不可变快照,且绝不会消费或修改它们:`dsh --profile tui --resume abc` 给你的应用 `['--resume', 'abc']`。
35
+ - `ctx.appExit`——在整棵树关闭后请求进程退出的方式,接到启动器的关停控制器上。
36
+ - `ctx.appReady`——成功启动信号,只在 Loader 树与 launcher 自有设置成功后提交。
37
+
38
+ 没有参数的启动会看到空列表——这是诚实的答案,而不是缺失的值。
30
39
 
31
- 它的 Loader 行不携带启动器标记,也没有特殊类型:
40
+ `exitOnStdinEnd(ctx, label)` 把已成功启动的 stdio 应用 EOF 绑定到 `ctx.appExit(0)`。它绝不读取或恢复 stdin,因此协议传输会收到挂载前已缓冲的字节;启动拒绝优先于竞态 EOF,拥有它的 fiber 会移除两项待处理监听。
41
+
42
+ ### 解析你的 flag
43
+
44
+ 你自带自己的 commander program:声明你的 flag 与 action,本包会针对内层参数运行它。校验只发生在你的 action 中,并由它发布你的行所需的任何值。插件的 Loader 行不携带特殊标记:
32
45
 
33
46
  ```yaml
34
47
  - id: web-startup
35
48
  name: '@deepseek-ai/dsh-web-app/startup'
36
49
  ```
37
50
 
38
- 所有由这些取值配置的行都使用普通服务注入,并在惰性配置中直接访问该服务:
51
+ 由解析值配置的行注入发布的服务,并在其配置中直接读取它:
39
52
 
40
53
  ```yaml
41
54
  - id: webserver
@@ -46,21 +59,67 @@ export function apply(ctx: Context): void {
46
59
  port: !!js ctx.webStartup.port ?? 3080
47
60
  ```
48
61
 
49
- `parseCmdline` 在加载时拒绝整棵命令树中没有任何命令声明 action 的 program,把每个命令的退出与输出都接到启动器上(commander 只在注册时把这些设置复制进子命令),再解析不可变参数;解析成功时 commander 运行被调用命令的同步 action。action 用 `program.error(...)` 拒绝无效调用——必须先拒绝后发布,因为写在拒绝之前的语句已经执行。遇到 `--help`、`--version`、解析错误或这种拒绝时,该适配器输出 commander 文本并请求退出;提供方什么也不发布,因此依赖行不会激活。
62
+ 结果:即使配置写的是 3080,`dsh --profile web --port 8080` 也会让服务器监听 8080 端口,因为 flag 优先。`--help` 打印你的应用帮助并以 0 退出、不启动任何内容;被拒绝的值(例如非数字端口)打印你的错误并以非零码退出,任何依赖解析值的行都不会启动。
63
+
64
+ ### flag 如何胜过配置值
65
+
66
+ 写在 `!!js` 表达式旁的值是后备:flag 存在时 flag 优先,否则使用写下的值。解析在启动时、你的解析器运行之后发生一次,因此 flag 绝不会被之后的配置重载悄悄重置。
67
+
68
+ ### 多个插件读取同一份参数
69
+
70
+ 任意数量的插件都可以读取同一份参数——读取绝不会消费它们——每个插件都能解析自己需要的部分并发布各自的值。启动器不会决定谁是命令行的所有者:没有读取方的应用会忽略自己的参数。
71
+
72
+ 本仓库之外构建的应用行为一致:即使它们自带 commander 副本,其 `--help` 也会打印并退出,而不是崩溃。
73
+
74
+ -----
75
+
76
+ <a id="understand-the-implementation"></a>
77
+ ## 理解实现
78
+
79
+ <details>
80
+ <summary>实现细节——点击展开</summary>
81
+
82
+ 本节解释上述结果如何实现,并指出实现它们的代码位置;这里的内容面向开发者,使用本包并不需要。
83
+
84
+ ### 设计说明
50
85
 
51
- ### 注入如何排列配置求值
86
+ - **启动器事实,而非配置。** `cmdlineArgs` 与 `appExit` 在树挂载前提供到宿主上下文上;它们不是 Loader 行,因此没有任何组合持有或覆盖它们。
87
+ - **按位置切分。** 启动器不认识任何应用行:自身 flag 之后的第一个 token 就是应用参数的起点,因此 flag 家族、`--help` 文本与解析错误都由应用自己持有。
88
+ - **结构化错误识别。** `isCommanderError` 读取 commander 的错误码前缀,而不是用 `instanceof`,因为树外插件会带来自己的一份 commander 副本,其 `CommanderError` 身份不同;`configureExitAndOutput` 会遍历每个子命令,因为 commander 只在注册时复制退出与输出设置。
89
+ - **可注入的输出流。** `internals` 持有输出流,使测试无需触碰进程即可捕获 commander 的文本。
52
90
 
53
- Loader 会把一行的 `!!js` 插值推迟到该行声明的注入全部激活之后,再基于该行的插件上下文求值。所以上例可以直接读取 `ctx.webStartup`:Loader 索取 `webserver` 的配置之前,Cordis 已经填入了这个注入服务。Include 树会保留嵌套表达式节点,直到各个目标行到达这一时点。提供方替换与活动 patch 重载都会针对当前注入服务重新插值,因此启动 flag 不会被悄悄重置。
91
+ ### 解析约定
54
92
 
55
- ### 共享不可变参数
93
+ 解析路径是一个只有两个所有者的小家族:`provideCmdline` 冻结宿主参数,并在任何配置树条目挂载前提供 `cmdlineArgs` 与 `appExit`;`parseCmdline` 针对不可变参数运行你的 commander program,把每个命令的 help、version 与错误输出都接到启动器上。被拒绝的值、`--help` 或 `--version` 会打印 commander 文本并请求 `ctx.appExit`,且不发布任何内容,因此依赖行绝不会激活;Loader 会把每行的 `!!js` 插值推迟到该行声明的注入全部激活之后。各导出的约定在代码中,不在本 README——见 [`src/index.ts`](src/index.ts)。
56
94
 
57
- `get()` 不会消费或修改 argv。多个插件可以解析同一份快照,并分别提供服务。启动器不会检查组合中的命令行所有者;没有读取方的 profile 只会忽略自己的应用参数。
95
+ ### 源码地图
58
96
 
59
- 树外插件会带来自己的一份 commander 副本,因此 commander 的控制流错误按结构识别,而不是按类身份识别;按身份判断会把已经打印出来的 help 重新抛成致命的加载失败。
97
+ | 文件 | 职责 |
98
+ |---|---|
99
+ | [`src/index.ts`](src/index.ts) | `CmdlineArgs`/`AppExit` 类型、`provideCmdline`、`parseCmdline`、commander 退出/输出路由 |
100
+ | [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件(无运行时不变式;Loader 结算会报告缺失的服务) |
60
101
 
102
+ </details>
103
+
104
+ -----
105
+
106
+ <a id="further-exploration"></a>
107
+ ## 进一步探索
108
+
109
+ 当包级约定不够用时阅读以下页面。它们从交接机制逐步进入消费它的应用及其背后的决策。
110
+
111
+ - [应用持有命令行决策](../../../.agents/notes/implemented/architecture/2026-08-06-app-owned-command-line.zh.md)——为什么 flag 家族由应用持有,以及交接如何运作。
112
+ - [命令行 seam 精简](../../../.agents/notes/implemented/architecture/2026-08-11-cmdline-seam-trim.zh.md)——缩减到既有接口的各 seam。
113
+ - [dsh-app-boot](../app-boot/README.zh.md)——提供这些启动器值的启动序列。
114
+ - [dsh-web-app 组合包](../../bundle/web-app/README.zh.md)——通过此包持有 Web flag 家族的应用。
115
+ - [dsh-headless 组合包](../../bundle/headless/README.zh.md)——从命令行读取任务的一次性 runner。
116
+
117
+ -----
118
+
119
+ <a id="model-experience"></a>
61
120
  ## 模型体验
62
121
 
63
- 无。本包在任何会话存在之前解析进程自身的命令行。
122
+ 无。本包在任何会话存在之前解析进程自身的命令行;配置行持有每一个模型可见的后果。
64
123
 
65
124
  #### KV Cache 影响
66
125
 
@@ -68,6 +127,25 @@ Loader 会把一行的 `!!js` 插值推迟到该行声明的注入全部激活
68
127
 
69
128
  ## 已知限制与延期工作
70
129
 
71
- - **启动器的 flag 必须写在应用参数之前**:切分按位置进行,启动器不认识的第一个 token 就是内层参数的起点,因此写在某个应用 flag 之后的 `--patch` 属于应用。启动器的解析器会消耗掉一个 `--`,因此必须以字面量 `--` 存活到应用的参数需要写成 `-- --`。
72
- - **应用自有服务没有静态声明的提供方**:消费行通过普通注入点名它;缺少提供方的组合包会在结算时失败,由待处理条目点名该服务,而不是在加载时失败。
73
- - **用户 patch 若整体替换某行的 `config`,会连同其中的表达式一起丢掉**:flag 胜过的是表达式旁写着的那个值,而不是用户用字面量替换掉表达式之后的结果;保留表达式才能保留 flag 的优先级。
130
+ <a id="known-limitations-and-deferred-work"></a>
131
+
132
+
133
+ 这些限制说明应用自有命令行在何时不合适,或何时需要特别注意。它们是当前包约束,不是任务积压。
134
+
135
+ - **启动器的 flag 必须写在应用参数之前**——切分按位置进行:启动器不认识的第一个 token 就是内层参数的起点,因此写在某个应用 flag 之后的 `--patch` 属于应用。启动器的解析器会消耗掉一个 `--`,因此必须以字面量 `--` 存活到应用的参数需要写成 `-- --`。
136
+ - **应用自有服务没有静态声明的提供方**——消费行通过普通注入点名它;缺少提供方的组合包会在结算时失败,由待处理条目点名该服务,而不是在加载时失败。
137
+ - **用户 patch 若整体替换某行的 `config`,会连同其中的表达式一起丢掉**——flag 胜过的是表达式旁写着的那个值,而不是用户用字面量替换掉表达式之后的结果;保留表达式才能保留 flag 的优先级。
138
+
139
+ <a id="dev-note"></a>
140
+ ### 开发备注
141
+
142
+ <details>
143
+ <summary>维护者的工作上下文——点击展开</summary>
144
+
145
+ 本开发备注是维护者的工作上下文:开放设计问题与尚未决定的探索方向。它明确不具权威性——已交付的行为、限制与既定理由以上文、包代码和相关 Agent Note 为准。
146
+
147
+ #### 待定:解析器表面
148
+
149
+ `parseCmdline` 是 commander 适配器,而不是命令行框架:help、version 与错误输出遵循 commander 的格式,退出/输出路由也假定 commander 的控制流模型。改用其他解析器需要它自己的路由与错误处理;`cmdlineArgs` 服务约定中没有任何内容依赖 commander。
150
+
151
+ </details>
package/lib/index.js CHANGED
@@ -17,23 +17,60 @@
17
17
  * @module @deepseek-ai/dsh-cmdline
18
18
  */
19
19
  /**
20
- * Provide the command line and the exit request on a host context before any
21
- * tree entry mounts. Both are launcher facts, not config: an embedding host
22
- * with no command line provides an empty argument list.
20
+ * Provide launcher facts on a host context before any tree entry mounts: the
21
+ * command line, bounded exit request, and optional successful-startup signal.
22
+ * An embedding host with no command line provides an empty argument list; a
23
+ * host that mounts a stdio application also provides readiness.
23
24
  * @param ctx - the host context the tree will mount under.
24
- * @param host - the invocation's arguments and its exit request.
25
+ * @param host - the invocation's arguments, exit request, and optional readiness signal.
25
26
  */
26
27
  function provideCmdline(ctx, host) {
27
28
  const snapshot = Object.freeze([...host.args]);
28
29
  ctx.provide("cmdlineArgs", { get: () => snapshot });
29
30
  ctx.provide("appExit", host.exit);
31
+ if (host.ready !== void 0) ctx.provide("appReady", host.ready);
30
32
  }
31
- /** The process streams commander output is written to; production writes to the process. */
33
+ /** Process streams used by app command lines and stdio lifetime binding; tests substitute them. */
32
34
  const internals = {
35
+ stdin: process.stdin,
33
36
  stdout: process.stdout,
34
37
  stderr: process.stderr
35
38
  };
36
39
  /**
40
+ * Make stdin EOF request the launcher's bounded successful shutdown after
41
+ * {@link AppReady} commits. A startup rejection therefore remains the process
42
+ * outcome when it races EOF. The caller invokes this only after its command
43
+ * action accepts the invocation, so help and usage failures start no transport
44
+ * lifecycle. This listener does not read or resume stdin: the protocol
45
+ * transport owns input and receives bytes buffered before it mounts. Disposal
46
+ * removes the EOF and readiness listeners.
47
+ * @param ctx - app plugin context carrying the launcher's exit request.
48
+ * @param label - effect label naming the owning application.
49
+ */
50
+ function exitOnStdinEnd(ctx, label) {
51
+ const exit = ctx.get("appExit");
52
+ const ready = ctx.get("appReady");
53
+ if (exit === void 0 || ready === void 0) throw new Error("stdio app: the launcher must provide ctx.appExit and ctx.appReady before the tree mounts");
54
+ const stdin = internals.stdin;
55
+ let active = true;
56
+ let ended = false;
57
+ let cancelReady = () => {};
58
+ const onEnd = () => {
59
+ if (!active || ended) return;
60
+ ended = true;
61
+ cancelReady = ready.onReady(() => {
62
+ exit(0);
63
+ });
64
+ };
65
+ ctx.effect(() => () => {
66
+ active = false;
67
+ cancelReady();
68
+ stdin.off("end", onEnd);
69
+ }, label);
70
+ stdin.once("end", onEnd);
71
+ if (stdin.readableEnded) queueMicrotask(onEnd);
72
+ }
73
+ /**
37
74
  * Parse the launcher's immutable argument snapshot with an app's commander
38
75
  * program. Commander runs the program's own synchronous action handler on a
39
76
  * successful parse; app code there publishes its service and rejects an
@@ -112,4 +149,4 @@ function isCommanderError(error) {
112
149
  return typeof candidate.code === "string" && candidate.code.startsWith("commander.") && typeof candidate.exitCode === "number";
113
150
  }
114
151
  //#endregion
115
- export { internals, parseCmdline, provideCmdline };
152
+ export { exitOnStdinEnd, internals, parseCmdline, provideCmdline };
@@ -37,12 +37,24 @@ export interface AppExit {
37
37
  */
38
38
  (code: number): void;
39
39
  }
40
+ /** Successful application-startup signal owned by the launcher. */
41
+ export interface AppReady {
42
+ /**
43
+ * Run a listener once successful startup is committed. A failed or
44
+ * externally terminated startup never calls it.
45
+ * @param listener - work that may begin only after successful startup.
46
+ * @returns a disposer that cancels a pending listener.
47
+ */
48
+ onReady(listener: () => void): () => void;
49
+ }
40
50
  declare module '@deepseek-ai/cordis' {
41
51
  interface Context {
42
52
  /** The invocation's inner arguments; provided by a launcher before the tree mounts. */
43
53
  cmdlineArgs?: CmdlineArgs;
44
54
  /** Bounded process-exit request; provided by a launcher before the tree mounts. */
45
55
  appExit?: AppExit;
56
+ /** Successful startup signal; provided by a launcher before the tree mounts. */
57
+ appReady?: AppReady;
46
58
  }
47
59
  }
48
60
  /** The launcher facts an app needs. */
@@ -51,17 +63,30 @@ export interface CmdlineHost {
51
63
  args: readonly string[];
52
64
  /** Bounded process-exit request. */
53
65
  exit: AppExit;
66
+ /** Successful startup signal for lifecycle work that must not mask boot failure. */
67
+ ready?: AppReady;
54
68
  }
55
69
  /**
56
- * Provide the command line and the exit request on a host context before any
57
- * tree entry mounts. Both are launcher facts, not config: an embedding host
58
- * with no command line provides an empty argument list.
70
+ * Provide launcher facts on a host context before any tree entry mounts: the
71
+ * command line, bounded exit request, and optional successful-startup signal.
72
+ * An embedding host with no command line provides an empty argument list; a
73
+ * host that mounts a stdio application also provides readiness.
59
74
  * @param ctx - the host context the tree will mount under.
60
- * @param host - the invocation's arguments and its exit request.
75
+ * @param host - the invocation's arguments, exit request, and optional readiness signal.
61
76
  */
62
77
  export declare function provideCmdline(ctx: Context, host: CmdlineHost): void;
63
- /** The process streams commander output is written to; production writes to the process. */
78
+ /** Process stdin operations used to bind a stdio application's lifetime. */
79
+ export interface AppStdin {
80
+ /** Whether EOF arrived before the application bound its listener. */
81
+ readonly readableEnded: boolean;
82
+ /** Subscribe once to stdin EOF. */
83
+ once(event: 'end', listener: () => void): unknown;
84
+ /** Remove a previously installed stdin EOF listener. */
85
+ off(event: 'end', listener: () => void): unknown;
86
+ }
87
+ /** Process streams used by app command lines and stdio lifetime binding; tests substitute them. */
64
88
  export declare const internals: {
89
+ stdin: AppStdin;
65
90
  stdout: {
66
91
  write(chunk: string): unknown;
67
92
  };
@@ -69,6 +94,18 @@ export declare const internals: {
69
94
  write(chunk: string): unknown;
70
95
  };
71
96
  };
97
+ /**
98
+ * Make stdin EOF request the launcher's bounded successful shutdown after
99
+ * {@link AppReady} commits. A startup rejection therefore remains the process
100
+ * outcome when it races EOF. The caller invokes this only after its command
101
+ * action accepts the invocation, so help and usage failures start no transport
102
+ * lifecycle. This listener does not read or resume stdin: the protocol
103
+ * transport owns input and receives bytes buffered before it mounts. Disposal
104
+ * removes the EOF and readiness listeners.
105
+ * @param ctx - app plugin context carrying the launcher's exit request.
106
+ * @param label - effect label naming the owning application.
107
+ */
108
+ export declare function exitOnStdinEnd(ctx: Context, label: string): void;
72
109
  /**
73
110
  * Parse the launcher's immutable argument snapshot with an app's commander
74
111
  * program. Commander runs the program's own synchronous action handler on a
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-cmdline",
3
3
  "description": "Immutable command-line handoff from a dsh launcher to any app plugin that injects cmdlineArgs",
4
- "version": "0.1.1-rc.2",
4
+ "version": "0.1.2-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -32,15 +32,15 @@
32
32
  ],
33
33
  "license": "MIT",
34
34
  "peerDependencies": {
35
- "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
36
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
37
- "@deepseek-ai/cordis": "^4.0.1"
35
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
36
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
37
+ "@deepseek-ai/cordis": "^4.0.2"
38
38
  },
39
39
  "devDependencies": {
40
40
  "commander": "^15.0.0",
41
- "@deepseek-ai/cordis-plugin-include": "^1.0.6",
42
- "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
43
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
44
- "@deepseek-ai/cordis": "^4.0.1"
41
+ "@deepseek-ai/cordis-plugin-include": "^1.0.7",
42
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
43
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
44
+ "@deepseek-ai/cordis": "^4.0.2"
45
45
  }
46
46
  }