@ontrails/cli 1.0.0-beta.0

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.
Files changed (70) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/.turbo/turbo-lint.log +3 -0
  3. package/.turbo/turbo-typecheck.log +1 -0
  4. package/CHANGELOG.md +20 -0
  5. package/README.md +166 -0
  6. package/dist/build.d.ts +25 -0
  7. package/dist/build.d.ts.map +1 -0
  8. package/dist/build.js +167 -0
  9. package/dist/build.js.map +1 -0
  10. package/dist/command.d.ts +47 -0
  11. package/dist/command.d.ts.map +1 -0
  12. package/dist/command.js +9 -0
  13. package/dist/command.js.map +1 -0
  14. package/dist/commander/blaze.d.ts +31 -0
  15. package/dist/commander/blaze.d.ts.map +1 -0
  16. package/dist/commander/blaze.js +42 -0
  17. package/dist/commander/blaze.js.map +1 -0
  18. package/dist/commander/index.d.ts +5 -0
  19. package/dist/commander/index.d.ts.map +1 -0
  20. package/dist/commander/index.js +3 -0
  21. package/dist/commander/index.js.map +1 -0
  22. package/dist/commander/to-commander.d.ts +12 -0
  23. package/dist/commander/to-commander.d.ts.map +1 -0
  24. package/dist/commander/to-commander.js +148 -0
  25. package/dist/commander/to-commander.js.map +1 -0
  26. package/dist/flags.d.ts +17 -0
  27. package/dist/flags.d.ts.map +1 -0
  28. package/dist/flags.js +180 -0
  29. package/dist/flags.js.map +1 -0
  30. package/dist/index.d.ts +11 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +13 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/layers.d.ts +21 -0
  35. package/dist/layers.d.ts.map +1 -0
  36. package/dist/layers.js +156 -0
  37. package/dist/layers.js.map +1 -0
  38. package/dist/on-result.d.ts +12 -0
  39. package/dist/on-result.d.ts.map +1 -0
  40. package/dist/on-result.js +21 -0
  41. package/dist/on-result.js.map +1 -0
  42. package/dist/output.d.ts +20 -0
  43. package/dist/output.d.ts.map +1 -0
  44. package/dist/output.js +82 -0
  45. package/dist/output.js.map +1 -0
  46. package/dist/prompt.d.ts +29 -0
  47. package/dist/prompt.d.ts.map +1 -0
  48. package/dist/prompt.js +12 -0
  49. package/dist/prompt.js.map +1 -0
  50. package/package.json +29 -0
  51. package/src/__tests__/blaze.test.ts +78 -0
  52. package/src/__tests__/build.test.ts +219 -0
  53. package/src/__tests__/flags.test.ts +176 -0
  54. package/src/__tests__/layers.test.ts +218 -0
  55. package/src/__tests__/on-result.test.ts +64 -0
  56. package/src/__tests__/output.test.ts +115 -0
  57. package/src/__tests__/to-commander.test.ts +133 -0
  58. package/src/build.ts +267 -0
  59. package/src/command.ts +73 -0
  60. package/src/commander/blaze.ts +67 -0
  61. package/src/commander/index.ts +5 -0
  62. package/src/commander/to-commander.ts +186 -0
  63. package/src/flags.ts +250 -0
  64. package/src/index.ts +28 -0
  65. package/src/layers.ts +231 -0
  66. package/src/on-result.ts +27 -0
  67. package/src/output.ts +101 -0
  68. package/src/prompt.ts +40 -0
  69. package/tsconfig.json +9 -0
  70. package/tsconfig.tsbuildinfo +1 -0
package/src/build.ts ADDED
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Build framework-agnostic CliCommand[] from an App's topology.
3
+ */
4
+
5
+ import type { Field, Layer, Topo, TrailContext } from '@ontrails/core';
6
+ import {
7
+ Result,
8
+ composeLayers,
9
+ createTrailContext,
10
+ deriveFields,
11
+ validateInput,
12
+ } from '@ontrails/core';
13
+
14
+ import type { AnyTrail, CliCommand, CliFlag } from './command.js';
15
+ import { dryRunPreset, toFlags } from './flags.js';
16
+ import type { InputResolver } from './prompt.js';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Public types
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /** Context passed to the onResult callback. */
23
+ export interface ActionResultContext {
24
+ readonly args: Record<string, unknown>;
25
+ readonly flags: Record<string, unknown>;
26
+ readonly input: unknown;
27
+ readonly result: Result<unknown, Error>;
28
+ readonly trail: AnyTrail;
29
+ }
30
+
31
+ /** Options for buildCliCommands. */
32
+ export interface BuildCliCommandsOptions {
33
+ createContext?: (() => TrailContext | Promise<TrailContext>) | undefined;
34
+ layers?: Layer[] | undefined;
35
+ onResult?: ((ctx: ActionResultContext) => Promise<void>) | undefined;
36
+ presets?: CliFlag[][] | undefined;
37
+ resolveInput?: InputResolver | undefined;
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Helpers
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /** Convert kebab-case flag name back to camelCase for input merging. */
45
+ const toCamel = (str: string): string =>
46
+ str.replaceAll(/-([a-z])/g, (_, ch: string) => ch.toUpperCase());
47
+
48
+ /**
49
+ * Parse a trail ID into group + command name.
50
+ * "entity.show" -> { group: "entity", name: "show" }
51
+ * "search" -> { group: undefined, name: "search" }
52
+ */
53
+ const parseTrailId = (
54
+ id: string
55
+ ): { group: string | undefined; name: string } => {
56
+ const dotIndex = id.indexOf('.');
57
+ if (dotIndex === -1) {
58
+ return { group: undefined, name: id };
59
+ }
60
+ return {
61
+ group: id.slice(0, dotIndex),
62
+ name: id.slice(dotIndex + 1),
63
+ };
64
+ };
65
+
66
+ /**
67
+ * Merge preset flags with schema-derived flags.
68
+ * Schema-derived flags take precedence on name collision.
69
+ */
70
+ const mergeFlags = (presets: CliFlag[], derived: CliFlag[]): CliFlag[] => {
71
+ const derivedNames = new Set(derived.map((f) => f.name));
72
+ const merged = [...derived];
73
+ for (const preset of presets) {
74
+ if (!derivedNames.has(preset.name)) {
75
+ merged.push(preset);
76
+ }
77
+ }
78
+ return merged;
79
+ };
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // buildCliCommands
83
+ // ---------------------------------------------------------------------------
84
+
85
+ /**
86
+ * Build an array of framework-agnostic CLI commands from an App.
87
+ *
88
+ * Iterates the topo, derives flags from input schemas, groups by
89
+ * dot-notation, and wires up the execute function with validation,
90
+ * layer composition, and onResult handling.
91
+ */
92
+ const META_FLAGS = new Set(['json', 'jsonl', 'output']);
93
+
94
+ /** Merge parsed args and flags into a camelCase input record. */
95
+ const mergeArgsAndFlags = (
96
+ parsedArgs: Record<string, unknown>,
97
+ parsedFlags: Record<string, unknown>
98
+ ): Record<string, unknown> => {
99
+ const mergedInput: Record<string, unknown> = { ...parsedArgs };
100
+ for (const [key, value] of Object.entries(parsedFlags)) {
101
+ if (!META_FLAGS.has(key)) {
102
+ mergedInput[toCamel(key)] = value;
103
+ }
104
+ }
105
+ return mergedInput;
106
+ };
107
+
108
+ /** Apply interactive prompting and merge results. */
109
+ const applyPrompting = async (
110
+ fields: readonly Field[],
111
+ mergedInput: Record<string, unknown>,
112
+ options?: BuildCliCommandsOptions
113
+ ): Promise<void> => {
114
+ if (!options?.resolveInput) {
115
+ return;
116
+ }
117
+ const resolved = await options.resolveInput(fields, mergedInput);
118
+ for (const [key, value] of Object.entries(resolved)) {
119
+ if (value !== undefined) {
120
+ mergedInput[key] = value;
121
+ }
122
+ }
123
+ };
124
+
125
+ /** Resolve a TrailContext from overrides, factory, or default. */
126
+ const resolveContext = (
127
+ ctxOverrides: Partial<TrailContext> | undefined,
128
+ options?: BuildCliCommandsOptions
129
+ ): Promise<TrailContext> => {
130
+ if (ctxOverrides) {
131
+ return Promise.resolve(createTrailContext(ctxOverrides));
132
+ }
133
+ if (options?.createContext) {
134
+ return Promise.resolve(options.createContext());
135
+ }
136
+ return Promise.resolve(createTrailContext());
137
+ };
138
+
139
+ /** Report a result via onResult callback if provided. */
140
+ const reportResult = async (
141
+ options: BuildCliCommandsOptions | undefined,
142
+ ctx: ActionResultContext
143
+ ): Promise<void> => {
144
+ if (options?.onResult) {
145
+ await options.onResult(ctx);
146
+ }
147
+ };
148
+
149
+ /** Execute a trail with validated input. */
150
+ const executeTrail = async (
151
+ t: AnyTrail,
152
+ validatedInput: unknown,
153
+ ctxOverrides: Partial<TrailContext> | undefined,
154
+ options?: BuildCliCommandsOptions
155
+ ): Promise<Result<unknown, Error>> => {
156
+ const ctx = await resolveContext(ctxOverrides, options);
157
+ const layers = options?.layers ?? [];
158
+ const impl = composeLayers(layers, t, t.implementation);
159
+ return impl(validatedInput, ctx);
160
+ };
161
+
162
+ /** Create the execute function for a CLI command. */
163
+ const createExecute =
164
+ (
165
+ t: AnyTrail,
166
+ fields: readonly Field[],
167
+ _flags: CliFlag[],
168
+ options?: BuildCliCommandsOptions
169
+ ) =>
170
+ async (
171
+ parsedArgs: Record<string, unknown>,
172
+ parsedFlags: Record<string, unknown>,
173
+ ctxOverrides?: Partial<TrailContext>
174
+ ): Promise<Result<unknown, Error>> => {
175
+ const mergedInput = mergeArgsAndFlags(parsedArgs, parsedFlags);
176
+ await applyPrompting(fields, mergedInput, options);
177
+
178
+ const validated = validateInput(t.input, mergedInput);
179
+ if (validated.isErr()) {
180
+ const errorResult: Result<unknown, Error> = Result.err(validated.error);
181
+ await reportResult(options, {
182
+ args: parsedArgs,
183
+ flags: parsedFlags,
184
+ input: mergedInput,
185
+ result: errorResult,
186
+ trail: t,
187
+ });
188
+ return errorResult;
189
+ }
190
+
191
+ const result = await executeTrail(
192
+ t,
193
+ validated.value,
194
+ ctxOverrides,
195
+ options
196
+ );
197
+ await reportResult(options, {
198
+ args: parsedArgs,
199
+ flags: parsedFlags,
200
+ input: validated.value,
201
+ result,
202
+ trail: t,
203
+ });
204
+ return result;
205
+ };
206
+
207
+ /** Derive and merge flags for a trail. */
208
+ const buildFlags = (
209
+ fields: readonly Field[],
210
+ destructive: boolean | undefined,
211
+ options?: BuildCliCommandsOptions
212
+ ): CliFlag[] => {
213
+ let flags = toFlags(fields);
214
+ if (options?.presets) {
215
+ flags = mergeFlags(options.presets.flat(), flags);
216
+ }
217
+ if (destructive) {
218
+ flags = mergeFlags(dryRunPreset(), flags);
219
+ }
220
+ return flags;
221
+ };
222
+
223
+ /** Convert a trail or route into a CLI command when it is publicly exposed. */
224
+ const toCliCommand = (
225
+ t: AnyTrail,
226
+ options?: BuildCliCommandsOptions
227
+ ): CliCommand => {
228
+ const { group, name } = parseTrailId(t.id);
229
+ const fields = deriveFields(t.input, t.fields);
230
+ const flags = buildFlags(fields, t.destructive, options);
231
+
232
+ return {
233
+ args: [],
234
+ description: t.description,
235
+ destructive: t.destructive,
236
+ execute: createExecute(t, fields, flags, options),
237
+ flags,
238
+ group,
239
+ idempotent: t.idempotent,
240
+ layers: options?.layers,
241
+ name,
242
+ readOnly: t.readOnly,
243
+ trail: t,
244
+ };
245
+ };
246
+
247
+ export const buildCliCommands = (
248
+ app: Topo,
249
+ options?: BuildCliCommandsOptions
250
+ ): CliCommand[] => {
251
+ const commands: CliCommand[] = [];
252
+
253
+ for (const item of app.list()) {
254
+ if (item.kind !== 'trail' && item.kind !== 'hike') {
255
+ continue;
256
+ }
257
+
258
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
259
+ const t = item as AnyTrail;
260
+ if (t.markers?.['internal'] === true) {
261
+ continue;
262
+ }
263
+ commands.push(toCliCommand(t, options));
264
+ }
265
+
266
+ return commands;
267
+ };
package/src/command.ts ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Framework-agnostic CLI command model.
3
+ *
4
+ * These interfaces are the intermediate representation that
5
+ * `buildCliCommands()` produces and framework adapters consume.
6
+ * No Commander (or any other framework) imports here.
7
+ */
8
+
9
+ import type { Layer, Result, Trail, TrailContext } from '@ontrails/core';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // AnyTrail -- type-erased trail for the CLI boundary
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /**
16
+ * Type-erased trail reference. At the CLI adapter boundary we lose
17
+ * generic type information since flags/args are parsed as strings.
18
+ * Using `any` here is intentional -- the Zod schema validates at runtime.
19
+ */
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ export type AnyTrail = Trail<any, any>;
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // CliFlag
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /** A single CLI flag derived from a Zod schema field or preset. */
28
+ export interface CliFlag {
29
+ readonly name: string;
30
+ readonly short?: string | undefined;
31
+ readonly description?: string | undefined;
32
+ readonly type: 'string' | 'number' | 'boolean' | 'string[]' | 'number[]';
33
+ readonly required: boolean;
34
+ readonly default?: unknown | undefined;
35
+ readonly choices?: string[] | undefined;
36
+ readonly variadic: boolean;
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // CliArg
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /** A positional CLI argument. */
44
+ export interface CliArg {
45
+ readonly name: string;
46
+ readonly description?: string | undefined;
47
+ readonly required: boolean;
48
+ readonly variadic: boolean;
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // CliCommand
53
+ // ---------------------------------------------------------------------------
54
+
55
+ /** A framework-agnostic representation of a CLI command. */
56
+ export interface CliCommand {
57
+ readonly name: string;
58
+ readonly description?: string | undefined;
59
+ readonly group?: string | undefined;
60
+ readonly flags: CliFlag[];
61
+ readonly args: CliArg[];
62
+ readonly trail: AnyTrail;
63
+ readonly layers?: Layer[] | undefined;
64
+ readonly readOnly?: boolean | undefined;
65
+ readonly destructive?: boolean | undefined;
66
+ readonly idempotent?: boolean | undefined;
67
+
68
+ execute(
69
+ parsedArgs: Record<string, unknown>,
70
+ parsedFlags: Record<string, unknown>,
71
+ ctx?: Partial<TrailContext>
72
+ ): Promise<Result<unknown, Error>>;
73
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The one-liner convenience for wiring an App to Commander.
3
+ */
4
+
5
+ import type { Layer, Topo, TrailContext } from '@ontrails/core';
6
+
7
+ import type { ActionResultContext } from '../build.js';
8
+ import { buildCliCommands } from '../build.js';
9
+ import type { CliFlag } from '../command.js';
10
+ import { defaultOnResult } from '../on-result.js';
11
+ import type { InputResolver } from '../prompt.js';
12
+ import type { ToCommanderOptions } from './to-commander.js';
13
+ import { toCommander } from './to-commander.js';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Options
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export interface BlazeCliOptions {
20
+ createContext?: (() => TrailContext | Promise<TrailContext>) | undefined;
21
+ description?: string | undefined;
22
+ layers?: Layer[] | undefined;
23
+ name?: string | undefined;
24
+ onResult?: ((ctx: ActionResultContext) => Promise<void>) | undefined;
25
+ presets?: CliFlag[][] | undefined;
26
+ resolveInput?: InputResolver | undefined;
27
+ version?: string | undefined;
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // blaze
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Wire an App to Commander and parse argv in one call.
36
+ *
37
+ * ```ts
38
+ * import { topo } from "@ontrails/core";
39
+ * import { blaze } from "@ontrails/cli/commander";
40
+ * import * as entity from "./trails/entity.ts";
41
+ *
42
+ * const app = topo("myapp", entity);
43
+ * blaze(app);
44
+ * ```
45
+ */
46
+ export const blaze = (app: Topo, options: BlazeCliOptions = {}): void => {
47
+ const commands = buildCliCommands(app, {
48
+ createContext: options.createContext,
49
+ layers: options.layers,
50
+ onResult: options.onResult ?? defaultOnResult,
51
+ presets: options.presets,
52
+ resolveInput: options.resolveInput,
53
+ });
54
+
55
+ const commanderOpts: ToCommanderOptions = {
56
+ name: options.name ?? app.name,
57
+ };
58
+ if (options.version !== undefined) {
59
+ commanderOpts.version = options.version;
60
+ }
61
+ if (options.description !== undefined) {
62
+ commanderOpts.description = options.description;
63
+ }
64
+
65
+ const program = toCommander(commands, commanderOpts);
66
+ program.parse();
67
+ };
@@ -0,0 +1,5 @@
1
+ export { toCommander } from './to-commander.js';
2
+ export type { ToCommanderOptions } from './to-commander.js';
3
+
4
+ export { blaze } from './blaze.js';
5
+ export type { BlazeCliOptions } from './blaze.js';
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Adapt framework-agnostic CliCommand[] to a Commander program.
3
+ */
4
+
5
+ import { exitCodeMap, isTrailsError } from '@ontrails/core';
6
+ import { Command, Option } from 'commander';
7
+
8
+ import type { CliCommand, CliFlag } from '../command.js';
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
+ /** Build a Commander Option from a CliFlag. */
45
+ const buildOption = (flag: CliFlag): Option => {
46
+ const opt = new Option(buildFlagString(flag), flag.description);
47
+ if (flag.choices) {
48
+ opt.choices(flag.choices);
49
+ }
50
+ if (flag.default !== undefined) {
51
+ opt.default(flag.default);
52
+ }
53
+ if (flag.type === 'number' || flag.type === 'number[]') {
54
+ opt.argParser(parseFloat);
55
+ }
56
+ return opt;
57
+ };
58
+
59
+ /** Add positional args to a Commander subcommand. */
60
+ const buildArgTemplate = (arg: CliCommand['args'][number]): string => {
61
+ if (arg.variadic) {
62
+ return arg.required ? `<${arg.name}...>` : `[${arg.name}...]`;
63
+ }
64
+ return arg.required ? `<${arg.name}>` : `[${arg.name}]`;
65
+ };
66
+
67
+ const addArgs = (sub: Command, cmd: CliCommand): void => {
68
+ for (const arg of cmd.args) {
69
+ const template = buildArgTemplate(arg);
70
+ sub.argument(template, arg.description);
71
+ }
72
+ };
73
+
74
+ /** Collect positional args from Commander's action callback into a record. */
75
+ const collectPositionalArgs = (
76
+ cmd: CliCommand,
77
+ actionArgs: unknown[]
78
+ ): Record<string, unknown> => {
79
+ const parsedArgs: Record<string, unknown> = {};
80
+ for (let i = 0; i < cmd.args.length; i += 1) {
81
+ const argDef = cmd.args[i];
82
+ if (argDef) {
83
+ parsedArgs[argDef.name] = actionArgs[i];
84
+ }
85
+ }
86
+ return parsedArgs;
87
+ };
88
+
89
+ /** Handle execution errors with appropriate exit codes. */
90
+ const handleError = (error: unknown): void => {
91
+ if (error instanceof Error) {
92
+ process.stderr.write(`Error: ${error.message}\n`);
93
+ if (isTrailsError(error)) {
94
+ process.exit(exitCodeMap[error.category]);
95
+ }
96
+ } else {
97
+ process.stderr.write(`Error: ${String(error)}\n`);
98
+ }
99
+ process.exit(8);
100
+ };
101
+
102
+ /** Wire a CliCommand's action to a Commander subcommand. */
103
+ const wireAction = (sub: Command, cmd: CliCommand): void => {
104
+ sub.action(async (...actionArgs: unknown[]) => {
105
+ const opts = sub.opts() as Record<string, unknown>;
106
+ const parsedArgs = collectPositionalArgs(cmd, actionArgs);
107
+ try {
108
+ await cmd.execute(parsedArgs, opts);
109
+ } catch (error: unknown) {
110
+ handleError(error);
111
+ }
112
+ });
113
+ };
114
+
115
+ /** Attach a subcommand to its group or to the top-level program. */
116
+ const attachToGroup = (
117
+ sub: Command,
118
+ cmd: CliCommand,
119
+ program: Command,
120
+ groups: Map<string, Command>
121
+ ): void => {
122
+ if (cmd.group) {
123
+ let groupCmd = groups.get(cmd.group);
124
+ if (!groupCmd) {
125
+ groupCmd = new Command(cmd.group);
126
+ groups.set(cmd.group, groupCmd);
127
+ program.addCommand(groupCmd);
128
+ }
129
+ groupCmd.addCommand(sub);
130
+ } else {
131
+ program.addCommand(sub);
132
+ }
133
+ };
134
+
135
+ /** Apply options to a Commander program. */
136
+ const applyOptions = (program: Command, options?: ToCommanderOptions): void => {
137
+ if (options?.name) {
138
+ program.name(options.name);
139
+ }
140
+ if (options?.version) {
141
+ program.version(options.version);
142
+ }
143
+ if (options?.description) {
144
+ program.description(options.description);
145
+ }
146
+ };
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // toCommander
150
+ // ---------------------------------------------------------------------------
151
+
152
+ /**
153
+ * Convert CliCommand[] into a configured Commander program.
154
+ *
155
+ * Groups commands by their `group` field into parent/subcommand structure.
156
+ * Wires each command's `.action()` to call `execute()` and handle errors.
157
+ */
158
+ /** Build a Commander subcommand from a CliCommand. */
159
+ const buildSubcommand = (cmd: CliCommand): Command => {
160
+ const sub = new Command(cmd.name);
161
+ if (cmd.description) {
162
+ sub.description(cmd.description);
163
+ }
164
+ for (const flag of cmd.flags) {
165
+ sub.addOption(buildOption(flag));
166
+ }
167
+ addArgs(sub, cmd);
168
+ wireAction(sub, cmd);
169
+ return sub;
170
+ };
171
+
172
+ export const toCommander = (
173
+ commands: CliCommand[],
174
+ options?: ToCommanderOptions
175
+ ): Command => {
176
+ const program = new Command();
177
+ applyOptions(program, options);
178
+ const groups = new Map<string, Command>();
179
+
180
+ for (const cmd of commands) {
181
+ const sub = buildSubcommand(cmd);
182
+ attachToGroup(sub, cmd, program, groups);
183
+ }
184
+
185
+ return program;
186
+ };