@ontrails/commander 1.0.0-beta.15

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/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # @ontrails/commander
2
+
3
+ ## 1.0.0-beta.15
4
+
5
+ Initial package placeholder.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @ontrails/commander
2
+
3
+ Commander adapter for Trails. Use this package when you want to expose a topo as a Commander-powered command-line program while keeping `@ontrails/cli` focused on framework-agnostic command derivation.
4
+
5
+ ## Usage
6
+
7
+ ```typescript
8
+ import { surface } from '@ontrails/commander';
9
+ import { graph } from './app';
10
+
11
+ await surface(graph);
12
+ ```
13
+
14
+ For program construction without parsing argv:
15
+
16
+ ```typescript
17
+ import { createProgram } from '@ontrails/commander';
18
+ import { graph } from './app';
19
+
20
+ const program = createProgram(graph, { name: 'myapp' });
21
+ ```
22
+
23
+ For lower-level adapter wiring, derive the command model with `@ontrails/cli`
24
+ and materialize it with `toCommander()`:
25
+
26
+ ```typescript
27
+ import { deriveCliCommands } from '@ontrails/cli';
28
+ import { toCommander } from '@ontrails/commander';
29
+ import { graph } from './app';
30
+
31
+ const commands = deriveCliCommands(graph);
32
+ if (commands.isErr()) {
33
+ throw commands.error;
34
+ }
35
+
36
+ const program = toCommander(commands.value, { name: 'myapp' });
37
+ ```
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ bun add @ontrails/cli @ontrails/commander
43
+ ```
44
+
45
+ ## Migration
46
+
47
+ <!-- warden-ignore-next-line -->
48
+ This package replaces the old `@ontrails/cli/commander` subpath.
49
+
50
+ <!-- warden-ignore-next-line -->
51
+ - Before: `import { surface } from '@ontrails/cli/commander'`
52
+ - After: `import { surface } from '@ontrails/commander'`
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@ontrails/commander",
3
+ "version": "1.0.0-beta.15",
4
+ "files": [
5
+ "src/**/*.ts",
6
+ "!src/**/__tests__/**",
7
+ "!src/**/*.test.ts",
8
+ "!src/**/*.test-d.ts",
9
+ "README.md",
10
+ "CHANGELOG.md"
11
+ ],
12
+ "type": "module",
13
+ "exports": {
14
+ ".": "./src/index.ts",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc -b",
19
+ "test": "bun test",
20
+ "typecheck": "tsc --noEmit",
21
+ "lint": "oxlint ./src",
22
+ "clean": "rm -rf dist *.tsbuildinfo"
23
+ },
24
+ "dependencies": {
25
+ "@ontrails/cli": "^1.0.0-beta.15",
26
+ "@ontrails/core": "^1.0.0-beta.15",
27
+ "commander": "^14.0.3"
28
+ },
29
+ "peerDependencies": {
30
+ "zod": "^4.3.5"
31
+ }
32
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { toCommander } from './to-commander.js';
2
+ export type { ToCommanderOptions } from './to-commander.js';
3
+
4
+ export { createProgram, surface } from './surface.js';
5
+ export type { CreateProgramOptions, SurfaceCliResult } from './surface.js';
package/src/surface.ts ADDED
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Surface helpers for wiring a topo to Commander.
3
+ */
4
+
5
+ import type {
6
+ BaseSurfaceOptions,
7
+ Layer,
8
+ ResourceOverrideMap,
9
+ Topo,
10
+ TrailContextInit,
11
+ } from '@ontrails/core';
12
+ import type {
13
+ ActionResultContext,
14
+ CliFlag,
15
+ InputResolver,
16
+ ResolveCliPermitFromToken,
17
+ } from '@ontrails/cli';
18
+ import { defaultOnResult, deriveCliCommands } from '@ontrails/cli';
19
+ import type { ToCommanderOptions } from './to-commander.js';
20
+ import { toCommander } from './to-commander.js';
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Options
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export interface CreateProgramOptions extends BaseSurfaceOptions {
27
+ readonly createContext?:
28
+ | (() => TrailContextInit | Promise<TrailContextInit>)
29
+ | undefined;
30
+ readonly description?: string | undefined;
31
+ readonly layers?: readonly Layer[] | undefined;
32
+ readonly name?: string | undefined;
33
+ readonly onResult?: ((ctx: ActionResultContext) => Promise<void>) | undefined;
34
+ readonly presets?: CliFlag[][] | undefined;
35
+ readonly resources?: ResourceOverrideMap | undefined;
36
+ readonly resolveInput?: InputResolver | undefined;
37
+ readonly resolvePermitFromToken?: ResolveCliPermitFromToken | undefined;
38
+ readonly version?: string | undefined;
39
+ }
40
+
41
+ export interface SurfaceCliResult {
42
+ readonly exitCode: number;
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // createProgram
47
+ // ---------------------------------------------------------------------------
48
+
49
+ const deriveCommanderOptions = (
50
+ graph: Topo,
51
+ options: CreateProgramOptions
52
+ ): ToCommanderOptions => {
53
+ const commanderOpts: ToCommanderOptions = {
54
+ name: options.name ?? graph.name,
55
+ };
56
+ if (options.version !== undefined || graph.version !== undefined) {
57
+ commanderOpts.version = options.version ?? graph.version;
58
+ }
59
+ if (options.description !== undefined || graph.description !== undefined) {
60
+ commanderOpts.description = options.description ?? graph.description;
61
+ }
62
+ return commanderOpts;
63
+ };
64
+
65
+ /**
66
+ * Create a Commander program from a topo without parsing argv.
67
+ *
68
+ * @remarks This is a host materialization boundary. Derivation failures are
69
+ * thrown for the caller's CLI bootstrap code after `deriveCliCommands` has
70
+ * already represented the framework error as a Result.
71
+ */
72
+ export const createProgram = (
73
+ graph: Topo,
74
+ options: CreateProgramOptions = {}
75
+ ) => {
76
+ const commandsResult = deriveCliCommands(graph, {
77
+ configValues: options.configValues,
78
+ createContext: options.createContext,
79
+ exclude: options.exclude,
80
+ include: options.include,
81
+ intent: options.intent,
82
+ layers: options.layers,
83
+ onResult: options.onResult ?? defaultOnResult,
84
+ presets: options.presets,
85
+ resolveInput: options.resolveInput,
86
+ resolvePermitFromToken: options.resolvePermitFromToken,
87
+ resources: options.resources,
88
+ validate: options.validate,
89
+ });
90
+
91
+ if (commandsResult.isErr()) {
92
+ throw commandsResult.error;
93
+ }
94
+
95
+ return toCommander(
96
+ commandsResult.value,
97
+ deriveCommanderOptions(graph, options)
98
+ );
99
+ };
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // surface
103
+ // ---------------------------------------------------------------------------
104
+
105
+ /**
106
+ * Parse argv for a topo through Commander.
107
+ *
108
+ * Returns the process exit code without calling `process.exit()`, so callers
109
+ * can run cleanup before terminating. The CLI `surface()` entry point
110
+ * delegates here and lets the process exit naturally.
111
+ */
112
+ export const surface = async (
113
+ graph: Topo,
114
+ options: CreateProgramOptions = {}
115
+ ): Promise<SurfaceCliResult> => {
116
+ const program = createProgram(graph, options);
117
+ await program.parseAsync();
118
+ const { exitCode } = process;
119
+ return { exitCode: typeof exitCode === 'number' ? exitCode : 0 };
120
+ };
@@ -0,0 +1,462 @@
1
+ /**
2
+ * Adapt framework-agnostic CliCommand[] to a Commander program.
3
+ */
4
+
5
+ import { projectPublicSurfaceError } from '@ontrails/core';
6
+ import type { CliCommand, CliFlag } from '@ontrails/cli';
7
+ import { applyCliFlagValueAliases, validateCliCommands } from '@ontrails/cli';
8
+ import { Command, InvalidArgumentError, Option } from 'commander';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Options
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export interface ToCommanderOptions {
15
+ description?: string | undefined;
16
+ name?: string | undefined;
17
+ version?: string | undefined;
18
+ }
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Helpers
22
+ // ---------------------------------------------------------------------------
23
+
24
+ /** Build the flag string portion of a Commander Option. */
25
+ const buildFlagArgument = (flag: CliFlag): string => {
26
+ if (flag.variadic) {
27
+ return flag.required ? '<values...>' : '[values...]';
28
+ }
29
+ return flag.required ? '<value>' : '[value]';
30
+ };
31
+
32
+ const buildFlagString = (flag: CliFlag): string => {
33
+ const long = `--${flag.name}`;
34
+ const short = flag.short ? `-${flag.short}` : undefined;
35
+
36
+ if (flag.type === 'boolean') {
37
+ return short ? `${short}, ${long}` : long;
38
+ }
39
+
40
+ const argPart = buildFlagArgument(flag);
41
+ return short ? `${short}, ${long} ${argPart}` : `${long} ${argPart}`;
42
+ };
43
+
44
+ /** Strict number parser that rejects partial parses and non-finite values. */
45
+ const strictParseNumber = (value: string): number => {
46
+ const n = Number(value);
47
+ if (Number.isNaN(n) || !Number.isFinite(n)) {
48
+ throw new InvalidArgumentError(`"${value}" is not a valid number`);
49
+ }
50
+ return n;
51
+ };
52
+
53
+ /** Apply common modifiers (choices, default, arg parser) to a Commander Option. */
54
+ const applyOptionModifiers = (opt: Option, flag: CliFlag): void => {
55
+ if (flag.choices) {
56
+ opt.choices(flag.choices);
57
+ }
58
+ if (flag.default !== undefined) {
59
+ opt.default(flag.default);
60
+ }
61
+ if (flag.type === 'number' || flag.type === 'number[]') {
62
+ opt.argParser(strictParseNumber);
63
+ }
64
+ };
65
+
66
+ /** Build Commander Option(s) from a CliFlag. Returns one or two options. */
67
+ const buildOptions = (flag: CliFlag): Option[] => {
68
+ const opt = new Option(buildFlagString(flag), flag.description);
69
+ applyOptionModifiers(opt, flag);
70
+ const valueAliasOptions = (flag.valueAliases ?? []).map(
71
+ (alias) =>
72
+ new Option(
73
+ `--${alias.name}`,
74
+ alias.description ?? `Shorthand for --${flag.name} ${alias.value}`
75
+ )
76
+ );
77
+ if (flag.type === 'boolean') {
78
+ const negation = new Option(
79
+ `--no-${flag.name}`,
80
+ flag.description ? `Negate ${flag.description}` : undefined
81
+ );
82
+ return [opt, negation, ...valueAliasOptions];
83
+ }
84
+ return [opt, ...valueAliasOptions];
85
+ };
86
+
87
+ /** Add positional args to a Commander subcommand. */
88
+ const buildArgTemplate = (
89
+ arg: CliCommand['args'][number],
90
+ required = arg.required
91
+ ): string => {
92
+ if (arg.variadic) {
93
+ return required ? `<${arg.name}...>` : `[${arg.name}...]`;
94
+ }
95
+ return required ? `<${arg.name}>` : `[${arg.name}]`;
96
+ };
97
+
98
+ const addArgs = (
99
+ sub: Command,
100
+ cmd: CliCommand,
101
+ options?: { readonly forceOptionalFirstArg?: boolean } | undefined
102
+ ): void => {
103
+ for (const [index, arg] of cmd.args.entries()) {
104
+ const template = buildArgTemplate(
105
+ arg,
106
+ options?.forceOptionalFirstArg === true && index === 0 ? false : undefined
107
+ );
108
+ sub.argument(template, arg.description);
109
+ }
110
+ };
111
+
112
+ /** Collect positional args from Commander's action callback into a record. */
113
+ const collectPositionalArgs = (
114
+ cmd: CliCommand,
115
+ actionArgs: unknown[]
116
+ ): Record<string, unknown> => {
117
+ const parsedArgs: Record<string, unknown> = {};
118
+ for (let i = 0; i < cmd.args.length; i += 1) {
119
+ const argDef = cmd.args[i];
120
+ if (argDef) {
121
+ parsedArgs[argDef.name] = actionArgs[i];
122
+ }
123
+ }
124
+ return parsedArgs;
125
+ };
126
+
127
+ const isUserSuppliedOption = (command: Command, name: string): boolean => {
128
+ const source = command.getOptionValueSource(name);
129
+ return source !== undefined && source !== 'default' && source !== 'implied';
130
+ };
131
+
132
+ const getCommandOptionNames = (command: Command): Set<string> =>
133
+ new Set(command.options.map((option) => option.attributeName()));
134
+
135
+ const hasUserSuppliedOptionOutside = (
136
+ sourceCommand: Command,
137
+ allowedCommand: Command
138
+ ): boolean => {
139
+ const allowedOptionNames = getCommandOptionNames(allowedCommand);
140
+ return sourceCommand.options.some((option) => {
141
+ const name = option.attributeName();
142
+ return (
143
+ isUserSuppliedOption(sourceCommand, name) && !allowedOptionNames.has(name)
144
+ );
145
+ });
146
+ };
147
+
148
+ const hasAnyPositionalValue = (
149
+ cmd: CliCommand,
150
+ parsedArgs: Readonly<Record<string, unknown>>
151
+ ): boolean => cmd.args.some((arg) => parsedArgs[arg.name] !== undefined);
152
+
153
+ const getActionTarget = (fallbackTarget: Command, actionArgs: unknown[]) => {
154
+ const candidate = actionArgs.at(-1);
155
+ return candidate instanceof Command ? candidate : fallbackTarget;
156
+ };
157
+
158
+ const getParsedFlags = (command: Command): Record<string, unknown> =>
159
+ command.optsWithGlobals() as Record<string, unknown>;
160
+
161
+ const getFlagOptionKeys = (flags: readonly CliCommand['flags'][number][]) =>
162
+ new Set(
163
+ flags.flatMap((flag) => [
164
+ flag.name.replaceAll(/-([a-zA-Z0-9])/g, (_, ch: string) =>
165
+ ch.toUpperCase()
166
+ ),
167
+ ...(flag.valueAliases ?? []).map((alias) =>
168
+ alias.name.replaceAll(/-([a-zA-Z0-9])/g, (_, ch: string) =>
169
+ ch.toUpperCase()
170
+ )
171
+ ),
172
+ ])
173
+ );
174
+
175
+ const getUserSuppliedFlagKeys = (
176
+ command: Command,
177
+ flags: readonly CliCommand['flags'][number][]
178
+ ): ReadonlySet<string> => {
179
+ const userSupplied = new Set<string>();
180
+ for (const key of getFlagOptionKeys(flags)) {
181
+ if (isUserSuppliedOption(command, key)) {
182
+ userSupplied.add(key);
183
+ }
184
+ }
185
+ return userSupplied;
186
+ };
187
+
188
+ const getFallbackParsedFlags = (
189
+ parentTarget: Command,
190
+ target: Command
191
+ ): Record<string, unknown> => {
192
+ const flags = { ...getParsedFlags(parentTarget) };
193
+ const parentOptionNames = getCommandOptionNames(parentTarget);
194
+ for (const option of target.options) {
195
+ const name = option.attributeName();
196
+ if (parentOptionNames.has(name) && isUserSuppliedOption(target, name)) {
197
+ flags[name] = target.getOptionValue(name);
198
+ }
199
+ }
200
+ return flags;
201
+ };
202
+
203
+ const getFallbackUserSuppliedFlagKeys = (
204
+ parentTarget: Command,
205
+ target: Command,
206
+ flags: readonly CliCommand['flags'][number][]
207
+ ): ReadonlySet<string> => {
208
+ const userSupplied = new Set<string>();
209
+ for (const key of getFlagOptionKeys(flags)) {
210
+ if (
211
+ isUserSuppliedOption(parentTarget, key) ||
212
+ isUserSuppliedOption(target, key)
213
+ ) {
214
+ userSupplied.add(key);
215
+ }
216
+ }
217
+ return userSupplied;
218
+ };
219
+
220
+ /** Handle execution errors with appropriate exit codes. */
221
+ const handleError = (error: unknown): void => {
222
+ const err = error instanceof Error ? error : new Error(String(error));
223
+ const projection = projectPublicSurfaceError('cli', err);
224
+ process.stderr.write(`Error: ${projection.message}\n`);
225
+ process.exit(projection.code);
226
+ };
227
+
228
+ interface BareChildFallback {
229
+ readonly argName: string;
230
+ readonly argValue: string;
231
+ readonly parentCommand: CliCommand;
232
+ readonly parentTarget: Command;
233
+ }
234
+
235
+ const maybeUseBareChildFallback = (
236
+ target: Command,
237
+ cmd: CliCommand,
238
+ parsedArgs: Readonly<Record<string, unknown>>,
239
+ fallback?: BareChildFallback | undefined
240
+ ): {
241
+ readonly command: CliCommand;
242
+ readonly parsedArgs: Record<string, unknown>;
243
+ readonly parsedFlags: Record<string, unknown>;
244
+ readonly userSuppliedFlagKeys: ReadonlySet<string>;
245
+ } => {
246
+ if (
247
+ !fallback ||
248
+ hasAnyPositionalValue(cmd, parsedArgs) ||
249
+ hasUserSuppliedOptionOutside(target, fallback.parentTarget)
250
+ ) {
251
+ return {
252
+ command: cmd,
253
+ parsedArgs: { ...parsedArgs },
254
+ parsedFlags: getParsedFlags(target),
255
+ userSuppliedFlagKeys: getUserSuppliedFlagKeys(target, cmd.flags),
256
+ };
257
+ }
258
+
259
+ return {
260
+ command: fallback.parentCommand,
261
+ parsedArgs: { [fallback.argName]: fallback.argValue },
262
+ parsedFlags: getFallbackParsedFlags(fallback.parentTarget, target),
263
+ userSuppliedFlagKeys: getFallbackUserSuppliedFlagKeys(
264
+ fallback.parentTarget,
265
+ target,
266
+ fallback.parentCommand.flags
267
+ ),
268
+ };
269
+ };
270
+
271
+ /** Wire a CliCommand's action to a Commander subcommand. */
272
+ const wireAction = (
273
+ target: Command,
274
+ cmd: CliCommand,
275
+ fallback?: BareChildFallback | undefined
276
+ ): void => {
277
+ target.action(async (...actionArgs: unknown[]) => {
278
+ const actionTarget = getActionTarget(target, actionArgs);
279
+ const parsedArgs = collectPositionalArgs(cmd, actionArgs);
280
+ const action = maybeUseBareChildFallback(
281
+ actionTarget,
282
+ cmd,
283
+ parsedArgs,
284
+ fallback === undefined
285
+ ? undefined
286
+ : {
287
+ ...fallback,
288
+ parentTarget: actionTarget.parent ?? fallback.parentTarget,
289
+ }
290
+ );
291
+ try {
292
+ await action.command.execute(
293
+ action.parsedArgs,
294
+ applyCliFlagValueAliases(
295
+ action.command.flags,
296
+ action.parsedFlags,
297
+ action.userSuppliedFlagKeys
298
+ )
299
+ );
300
+ } catch (error: unknown) {
301
+ handleError(error);
302
+ }
303
+ });
304
+ };
305
+
306
+ /** Apply options to a Commander program. */
307
+ const applyOptions = (program: Command, options?: ToCommanderOptions): void => {
308
+ if (options?.name) {
309
+ program.name(options.name);
310
+ }
311
+ if (options?.version) {
312
+ program.version(options.version);
313
+ }
314
+ if (options?.description) {
315
+ program.description(options.description);
316
+ }
317
+ };
318
+
319
+ // ---------------------------------------------------------------------------
320
+ // toCommander
321
+ // ---------------------------------------------------------------------------
322
+
323
+ /**
324
+ * Convert CliCommand[] into a configured Commander program.
325
+ *
326
+ * Builds a nested command tree from each command's full ordered path.
327
+ * Wires each command's `.action()` to call `execute()` and handle errors.
328
+ */
329
+ const pathKey = (path: readonly string[]): string => path.join('\0');
330
+
331
+ interface CommandNodeState {
332
+ readonly command: Command;
333
+ cliCommand?: CliCommand | undefined;
334
+ executable: boolean;
335
+ }
336
+
337
+ const getPathSegment = (path: readonly string[], index: number): string => {
338
+ const segment = path[index];
339
+ if (segment === undefined) {
340
+ throw new Error('CLI command path contains an undefined segment');
341
+ }
342
+ return segment;
343
+ };
344
+
345
+ const getOrCreateCommandNode = (
346
+ key: string,
347
+ segment: string,
348
+ parent: Command,
349
+ nodes: Map<string, CommandNodeState>
350
+ ): CommandNodeState => {
351
+ const existing = nodes.get(key);
352
+ if (existing) {
353
+ return existing;
354
+ }
355
+
356
+ const command = new Command(segment);
357
+ const state = { command, executable: false };
358
+ nodes.set(key, state);
359
+ parent.addCommand(command);
360
+ return state;
361
+ };
362
+
363
+ const ensureCommandNode = (
364
+ path: readonly string[],
365
+ program: Command,
366
+ nodes: Map<string, CommandNodeState>
367
+ ): CommandNodeState => {
368
+ let parent = program;
369
+ let state: CommandNodeState | undefined;
370
+
371
+ for (let index = 0; index < path.length; index += 1) {
372
+ const segment = getPathSegment(path, index);
373
+ const key = pathKey(path.slice(0, index + 1));
374
+ state = getOrCreateCommandNode(key, segment, parent, nodes);
375
+ parent = state.command;
376
+ }
377
+
378
+ if (!state) {
379
+ throw new Error('CLI command path cannot be empty');
380
+ }
381
+
382
+ return state;
383
+ };
384
+
385
+ const createBareChildFallback = (
386
+ cmd: CliCommand,
387
+ parentState?: CommandNodeState | undefined
388
+ ): BareChildFallback | undefined => {
389
+ if (!parentState?.cliCommand || cmd.path.length < 2) {
390
+ return undefined;
391
+ }
392
+
393
+ const [parentArg] = parentState.cliCommand.args;
394
+ const [childArg] = cmd.args;
395
+ const childSegment = cmd.path.at(-1);
396
+ if (
397
+ parentArg === undefined ||
398
+ childArg === undefined ||
399
+ parentArg.required ||
400
+ parentArg.variadic ||
401
+ childArg.variadic ||
402
+ childArg.name !== parentArg.name ||
403
+ childSegment === undefined
404
+ ) {
405
+ return undefined;
406
+ }
407
+
408
+ return {
409
+ argName: parentArg.name,
410
+ argValue: childSegment,
411
+ parentCommand: parentState.cliCommand,
412
+ parentTarget: parentState.command,
413
+ };
414
+ };
415
+
416
+ const applyCliCommand = (
417
+ state: CommandNodeState,
418
+ cmd: CliCommand,
419
+ fallback?: BareChildFallback | undefined
420
+ ): void => {
421
+ if (state.executable) {
422
+ throw new Error(`Duplicate CLI path: ${cmd.path.join(' ')}`);
423
+ }
424
+
425
+ if (cmd.description) {
426
+ state.command.description(cmd.description);
427
+ }
428
+ for (const flag of cmd.flags) {
429
+ for (const opt of buildOptions(flag)) {
430
+ state.command.addOption(opt);
431
+ }
432
+ }
433
+ addArgs(state.command, cmd, {
434
+ forceOptionalFirstArg: fallback !== undefined,
435
+ });
436
+ wireAction(state.command, cmd, fallback);
437
+ state.cliCommand = cmd;
438
+ state.executable = true;
439
+ };
440
+
441
+ export const toCommander = (
442
+ commands: CliCommand[],
443
+ options?: ToCommanderOptions
444
+ ): Command => {
445
+ validateCliCommands(commands);
446
+ const program = new Command();
447
+ applyOptions(program, options);
448
+ const nodes = new Map<string, CommandNodeState>();
449
+
450
+ for (const cmd of commands.toSorted((a, b) =>
451
+ a.path.length === b.path.length
452
+ ? a.path.join('.').localeCompare(b.path.join('.'))
453
+ : a.path.length - b.path.length
454
+ )) {
455
+ const state = ensureCommandNode(cmd.path, program, nodes);
456
+ const parentKey = pathKey(cmd.path.slice(0, -1));
457
+ const fallback = createBareChildFallback(cmd, nodes.get(parentKey));
458
+ applyCliCommand(state, cmd, fallback);
459
+ }
460
+
461
+ return program;
462
+ };