-
Notifications
You must be signed in to change notification settings - Fork 29
Expand file tree
/
Copy pathindex.ts
More file actions
239 lines (225 loc) · 9.83 KB
/
Copy pathindex.ts
File metadata and controls
239 lines (225 loc) · 9.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
/**
* @deepseek-ai/dsh-cmdline — 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 through the
* {@link CmdlineArgs} service, so an app owns its flag family, its `--help`
* text, and its parse errors instead of the launcher knowing them.
*
* Any app plugin can inject `cmdlineArgs` and call {@link parseCmdline}. A
* provider may publish the parsed values as its own service from its program's
* commander action, and ordinary rows
* can inject that service and read it from lazily resolved config —
* `port: !!js ctx.webStartup.port ?? 3080` — so a flag beats the value written
* beside it. No row has launcher-level command-line status.
* @module @deepseek-ai/dsh-cmdline
*/
import type { Command } from 'commander'
import type { Context } from '@deepseek-ai/cordis'
/**
* The invocation's inner arguments: everything after the launcher's own flags,
* verbatim and in argv order. `dsh --profile tui --resume abc` yields
* `['--resume', 'abc']`.
*/
export interface CmdlineArgs {
/**
* Read the inner arguments.
* @returns the arguments in argv order; empty when the invocation carried none.
*/
get(): readonly string[]
}
/** Request bounded process exit; the launcher wires it to its shutdown controller. */
export interface AppExit {
/**
* Request exit once the tree has been disposed.
* @param code - the process exit code.
*/
(code: number): void
}
/** Successful application-startup signal owned by the launcher. */
export interface AppReady {
/**
* Run a listener once successful startup is committed. A failed or
* externally terminated startup never calls it.
* @param listener - work that may begin only after successful startup.
* @returns a disposer that cancels a pending listener.
*/
onReady(listener: () => void): () => void
}
declare module '@deepseek-ai/cordis' {
interface Context {
/** The invocation's inner arguments; provided by a launcher before the tree mounts. */
cmdlineArgs?: CmdlineArgs
/** Bounded process-exit request; provided by a launcher before the tree mounts. */
appExit?: AppExit
/** Successful startup signal; provided by a launcher before the tree mounts. */
appReady?: AppReady
}
}
/** The launcher facts an app needs. */
export interface CmdlineHost {
/** The invocation's inner arguments, in argv order. */
args: readonly string[]
/** Bounded process-exit request. */
exit: AppExit
/** Successful startup signal for lifecycle work that must not mask boot failure. */
ready?: AppReady
}
/**
* Provide launcher facts on a host context before any tree entry mounts: the
* command line, bounded exit request, and optional successful-startup signal.
* An embedding host with no command line provides an empty argument list; a
* host that mounts a stdio application also provides readiness.
* @param ctx - the host context the tree will mount under.
* @param host - the invocation's arguments, exit request, and optional readiness signal.
*/
export function provideCmdline(ctx: Context, host: CmdlineHost): void {
const snapshot: readonly string[] = Object.freeze([...host.args])
ctx.provide('cmdlineArgs', { get: () => snapshot })
ctx.provide('appExit', host.exit)
if (host.ready !== undefined) ctx.provide('appReady', host.ready)
}
/** Process stdin operations used to bind a stdio application's lifetime. */
export interface AppStdin {
/** Whether EOF arrived before the application bound its listener. */
readonly readableEnded: boolean
/** Subscribe once to stdin EOF. */
once(event: 'end', listener: () => void): unknown
/** Remove a previously installed stdin EOF listener. */
off(event: 'end', listener: () => void): unknown
}
/** Process streams used by app command lines and stdio lifetime binding; tests substitute them. */
export const internals: {
stdin: AppStdin
stdout: { write(chunk: string): unknown }
stderr: { write(chunk: string): unknown }
} = {
stdin: process.stdin,
stdout: process.stdout,
stderr: process.stderr,
}
/**
* Make stdin EOF request the launcher's bounded successful shutdown after
* {@link AppReady} commits. A startup rejection therefore remains the process
* outcome when it races EOF. The caller invokes this only after its command
* action accepts the invocation, so help and usage failures start no transport
* lifecycle. This listener does not read or resume stdin: the protocol
* transport owns input and receives bytes buffered before it mounts. Disposal
* removes the EOF and readiness listeners.
* @param ctx - app plugin context carrying the launcher's exit request.
* @param label - effect label naming the owning application.
*/
export function exitOnStdinEnd(ctx: Context, label: string): void {
const exit = ctx.get('appExit')
const ready = ctx.get('appReady')
if (exit === undefined || ready === undefined) {
throw new Error('stdio app: the launcher must provide ctx.appExit and ctx.appReady before the tree mounts')
}
const stdin = internals.stdin
let active = true
let ended = false
let cancelReady = (): void => {}
const onEnd = (): void => {
if (!active || ended) return
ended = true
cancelReady = ready.onReady(() => { exit(0) })
}
ctx.effect(() => () => {
active = false
cancelReady()
stdin.off('end', onEnd)
}, label)
stdin.once('end', onEnd)
if (stdin.readableEnded) queueMicrotask(onEnd)
}
/**
* Parse the launcher's immutable argument snapshot with an app's commander
* program. Commander runs the program's own synchronous action handler on a
* successful parse; app code there publishes its service and rejects an
* invalid invocation with `program.error(...)`. This helper has no Loader-row
* or service ownership semantics.
*
* Help, version, and rejected arguments — from the grammar or from an action
* — are terminal for the process: commander writes the text and the helper
* requests `ctx.appExit`. The action never runs on help, version, or a
* grammar rejection; an action must reject before it publishes, because
* statements before its `program.error(...)` have already run.
* @param ctx - plugin context carrying `cmdlineArgs` and `appExit`.
* @param program - the app's commander program, with its flags, description,
* actions, and any subcommands already declared.
* @throws when the launcher did not provide the command line and exit request,
* or when no command in the program declares an action.
*/
export function parseCmdline(ctx: Context, program: Command): void {
// Read through the global service store, not the property proxy: appExit is
// an optional host value and the plugin only needs to inject cmdlineArgs.
const args = ctx.get('cmdlineArgs')
const exit = ctx.get('appExit')
if (args === undefined || exit === undefined) {
throw new Error(`${program.name()}: the launcher must provide ctx.cmdlineArgs and ctx.appExit before the tree mounts`)
}
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`)
}
configureExitAndOutput(program)
try {
program.parse(args.get(), { from: 'user' })
} catch (error) {
// exitOverride turns help, version, a parse error, and the action's own
// program.error() into a CommanderError; commander has already written the
// text through the output configured above.
if (!isCommanderError(error)) throw error
exit(error.exitCode)
}
}
/**
* Whether any command in the tree declares an action handler.
*
* The `Command` type cannot express the action precondition, so the handler is
* read structurally (as {@link isCommanderError} reads commander's control-flow
* errors): without this guard, a program that forgot its action would parse
* successfully, publish nothing, and surface only as dependent rows pending on
* the absent service.
* @param command - the command whose tree is inspected.
* @returns true when the command or any registered subcommand has an action.
*/
function hasAction(command: Command): boolean {
if (typeof (command as unknown as { _actionHandler?: unknown })._actionHandler === 'function') return true
return command.commands.some(hasAction)
}
/**
* Route every command's exit and output through the launcher adapter.
*
* Commander copies `exitOverride` and output configuration into a subcommand
* only at registration, so a root-only override would let an
* already-registered subcommand's rejection write to the process streams and
* call `process.exit` directly, bypassing `ctx.appExit`.
* @param command - the root of the command tree to configure.
*/
function configureExitAndOutput(command: Command): void {
command
.exitOverride()
.configureOutput({
writeOut: text => void internals.stdout.write(text),
writeErr: text => void internals.stderr.write(text),
})
for (const child of command.commands) configureExitAndOutput(child)
}
/**
* Whether a thrown value is commander's own control-flow error (help, version,
* a parse error, or `program.error`).
*
* Detected structurally, not with `instanceof`: an out-of-tree plugin brings
* its own commander copy, whose `CommanderError` class is a different identity
* from this package's, and an identity check there would rethrow a printed
* help as a fatal load failure.
* @param error - the thrown value.
* @returns true when the value carries commander's error code and exit code.
*/
function isCommanderError(error: unknown): error is { code: string; exitCode: number } {
if (typeof error !== 'object' || error === null) return false
const candidate = error as { code?: unknown; exitCode?: unknown }
return typeof candidate.code === 'string' && candidate.code.startsWith('commander.')
&& typeof candidate.exitCode === 'number'
}