@rune-cli/rune 0.0.6 → 0.0.8

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/dist/cli.mjs CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { a as isHelpFlag, i as successResult, n as runManifestCommand, o as isVersionFlag, r as failureResult, t as writeCommandExecutionResult } from "./write-result-DOrdlrbw.mjs";
2
+ import "./dist-uz53Uv1e.mjs";
3
+ import { n as isHelpFlag, r as isVersionFlag, t as runManifestCommand } from "./run-manifest-command-Dq_lBv-H.mjs";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { build } from "esbuild";
5
6
  import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
6
7
  import path from "node:path";
7
8
  import ts from "typescript";
8
9
  //#region package.json
9
- var version = "0.0.6";
10
+ var version = "0.0.8";
10
11
  //#endregion
11
12
  //#region src/manifest/generate-manifest.ts
12
13
  const COMMAND_ENTRY_FILE = "index.ts";
@@ -143,6 +144,32 @@ async function assertCommandsDirectoryExists(commandsDirectory) {
143
144
  }))?.isDirectory()) throw new Error(`Commands directory not found at ${COMMANDS_DIRECTORY_NAME}. Create it or check the --project <path> option.`);
144
145
  }
145
146
  //#endregion
147
+ //#region src/cli/write-result.ts
148
+ async function writeStream(stream, contents) {
149
+ if (contents.length === 0) return;
150
+ await new Promise((resolve, reject) => {
151
+ stream.write(contents, (error) => {
152
+ if (error) {
153
+ reject(error);
154
+ return;
155
+ }
156
+ resolve();
157
+ });
158
+ });
159
+ }
160
+ function ensureTrailingNewline(contents) {
161
+ return contents.endsWith("\n") ? contents : `${contents}\n`;
162
+ }
163
+ async function writeStdout(contents) {
164
+ await writeStream(process.stdout, contents);
165
+ }
166
+ async function writeStderr(contents) {
167
+ await writeStream(process.stderr, contents);
168
+ }
169
+ async function writeStderrLine(contents) {
170
+ await writeStderr(ensureTrailingNewline(contents));
171
+ }
172
+ //#endregion
146
173
  //#region src/cli/build-command.ts
147
174
  const BUILD_CLI_FILENAME = "cli.mjs";
148
175
  const BUILD_MANIFEST_FILENAME = "manifest.json";
@@ -209,7 +236,7 @@ function renderBuiltCliEntry(cliName, version, runtimeImportPath) {
209
236
  return `import { readFile } from "node:fs/promises";
210
237
  import { fileURLToPath } from "node:url";
211
238
 
212
- import { runManifestCommand, writeCommandExecutionResult } from ${JSON.stringify(runtimeImportPath)};
239
+ import { runManifestCommand } from ${JSON.stringify(runtimeImportPath)};
213
240
 
214
241
  const cliName = ${JSON.stringify(cliName)};
215
242
  const version = ${JSON.stringify(version)};
@@ -228,15 +255,13 @@ const runtimeManifest = {
228
255
  : node,
229
256
  ),
230
257
  };
231
- const result = await runManifestCommand({
258
+ process.exitCode = await runManifestCommand({
232
259
  manifest: runtimeManifest,
233
260
  rawArgs: process.argv.slice(2),
234
261
  cliName,
235
262
  version,
236
263
  cwd: process.cwd(),
237
264
  });
238
-
239
- await writeCommandExecutionResult(result);
240
265
  `;
241
266
  }
242
267
  function collectCommandEntryPoints(manifest) {
@@ -359,10 +384,15 @@ async function runBuildCommand(options) {
359
384
  buildCliEntry(projectRoot, distDirectory, cliInfo.name, cliInfo.version),
360
385
  copyBuiltAssets(sourceDirectory, distDirectory)
361
386
  ]);
362
- return successResult(`Built CLI to ${path.join(distDirectory, BUILD_CLI_FILENAME)}\n`);
387
+ await writeStdout(`Built CLI to ${path.join(distDirectory, BUILD_CLI_FILENAME)}\n`);
388
+ return 0;
363
389
  } catch (error) {
364
- if (isBuildFailure(error)) return failureResult(formatBuildFailure(projectRoot, error));
365
- return failureResult(error instanceof Error ? error.message : "Failed to run rune build");
390
+ if (isBuildFailure(error)) {
391
+ await writeStderrLine(formatBuildFailure(projectRoot, error));
392
+ return 1;
393
+ }
394
+ await writeStderrLine(error instanceof Error ? error.message : "Failed to run rune build");
395
+ return 1;
366
396
  }
367
397
  }
368
398
  //#endregion
@@ -394,7 +424,10 @@ async function runDevCommand(options) {
394
424
  try {
395
425
  const projectRoot = resolveProjectPath(options);
396
426
  const cliInfo = await readProjectCliInfo(projectRoot);
397
- if (cliInfo.version && options.rawArgs.length === 1 && isVersionFlag(options.rawArgs[0])) return successResult(`${cliInfo.name} v${cliInfo.version}\n`);
427
+ if (cliInfo.version && options.rawArgs.length === 1 && isVersionFlag(options.rawArgs[0])) {
428
+ await writeStdout(`${cliInfo.name} v${cliInfo.version}\n`);
429
+ return 0;
430
+ }
398
431
  const commandsDirectory = resolveCommandsDirectory(projectRoot);
399
432
  await assertCommandsDirectoryExists(commandsDirectory);
400
433
  const manifest = await generateCommandManifest({ commandsDirectory });
@@ -407,21 +440,34 @@ async function runDevCommand(options) {
407
440
  cwd: options.cwd
408
441
  });
409
442
  } catch (error) {
410
- return failureResult(error instanceof Error ? error.message : "Failed to run rune dev");
443
+ await writeStderrLine(error instanceof Error ? error.message : "Failed to run rune dev");
444
+ return 1;
411
445
  }
412
446
  }
413
447
  //#endregion
414
448
  //#region src/cli/rune-cli.ts
449
+ async function writeEarlyExit(exit) {
450
+ if (exit.stream === "stdout") await writeStdout(exit.output);
451
+ else await writeStderrLine(exit.output);
452
+ return exit.exitCode;
453
+ }
415
454
  function tryParseProjectOption(argv, index) {
416
455
  const token = argv[index];
417
456
  if (token.startsWith("--project=")) return {
457
+ ok: true,
418
458
  projectPath: token.slice(10),
419
459
  nextIndex: index + 1
420
460
  };
421
461
  if (token === "--project") {
422
462
  const nextToken = argv[index + 1];
423
- if (!nextToken) return failureResult("Missing value for --project. Usage: --project <path>");
463
+ if (!nextToken) return {
464
+ ok: false,
465
+ exitCode: 1,
466
+ output: "Missing value for --project. Usage: --project <path>",
467
+ stream: "stderr"
468
+ };
424
469
  return {
470
+ ok: true,
425
471
  projectPath: nextToken,
426
472
  nextIndex: index + 2
427
473
  };
@@ -438,25 +484,33 @@ function parseDevArgs(argv) {
438
484
  if (token === "--") {
439
485
  commandArgs.push(...argv.slice(index + 1));
440
486
  return {
487
+ ok: true,
441
488
  projectPath,
442
489
  commandArgs
443
490
  };
444
491
  }
445
- if (isHelpFlag(token)) return successResult(renderRuneDevHelp());
492
+ if (isHelpFlag(token)) return {
493
+ ok: false,
494
+ exitCode: 0,
495
+ output: renderRuneDevHelp(),
496
+ stream: "stdout"
497
+ };
446
498
  const projectResult = tryParseProjectOption(argv, index);
447
499
  if (projectResult) {
448
- if ("exitCode" in projectResult) return projectResult;
500
+ if (!projectResult.ok) return projectResult;
449
501
  projectPath = projectResult.projectPath;
450
502
  index = projectResult.nextIndex - 1;
451
503
  continue;
452
504
  }
453
505
  commandArgs.push(token, ...argv.slice(index + 1));
454
506
  return {
507
+ ok: true,
455
508
  projectPath,
456
509
  commandArgs
457
510
  };
458
511
  }
459
512
  return {
513
+ ok: true,
460
514
  projectPath,
461
515
  commandArgs
462
516
  };
@@ -465,17 +519,30 @@ function parseBuildArgs(argv) {
465
519
  let projectPath;
466
520
  for (let index = 0; index < argv.length; index += 1) {
467
521
  const token = argv[index];
468
- if (isHelpFlag(token)) return successResult(renderRuneBuildHelp());
522
+ if (isHelpFlag(token)) return {
523
+ ok: false,
524
+ exitCode: 0,
525
+ output: renderRuneBuildHelp(),
526
+ stream: "stdout"
527
+ };
469
528
  const projectResult = tryParseProjectOption(argv, index);
470
529
  if (projectResult) {
471
- if ("exitCode" in projectResult) return projectResult;
530
+ if (!projectResult.ok) return projectResult;
472
531
  projectPath = projectResult.projectPath;
473
532
  index = projectResult.nextIndex - 1;
474
533
  continue;
475
534
  }
476
- return failureResult(`Unexpected argument for rune build: ${token}`);
535
+ return {
536
+ ok: false,
537
+ exitCode: 1,
538
+ output: `Unexpected argument for rune build: ${token}`,
539
+ stream: "stderr"
540
+ };
477
541
  }
478
- return { projectPath };
542
+ return {
543
+ ok: true,
544
+ projectPath
545
+ };
479
546
  }
480
547
  function renderRuneCliHelp() {
481
548
  return `\
@@ -492,11 +559,17 @@ Options:
492
559
  }
493
560
  async function runRuneCli(options) {
494
561
  const [subcommand, ...restArgs] = options.argv;
495
- if (!subcommand || isHelpFlag(subcommand)) return successResult(renderRuneCliHelp());
496
- if (isVersionFlag(subcommand)) return successResult(`rune v${getRuneVersion()}\n`);
562
+ if (!subcommand || isHelpFlag(subcommand)) {
563
+ await writeStdout(renderRuneCliHelp());
564
+ return 0;
565
+ }
566
+ if (isVersionFlag(subcommand)) {
567
+ await writeStdout(`rune v${getRuneVersion()}\n`);
568
+ return 0;
569
+ }
497
570
  if (subcommand === "dev") {
498
571
  const parsedDevArgs = parseDevArgs(restArgs);
499
- if ("exitCode" in parsedDevArgs) return parsedDevArgs;
572
+ if (!parsedDevArgs.ok) return writeEarlyExit(parsedDevArgs);
500
573
  return runDevCommand({
501
574
  rawArgs: parsedDevArgs.commandArgs,
502
575
  projectPath: parsedDevArgs.projectPath,
@@ -505,19 +578,20 @@ async function runRuneCli(options) {
505
578
  }
506
579
  if (subcommand === "build") {
507
580
  const parsedBuildArgs = parseBuildArgs(restArgs);
508
- if ("exitCode" in parsedBuildArgs) return parsedBuildArgs;
581
+ if (!parsedBuildArgs.ok) return writeEarlyExit(parsedBuildArgs);
509
582
  return runBuildCommand({
510
583
  projectPath: parsedBuildArgs.projectPath,
511
584
  cwd: options.cwd
512
585
  });
513
586
  }
514
- return failureResult(`Unknown command: ${subcommand}. Available commands: build, dev`);
587
+ await writeStderrLine(`Unknown command: ${subcommand}. Available commands: build, dev`);
588
+ return 1;
515
589
  }
516
590
  //#endregion
517
591
  //#region src/cli.ts
518
- await writeCommandExecutionResult(await runRuneCli({
592
+ process.exitCode = await runRuneCli({
519
593
  argv: process.argv.slice(2),
520
594
  cwd: process.cwd()
521
- }));
595
+ });
522
596
  //#endregion
523
597
  export {};
@@ -3,7 +3,35 @@ import { format, parseArgs } from "node:util";
3
3
  function isSchemaField(field) {
4
4
  return "schema" in field && field.schema !== void 0;
5
5
  }
6
- const EMPTY_FIELDS = [];
6
+ const DEFINED_COMMAND_BRAND = Symbol.for("@rune-cli/defined-command");
7
+ const OPTION_NAME_RE = /^[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*$/;
8
+ const ALIAS_RE = /^[a-zA-Z]$/;
9
+ function validateFieldShape(fields, kind) {
10
+ for (const field of fields) {
11
+ const raw = field;
12
+ if (raw.schema === void 0 && raw.type === void 0) throw new Error(`${kind === "argument" ? "Argument" : "Option"} "${raw.name}" must have either a "type" or "schema" property.`);
13
+ }
14
+ }
15
+ function validateUniqueFieldNames(fields, kind) {
16
+ const seen = /* @__PURE__ */ new Set();
17
+ for (const field of fields) {
18
+ if (field.name.length === 0) throw new Error(`Invalid ${kind} name "${field.name}". Names must be non-empty.`);
19
+ if (seen.has(field.name)) throw new Error(`Duplicate ${kind} name "${field.name}".`);
20
+ seen.add(field.name);
21
+ }
22
+ }
23
+ function validateOptionNames(options) {
24
+ for (const field of options) if (!OPTION_NAME_RE.test(field.name)) throw new Error(`Invalid option name "${field.name}". Option names must start with a letter and contain only letters, numbers, and internal hyphens.`);
25
+ }
26
+ function validateOptionAliases(options) {
27
+ const seen = /* @__PURE__ */ new Set();
28
+ for (const field of options) {
29
+ if (field.alias === void 0) continue;
30
+ if (!ALIAS_RE.test(field.alias)) throw new Error(`Invalid alias "${field.alias}" for option "${field.name}". Alias must be a single letter.`);
31
+ if (seen.has(field.alias)) throw new Error(`Duplicate alias "${field.alias}" for option "${field.name}".`);
32
+ seen.add(field.alias);
33
+ }
34
+ }
7
35
  function isOptionalArg(field) {
8
36
  if (isSchemaField(field)) return;
9
37
  return field.required !== true || field.default !== void 0;
@@ -69,19 +97,56 @@ function validateArgOrdering(args) {
69
97
  * the ordering check is skipped for that field.
70
98
  */
71
99
  function defineCommand(input) {
72
- if (input.args) validateArgOrdering(input.args);
73
- return {
100
+ if (input.args) {
101
+ validateFieldShape(input.args, "argument");
102
+ validateUniqueFieldNames(input.args, "argument");
103
+ validateArgOrdering(input.args);
104
+ }
105
+ if (input.options) {
106
+ validateFieldShape(input.options, "option");
107
+ validateUniqueFieldNames(input.options, "option");
108
+ validateOptionNames(input.options);
109
+ validateOptionAliases(input.options);
110
+ }
111
+ const command = {
74
112
  description: input.description,
75
- args: input.args ?? EMPTY_FIELDS,
76
- options: input.options ?? EMPTY_FIELDS,
113
+ args: input.args ?? [],
114
+ options: input.options ?? [],
77
115
  run: input.run
78
116
  };
117
+ Object.defineProperty(command, DEFINED_COMMAND_BRAND, {
118
+ value: true,
119
+ enumerable: false
120
+ });
121
+ return command;
122
+ }
123
+ function isDefinedCommand(value) {
124
+ return typeof value === "object" && value !== null && value[DEFINED_COMMAND_BRAND] === true;
79
125
  }
80
126
  function formatExecutionError(error) {
81
127
  if (error instanceof Error) return error.message === "" ? "" : error.message || error.name || "Unknown error";
82
128
  if (typeof error === "string") return error;
83
129
  return "Unknown error";
84
130
  }
131
+ async function executeCommand(command, input = {}) {
132
+ try {
133
+ const options = { ...input.options };
134
+ for (const field of command.options) if (options[field.name] === void 0 && !isSchemaField(field) && field.type === "boolean") options[field.name] = false;
135
+ await command.run({
136
+ options,
137
+ args: input.args ?? {},
138
+ cwd: input.cwd ?? process.cwd(),
139
+ rawArgs: input.rawArgs ?? []
140
+ });
141
+ return { exitCode: 0 };
142
+ } catch (error) {
143
+ const message = formatExecutionError(error);
144
+ return message ? {
145
+ exitCode: 1,
146
+ errorMessage: message
147
+ } : { exitCode: 1 };
148
+ }
149
+ }
85
150
  async function captureProcessOutput(action) {
86
151
  const stdoutChunks = [];
87
152
  const stderrChunks = [];
@@ -119,17 +184,18 @@ async function captureProcessOutput(action) {
119
184
  ]) console[method] = (...args) => captureConsole(stdoutChunks, args);
120
185
  for (const method of ["warn", "error"]) console[method] = (...args) => captureConsole(stderrChunks, args);
121
186
  try {
122
- const value = await action();
123
187
  return {
188
+ ok: true,
189
+ value: await action(),
124
190
  stdout: stdoutChunks.join(""),
125
- stderr: stderrChunks.join(""),
126
- value
191
+ stderr: stderrChunks.join("")
127
192
  };
128
193
  } catch (error) {
129
194
  return {
195
+ ok: false,
196
+ error,
130
197
  stdout: stdoutChunks.join(""),
131
- stderr: stderrChunks.join(""),
132
- error
198
+ stderr: stderrChunks.join("")
133
199
  };
134
200
  } finally {
135
201
  process.stdout.write = originalStdoutWrite;
@@ -137,34 +203,11 @@ async function captureProcessOutput(action) {
137
203
  Object.assign(console, originalConsoleMethods);
138
204
  }
139
205
  }
140
- const EMPTY_ARGS = [];
141
- async function executeCommand(command, input = {}) {
142
- const result = await captureProcessOutput(async () => {
143
- await command.run({
144
- options: input.options ?? {},
145
- args: input.args ?? {},
146
- cwd: input.cwd ?? process.cwd(),
147
- rawArgs: input.rawArgs ?? EMPTY_ARGS
148
- });
149
- });
150
- if (result.error === void 0) return {
151
- exitCode: 0,
152
- stdout: result.stdout,
153
- stderr: result.stderr
154
- };
155
- const message = formatExecutionError(result.error);
156
- return {
157
- exitCode: 1,
158
- stdout: result.stdout,
159
- stderr: `${result.stderr}${message ? `${message}\n` : ""}`
160
- };
161
- }
162
- function formatFieldTypeHint(field) {
163
- if (isSchemaField(field)) return "";
164
- return ` <${field.type}>`;
206
+ function formatTypeHint(field) {
207
+ return isSchemaField(field) ? "" : ` <${field.type}>`;
165
208
  }
166
209
  function formatOptionLabel(field) {
167
- return `--${field.name}${formatFieldTypeHint(field)}`;
210
+ return `--${field.name}${formatTypeHint(field)}`;
168
211
  }
169
212
  function formatArgumentLabel(field) {
170
213
  return field.name;
@@ -386,6 +429,7 @@ async function parseCommand(command, rawArgs) {
386
429
  const result = await resolveMissingField(field, () => missingRequiredOption(field));
387
430
  if (!result.ok) return result;
388
431
  if (result.present) parsedOptions[field.name] = result.value;
432
+ else if (!isSchemaField(field) && field.type === "boolean") parsedOptions[field.name] = false;
389
433
  }
390
434
  return {
391
435
  ok: true,
@@ -397,4 +441,4 @@ async function parseCommand(command, rawArgs) {
397
441
  };
398
442
  }
399
443
  //#endregion
400
- export { parseCommand as a, isSchemaField as i, executeCommand as n, formatFieldTypeHint as r, defineCommand as t };
444
+ export { isSchemaField as a, isDefinedCommand as i, defineCommand as n, parseCommand as o, executeCommand as r, captureProcessOutput as t };
@@ -81,7 +81,13 @@ declare namespace StandardSchemaV1 {
81
81
  type PrimitiveFieldType = "string" | "number" | "boolean";
82
82
  type PrimitiveFieldValue<TType extends PrimitiveFieldType> = TType extends "string" ? string : TType extends "number" ? number : boolean;
83
83
  interface NamedField<TName extends string = string> {
84
- /** Identifier used as the key in `ctx.args` / `ctx.options` and as the CLI flag name for options. */
84
+ /**
85
+ * Identifier used as the key in `ctx.args` / `ctx.options`.
86
+ *
87
+ * For args, any non-empty name is allowed.
88
+ * For options, names must start with a letter and may contain only letters,
89
+ * numbers, and internal hyphens (for example: `dry-run`, `dryRun`, `v2`).
90
+ */
85
91
  readonly name: TName;
86
92
  /** One-line help text shown in `--help` output. */
87
93
  readonly description?: string | undefined;
@@ -91,7 +97,8 @@ interface PrimitiveFieldBase<TName extends string, TType extends PrimitiveFieldT
91
97
  readonly type: TType;
92
98
  /**
93
99
  * When `true`, the field must be provided by the user.
94
- * Omitted or `false` makes the field optional (absent fields are `undefined` in `ctx`).
100
+ * Omitted or `false` makes the field optional. Absent fields are `undefined`
101
+ * in `ctx`, except primitive boolean options, which default to `false`.
95
102
  */
96
103
  readonly required?: boolean | undefined;
97
104
  /** Value used when the user does not provide this field. Makes the field always present in `ctx`. */
@@ -153,9 +160,13 @@ type FieldInputValue<TField> = TField extends {
153
160
  type HasDefaultValue<TField> = TField extends {
154
161
  readonly default: infer TDefault;
155
162
  } ? [TDefault] extends [undefined] ? false : true : false;
156
- type IsRequiredField<TField> = TField extends {
163
+ type IsRequiredField<TField, TBooleanAlwaysPresent extends boolean = false> = TField extends {
157
164
  readonly schema: infer TSchema;
158
- } ? IsOptionalSchemaOutput<InferSchemaOutput<TSchema>> extends true ? false : true : HasDefaultValue<TField> extends true ? true : TField extends {
165
+ } ? IsOptionalSchemaOutput<InferSchemaOutput<TSchema>> extends true ? false : true : HasDefaultValue<TField> extends true ? true : TBooleanAlwaysPresent extends true ? TField extends {
166
+ readonly type: "boolean";
167
+ } ? true : TField extends {
168
+ readonly required: true;
169
+ } ? true : false : TField extends {
159
170
  readonly required: true;
160
171
  } ? true : false;
161
172
  type IsArgOptional<TField> = TField extends {
@@ -170,7 +181,7 @@ type ValidateArgOrder<TArgs> = TArgs extends readonly CommandArgField[] ? IsVali
170
181
  readonly args: never;
171
182
  } : unknown : unknown;
172
183
  type Simplify<TValue> = { [TKey in keyof TValue]: TValue[TKey] };
173
- type InferNamedFields<TFields extends readonly NamedField[]> = Simplify<{ [TField in TFields[number] as IsRequiredField<TField> extends true ? FieldName<TField> : never]: FieldValue<TField> } & { [TField in TFields[number] as IsRequiredField<TField> extends true ? never : FieldName<TField>]?: FieldValue<TField> }>;
184
+ type InferNamedFields<TFields extends readonly NamedField[], TBooleanAlwaysPresent extends boolean = false> = Simplify<{ [TField in TFields[number] as IsRequiredField<TField, TBooleanAlwaysPresent> extends true ? FieldName<TField> : never]: FieldValue<TField> } & { [TField in TFields[number] as IsRequiredField<TField, TBooleanAlwaysPresent> extends true ? never : FieldName<TField>]?: FieldValue<TField> }>;
174
185
  type InferExecutionFields<TFields extends readonly NamedField[]> = Simplify<{ [TField in TFields[number] as FieldName<TField>]?: FieldInputValue<TField> }>;
175
186
  /** Runtime data passed into a command's `run` function. */
176
187
  interface CommandContext<TOptions, TArgs> {
@@ -193,6 +204,7 @@ interface DefineCommandInput<TArgsFields extends readonly CommandArgField[] | un
193
204
  /**
194
205
  * Positional arguments declared in the order they appear on the command line.
195
206
  * Required arguments must come before optional ones.
207
+ * Argument names must be non-empty and unique within the command.
196
208
  *
197
209
  * Each entry is either a primitive field (`{ name, type }`) or a schema
198
210
  * field (`{ name, schema }`).
@@ -200,6 +212,8 @@ interface DefineCommandInput<TArgsFields extends readonly CommandArgField[] | un
200
212
  readonly args?: TArgsFields;
201
213
  /**
202
214
  * Options declared as `--name` flags, with optional single-character aliases.
215
+ * Option names must be unique within the command, start with a letter, and
216
+ * contain only letters, numbers, and internal hyphens.
203
217
  *
204
218
  * Each entry is either a primitive field (`{ name, type }`) or a schema
205
219
  * field (`{ name, schema }`).
@@ -209,13 +223,13 @@ interface DefineCommandInput<TArgsFields extends readonly CommandArgField[] | un
209
223
  * The function executed when this command is invoked.
210
224
  * Receives a {@link CommandContext} with fully parsed `args` and `options`.
211
225
  */
212
- readonly run: (ctx: CommandContext<InferNamedFields<NormalizeFields<TOptionsFields, CommandOptionField>>, InferNamedFields<NormalizeFields<TArgsFields, CommandArgField>>>) => void | Promise<void>;
226
+ readonly run: (ctx: CommandContext<InferNamedFields<NormalizeFields<TOptionsFields, CommandOptionField>, true>, InferNamedFields<NormalizeFields<TArgsFields, CommandArgField>>>) => void | Promise<void>;
213
227
  }
214
228
  interface DefinedCommand<TArgsFields extends readonly CommandArgField[] = readonly [], TOptionsFields extends readonly CommandOptionField[] = readonly []> {
215
229
  readonly description?: string | undefined;
216
230
  readonly args: TArgsFields;
217
231
  readonly options: TOptionsFields;
218
- readonly run: (ctx: CommandContext<InferNamedFields<TOptionsFields>, InferNamedFields<TArgsFields>>) => void | Promise<void>;
232
+ readonly run: (ctx: CommandContext<InferNamedFields<TOptionsFields, true>, InferNamedFields<TArgsFields>>) => void | Promise<void>;
219
233
  } //#endregion
220
234
  //#region src/define-command.d.ts
221
235
  /**
@@ -269,7 +283,8 @@ interface DefinedCommand<TArgsFields extends readonly CommandArgField[] = readon
269
283
  * a variable without a concrete type), optionality information is lost and
270
284
  * the ordering check is skipped for that field.
271
285
  */
272
- declare function defineCommand<const TArgsFields extends readonly CommandArgField[] | undefined = undefined, const TOptionsFields extends readonly CommandOptionField[] | undefined = undefined>(input: DefineCommandInput<TArgsFields, TOptionsFields> & ValidateArgOrder<TArgsFields>): DefinedCommand<NormalizeFields<TArgsFields, CommandArgField>, NormalizeFields<TOptionsFields, CommandOptionField>>; //#endregion
286
+ declare function defineCommand<const TArgsFields extends readonly CommandArgField[] | undefined = undefined, const TOptionsFields extends readonly CommandOptionField[] | undefined = undefined>(input: DefineCommandInput<TArgsFields, TOptionsFields> & ValidateArgOrder<TArgsFields>): DefinedCommand<NormalizeFields<TArgsFields, CommandArgField>, NormalizeFields<TOptionsFields, CommandOptionField>>;
287
+ //#endregion
273
288
  //#region src/execute-command.d.ts
274
289
  interface ExecuteCommandInput<TOptions, TArgs> {
275
290
  readonly options?: TOptions | undefined;
@@ -277,10 +292,5 @@ interface ExecuteCommandInput<TOptions, TArgs> {
277
292
  readonly cwd?: string | undefined;
278
293
  readonly rawArgs?: readonly string[] | undefined;
279
294
  }
280
- interface CommandExecutionResult {
281
- readonly exitCode: number;
282
- readonly stdout: string;
283
- readonly stderr: string;
284
- }
285
295
  //#endregion
286
- export { DefinedCommand as a, PrimitiveArgField as c, SchemaArgField as d, SchemaOptionField as f, CommandOptionField as i, PrimitiveFieldType as l, CommandContext as n, ExecuteCommandInput as o, defineCommand as p, CommandExecutionResult as r, InferExecutionFields as s, CommandArgField as t, PrimitiveOptionField as u };
296
+ export { ExecuteCommandInput as a, PrimitiveFieldType as c, SchemaOptionField as d, defineCommand as f, DefinedCommand as i, PrimitiveOptionField as l, CommandContext as n, InferExecutionFields as o, CommandOptionField as r, PrimitiveArgField as s, CommandArgField as t, SchemaArgField as u };
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as DefinedCommand, c as PrimitiveArgField, d as SchemaArgField, f as SchemaOptionField, i as CommandOptionField, l as PrimitiveFieldType, n as CommandContext, o as ExecuteCommandInput, p as defineCommand, r as CommandExecutionResult, s as InferExecutionFields, t as CommandArgField, u as PrimitiveOptionField } from "./index-BF5_G9J2.mjs";
2
- export { type CommandArgField, type CommandContext, type CommandExecutionResult, type CommandOptionField, type DefinedCommand, type ExecuteCommandInput, type InferExecutionFields, type PrimitiveArgField, type PrimitiveFieldType, type PrimitiveOptionField, type SchemaArgField, type SchemaOptionField, defineCommand };
1
+ import { a as ExecuteCommandInput, c as PrimitiveFieldType, d as SchemaOptionField, f as defineCommand, i as DefinedCommand, l as PrimitiveOptionField, n as CommandContext, o as InferExecutionFields, r as CommandOptionField, s as PrimitiveArgField, t as CommandArgField, u as SchemaArgField } from "./index-BWxfSwrT.mjs";
2
+ export { type CommandArgField, type CommandContext, type CommandOptionField, type DefinedCommand, type ExecuteCommandInput, type InferExecutionFields, type PrimitiveArgField, type PrimitiveFieldType, type PrimitiveOptionField, type SchemaArgField, type SchemaOptionField, defineCommand };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as defineCommand } from "./dist-Bcn0FpHi.mjs";
1
+ import { n as defineCommand } from "./dist-uz53Uv1e.mjs";
2
2
  export { defineCommand };
@@ -1,4 +1,4 @@
1
- import { a as parseCommand, i as isSchemaField, n as executeCommand, r as formatFieldTypeHint } from "./dist-Bcn0FpHi.mjs";
1
+ import { a as isSchemaField, i as isDefinedCommand, o as parseCommand, r as executeCommand } from "./dist-uz53Uv1e.mjs";
2
2
  import { pathToFileURL } from "node:url";
3
3
  //#region src/cli/flags.ts
4
4
  function isHelpFlag(token) {
@@ -8,20 +8,40 @@ function isVersionFlag(token) {
8
8
  return token === "--version" || token === "-V";
9
9
  }
10
10
  //#endregion
11
- //#region src/cli/result.ts
12
- function successResult(stdout) {
13
- return {
14
- exitCode: 0,
15
- stdout,
16
- stderr: ""
17
- };
11
+ //#region src/manifest/command-loader.ts
12
+ async function loadCommandFromModule(sourceFilePath) {
13
+ const loadedModule = await import(pathToFileURL(sourceFilePath).href);
14
+ if (loadedModule.default === void 0) throw new Error(`Command module did not export a default command: ${sourceFilePath}`);
15
+ if (!isDefinedCommand(loadedModule.default)) throw new Error(`Command module must export a value created with defineCommand(). Got ${describeCommandModuleExport(loadedModule.default)}.`);
16
+ return loadedModule.default;
18
17
  }
19
- function failureResult(stderr) {
20
- return {
21
- exitCode: 1,
22
- stdout: "",
23
- stderr: stderr.endsWith("\n") ? stderr : `${stderr}\n`
24
- };
18
+ function describeCommandModuleExport(value) {
19
+ if (value === null) return "null";
20
+ if (Array.isArray(value)) return "an array";
21
+ if (typeof value === "object") return "a plain object";
22
+ if (typeof value === "string") return "a string";
23
+ if (typeof value === "number") return "a number";
24
+ if (typeof value === "boolean") return "a boolean";
25
+ if (typeof value === "bigint") return "a bigint";
26
+ if (typeof value === "symbol") return "a symbol";
27
+ if (typeof value === "function") return "a function";
28
+ return "an unsupported value";
29
+ }
30
+ const defaultLoadCommand = (node) => loadCommandFromModule(node.sourceFilePath);
31
+ //#endregion
32
+ //#region src/manifest/damerau-levenshtein.ts
33
+ function damerauLevenshteinDistance(left, right) {
34
+ const rows = left.length + 1;
35
+ const cols = right.length + 1;
36
+ const matrix = Array.from({ length: rows }, () => Array(cols).fill(0));
37
+ for (let row = 0; row < rows; row += 1) matrix[row][0] = row;
38
+ for (let col = 0; col < cols; col += 1) matrix[0][col] = col;
39
+ for (let row = 1; row < rows; row += 1) for (let col = 1; col < cols; col += 1) {
40
+ const substitutionCost = left[row - 1] === right[col - 1] ? 0 : 1;
41
+ matrix[row][col] = Math.min(matrix[row - 1][col] + 1, matrix[row][col - 1] + 1, matrix[row - 1][col - 1] + substitutionCost);
42
+ if (row > 1 && col > 1 && left[row - 1] === right[col - 2] && left[row - 2] === right[col - 1]) matrix[row][col] = Math.min(matrix[row][col], matrix[row - 2][col - 2] + 1);
43
+ }
44
+ return matrix[left.length][right.length];
25
45
  }
26
46
  //#endregion
27
47
  //#region src/manifest/manifest-map.ts
@@ -32,24 +52,80 @@ function createCommandManifestNodeMap(manifest) {
32
52
  return Object.fromEntries(manifest.nodes.map((node) => [commandManifestPathToKey(node.pathSegments), node]));
33
53
  }
34
54
  //#endregion
35
- //#region src/manifest/render-help.ts
36
- async function loadCommandFromModule(sourceFilePath) {
37
- const loadedModule = await import(pathToFileURL(sourceFilePath).href);
38
- if (loadedModule.default === void 0) throw new Error(`Command module did not export a default command: ${sourceFilePath}`);
39
- return loadedModule.default;
55
+ //#region src/manifest/resolve-command-path.ts
56
+ function isOptionLikeToken(token) {
57
+ return token === "--" || token.startsWith("-");
40
58
  }
41
- const defaultLoadCommand = (node) => loadCommandFromModule(node.sourceFilePath);
59
+ function getHelpRequested(args) {
60
+ return args.some(isHelpFlag);
61
+ }
62
+ function getSuggestionThreshold(candidate) {
63
+ return Math.max(2, Math.floor(candidate.length / 3));
64
+ }
65
+ function getSuggestedChildNames(unknownSegment, childNames) {
66
+ return [...childNames].map((childName) => ({
67
+ childName,
68
+ distance: damerauLevenshteinDistance(unknownSegment, childName)
69
+ })).filter(({ childName, distance }) => distance <= getSuggestionThreshold(childName)).sort((left, right) => left.distance - right.distance || left.childName.localeCompare(right.childName)).slice(0, 3).map(({ childName }) => childName);
70
+ }
71
+ function resolveCommandPath(manifest, rawArgs) {
72
+ const nodeMap = createCommandManifestNodeMap(manifest);
73
+ const rootNode = nodeMap[""];
74
+ if (rootNode === void 0) throw new Error("Manifest root node is missing");
75
+ let currentNode = rootNode;
76
+ let tokenIndex = 0;
77
+ while (tokenIndex < rawArgs.length) {
78
+ const token = rawArgs[tokenIndex];
79
+ if (isOptionLikeToken(token)) break;
80
+ const childNode = nodeMap[commandManifestPathToKey([...currentNode.pathSegments, token])];
81
+ if (childNode === void 0) {
82
+ const suggestions = getSuggestedChildNames(token, currentNode.childNames);
83
+ if (currentNode.kind === "group" || suggestions.length > 0) return {
84
+ kind: "unknown",
85
+ attemptedPath: [...currentNode.pathSegments, token],
86
+ matchedPath: currentNode.pathSegments,
87
+ unknownSegment: token,
88
+ availableChildNames: currentNode.childNames,
89
+ suggestions
90
+ };
91
+ break;
92
+ }
93
+ currentNode = childNode;
94
+ tokenIndex += 1;
95
+ }
96
+ const remainingArgs = rawArgs.slice(tokenIndex);
97
+ const helpRequested = getHelpRequested(remainingArgs);
98
+ if (currentNode.kind === "group") return {
99
+ kind: "group",
100
+ node: currentNode,
101
+ matchedPath: currentNode.pathSegments,
102
+ remainingArgs,
103
+ helpRequested
104
+ };
105
+ return {
106
+ kind: "command",
107
+ node: currentNode,
108
+ matchedPath: currentNode.pathSegments,
109
+ remainingArgs,
110
+ helpRequested
111
+ };
112
+ }
113
+ //#endregion
114
+ //#region src/manifest/render-help.ts
42
115
  function formatCommandName(cliName, pathSegments) {
43
116
  return pathSegments.length === 0 ? cliName : `${cliName} ${pathSegments.join(" ")}`;
44
117
  }
45
118
  function formatSectionEntries(entries) {
46
119
  return entries.map(({ label, description }) => ` ${label}${description ? ` ${description}` : ""}`).join("\n");
47
120
  }
121
+ function formatTypeHint(field) {
122
+ return isSchemaField(field) ? "" : ` <${field.type}>`;
123
+ }
48
124
  function formatArgumentLabel(field) {
49
- return `${field.name}${formatFieldTypeHint(field)}`;
125
+ return `${field.name}${formatTypeHint(field)}`;
50
126
  }
51
127
  function formatOptionLabel(field) {
52
- const longOptionLabel = `--${field.name}${formatFieldTypeHint(field)}`;
128
+ const longOptionLabel = `--${field.name}${formatTypeHint(field)}`;
53
129
  if (!field.alias) return longOptionLabel;
54
130
  return `-${field.alias}, ${longOptionLabel}`;
55
131
  }
@@ -117,6 +193,8 @@ function renderUnknownCommandMessage(route, cliName) {
117
193
  if (route.suggestions.length > 0) parts.push(`Did you mean?\n${route.suggestions.map((name) => ` ${name}`).join("\n")}`);
118
194
  return `${parts.join("\n\n")}\n`;
119
195
  }
196
+ //#endregion
197
+ //#region src/manifest/resolve-help.ts
120
198
  async function renderResolvedHelp(options) {
121
199
  if (options.route.kind === "unknown") return renderUnknownCommandMessage(options.route, options.cliName);
122
200
  if (options.route.kind === "group") return renderGroupHelp({
@@ -128,122 +206,55 @@ async function renderResolvedHelp(options) {
128
206
  return renderCommandHelp(await (options.loadCommand ?? defaultLoadCommand)(options.route.node), options.route.matchedPath, options.cliName);
129
207
  }
130
208
  //#endregion
131
- //#region src/manifest/damerau-levenshtein.ts
132
- function damerauLevenshteinDistance(left, right) {
133
- const rows = left.length + 1;
134
- const cols = right.length + 1;
135
- const matrix = Array.from({ length: rows }, () => Array(cols).fill(0));
136
- for (let row = 0; row < rows; row += 1) matrix[row][0] = row;
137
- for (let col = 0; col < cols; col += 1) matrix[0][col] = col;
138
- for (let row = 1; row < rows; row += 1) for (let col = 1; col < cols; col += 1) {
139
- const substitutionCost = left[row - 1] === right[col - 1] ? 0 : 1;
140
- matrix[row][col] = Math.min(matrix[row - 1][col] + 1, matrix[row][col - 1] + 1, matrix[row - 1][col - 1] + substitutionCost);
141
- if (row > 1 && col > 1 && left[row - 1] === right[col - 2] && left[row - 2] === right[col - 1]) matrix[row][col] = Math.min(matrix[row][col], matrix[row - 2][col - 2] + 1);
142
- }
143
- return matrix[left.length][right.length];
144
- }
145
- //#endregion
146
- //#region src/manifest/resolve-command-path.ts
147
- function isOptionLikeToken(token) {
148
- return token === "--" || token.startsWith("-");
149
- }
150
- function getHelpRequested(args) {
151
- return args.some(isHelpFlag);
152
- }
153
- function getSuggestionThreshold(candidate) {
154
- return Math.max(2, Math.floor(candidate.length / 3));
155
- }
156
- function getSuggestedChildNames(unknownSegment, childNames) {
157
- return [...childNames].map((childName) => ({
158
- childName,
159
- distance: damerauLevenshteinDistance(unknownSegment, childName)
160
- })).filter(({ childName, distance }) => distance <= getSuggestionThreshold(childName)).sort((left, right) => left.distance - right.distance || left.childName.localeCompare(right.childName)).slice(0, 3).map(({ childName }) => childName);
209
+ //#region src/manifest/run-manifest-command.ts
210
+ function ensureTrailingNewline(text) {
211
+ return text.endsWith("\n") ? text : `${text}\n`;
161
212
  }
162
- function resolveCommandPath(manifest, rawArgs) {
163
- const nodeMap = createCommandManifestNodeMap(manifest);
164
- const rootNode = nodeMap[""];
165
- if (rootNode === void 0) throw new Error("Manifest root node is missing");
166
- let currentNode = rootNode;
167
- let tokenIndex = 0;
168
- while (tokenIndex < rawArgs.length) {
169
- const token = rawArgs[tokenIndex];
170
- if (isOptionLikeToken(token)) break;
171
- const childNode = nodeMap[commandManifestPathToKey([...currentNode.pathSegments, token])];
172
- if (childNode === void 0) {
173
- const suggestions = getSuggestedChildNames(token, currentNode.childNames);
174
- if (currentNode.kind === "group" || suggestions.length > 0) return {
175
- kind: "unknown",
176
- attemptedPath: [...currentNode.pathSegments, token],
177
- matchedPath: currentNode.pathSegments,
178
- unknownSegment: token,
179
- availableChildNames: currentNode.childNames,
180
- suggestions
181
- };
182
- break;
183
- }
184
- currentNode = childNode;
185
- tokenIndex += 1;
186
- }
187
- const remainingArgs = rawArgs.slice(tokenIndex);
188
- const helpRequested = getHelpRequested(remainingArgs);
189
- if (currentNode.kind === "group") return {
190
- kind: "group",
191
- node: currentNode,
192
- matchedPath: currentNode.pathSegments,
193
- remainingArgs,
194
- helpRequested
195
- };
196
- return {
197
- kind: "command",
198
- node: currentNode,
199
- matchedPath: currentNode.pathSegments,
200
- remainingArgs,
201
- helpRequested
202
- };
213
+ function formatRuntimeError(error) {
214
+ if (error instanceof Error) return error.message || error.name || "Failed to run command";
215
+ if (typeof error === "string" && error.length > 0) return error;
216
+ return "Failed to run command";
203
217
  }
204
- //#endregion
205
- //#region src/manifest/run-manifest-command.ts
206
218
  async function runManifestCommand(options) {
207
- if (options.version && options.rawArgs.length === 1 && isVersionFlag(options.rawArgs[0])) return successResult(`${options.cliName} v${options.version}\n`);
208
- const route = resolveCommandPath(options.manifest, options.rawArgs);
209
- if (route.kind === "unknown" || route.kind === "group" || route.helpRequested) {
210
- const output = await renderResolvedHelp({
211
- manifest: options.manifest,
212
- route,
213
- cliName: options.cliName,
214
- version: options.version,
215
- loadCommand: options.loadCommand
216
- });
217
- return route.kind === "unknown" ? failureResult(output) : successResult(output);
218
- }
219
- const command = await (options.loadCommand ?? defaultLoadCommand)(route.node);
220
- const parsed = await parseCommand(command, route.remainingArgs);
221
- if (!parsed.ok) return failureResult(parsed.error.message);
222
- return executeCommand(command, {
223
- options: parsed.value.options,
224
- args: parsed.value.args,
225
- cwd: options.cwd,
226
- rawArgs: parsed.value.rawArgs
227
- });
228
- }
229
- //#endregion
230
- //#region src/cli/write-result.ts
231
- async function writeStream(stream, contents) {
232
- if (contents.length === 0) return;
233
- await new Promise((resolve, reject) => {
234
- stream.write(contents, (error) => {
235
- if (error) {
236
- reject(error);
237
- return;
219
+ try {
220
+ if (options.version && options.rawArgs.length === 1 && isVersionFlag(options.rawArgs[0])) {
221
+ process.stdout.write(`${options.cliName} v${options.version}\n`);
222
+ return 0;
223
+ }
224
+ const route = resolveCommandPath(options.manifest, options.rawArgs);
225
+ if (route.kind === "unknown" || route.kind === "group" || route.helpRequested) {
226
+ const output = await renderResolvedHelp({
227
+ manifest: options.manifest,
228
+ route,
229
+ cliName: options.cliName,
230
+ version: options.version,
231
+ loadCommand: options.loadCommand
232
+ });
233
+ if (route.kind === "unknown") {
234
+ process.stderr.write(ensureTrailingNewline(output));
235
+ return 1;
238
236
  }
239
- resolve();
237
+ process.stdout.write(output);
238
+ return 0;
239
+ }
240
+ const command = await (options.loadCommand ?? defaultLoadCommand)(route.node);
241
+ const parsed = await parseCommand(command, route.remainingArgs);
242
+ if (!parsed.ok) {
243
+ process.stderr.write(ensureTrailingNewline(parsed.error.message));
244
+ return 1;
245
+ }
246
+ const result = await executeCommand(command, {
247
+ options: parsed.value.options,
248
+ args: parsed.value.args,
249
+ cwd: options.cwd,
250
+ rawArgs: parsed.value.rawArgs
240
251
  });
241
- });
242
- }
243
- async function writeCommandExecutionResult(result) {
244
- await writeStream(process.stdout, result.stdout);
245
- await writeStream(process.stderr, result.stderr);
246
- process.exitCode = result.exitCode;
252
+ if (result.errorMessage) process.stderr.write(ensureTrailingNewline(result.errorMessage));
253
+ return result.exitCode;
254
+ } catch (error) {
255
+ process.stderr.write(ensureTrailingNewline(formatRuntimeError(error)));
256
+ return 1;
257
+ }
247
258
  }
248
259
  //#endregion
249
- export { isHelpFlag as a, successResult as i, runManifestCommand as n, isVersionFlag as o, failureResult as r, writeCommandExecutionResult as t };
260
+ export { isHelpFlag as n, isVersionFlag as r, runManifestCommand as t };
@@ -1,4 +1,4 @@
1
- import { a as DefinedCommand, i as CommandOptionField, r as CommandExecutionResult, t as CommandArgField } from "./index-BF5_G9J2.mjs";
1
+ import { i as DefinedCommand, r as CommandOptionField, t as CommandArgField } from "./index-BWxfSwrT.mjs";
2
2
 
3
3
  //#region src/manifest/manifest-types.d.ts
4
4
  type CommandManifestPath = readonly string[];
@@ -22,7 +22,7 @@ interface CommandManifest {
22
22
  readonly nodes: readonly CommandManifestNode[];
23
23
  }
24
24
  //#endregion
25
- //#region src/manifest/render-help.d.ts
25
+ //#region src/manifest/command-loader.d.ts
26
26
  type LoadCommandFn = (node: CommandManifestCommandNode) => Promise<DefinedCommand<readonly CommandArgField[], readonly CommandOptionField[]>>;
27
27
  //#endregion
28
28
  //#region src/manifest/run-manifest-command.d.ts
@@ -34,9 +34,6 @@ interface RunManifestCommandOptions {
34
34
  readonly cwd?: string | undefined;
35
35
  readonly loadCommand?: LoadCommandFn | undefined;
36
36
  }
37
- declare function runManifestCommand(options: RunManifestCommandOptions): Promise<CommandExecutionResult>;
37
+ declare function runManifestCommand(options: RunManifestCommandOptions): Promise<number>;
38
38
  //#endregion
39
- //#region src/cli/write-result.d.ts
40
- declare function writeCommandExecutionResult(result: CommandExecutionResult): Promise<void>;
41
- //#endregion
42
- export { runManifestCommand, writeCommandExecutionResult };
39
+ export { runManifestCommand };
package/dist/runtime.mjs CHANGED
@@ -1,2 +1,3 @@
1
- import { n as runManifestCommand, t as writeCommandExecutionResult } from "./write-result-DOrdlrbw.mjs";
2
- export { runManifestCommand, writeCommandExecutionResult };
1
+ import "./dist-uz53Uv1e.mjs";
2
+ import { t as runManifestCommand } from "./run-manifest-command-Dq_lBv-H.mjs";
3
+ export { runManifestCommand };
package/dist/test.d.mts CHANGED
@@ -1,7 +1,50 @@
1
- import { a as DefinedCommand, i as CommandOptionField, o as ExecuteCommandInput, r as CommandExecutionResult, s as InferExecutionFields, t as CommandArgField } from "./index-BF5_G9J2.mjs";
1
+ import { a as ExecuteCommandInput, i as DefinedCommand, o as InferExecutionFields, r as CommandOptionField, t as CommandArgField } from "./index-BWxfSwrT.mjs";
2
2
 
3
3
  //#region src/test.d.ts
4
4
  type RunCommandOptions<TOptions, TArgs> = ExecuteCommandInput<TOptions, TArgs>;
5
+ interface CommandExecutionResult {
6
+ readonly exitCode: number;
7
+ readonly stdout: string;
8
+ readonly stderr: string;
9
+ readonly errorMessage?: string | undefined;
10
+ }
11
+ /**
12
+ * Runs a command definition directly in-process for testing.
13
+ *
14
+ * This helper bypasses Rune's CLI parser and validation layers. Callers
15
+ * provide already-normalized `options` and `args` values, and the command's
16
+ * `run` function is executed with those values injected into the context.
17
+ *
18
+ * All output written to `process.stdout`, `process.stderr`, and `console` is
19
+ * captured and returned as strings so tests can assert on them.
20
+ *
21
+ * @param command - A command created with {@link defineCommand}.
22
+ * @param options - Pre-validated options, args, cwd, and rawArgs to inject.
23
+ * @returns The exit code, captured stdout/stderr, and an optional error message.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * import { defineCommand } from "rune";
28
+ * import { runCommand } from "rune/test";
29
+ * import { expect, test } from "vitest";
30
+ *
31
+ * const hello = defineCommand({
32
+ * options: [{ name: "name", type: "string", required: true }],
33
+ * run(ctx) {
34
+ * console.log(`Hello, ${ctx.options.name}!`);
35
+ * },
36
+ * });
37
+ *
38
+ * test("hello command", async () => {
39
+ * const result = await runCommand(hello, {
40
+ * options: { name: "Rune" },
41
+ * });
42
+ *
43
+ * expect(result.exitCode).toBe(0);
44
+ * expect(result.stdout).toBe("Hello, Rune!\n");
45
+ * });
46
+ * ```
47
+ */
5
48
  declare function runCommand<TArgsFields extends readonly CommandArgField[], TOptionsFields extends readonly CommandOptionField[]>(command: DefinedCommand<TArgsFields, TOptionsFields>, options?: RunCommandOptions<InferExecutionFields<TOptionsFields>, InferExecutionFields<TArgsFields>>): Promise<CommandExecutionResult>;
6
49
  //#endregion
7
- export { RunCommandOptions, runCommand };
50
+ export { CommandExecutionResult, RunCommandOptions, runCommand };
package/dist/test.mjs CHANGED
@@ -1,7 +1,51 @@
1
- import { n as executeCommand } from "./dist-Bcn0FpHi.mjs";
1
+ import { r as executeCommand, t as captureProcessOutput } from "./dist-uz53Uv1e.mjs";
2
2
  //#region src/test.ts
3
+ /**
4
+ * Runs a command definition directly in-process for testing.
5
+ *
6
+ * This helper bypasses Rune's CLI parser and validation layers. Callers
7
+ * provide already-normalized `options` and `args` values, and the command's
8
+ * `run` function is executed with those values injected into the context.
9
+ *
10
+ * All output written to `process.stdout`, `process.stderr`, and `console` is
11
+ * captured and returned as strings so tests can assert on them.
12
+ *
13
+ * @param command - A command created with {@link defineCommand}.
14
+ * @param options - Pre-validated options, args, cwd, and rawArgs to inject.
15
+ * @returns The exit code, captured stdout/stderr, and an optional error message.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { defineCommand } from "rune";
20
+ * import { runCommand } from "rune/test";
21
+ * import { expect, test } from "vitest";
22
+ *
23
+ * const hello = defineCommand({
24
+ * options: [{ name: "name", type: "string", required: true }],
25
+ * run(ctx) {
26
+ * console.log(`Hello, ${ctx.options.name}!`);
27
+ * },
28
+ * });
29
+ *
30
+ * test("hello command", async () => {
31
+ * const result = await runCommand(hello, {
32
+ * options: { name: "Rune" },
33
+ * });
34
+ *
35
+ * expect(result.exitCode).toBe(0);
36
+ * expect(result.stdout).toBe("Hello, Rune!\n");
37
+ * });
38
+ * ```
39
+ */
3
40
  async function runCommand(command, options = {}) {
4
- return executeCommand(command, options);
41
+ const captured = await captureProcessOutput(() => executeCommand(command, options));
42
+ if (!captured.ok) throw captured.error;
43
+ return {
44
+ exitCode: captured.value.exitCode,
45
+ stdout: captured.stdout,
46
+ stderr: captured.stderr,
47
+ errorMessage: captured.value.errorMessage
48
+ };
5
49
  }
6
50
  //#endregion
7
51
  export { runCommand };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rune-cli/rune",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Rune is a CLI framework built around the concept of file-based command routing.",
5
5
  "homepage": "https://github.com/morinokami/rune#readme",
6
6
  "bugs": {