@rune-cli/rune 0.0.6 → 0.0.7

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-FWLEc-op.mjs";
3
+ import { n as isHelpFlag, r as isVersionFlag, t as runManifestCommand } from "./run-manifest-command-BmeVwPA5.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.7";
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,7 @@ 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
7
  function isOptionalArg(field) {
8
8
  if (isSchemaField(field)) return;
9
9
  return field.required !== true || field.default !== void 0;
@@ -70,18 +70,43 @@ function validateArgOrdering(args) {
70
70
  */
71
71
  function defineCommand(input) {
72
72
  if (input.args) validateArgOrdering(input.args);
73
- return {
73
+ const command = {
74
74
  description: input.description,
75
- args: input.args ?? EMPTY_FIELDS,
76
- options: input.options ?? EMPTY_FIELDS,
75
+ args: input.args ?? [],
76
+ options: input.options ?? [],
77
77
  run: input.run
78
78
  };
79
+ Object.defineProperty(command, DEFINED_COMMAND_BRAND, {
80
+ value: true,
81
+ enumerable: false
82
+ });
83
+ return command;
84
+ }
85
+ function isDefinedCommand(value) {
86
+ return typeof value === "object" && value !== null && value[DEFINED_COMMAND_BRAND] === true;
79
87
  }
80
88
  function formatExecutionError(error) {
81
89
  if (error instanceof Error) return error.message === "" ? "" : error.message || error.name || "Unknown error";
82
90
  if (typeof error === "string") return error;
83
91
  return "Unknown error";
84
92
  }
93
+ async function executeCommand(command, input = {}) {
94
+ try {
95
+ await command.run({
96
+ options: input.options ?? {},
97
+ args: input.args ?? {},
98
+ cwd: input.cwd ?? process.cwd(),
99
+ rawArgs: input.rawArgs ?? []
100
+ });
101
+ return { exitCode: 0 };
102
+ } catch (error) {
103
+ const message = formatExecutionError(error);
104
+ return message ? {
105
+ exitCode: 1,
106
+ errorMessage: message
107
+ } : { exitCode: 1 };
108
+ }
109
+ }
85
110
  async function captureProcessOutput(action) {
86
111
  const stdoutChunks = [];
87
112
  const stderrChunks = [];
@@ -119,17 +144,18 @@ async function captureProcessOutput(action) {
119
144
  ]) console[method] = (...args) => captureConsole(stdoutChunks, args);
120
145
  for (const method of ["warn", "error"]) console[method] = (...args) => captureConsole(stderrChunks, args);
121
146
  try {
122
- const value = await action();
123
147
  return {
148
+ ok: true,
149
+ value: await action(),
124
150
  stdout: stdoutChunks.join(""),
125
- stderr: stderrChunks.join(""),
126
- value
151
+ stderr: stderrChunks.join("")
127
152
  };
128
153
  } catch (error) {
129
154
  return {
155
+ ok: false,
156
+ error,
130
157
  stdout: stdoutChunks.join(""),
131
- stderr: stderrChunks.join(""),
132
- error
158
+ stderr: stderrChunks.join("")
133
159
  };
134
160
  } finally {
135
161
  process.stdout.write = originalStdoutWrite;
@@ -137,34 +163,11 @@ async function captureProcessOutput(action) {
137
163
  Object.assign(console, originalConsoleMethods);
138
164
  }
139
165
  }
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}>`;
166
+ function formatTypeHint(field) {
167
+ return isSchemaField(field) ? "" : ` <${field.type}>`;
165
168
  }
166
169
  function formatOptionLabel(field) {
167
- return `--${field.name}${formatFieldTypeHint(field)}`;
170
+ return `--${field.name}${formatTypeHint(field)}`;
168
171
  }
169
172
  function formatArgumentLabel(field) {
170
173
  return field.name;
@@ -397,4 +400,4 @@ async function parseCommand(command, rawArgs) {
397
400
  };
398
401
  }
399
402
  //#endregion
400
- export { parseCommand as a, isSchemaField as i, executeCommand as n, formatFieldTypeHint as r, defineCommand as t };
403
+ export { isSchemaField as a, isDefinedCommand as i, defineCommand as n, parseCommand as o, executeCommand as r, captureProcessOutput as t };
@@ -269,7 +269,8 @@ interface DefinedCommand<TArgsFields extends readonly CommandArgField[] = readon
269
269
  * a variable without a concrete type), optionality information is lost and
270
270
  * the ordering check is skipped for that field.
271
271
  */
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
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>>;
273
+ //#endregion
273
274
  //#region src/execute-command.d.ts
274
275
  interface ExecuteCommandInput<TOptions, TArgs> {
275
276
  readonly options?: TOptions | undefined;
@@ -277,10 +278,5 @@ interface ExecuteCommandInput<TOptions, TArgs> {
277
278
  readonly cwd?: string | undefined;
278
279
  readonly rawArgs?: readonly string[] | undefined;
279
280
  }
280
- interface CommandExecutionResult {
281
- readonly exitCode: number;
282
- readonly stdout: string;
283
- readonly stderr: string;
284
- }
285
281
  //#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 };
282
+ 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-B6XsJ6ON.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-FWLEc-op.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-FWLEc-op.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-B6XsJ6ON.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-FWLEc-op.mjs";
2
+ import { t as runManifestCommand } from "./run-manifest-command-BmeVwPA5.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-B6XsJ6ON.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-FWLEc-op.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.7",
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": {