@x1a0f3n9/dsh-cmdline 0.1.2-alpha.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/boot/cmdline/README.md
5
+ README.md: 2025cbdf3c32df3c6b750cfb6e160760b2835d03
6
+ README.zh.md: 6b1ef59b3557fe00d1b2e583e095563d60355dfa
package/README.md ADDED
@@ -0,0 +1,151 @@
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
+ # @x1a0f3n9/dsh-cmdline
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
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.
13
+
14
+ ## Table of Contents
15
+
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)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
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.
29
+
30
+ ### The launcher values
31
+
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.
39
+
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:
45
+
46
+ ```yaml
47
+ - id: web-startup
48
+ name: '@x1a0f3n9/dsh-web-app/startup'
49
+ ```
50
+
51
+ Rows configured from the parsed values inject the published service and read it directly in their config:
52
+
53
+ ```yaml
54
+ - id: webserver
55
+ name: '@x1a0f3n9/dsh-host-webserver'
56
+ inject: [webStartup]
57
+ config:
58
+ host: !!js ctx.webStartup.host ?? '127.0.0.1'
59
+ port: !!js ctx.webStartup.port ?? 3080
60
+ ```
61
+
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
85
+
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.
90
+
91
+ ### Parsing contract
92
+
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).
94
+
95
+ ### Source map
96
+
97
+ | File | Role |
98
+ |---|---|
99
+ | [`src/index.ts`](src/index.ts) | `CmdlineArgs`/`AppExit` types, `provideCmdline`, `parseCmdline`, commander exit/output routing |
100
+ | — | No runtime invariant companion is published; `cmdlineArgs` is an immutable launcher fact that any number of ordinary plugins may read. App-owned providers and consumers use normal Cordis service injection, whose missing dependencies are already reported by Loader settlement. |
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>
120
+ ## Model Experience
121
+
122
+ None, as this package resolves the process command line before any session exists; configured rows own every model-visible consequence.
123
+
124
+ #### KV Cache effect
125
+
126
+ None; this package neither assembles nor sends a provider request.
127
+
128
+ ## Known Limitations and Deferred Work
129
+
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 ADDED
@@ -0,0 +1,151 @@
1
+ ---
2
+ description: "dsh app bin 的应用自有命令行:应用从启动器剩余参数中解析自己的 flag、--help 与退出行为。"
3
+ kind: "package-library"
4
+ ---
5
+
6
+ # @x1a0f3n9/dsh-cmdline
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ `dsh-cmdline` 让你的应用持有自己的命令行:启动器只保留属于自己的 flag(`--profile`、`--patch`、配置 dump),并把**其后的一切**原样交给你的应用,因此 flag、`--help` 文本与解析错误都由你的应用决定。你从这些参数解析出的值会胜过配置中写下的任何默认值,且无需写回任何内容。你的应用还获得一个有边界的进程退出请求,接到启动器的关停上。当你编写接受自有 flag 的应用 bin 时使用它;它本身不增加任何提示词、schema 或面向模型的表面。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 你的应用在启动时读取本次调用的内层参数,任意数量的插件都可以使用它们。常用路径是:启动插件读取参数、解析它们,再发布解析后的值;其他行由这些值配置自身。
29
+
30
+ ### 启动器提供的值
31
+
32
+ 启动器向你的应用提供三样东西:
33
+
34
+ - `ctx.cmdlineArgs`——本次调用的内层参数。读取它返回一份不可变快照,且绝不会消费或修改它们:`dsh --profile tui --resume abc` 给你的应用 `['--resume', 'abc']`。
35
+ - `ctx.appExit`——在整棵树关闭后请求进程退出的方式,接到启动器的关停控制器上。
36
+ - `ctx.appReady`——成功启动信号,只在 Loader 树与 launcher 自有设置成功后提交。
37
+
38
+ 没有参数的启动会看到空列表——这是诚实的答案,而不是缺失的值。
39
+
40
+ `exitOnStdinEnd(ctx, label)` 把已成功启动的 stdio 应用 EOF 绑定到 `ctx.appExit(0)`。它绝不读取或恢复 stdin,因此协议传输会收到挂载前已缓冲的字节;启动拒绝优先于竞态 EOF,拥有它的 fiber 会移除两项待处理监听。
41
+
42
+ ### 解析你的 flag
43
+
44
+ 你自带自己的 commander program:声明你的 flag 与 action,本包会针对内层参数运行它。校验只发生在你的 action 中,并由它发布你的行所需的任何值。插件的 Loader 行不携带特殊标记:
45
+
46
+ ```yaml
47
+ - id: web-startup
48
+ name: '@x1a0f3n9/dsh-web-app/startup'
49
+ ```
50
+
51
+ 由解析值配置的行注入发布的服务,并在其配置中直接读取它:
52
+
53
+ ```yaml
54
+ - id: webserver
55
+ name: '@x1a0f3n9/dsh-host-webserver'
56
+ inject: [webStartup]
57
+ config:
58
+ host: !!js ctx.webStartup.host ?? '127.0.0.1'
59
+ port: !!js ctx.webStartup.port ?? 3080
60
+ ```
61
+
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
+ ### 设计说明
85
+
86
+ - **启动器事实,而非配置。** `cmdlineArgs` 与 `appExit` 在树挂载前提供到宿主上下文上;它们不是 Loader 行,因此没有任何组合持有或覆盖它们。
87
+ - **按位置切分。** 启动器不认识任何应用行:自身 flag 之后的第一个 token 就是应用参数的起点,因此 flag 家族、`--help` 文本与解析错误都由应用自己持有。
88
+ - **结构化错误识别。** `isCommanderError` 读取 commander 的错误码前缀,而不是用 `instanceof`,因为树外插件会带来自己的一份 commander 副本,其 `CommanderError` 身份不同;`configureExitAndOutput` 会遍历每个子命令,因为 commander 只在注册时复制退出与输出设置。
89
+ - **可注入的输出流。** `internals` 持有输出流,使测试无需触碰进程即可捕获 commander 的文本。
90
+
91
+ ### 解析约定
92
+
93
+ 解析路径是一个只有两个所有者的小家族:`provideCmdline` 冻结宿主参数,并在任何配置树条目挂载前提供 `cmdlineArgs` 与 `appExit`;`parseCmdline` 针对不可变参数运行你的 commander program,把每个命令的 help、version 与错误输出都接到启动器上。被拒绝的值、`--help` 或 `--version` 会打印 commander 文本并请求 `ctx.appExit`,且不发布任何内容,因此依赖行绝不会激活;Loader 会把每行的 `!!js` 插值推迟到该行声明的注入全部激活之后。各导出的约定在代码中,不在本 README——见 [`src/index.ts`](src/index.ts)。
94
+
95
+ ### 源码地图
96
+
97
+ | 文件 | 职责 |
98
+ |---|---|
99
+ | [`src/index.ts`](src/index.ts) | `CmdlineArgs`/`AppExit` 类型、`provideCmdline`、`parseCmdline`、commander 退出/输出路由 |
100
+ | — | 不发布运行时不变式伴生入口;Loader 结算会报告缺失的服务。 |
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>
120
+ ## 模型体验
121
+
122
+ 无。本包在任何会话存在之前解析进程自身的命令行;配置行持有每一个模型可见的后果。
123
+
124
+ #### KV Cache 影响
125
+
126
+ 无;本包既不组装也不发送提供方请求。
127
+
128
+ ## 已知限制与延期工作
129
+
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 ADDED
@@ -0,0 +1,152 @@
1
+ //#region lib/types/index.js
2
+ /**
3
+ * @x1a0f3n9/dsh-cmdline — the command line a dsh launcher hands to the app
4
+ * it boots.
5
+ *
6
+ * The launcher parses only its own flags (`--profile`, `--patch`, the config
7
+ * dumps) and hands everything after them to the tree verbatim through the
8
+ * {@link CmdlineArgs} service, so an app owns its flag family, its `--help`
9
+ * text, and its parse errors instead of the launcher knowing them.
10
+ *
11
+ * Any app plugin can inject `cmdlineArgs` and call {@link parseCmdline}. A
12
+ * provider may publish the parsed values as its own service from its program's
13
+ * commander action, and ordinary rows
14
+ * can inject that service and read it from lazily resolved config —
15
+ * `port: !!js ctx.webStartup.port ?? 3080` — so a flag beats the value written
16
+ * beside it. No row has launcher-level command-line status.
17
+ * @module @x1a0f3n9/dsh-cmdline
18
+ */
19
+ /**
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.
24
+ * @param ctx - the host context the tree will mount under.
25
+ * @param host - the invocation's arguments, exit request, and optional readiness signal.
26
+ */
27
+ function provideCmdline(ctx, host) {
28
+ const snapshot = Object.freeze([...host.args]);
29
+ ctx.provide("cmdlineArgs", { get: () => snapshot });
30
+ ctx.provide("appExit", host.exit);
31
+ if (host.ready !== void 0) ctx.provide("appReady", host.ready);
32
+ }
33
+ /** Process streams used by app command lines and stdio lifetime binding; tests substitute them. */
34
+ const internals = {
35
+ stdin: process.stdin,
36
+ stdout: process.stdout,
37
+ stderr: process.stderr
38
+ };
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
+ /**
74
+ * Parse the launcher's immutable argument snapshot with an app's commander
75
+ * program. Commander runs the program's own synchronous action handler on a
76
+ * successful parse; app code there publishes its service and rejects an
77
+ * invalid invocation with `program.error(...)`. This helper has no Loader-row
78
+ * or service ownership semantics.
79
+ *
80
+ * Help, version, and rejected arguments — from the grammar or from an action
81
+ * — are terminal for the process: commander writes the text and the helper
82
+ * requests `ctx.appExit`. The action never runs on help, version, or a
83
+ * grammar rejection; an action must reject before it publishes, because
84
+ * statements before its `program.error(...)` have already run.
85
+ * @param ctx - plugin context carrying `cmdlineArgs` and `appExit`.
86
+ * @param program - the app's commander program, with its flags, description,
87
+ * actions, and any subcommands already declared.
88
+ * @throws when the launcher did not provide the command line and exit request,
89
+ * or when no command in the program declares an action.
90
+ */
91
+ function parseCmdline(ctx, program) {
92
+ const args = ctx.get("cmdlineArgs");
93
+ const exit = ctx.get("appExit");
94
+ if (args === void 0 || exit === void 0) throw new Error(`${program.name()}: the launcher must provide ctx.cmdlineArgs and ctx.appExit before the tree mounts`);
95
+ if (!hasAction(program)) throw new Error(`${program.name()}: no command in the program declares an action; parseCmdline runs the invoked command's action on a successful parse, and app code there publishes its service`);
96
+ configureExitAndOutput(program);
97
+ try {
98
+ program.parse(args.get(), { from: "user" });
99
+ } catch (error) {
100
+ if (!isCommanderError(error)) throw error;
101
+ exit(error.exitCode);
102
+ }
103
+ }
104
+ /**
105
+ * Whether any command in the tree declares an action handler.
106
+ *
107
+ * The `Command` type cannot express the action precondition, so the handler is
108
+ * read structurally (as {@link isCommanderError} reads commander's control-flow
109
+ * errors): without this guard, a program that forgot its action would parse
110
+ * successfully, publish nothing, and surface only as dependent rows pending on
111
+ * the absent service.
112
+ * @param command - the command whose tree is inspected.
113
+ * @returns true when the command or any registered subcommand has an action.
114
+ */
115
+ function hasAction(command) {
116
+ if (typeof command._actionHandler === "function") return true;
117
+ return command.commands.some(hasAction);
118
+ }
119
+ /**
120
+ * Route every command's exit and output through the launcher adapter.
121
+ *
122
+ * Commander copies `exitOverride` and output configuration into a subcommand
123
+ * only at registration, so a root-only override would let an
124
+ * already-registered subcommand's rejection write to the process streams and
125
+ * call `process.exit` directly, bypassing `ctx.appExit`.
126
+ * @param command - the root of the command tree to configure.
127
+ */
128
+ function configureExitAndOutput(command) {
129
+ command.exitOverride().configureOutput({
130
+ writeOut: (text) => void internals.stdout.write(text),
131
+ writeErr: (text) => void internals.stderr.write(text)
132
+ });
133
+ for (const child of command.commands) configureExitAndOutput(child);
134
+ }
135
+ /**
136
+ * Whether a thrown value is commander's own control-flow error (help, version,
137
+ * a parse error, or `program.error`).
138
+ *
139
+ * Detected structurally, not with `instanceof`: an out-of-tree plugin brings
140
+ * its own commander copy, whose `CommanderError` class is a different identity
141
+ * from this package's, and an identity check there would rethrow a printed
142
+ * help as a fatal load failure.
143
+ * @param error - the thrown value.
144
+ * @returns true when the value carries commander's error code and exit code.
145
+ */
146
+ function isCommanderError(error) {
147
+ if (typeof error !== "object" || error === null) return false;
148
+ const candidate = error;
149
+ return typeof candidate.code === "string" && candidate.code.startsWith("commander.") && typeof candidate.exitCode === "number";
150
+ }
151
+ //#endregion
152
+ export { exitOnStdinEnd, internals, parseCmdline, provideCmdline };
@@ -0,0 +1,128 @@
1
+ /**
2
+ * @x1a0f3n9/dsh-cmdline — the command line a dsh launcher hands to the app
3
+ * it boots.
4
+ *
5
+ * The launcher parses only its own flags (`--profile`, `--patch`, the config
6
+ * dumps) and hands everything after them to the tree verbatim through the
7
+ * {@link CmdlineArgs} service, so an app owns its flag family, its `--help`
8
+ * text, and its parse errors instead of the launcher knowing them.
9
+ *
10
+ * Any app plugin can inject `cmdlineArgs` and call {@link parseCmdline}. A
11
+ * provider may publish the parsed values as its own service from its program's
12
+ * commander action, and ordinary rows
13
+ * can inject that service and read it from lazily resolved config —
14
+ * `port: !!js ctx.webStartup.port ?? 3080` — so a flag beats the value written
15
+ * beside it. No row has launcher-level command-line status.
16
+ * @module @x1a0f3n9/dsh-cmdline
17
+ */
18
+ import type { Command } from 'commander';
19
+ import type { Context } from '@deepseek-ai/cordis';
20
+ /**
21
+ * The invocation's inner arguments: everything after the launcher's own flags,
22
+ * verbatim and in argv order. `dsh --profile tui --resume abc` yields
23
+ * `['--resume', 'abc']`.
24
+ */
25
+ export interface CmdlineArgs {
26
+ /**
27
+ * Read the inner arguments.
28
+ * @returns the arguments in argv order; empty when the invocation carried none.
29
+ */
30
+ get(): readonly string[];
31
+ }
32
+ /** Request bounded process exit; the launcher wires it to its shutdown controller. */
33
+ export interface AppExit {
34
+ /**
35
+ * Request exit once the tree has been disposed.
36
+ * @param code - the process exit code.
37
+ */
38
+ (code: number): void;
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
+ }
50
+ declare module '@deepseek-ai/cordis' {
51
+ interface Context {
52
+ /** The invocation's inner arguments; provided by a launcher before the tree mounts. */
53
+ cmdlineArgs?: CmdlineArgs;
54
+ /** Bounded process-exit request; provided by a launcher before the tree mounts. */
55
+ appExit?: AppExit;
56
+ /** Successful startup signal; provided by a launcher before the tree mounts. */
57
+ appReady?: AppReady;
58
+ }
59
+ }
60
+ /** The launcher facts an app needs. */
61
+ export interface CmdlineHost {
62
+ /** The invocation's inner arguments, in argv order. */
63
+ args: readonly string[];
64
+ /** Bounded process-exit request. */
65
+ exit: AppExit;
66
+ /** Successful startup signal for lifecycle work that must not mask boot failure. */
67
+ ready?: AppReady;
68
+ }
69
+ /**
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.
74
+ * @param ctx - the host context the tree will mount under.
75
+ * @param host - the invocation's arguments, exit request, and optional readiness signal.
76
+ */
77
+ export declare function provideCmdline(ctx: Context, host: CmdlineHost): void;
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. */
88
+ export declare const internals: {
89
+ stdin: AppStdin;
90
+ stdout: {
91
+ write(chunk: string): unknown;
92
+ };
93
+ stderr: {
94
+ write(chunk: string): unknown;
95
+ };
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;
109
+ /**
110
+ * Parse the launcher's immutable argument snapshot with an app's commander
111
+ * program. Commander runs the program's own synchronous action handler on a
112
+ * successful parse; app code there publishes its service and rejects an
113
+ * invalid invocation with `program.error(...)`. This helper has no Loader-row
114
+ * or service ownership semantics.
115
+ *
116
+ * Help, version, and rejected arguments — from the grammar or from an action
117
+ * — are terminal for the process: commander writes the text and the helper
118
+ * requests `ctx.appExit`. The action never runs on help, version, or a
119
+ * grammar rejection; an action must reject before it publishes, because
120
+ * statements before its `program.error(...)` have already run.
121
+ * @param ctx - plugin context carrying `cmdlineArgs` and `appExit`.
122
+ * @param program - the app's commander program, with its flags, description,
123
+ * actions, and any subcommands already declared.
124
+ * @throws when the launcher did not provide the command line and exit request,
125
+ * or when no command in the program declares an action.
126
+ */
127
+ export declare function parseCmdline(ctx: Context, program: Command): void;
128
+ //# sourceMappingURL=index.d.ts.map
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@x1a0f3n9/dsh-cmdline",
3
+ "description": "Immutable command-line handoff from a dsh launcher to any app plugin that injects cmdlineArgs",
4
+ "version": "0.1.2-alpha.6",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/boot/cmdline"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./src/*": "./src/*",
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "lib/index.js",
26
+ "lib/types/**/*.d.ts"
27
+ ],
28
+ "license": "MIT",
29
+ "peerDependencies": {
30
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
31
+ "@deepseek-ai/cordis": "^4.0.2"
32
+ },
33
+ "devDependencies": {
34
+ "commander": "^15.0.0",
35
+ "@deepseek-ai/cordis-plugin-include": "^1.0.7",
36
+ "@deepseek-ai/cordis": "^4.0.2",
37
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.3"
38
+ }
39
+ }