@rune-cli/rune 0.0.8 → 0.0.10

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,16 +1,23 @@
1
1
  #!/usr/bin/env node
2
- import "./dist-uz53Uv1e.mjs";
3
- import { n as isHelpFlag, r as isVersionFlag, t as runManifestCommand } from "./run-manifest-command-Dq_lBv-H.mjs";
2
+ import "./dist-CSbOseWZ.mjs";
3
+ import { n as isHelpFlag, r as isVersionFlag, t as runManifestCommand } from "./run-manifest-command-CSsdj02B.mjs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { build } from "esbuild";
6
6
  import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
7
7
  import path from "node:path";
8
8
  import ts from "typescript";
9
9
  //#region package.json
10
- var version = "0.0.8";
10
+ var version = "0.0.10";
11
11
  //#endregion
12
12
  //#region src/manifest/generate-manifest.ts
13
13
  const COMMAND_ENTRY_FILE = "index.ts";
14
+ const GROUP_META_FILE = "_group.ts";
15
+ const BARE_COMMAND_EXTENSION = ".ts";
16
+ const DECLARATION_FILE_SUFFIXES = [
17
+ ".d.ts",
18
+ ".d.mts",
19
+ ".d.cts"
20
+ ];
14
21
  function comparePathSegments(left, right) {
15
22
  const length = Math.min(left.length, right.length);
16
23
  for (let index = 0; index < length; index += 1) {
@@ -26,13 +33,22 @@ function getStaticDescriptionValue(expression) {
26
33
  if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
27
34
  }
28
35
  function isDefineCommandExpression(expression) {
29
- if (ts.isIdentifier(expression)) return expression.text === "defineCommand";
30
- if (ts.isPropertyAccessExpression(expression)) return expression.name.text === "defineCommand";
36
+ return isNamedCallExpression(expression, "defineCommand");
37
+ }
38
+ function isDefineGroupExpression(expression) {
39
+ return isNamedCallExpression(expression, "defineGroup");
40
+ }
41
+ function isNamedCallExpression(expression, name) {
42
+ if (ts.isIdentifier(expression)) return expression.text === name;
43
+ if (ts.isPropertyAccessExpression(expression)) return expression.name.text === name;
31
44
  return false;
32
45
  }
46
+ function isDefineCallExpression(expression) {
47
+ return ts.isCallExpression(expression) && (isDefineCommandExpression(expression.expression) || isDefineGroupExpression(expression.expression));
48
+ }
33
49
  function extractDescriptionFromCommandDefinition(expression, knownDescriptions) {
34
50
  if (ts.isIdentifier(expression)) return knownDescriptions.get(expression.text);
35
- if (!ts.isCallExpression(expression) || !isDefineCommandExpression(expression.expression)) return;
51
+ if (!ts.isCallExpression(expression) || !isDefineCallExpression(expression)) return;
36
52
  const [definition] = expression.arguments;
37
53
  if (!definition || !ts.isObjectLiteralExpression(definition)) return;
38
54
  for (const property of definition.properties) {
@@ -56,6 +72,26 @@ async function extractDescriptionFromSourceFile(sourceFilePath) {
56
72
  if (ts.isExportAssignment(statement)) return extractDescriptionFromCommandDefinition(statement.expression, knownDescriptions);
57
73
  }
58
74
  }
75
+ async function validateGroupMetaFile(sourceFilePath) {
76
+ const sourceText = await readFile(sourceFilePath, "utf8");
77
+ const sourceFile = ts.createSourceFile(sourceFilePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
78
+ for (const statement of sourceFile.statements) {
79
+ if (ts.isVariableStatement(statement)) continue;
80
+ if (ts.isImportDeclaration(statement)) continue;
81
+ if (ts.isExportAssignment(statement)) {
82
+ const expression = ts.isIdentifier(statement.expression) ? findVariableInitializer(sourceFile, statement.expression.text) : statement.expression;
83
+ if (expression && ts.isCallExpression(expression) && isDefineGroupExpression(expression.expression)) return;
84
+ throw new Error(`${sourceFilePath}: _group.ts must use "export default defineGroup(...)". Found a default export that is not a defineGroup() call.`);
85
+ }
86
+ }
87
+ throw new Error(`${sourceFilePath}: _group.ts must have a default export using defineGroup().`);
88
+ }
89
+ function findVariableInitializer(sourceFile, name) {
90
+ for (const statement of sourceFile.statements) {
91
+ if (!ts.isVariableStatement(statement)) continue;
92
+ for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name) && declaration.name.text === name && declaration.initializer) return declaration.initializer;
93
+ }
94
+ }
59
95
  async function walkCommandsDirectory(absoluteDirectoryPath, pathSegments, extractDescription) {
60
96
  const entries = await readdir(absoluteDirectoryPath, { withFileTypes: true });
61
97
  const childDirectoryNames = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
@@ -65,11 +101,28 @@ async function walkCommandsDirectory(absoluteDirectoryPath, pathSegments, extrac
65
101
  result: await walkCommandsDirectory(path.join(absoluteDirectoryPath, directoryName), [...pathSegments, directoryName], extractDescription)
66
102
  };
67
103
  }));
104
+ const bareCommandFiles = entries.filter((entry) => entry.isFile() && entry.name.endsWith(BARE_COMMAND_EXTENSION) && entry.name !== COMMAND_ENTRY_FILE && entry.name !== GROUP_META_FILE && !DECLARATION_FILE_SUFFIXES.some((suffix) => entry.name.endsWith(suffix))).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
105
+ const childDirectoriesWithNodes = new Set(childResults.filter(({ result }) => result.hasNode).map(({ directoryName }) => directoryName));
106
+ const bareCommandNodes = await Promise.all(bareCommandFiles.map(async (fileName) => {
107
+ const commandName = fileName.slice(0, -3);
108
+ if (childDirectoriesWithNodes.has(commandName)) throw new Error(`Conflicting command definitions: both "${commandName}${BARE_COMMAND_EXTENSION}" and "${commandName}/" exist. A bare command file cannot coexist with a command directory.`);
109
+ const sourceFilePath = path.join(absoluteDirectoryPath, fileName);
110
+ return {
111
+ pathSegments: [...pathSegments, commandName],
112
+ kind: "command",
113
+ sourceFilePath,
114
+ childNames: [],
115
+ description: await extractDescription(sourceFilePath)
116
+ };
117
+ }));
68
118
  const childNodes = childResults.flatMap(({ result }) => result.nodes);
69
- const childNames = childResults.filter(({ result }) => result.hasNode).map(({ directoryName }) => directoryName);
119
+ const childNames = [...childResults.filter(({ result }) => result.hasNode).map(({ directoryName }) => directoryName), ...bareCommandFiles.map((fileName) => fileName.slice(0, -3))].sort((left, right) => left.localeCompare(right));
70
120
  const hasCommandEntry = entries.some((entry) => entry.isFile() && entry.name === COMMAND_ENTRY_FILE);
71
- if (!hasCommandEntry && childNames.length === 0) return {
72
- nodes: childNodes,
121
+ const hasGroupMeta = entries.some((entry) => entry.isFile() && entry.name === GROUP_META_FILE);
122
+ if (hasGroupMeta && hasCommandEntry) throw new Error(`Conflicting definitions: both "${GROUP_META_FILE}" and "${COMMAND_ENTRY_FILE}" exist in the same directory. A directory is either a group (_group.ts) or an executable command (index.ts), not both.`);
123
+ if (hasGroupMeta && childNames.length === 0) throw new Error(`${path.join(absoluteDirectoryPath, GROUP_META_FILE)}: _group.ts exists but the directory has no subcommands.`);
124
+ if (!hasCommandEntry && !hasGroupMeta && childNames.length === 0) return {
125
+ nodes: [...childNodes, ...bareCommandNodes],
73
126
  hasNode: false
74
127
  };
75
128
  let node;
@@ -82,19 +135,32 @@ async function walkCommandsDirectory(absoluteDirectoryPath, pathSegments, extrac
82
135
  childNames,
83
136
  description: await extractDescription(sourceFilePath)
84
137
  };
85
- } else node = {
86
- pathSegments,
87
- kind: "group",
88
- childNames
89
- };
138
+ } else {
139
+ const groupMetaPath = path.join(absoluteDirectoryPath, GROUP_META_FILE);
140
+ if (hasGroupMeta) await validateGroupMetaFile(groupMetaPath);
141
+ const description = hasGroupMeta ? await extractDescription(groupMetaPath) : void 0;
142
+ if (hasGroupMeta && !description) throw new Error(`${groupMetaPath}: _group.ts must export a defineGroup() call with a non-empty "description" string.`);
143
+ node = {
144
+ pathSegments,
145
+ kind: "group",
146
+ childNames,
147
+ ...description !== void 0 ? { description } : {}
148
+ };
149
+ }
90
150
  return {
91
- nodes: [node, ...childNodes],
151
+ nodes: [
152
+ node,
153
+ ...childNodes,
154
+ ...bareCommandNodes
155
+ ],
92
156
  hasNode: true
93
157
  };
94
158
  }
95
159
  async function generateCommandManifest(options) {
96
160
  const extractDescription = options.extractDescription ?? extractDescriptionFromSourceFile;
97
- return { nodes: [...(await walkCommandsDirectory(options.commandsDirectory, [], extractDescription)).nodes].sort((left, right) => comparePathSegments(left.pathSegments, right.pathSegments)) };
161
+ const walkResult = await walkCommandsDirectory(options.commandsDirectory, [], extractDescription);
162
+ if (walkResult.nodes.length === 0) throw new Error("No commands found in src/commands/. Create a command file like src/commands/hello.ts or src/commands/hello/index.ts");
163
+ return { nodes: [...walkResult.nodes].sort((left, right) => comparePathSegments(left.pathSegments, right.pathSegments)) };
98
164
  }
99
165
  function serializeCommandManifest(manifest) {
100
166
  return JSON.stringify(manifest, null, 2);
@@ -1,4 +1,4 @@
1
- import { format, parseArgs } from "node:util";
1
+ import { parseArgs } from "node:util";
2
2
  //#region ../core/dist/index.mjs
3
3
  function isSchemaField(field) {
4
4
  return "schema" in field && field.schema !== void 0;
@@ -97,6 +97,7 @@ function validateArgOrdering(args) {
97
97
  * the ordering check is skipped for that field.
98
98
  */
99
99
  function defineCommand(input) {
100
+ if (typeof input.run !== "function") throw new Error("defineCommand() requires a \"run\" function.");
100
101
  if (input.args) {
101
102
  validateFieldShape(input.args, "argument");
102
103
  validateUniqueFieldNames(input.args, "argument");
@@ -123,6 +124,31 @@ function defineCommand(input) {
123
124
  function isDefinedCommand(value) {
124
125
  return typeof value === "object" && value !== null && value[DEFINED_COMMAND_BRAND] === true;
125
126
  }
127
+ const DEFINED_GROUP_BRAND = Symbol.for("@rune-cli/defined-group");
128
+ /**
129
+ * Defines metadata for a command group (a directory that only groups
130
+ * subcommands without being executable itself).
131
+ *
132
+ * Place the default export of this function in a `_group.ts` file inside a
133
+ * command directory.
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * // src/commands/project/_group.ts
138
+ * export default defineGroup({
139
+ * description: "Manage projects",
140
+ * });
141
+ * ```
142
+ */
143
+ function defineGroup(input) {
144
+ if (typeof input.description !== "string" || input.description.length === 0) throw new Error("defineGroup() requires a non-empty \"description\" string.");
145
+ const group = { description: input.description };
146
+ Object.defineProperty(group, DEFINED_GROUP_BRAND, {
147
+ value: true,
148
+ enumerable: false
149
+ });
150
+ return group;
151
+ }
126
152
  function formatExecutionError(error) {
127
153
  if (error instanceof Error) return error.message === "" ? "" : error.message || error.name || "Unknown error";
128
154
  if (typeof error === "string") return error;
@@ -147,62 +173,6 @@ async function executeCommand(command, input = {}) {
147
173
  } : { exitCode: 1 };
148
174
  }
149
175
  }
150
- async function captureProcessOutput(action) {
151
- const stdoutChunks = [];
152
- const stderrChunks = [];
153
- const originalStdoutWrite = process.stdout.write.bind(process.stdout);
154
- const originalStderrWrite = process.stderr.write.bind(process.stderr);
155
- const originalConsoleMethods = {
156
- log: console.log,
157
- info: console.info,
158
- debug: console.debug,
159
- warn: console.warn,
160
- error: console.error
161
- };
162
- const captureChunk = (chunks, chunk, encoding) => {
163
- if (typeof chunk === "string") {
164
- chunks.push(chunk);
165
- return;
166
- }
167
- chunks.push(Buffer.from(chunk).toString(encoding));
168
- };
169
- const captureConsole = (chunks, args) => {
170
- chunks.push(`${format(...args)}\n`);
171
- };
172
- const createWriteCapture = (chunks) => ((chunk, encoding, cb) => {
173
- captureChunk(chunks, chunk, typeof encoding === "string" ? encoding : void 0);
174
- if (typeof encoding === "function") encoding(null);
175
- else cb?.(null);
176
- return true;
177
- });
178
- process.stdout.write = createWriteCapture(stdoutChunks);
179
- process.stderr.write = createWriteCapture(stderrChunks);
180
- for (const method of [
181
- "log",
182
- "info",
183
- "debug"
184
- ]) console[method] = (...args) => captureConsole(stdoutChunks, args);
185
- for (const method of ["warn", "error"]) console[method] = (...args) => captureConsole(stderrChunks, args);
186
- try {
187
- return {
188
- ok: true,
189
- value: await action(),
190
- stdout: stdoutChunks.join(""),
191
- stderr: stderrChunks.join("")
192
- };
193
- } catch (error) {
194
- return {
195
- ok: false,
196
- error,
197
- stdout: stdoutChunks.join(""),
198
- stderr: stderrChunks.join("")
199
- };
200
- } finally {
201
- process.stdout.write = originalStdoutWrite;
202
- process.stderr.write = originalStderrWrite;
203
- Object.assign(console, originalConsoleMethods);
204
- }
205
- }
206
176
  function formatTypeHint(field) {
207
177
  return isSchemaField(field) ? "" : ` <${field.type}>`;
208
178
  }
@@ -212,60 +182,48 @@ function formatOptionLabel(field) {
212
182
  function formatArgumentLabel(field) {
213
183
  return field.name;
214
184
  }
215
- function missingRequiredOption(field) {
185
+ function createMissingOptionError(field) {
216
186
  return {
217
187
  ok: false,
218
188
  error: { message: `Missing required option:\n\n ${formatOptionLabel(field)}` }
219
189
  };
220
190
  }
221
- function missingRequiredArgument(field) {
191
+ function createMissingArgumentError(field) {
222
192
  return {
223
193
  ok: false,
224
194
  error: { message: `Missing required argument:\n\n ${formatArgumentLabel(field)}` }
225
195
  };
226
196
  }
227
- function invalidOptionValue(field, messages) {
197
+ function createInvalidOptionError(field, messages) {
228
198
  return {
229
199
  ok: false,
230
200
  error: { message: `Invalid value for option ${formatOptionLabel(field)}:\n\n ${messages.join("\n ")}` }
231
201
  };
232
202
  }
233
- function invalidArgumentValue(field, messages) {
203
+ function createInvalidArgumentError(field, messages) {
234
204
  return {
235
205
  ok: false,
236
206
  error: { message: `Invalid value for argument ${formatArgumentLabel(field)}:\n\n ${messages.join("\n ")}` }
237
207
  };
238
208
  }
239
- function unknownOption(token) {
209
+ function createUnknownOptionError(token) {
240
210
  return {
241
211
  ok: false,
242
212
  error: { message: `Unknown option "${token}"` }
243
213
  };
244
214
  }
245
- function duplicateOption(field) {
215
+ function createDuplicateOptionError(field) {
246
216
  return {
247
217
  ok: false,
248
218
  error: { message: `Duplicate option "${formatOptionLabel(field)}" is not supported` }
249
219
  };
250
220
  }
251
- function unexpectedArgument(token) {
221
+ function createUnexpectedArgumentError(token) {
252
222
  return {
253
223
  ok: false,
254
224
  error: { message: `Unexpected argument "${token}"` }
255
225
  };
256
226
  }
257
- function normalizeParseArgsError(error) {
258
- if (!(error instanceof Error)) return {
259
- ok: false,
260
- error: { message: "Argument parsing failed" }
261
- };
262
- const unknownMatch = error.message.match(/Unknown option '([^']+)'/);
263
- if (unknownMatch) return unknownOption(unknownMatch[1]);
264
- return {
265
- ok: false,
266
- error: { message: error.message }
267
- };
268
- }
269
227
  function parsePrimitiveValue(field, rawValue) {
270
228
  if (isSchemaField(field)) throw new Error("Schema fields must be handled separately");
271
229
  switch (field.type) {
@@ -352,9 +310,9 @@ async function resolveMissingField(field, missingRequired) {
352
310
  };
353
311
  }
354
312
  async function parseArgumentField(field, rawValue) {
355
- if (rawValue === void 0) return resolveMissingField(field, () => missingRequiredArgument(field));
313
+ if (rawValue === void 0) return resolveMissingField(field, () => createMissingArgumentError(field));
356
314
  const result = await parseProvidedField(field, rawValue);
357
- if (!result.ok) return invalidArgumentValue(field, result.error.message.split("\n"));
315
+ if (!result.ok) return createInvalidArgumentError(field, result.error.message.split("\n"));
358
316
  return {
359
317
  ok: true,
360
318
  present: true,
@@ -363,13 +321,25 @@ async function parseArgumentField(field, rawValue) {
363
321
  }
364
322
  async function parseOptionField(field, rawValue) {
365
323
  const result = await parseProvidedField(field, rawValue);
366
- if (!result.ok) return invalidOptionValue(field, result.error.message.split("\n"));
324
+ if (!result.ok) return createInvalidOptionError(field, result.error.message.split("\n"));
367
325
  return {
368
326
  ok: true,
369
327
  present: true,
370
328
  value: result.value
371
329
  };
372
330
  }
331
+ function normalizeParseArgsError(error) {
332
+ if (!(error instanceof Error)) return {
333
+ ok: false,
334
+ error: { message: "Argument parsing failed" }
335
+ };
336
+ const unknownMatch = error.message.match(/Unknown option '([^']+)'/);
337
+ if (unknownMatch) return createUnknownOptionError(unknownMatch[1]);
338
+ return {
339
+ ok: false,
340
+ error: { message: error.message }
341
+ };
342
+ }
373
343
  function getOptionParseType(field) {
374
344
  if (isSchemaField(field)) return field.flag ? "boolean" : "string";
375
345
  return field.type === "boolean" ? "boolean" : "string";
@@ -390,7 +360,7 @@ function detectDuplicateOption(options, tokens) {
390
360
  counts.set(token.name, nextCount);
391
361
  if (nextCount > 1) {
392
362
  const field = options.find((option) => option.name === token.name);
393
- if (field) return duplicateOption(field);
363
+ if (field) return createDuplicateOptionError(field);
394
364
  }
395
365
  }
396
366
  }
@@ -417,7 +387,7 @@ async function parseCommand(command, rawArgs) {
417
387
  if (!result.ok) return result;
418
388
  if (result.present) parsedArgs[field.name] = result.value;
419
389
  }
420
- if (parsed.positionals.length > command.args.length) return unexpectedArgument(parsed.positionals[command.args.length]);
390
+ if (parsed.positionals.length > command.args.length) return createUnexpectedArgumentError(parsed.positionals[command.args.length]);
421
391
  for (const field of command.options) {
422
392
  const rawValue = parsed.values[field.name];
423
393
  if (rawValue !== void 0) {
@@ -426,7 +396,7 @@ async function parseCommand(command, rawArgs) {
426
396
  if (result.present) parsedOptions[field.name] = result.value;
427
397
  continue;
428
398
  }
429
- const result = await resolveMissingField(field, () => missingRequiredOption(field));
399
+ const result = await resolveMissingField(field, () => createMissingOptionError(field));
430
400
  if (!result.ok) return result;
431
401
  if (result.present) parsedOptions[field.name] = result.value;
432
402
  else if (!isSchemaField(field) && field.type === "boolean") parsedOptions[field.name] = false;
@@ -441,4 +411,4 @@ async function parseCommand(command, rawArgs) {
441
411
  };
442
412
  }
443
413
  //#endregion
444
- export { isSchemaField as a, isDefinedCommand as i, defineCommand as n, parseCommand as o, executeCommand as r, captureProcessOutput as t };
414
+ export { isSchemaField as a, isDefinedCommand as i, defineGroup as n, parseCommand as o, executeCommand as r, defineCommand as t };
@@ -285,6 +285,33 @@ interface DefinedCommand<TArgsFields extends readonly CommandArgField[] = readon
285
285
  */
286
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
287
  //#endregion
288
+ //#region src/define-group.d.ts
289
+ /** The group definition object accepted by {@link defineGroup}. */
290
+ interface DefineGroupInput {
291
+ /** One-line summary shown in `--help` output. */
292
+ readonly description: string;
293
+ }
294
+ /** The normalized group object returned by `defineGroup`. */
295
+ interface DefinedGroup {
296
+ readonly description: string;
297
+ }
298
+ /**
299
+ * Defines metadata for a command group (a directory that only groups
300
+ * subcommands without being executable itself).
301
+ *
302
+ * Place the default export of this function in a `_group.ts` file inside a
303
+ * command directory.
304
+ *
305
+ * @example
306
+ * ```ts
307
+ * // src/commands/project/_group.ts
308
+ * export default defineGroup({
309
+ * description: "Manage projects",
310
+ * });
311
+ * ```
312
+ */
313
+ declare function defineGroup(input: DefineGroupInput): DefinedGroup;
314
+ //#endregion
288
315
  //#region src/execute-command.d.ts
289
316
  interface ExecuteCommandInput<TOptions, TArgs> {
290
317
  readonly options?: TOptions | undefined;
@@ -293,4 +320,4 @@ interface ExecuteCommandInput<TOptions, TArgs> {
293
320
  readonly rawArgs?: readonly string[] | undefined;
294
321
  }
295
322
  //#endregion
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 };
323
+ export { DefinedCommand as a, InferExecutionFields as c, PrimitiveOptionField as d, SchemaArgField as f, defineGroup as h, DefineGroupInput as i, PrimitiveArgField as l, defineCommand as m, CommandContext as n, DefinedGroup as o, SchemaOptionField as p, CommandOptionField as r, ExecuteCommandInput as s, CommandArgField as t, PrimitiveFieldType as u };
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
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 };
1
+ import { a as DefinedCommand, c as InferExecutionFields, d as PrimitiveOptionField, f as SchemaArgField, h as defineGroup, i as DefineGroupInput, l as PrimitiveArgField, m as defineCommand, n as CommandContext, o as DefinedGroup, p as SchemaOptionField, r as CommandOptionField, s as ExecuteCommandInput, t as CommandArgField, u as PrimitiveFieldType } from "./index-CHUchkja.mjs";
2
+ export { type CommandArgField, type CommandContext, type CommandOptionField, type DefineGroupInput, type DefinedCommand, type DefinedGroup, type ExecuteCommandInput, type InferExecutionFields, type PrimitiveArgField, type PrimitiveFieldType, type PrimitiveOptionField, type SchemaArgField, type SchemaOptionField, defineCommand, defineGroup };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { n as defineCommand } from "./dist-uz53Uv1e.mjs";
2
- export { defineCommand };
1
+ import { n as defineGroup, t as defineCommand } from "./dist-CSbOseWZ.mjs";
2
+ export { defineCommand, defineGroup };
@@ -1,4 +1,4 @@
1
- import { a as isSchemaField, i as isDefinedCommand, o as parseCommand, r as executeCommand } from "./dist-uz53Uv1e.mjs";
1
+ import { a as isSchemaField, i as isDefinedCommand, o as parseCommand, r as executeCommand } from "./dist-CSbOseWZ.mjs";
2
2
  import { pathToFileURL } from "node:url";
3
3
  //#region src/cli/flags.ts
4
4
  function isHelpFlag(token) {
@@ -153,7 +153,10 @@ function renderGroupHelp(options) {
153
153
  description: nodeMap[commandManifestPathToKey([...node.pathSegments, childName])]?.description
154
154
  };
155
155
  });
156
- const parts = [`Usage: ${formatCommandName(cliName, node.pathSegments)} <command>`];
156
+ const commandName = formatCommandName(cliName, node.pathSegments);
157
+ const parts = [];
158
+ if (node.description) parts.push(node.description);
159
+ parts.push(`Usage: ${commandName} <command>`);
157
160
  if (entries.length > 0) parts.push(`Subcommands:\n${formatSectionEntries(entries)}`);
158
161
  const optionEntries = [{
159
162
  label: "-h, --help",
@@ -1,4 +1,4 @@
1
- import { i as DefinedCommand, r as CommandOptionField, t as CommandArgField } from "./index-BWxfSwrT.mjs";
1
+ import { a as DefinedCommand, r as CommandOptionField, t as CommandArgField } from "./index-CHUchkja.mjs";
2
2
 
3
3
  //#region src/manifest/manifest-types.d.ts
4
4
  type CommandManifestPath = readonly string[];
package/dist/runtime.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import "./dist-uz53Uv1e.mjs";
2
- import { t as runManifestCommand } from "./run-manifest-command-Dq_lBv-H.mjs";
1
+ import "./dist-CSbOseWZ.mjs";
2
+ import { t as runManifestCommand } from "./run-manifest-command-CSsdj02B.mjs";
3
3
  export { runManifestCommand };
package/dist/test.d.mts CHANGED
@@ -1,50 +1,267 @@
1
- import { a as ExecuteCommandInput, i as DefinedCommand, o as InferExecutionFields, r as CommandOptionField, t as CommandArgField } from "./index-BWxfSwrT.mjs";
2
-
3
- //#region src/test.d.ts
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;
1
+ //#region ../test-utils/dist/index.d.mts
2
+ //#endregion
3
+ //#region ../core/dist/index.d.mts
4
+ //#region ../../node_modules/.pnpm/@standard-schema+spec@1.1.0/node_modules/@standard-schema/spec/dist/index.d.ts
5
+ /** The Standard Typed interface. This is a base type extended by other specs. */
6
+ interface StandardTypedV1<Input = unknown, Output = Input> {
7
+ /** The Standard properties. */
8
+ readonly "~standard": StandardTypedV1.Props<Input, Output>;
9
+ }
10
+ declare namespace StandardTypedV1 {
11
+ /** The Standard Typed properties interface. */
12
+ interface Props<Input = unknown, Output = Input> {
13
+ /** The version number of the standard. */
14
+ readonly version: 1;
15
+ /** The vendor name of the schema library. */
16
+ readonly vendor: string;
17
+ /** Inferred types associated with the schema. */
18
+ readonly types?: Types<Input, Output> | undefined;
19
+ }
20
+ /** The Standard Typed types interface. */
21
+ interface Types<Input = unknown, Output = Input> {
22
+ /** The input type of the schema. */
23
+ readonly input: Input;
24
+ /** The output type of the schema. */
25
+ readonly output: Output;
26
+ }
27
+ /** Infers the input type of a Standard Typed. */
28
+ type InferInput<Schema extends StandardTypedV1> = NonNullable<Schema["~standard"]["types"]>["input"];
29
+ /** Infers the output type of a Standard Typed. */
30
+ type InferOutput<Schema extends StandardTypedV1> = NonNullable<Schema["~standard"]["types"]>["output"];
31
+ }
32
+ /** The Standard Schema interface. */
33
+ interface StandardSchemaV1<Input = unknown, Output = Input> {
34
+ /** The Standard Schema properties. */
35
+ readonly "~standard": StandardSchemaV1.Props<Input, Output>;
36
+ }
37
+ declare namespace StandardSchemaV1 {
38
+ /** The Standard Schema properties interface. */
39
+ interface Props<Input = unknown, Output = Input> extends StandardTypedV1.Props<Input, Output> {
40
+ /** Validates unknown input values. */
41
+ readonly validate: (value: unknown, options?: StandardSchemaV1.Options | undefined) => Result<Output> | Promise<Result<Output>>;
42
+ }
43
+ /** The result interface of the validate function. */
44
+ type Result<Output> = SuccessResult<Output> | FailureResult;
45
+ /** The result interface if validation succeeds. */
46
+ interface SuccessResult<Output> {
47
+ /** The typed output value. */
48
+ readonly value: Output;
49
+ /** A falsy value for `issues` indicates success. */
50
+ readonly issues?: undefined;
51
+ }
52
+ interface Options {
53
+ /** Explicit support for additional vendor-specific parameters, if needed. */
54
+ readonly libraryOptions?: Record<string, unknown> | undefined;
55
+ }
56
+ /** The result interface if validation fails. */
57
+ interface FailureResult {
58
+ /** The issues of failed validation. */
59
+ readonly issues: ReadonlyArray<Issue>;
60
+ }
61
+ /** The issue interface of the failure output. */
62
+ interface Issue {
63
+ /** The error message of the issue. */
64
+ readonly message: string;
65
+ /** The path of the issue, if any. */
66
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
67
+ }
68
+ /** The path segment interface of the issue. */
69
+ interface PathSegment {
70
+ /** The key representing a path segment. */
71
+ readonly key: PropertyKey;
72
+ }
73
+ /** The Standard types interface. */
74
+ interface Types<Input = unknown, Output = Input> extends StandardTypedV1.Types<Input, Output> {}
75
+ /** Infers the input type of a Standard. */
76
+ type InferInput<Schema extends StandardTypedV1> = StandardTypedV1.InferInput<Schema>;
77
+ /** Infers the output type of a Standard. */
78
+ type InferOutput<Schema extends StandardTypedV1> = StandardTypedV1.InferOutput<Schema>;
79
+ }
80
+ /** The Standard JSON Schema interface. */
81
+ //#endregion
82
+ //#region src/command-types.d.ts
83
+ type PrimitiveFieldType = "string" | "number" | "boolean";
84
+ type PrimitiveFieldValue<TType extends PrimitiveFieldType> = TType extends "string" ? string : TType extends "number" ? number : boolean;
85
+ interface NamedField<TName extends string = string> {
86
+ /**
87
+ * Identifier used as the key in `ctx.args` / `ctx.options`.
88
+ *
89
+ * For args, any non-empty name is allowed.
90
+ * For options, names must start with a letter and may contain only letters,
91
+ * numbers, and internal hyphens (for example: `dry-run`, `dryRun`, `v2`).
92
+ */
93
+ readonly name: TName;
94
+ /** One-line help text shown in `--help` output. */
95
+ readonly description?: string | undefined;
96
+ }
97
+ interface PrimitiveFieldBase<TName extends string, TType extends PrimitiveFieldType> extends NamedField<TName> {
98
+ /** Primitive type that Rune parses the raw CLI token into (`"string"`, `"number"`, or `"boolean"`). */
99
+ readonly type: TType;
100
+ /**
101
+ * When `true`, the field must be provided by the user.
102
+ * Omitted or `false` makes the field optional. Absent fields are `undefined`
103
+ * in `ctx`, except primitive boolean options, which default to `false`.
104
+ */
105
+ readonly required?: boolean | undefined;
106
+ /** Value used when the user does not provide this field. Makes the field always present in `ctx`. */
107
+ readonly default?: PrimitiveFieldValue<TType> | undefined;
108
+ readonly schema?: never;
109
+ }
110
+ interface SchemaFieldBase<TName extends string, TSchema extends StandardSchemaV1 = StandardSchemaV1> extends NamedField<TName> {
111
+ /**
112
+ * A Standard Schema object (e.g. `z.string()`, `v.number()`) used to
113
+ * validate and transform the raw CLI token. Required/optional and default
114
+ * semantics are derived from the schema itself.
115
+ */
116
+ readonly schema: TSchema;
117
+ readonly type?: never;
118
+ readonly required?: never;
119
+ readonly default?: never;
120
+ }
121
+ interface PrimitiveArgField<TName extends string = string, TType extends PrimitiveFieldType = PrimitiveFieldType> extends PrimitiveFieldBase<TName, TType> {
122
+ readonly alias?: never;
123
+ readonly flag?: never;
124
+ }
125
+ interface SchemaArgField<TName extends string = string, TSchema extends StandardSchemaV1 = StandardSchemaV1> extends SchemaFieldBase<TName, TSchema> {
126
+ readonly alias?: never;
127
+ readonly flag?: never;
128
+ }
129
+ interface PrimitiveOptionField<TName extends string = string, TType extends PrimitiveFieldType = PrimitiveFieldType> extends PrimitiveFieldBase<TName, TType> {
130
+ /** Single-character shorthand (e.g. `"v"` for `--verbose` → `-v`). */
131
+ readonly alias?: string | undefined;
132
+ readonly flag?: never;
133
+ }
134
+ interface SchemaOptionField<TName extends string = string, TSchema extends StandardSchemaV1 = StandardSchemaV1> extends SchemaFieldBase<TName, TSchema> {
135
+ /** Single-character shorthand (e.g. `"v"` for `--verbose` → `-v`). */
136
+ readonly alias?: string | undefined;
137
+ /**
138
+ * When `true`, the option is parsed as a boolean flag (no value expected).
139
+ * The schema receives `true` when the flag is present, `undefined` when absent.
140
+ */
141
+ readonly flag?: true | undefined;
10
142
  }
143
+ type CommandArgField = PrimitiveArgField | SchemaArgField;
144
+ type CommandOptionField = PrimitiveOptionField | SchemaOptionField;
145
+ type FieldName<TField> = TField extends {
146
+ readonly name: infer TName extends string;
147
+ } ? TName : never;
148
+ type InferSchemaOutput<TSchema> = TSchema extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<TSchema> : never;
149
+ type InferSchemaInput<TSchema> = TSchema extends StandardSchemaV1 ? StandardSchemaV1.InferInput<TSchema> : never;
150
+ type IsOptionalSchemaOutput<TValue> = undefined extends TValue ? true : false;
151
+ type FieldValue<TField> = TField extends {
152
+ readonly schema: infer TSchema;
153
+ } ? Exclude<InferSchemaOutput<TSchema>, undefined> : TField extends {
154
+ readonly type: infer TType extends PrimitiveFieldType;
155
+ } ? PrimitiveFieldValue<TType> : never;
156
+ type FieldInputValue<TField> = TField extends {
157
+ readonly schema: infer TSchema;
158
+ } ? InferSchemaInput<TSchema> : TField extends {
159
+ readonly type: infer TType extends PrimitiveFieldType;
160
+ } ? PrimitiveFieldValue<TType> : never;
161
+ type HasDefaultValue<TField> = TField extends {
162
+ readonly default: infer TDefault;
163
+ } ? [TDefault] extends [undefined] ? false : true : false;
164
+ type IsRequiredField<TField, TBooleanAlwaysPresent extends boolean = false> = TField extends {
165
+ readonly schema: infer TSchema;
166
+ } ? IsOptionalSchemaOutput<InferSchemaOutput<TSchema>> extends true ? false : true : HasDefaultValue<TField> extends true ? true : TBooleanAlwaysPresent extends true ? TField extends {
167
+ readonly type: "boolean";
168
+ } ? true : TField extends {
169
+ readonly required: true;
170
+ } ? true : false : TField extends {
171
+ readonly required: true;
172
+ } ? true : false;
173
+ type Simplify<TValue> = { [TKey in keyof TValue]: TValue[TKey] };
174
+ 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> }>;
175
+ type InferExecutionFields<TFields extends readonly NamedField[]> = Simplify<{ [TField in TFields[number] as FieldName<TField>]?: FieldInputValue<TField> }>;
176
+ /** Runtime data passed into a command's `run` function. */
177
+ interface CommandContext<TOptions, TArgs> {
178
+ /** Parsed and validated positional argument values keyed by field name. */
179
+ readonly args: TArgs;
180
+ /** Parsed and validated option values keyed by field name. */
181
+ readonly options: TOptions;
182
+ /** Working directory the CLI was invoked from. */
183
+ readonly cwd: string;
184
+ /**
185
+ * Unparsed argv tokens passed to this command, before Rune splits them
186
+ * into `args` and `options`. Useful for forwarding to child processes.
187
+ */
188
+ readonly rawArgs: readonly string[];
189
+ }
190
+ /** The command definition object accepted by {@link defineCommand}. */
191
+ interface DefinedCommand<TArgsFields extends readonly CommandArgField[] = readonly [], TOptionsFields extends readonly CommandOptionField[] = readonly []> {
192
+ readonly description?: string | undefined;
193
+ readonly args: TArgsFields;
194
+ readonly options: TOptionsFields;
195
+ readonly run: (ctx: CommandContext<InferNamedFields<TOptionsFields, true>, InferNamedFields<TArgsFields>>) => void | Promise<void>;
196
+ } //#endregion
197
+ //#region src/define-command.d.ts
11
198
  /**
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.
199
+ * Defines a CLI command with a description, positional arguments, options,
200
+ * and a function to execute when the command is invoked.
20
201
  *
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.
202
+ * The command module's default export should be the return value of this function.
24
203
  *
25
204
  * @example
26
205
  * ```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 }],
206
+ * export default defineCommand({
207
+ * description: "Greet someone",
208
+ * args: [
209
+ * { name: "name", type: "string", required: true },
210
+ * ],
211
+ * options: [
212
+ * { name: "loud", type: "boolean", alias: "l" },
213
+ * ],
33
214
  * run(ctx) {
34
- * console.log(`Hello, ${ctx.options.name}!`);
215
+ * const greeting = `Hello, ${ctx.args.name}!`;
216
+ * console.log(ctx.options.loud ? greeting.toUpperCase() : greeting);
35
217
  * },
36
218
  * });
219
+ * ```
37
220
  *
38
- * test("hello command", async () => {
39
- * const result = await runCommand(hello, {
40
- * options: { name: "Rune" },
41
- * });
221
+ * Required positional arguments must precede optional ones. This ordering is
222
+ * enforced at the type level for concrete schema types and at runtime for
223
+ * primitive fields:
42
224
  *
43
- * expect(result.exitCode).toBe(0);
44
- * expect(result.stdout).toBe("Hello, Rune!\n");
225
+ * ```ts
226
+ * // Type error — required arg after optional arg
227
+ * defineCommand({
228
+ * args: [
229
+ * { name: "source", type: "string" },
230
+ * { name: "target", type: "string", required: true },
231
+ * ],
232
+ * run() {},
233
+ * });
234
+ *
235
+ * // Type error — required primitive arg after optional schema arg
236
+ * defineCommand({
237
+ * args: [
238
+ * { name: "mode", schema: z.string().optional() },
239
+ * { name: "target", type: "string", required: true },
240
+ * ],
241
+ * run() {},
45
242
  * });
46
243
  * ```
244
+ *
245
+ * When a schema type is widened to plain `StandardSchemaV1` (e.g. stored in
246
+ * a variable without a concrete type), optionality information is lost and
247
+ * the ordering check is skipped for that field.
47
248
  */
48
- declare function runCommand<TArgsFields extends readonly CommandArgField[], TOptionsFields extends readonly CommandOptionField[]>(command: DefinedCommand<TArgsFields, TOptionsFields>, options?: RunCommandOptions<InferExecutionFields<TOptionsFields>, InferExecutionFields<TArgsFields>>): Promise<CommandExecutionResult>;
49
249
  //#endregion
50
- export { CommandExecutionResult, RunCommandOptions, runCommand };
250
+ //#region src/execute-command.d.ts
251
+ interface ExecuteCommandInput<TOptions, TArgs> {
252
+ readonly options?: TOptions | undefined;
253
+ readonly args?: TArgs | undefined;
254
+ readonly cwd?: string | undefined;
255
+ readonly rawArgs?: readonly string[] | undefined;
256
+ } //#endregion
257
+ //#region src/run-command.d.ts
258
+ type RunCommandOptions<TOptions, TArgs> = ExecuteCommandInput<TOptions, TArgs>;
259
+ interface CommandExecutionResult {
260
+ readonly exitCode: number;
261
+ readonly stdout: string;
262
+ readonly stderr: string;
263
+ readonly errorMessage?: string | undefined;
264
+ }
265
+ declare function runCommand<TArgsFields extends readonly CommandArgField[], TOptionsFields extends readonly CommandOptionField[]>(command: DefinedCommand<TArgsFields, TOptionsFields>, options?: RunCommandOptions<InferExecutionFields<TOptionsFields>, InferExecutionFields<TArgsFields>>): Promise<CommandExecutionResult>; //#endregion
266
+ //#endregion
267
+ export { type CommandExecutionResult, type RunCommandOptions, runCommand };
package/dist/test.mjs CHANGED
@@ -1,42 +1,88 @@
1
- import { r as executeCommand, t as captureProcessOutput } from "./dist-uz53Uv1e.mjs";
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
- */
1
+ import { format } from "node:util";
2
+ //#region ../test-utils/dist/index.mjs
3
+ async function captureProcessOutput(action) {
4
+ const stdoutChunks = [];
5
+ const stderrChunks = [];
6
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
7
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
8
+ const originalConsoleMethods = {
9
+ log: console.log,
10
+ info: console.info,
11
+ debug: console.debug,
12
+ warn: console.warn,
13
+ error: console.error
14
+ };
15
+ const captureChunk = (chunks, chunk, encoding) => {
16
+ if (typeof chunk === "string") {
17
+ chunks.push(chunk);
18
+ return;
19
+ }
20
+ chunks.push(Buffer.from(chunk).toString(encoding));
21
+ };
22
+ const captureConsole = (chunks, args) => {
23
+ chunks.push(`${format(...args)}\n`);
24
+ };
25
+ const createWriteCapture = (chunks) => ((chunk, encoding, cb) => {
26
+ captureChunk(chunks, chunk, typeof encoding === "string" ? encoding : void 0);
27
+ if (typeof encoding === "function") encoding(null);
28
+ else cb?.(null);
29
+ return true;
30
+ });
31
+ process.stdout.write = createWriteCapture(stdoutChunks);
32
+ process.stderr.write = createWriteCapture(stderrChunks);
33
+ for (const method of [
34
+ "log",
35
+ "info",
36
+ "debug"
37
+ ]) console[method] = (...args) => captureConsole(stdoutChunks, args);
38
+ for (const method of ["warn", "error"]) console[method] = (...args) => captureConsole(stderrChunks, args);
39
+ try {
40
+ return {
41
+ ok: true,
42
+ value: await action(),
43
+ stdout: stdoutChunks.join(""),
44
+ stderr: stderrChunks.join("")
45
+ };
46
+ } catch (error) {
47
+ return {
48
+ ok: false,
49
+ error,
50
+ stdout: stdoutChunks.join(""),
51
+ stderr: stderrChunks.join("")
52
+ };
53
+ } finally {
54
+ process.stdout.write = originalStdoutWrite;
55
+ process.stderr.write = originalStderrWrite;
56
+ Object.assign(console, originalConsoleMethods);
57
+ }
58
+ }
59
+ function isSchemaField(field) {
60
+ return "schema" in field && field.schema !== void 0;
61
+ }
62
+ function formatExecutionError(error) {
63
+ if (error instanceof Error) return error.message === "" ? "" : error.message || error.name || "Unknown error";
64
+ if (typeof error === "string") return error;
65
+ return "Unknown error";
66
+ }
67
+ async function executeCommand(command, input = {}) {
68
+ try {
69
+ const options = { ...input.options };
70
+ for (const field of command.options) if (options[field.name] === void 0 && !isSchemaField(field) && field.type === "boolean") options[field.name] = false;
71
+ await command.run({
72
+ options,
73
+ args: input.args ?? {},
74
+ cwd: input.cwd ?? process.cwd(),
75
+ rawArgs: input.rawArgs ?? []
76
+ });
77
+ return { exitCode: 0 };
78
+ } catch (error) {
79
+ const message = formatExecutionError(error);
80
+ return message ? {
81
+ exitCode: 1,
82
+ errorMessage: message
83
+ } : { exitCode: 1 };
84
+ }
85
+ }
40
86
  async function runCommand(command, options = {}) {
41
87
  const captured = await captureProcessOutput(() => executeCommand(command, options));
42
88
  if (!captured.ok) throw captured.error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rune-cli/rune",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
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": {
@@ -46,7 +46,8 @@
46
46
  "@typescript/native-preview": "7.0.0-dev.20260322.1",
47
47
  "typescript": "5.9.3",
48
48
  "vite-plus": "v0.1.13",
49
- "@rune-cli/core": "0.0.0"
49
+ "@rune-cli/core": "0.0.0",
50
+ "@rune-cli/test-utils": "0.0.0"
50
51
  },
51
52
  "peerDependencies": {
52
53
  "typescript": ">=5.0.0"