@rune-cli/rune 0.0.7 → 0.0.9

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,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import "./dist-FWLEc-op.mjs";
3
- import { n as isHelpFlag, r as isVersionFlag, t as runManifestCommand } from "./run-manifest-command-BmeVwPA5.mjs";
2
+ import "./dist-DuisScgY.mjs";
3
+ import { n as isHelpFlag, r as isVersionFlag, t as runManifestCommand } from "./run-manifest-command-BphalAwU.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.7";
10
+ var version = "0.0.9";
11
11
  //#endregion
12
12
  //#region src/manifest/generate-manifest.ts
13
13
  const COMMAND_ENTRY_FILE = "index.ts";
@@ -94,7 +94,9 @@ async function walkCommandsDirectory(absoluteDirectoryPath, pathSegments, extrac
94
94
  }
95
95
  async function generateCommandManifest(options) {
96
96
  const extractDescription = options.extractDescription ?? extractDescriptionFromSourceFile;
97
- return { nodes: [...(await walkCommandsDirectory(options.commandsDirectory, [], extractDescription)).nodes].sort((left, right) => comparePathSegments(left.pathSegments, right.pathSegments)) };
97
+ const walkResult = await walkCommandsDirectory(options.commandsDirectory, [], extractDescription);
98
+ if (walkResult.nodes.length === 0) throw new Error("No commands found in src/commands/. Create a command file like src/commands/hello/index.ts");
99
+ return { nodes: [...walkResult.nodes].sort((left, right) => comparePathSegments(left.pathSegments, right.pathSegments)) };
98
100
  }
99
101
  function serializeCommandManifest(manifest) {
100
102
  return JSON.stringify(manifest, null, 2);
@@ -4,6 +4,34 @@ function isSchemaField(field) {
4
4
  return "schema" in field && field.schema !== void 0;
5
5
  }
6
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,7 +97,18 @@ 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);
100
+ if (typeof input.run !== "function") throw new Error("defineCommand() requires a \"run\" function.");
101
+ if (input.args) {
102
+ validateFieldShape(input.args, "argument");
103
+ validateUniqueFieldNames(input.args, "argument");
104
+ validateArgOrdering(input.args);
105
+ }
106
+ if (input.options) {
107
+ validateFieldShape(input.options, "option");
108
+ validateUniqueFieldNames(input.options, "option");
109
+ validateOptionNames(input.options);
110
+ validateOptionAliases(input.options);
111
+ }
73
112
  const command = {
74
113
  description: input.description,
75
114
  args: input.args ?? [],
@@ -92,8 +131,10 @@ function formatExecutionError(error) {
92
131
  }
93
132
  async function executeCommand(command, input = {}) {
94
133
  try {
134
+ const options = { ...input.options };
135
+ for (const field of command.options) if (options[field.name] === void 0 && !isSchemaField(field) && field.type === "boolean") options[field.name] = false;
95
136
  await command.run({
96
- options: input.options ?? {},
137
+ options,
97
138
  args: input.args ?? {},
98
139
  cwd: input.cwd ?? process.cwd(),
99
140
  rawArgs: input.rawArgs ?? []
@@ -172,60 +213,48 @@ function formatOptionLabel(field) {
172
213
  function formatArgumentLabel(field) {
173
214
  return field.name;
174
215
  }
175
- function missingRequiredOption(field) {
216
+ function createMissingOptionError(field) {
176
217
  return {
177
218
  ok: false,
178
219
  error: { message: `Missing required option:\n\n ${formatOptionLabel(field)}` }
179
220
  };
180
221
  }
181
- function missingRequiredArgument(field) {
222
+ function createMissingArgumentError(field) {
182
223
  return {
183
224
  ok: false,
184
225
  error: { message: `Missing required argument:\n\n ${formatArgumentLabel(field)}` }
185
226
  };
186
227
  }
187
- function invalidOptionValue(field, messages) {
228
+ function createInvalidOptionError(field, messages) {
188
229
  return {
189
230
  ok: false,
190
231
  error: { message: `Invalid value for option ${formatOptionLabel(field)}:\n\n ${messages.join("\n ")}` }
191
232
  };
192
233
  }
193
- function invalidArgumentValue(field, messages) {
234
+ function createInvalidArgumentError(field, messages) {
194
235
  return {
195
236
  ok: false,
196
237
  error: { message: `Invalid value for argument ${formatArgumentLabel(field)}:\n\n ${messages.join("\n ")}` }
197
238
  };
198
239
  }
199
- function unknownOption(token) {
240
+ function createUnknownOptionError(token) {
200
241
  return {
201
242
  ok: false,
202
243
  error: { message: `Unknown option "${token}"` }
203
244
  };
204
245
  }
205
- function duplicateOption(field) {
246
+ function createDuplicateOptionError(field) {
206
247
  return {
207
248
  ok: false,
208
249
  error: { message: `Duplicate option "${formatOptionLabel(field)}" is not supported` }
209
250
  };
210
251
  }
211
- function unexpectedArgument(token) {
252
+ function createUnexpectedArgumentError(token) {
212
253
  return {
213
254
  ok: false,
214
255
  error: { message: `Unexpected argument "${token}"` }
215
256
  };
216
257
  }
217
- function normalizeParseArgsError(error) {
218
- if (!(error instanceof Error)) return {
219
- ok: false,
220
- error: { message: "Argument parsing failed" }
221
- };
222
- const unknownMatch = error.message.match(/Unknown option '([^']+)'/);
223
- if (unknownMatch) return unknownOption(unknownMatch[1]);
224
- return {
225
- ok: false,
226
- error: { message: error.message }
227
- };
228
- }
229
258
  function parsePrimitiveValue(field, rawValue) {
230
259
  if (isSchemaField(field)) throw new Error("Schema fields must be handled separately");
231
260
  switch (field.type) {
@@ -312,9 +341,9 @@ async function resolveMissingField(field, missingRequired) {
312
341
  };
313
342
  }
314
343
  async function parseArgumentField(field, rawValue) {
315
- if (rawValue === void 0) return resolveMissingField(field, () => missingRequiredArgument(field));
344
+ if (rawValue === void 0) return resolveMissingField(field, () => createMissingArgumentError(field));
316
345
  const result = await parseProvidedField(field, rawValue);
317
- if (!result.ok) return invalidArgumentValue(field, result.error.message.split("\n"));
346
+ if (!result.ok) return createInvalidArgumentError(field, result.error.message.split("\n"));
318
347
  return {
319
348
  ok: true,
320
349
  present: true,
@@ -323,13 +352,25 @@ async function parseArgumentField(field, rawValue) {
323
352
  }
324
353
  async function parseOptionField(field, rawValue) {
325
354
  const result = await parseProvidedField(field, rawValue);
326
- if (!result.ok) return invalidOptionValue(field, result.error.message.split("\n"));
355
+ if (!result.ok) return createInvalidOptionError(field, result.error.message.split("\n"));
327
356
  return {
328
357
  ok: true,
329
358
  present: true,
330
359
  value: result.value
331
360
  };
332
361
  }
362
+ function normalizeParseArgsError(error) {
363
+ if (!(error instanceof Error)) return {
364
+ ok: false,
365
+ error: { message: "Argument parsing failed" }
366
+ };
367
+ const unknownMatch = error.message.match(/Unknown option '([^']+)'/);
368
+ if (unknownMatch) return createUnknownOptionError(unknownMatch[1]);
369
+ return {
370
+ ok: false,
371
+ error: { message: error.message }
372
+ };
373
+ }
333
374
  function getOptionParseType(field) {
334
375
  if (isSchemaField(field)) return field.flag ? "boolean" : "string";
335
376
  return field.type === "boolean" ? "boolean" : "string";
@@ -350,7 +391,7 @@ function detectDuplicateOption(options, tokens) {
350
391
  counts.set(token.name, nextCount);
351
392
  if (nextCount > 1) {
352
393
  const field = options.find((option) => option.name === token.name);
353
- if (field) return duplicateOption(field);
394
+ if (field) return createDuplicateOptionError(field);
354
395
  }
355
396
  }
356
397
  }
@@ -377,7 +418,7 @@ async function parseCommand(command, rawArgs) {
377
418
  if (!result.ok) return result;
378
419
  if (result.present) parsedArgs[field.name] = result.value;
379
420
  }
380
- if (parsed.positionals.length > command.args.length) return unexpectedArgument(parsed.positionals[command.args.length]);
421
+ if (parsed.positionals.length > command.args.length) return createUnexpectedArgumentError(parsed.positionals[command.args.length]);
381
422
  for (const field of command.options) {
382
423
  const rawValue = parsed.values[field.name];
383
424
  if (rawValue !== void 0) {
@@ -386,9 +427,10 @@ async function parseCommand(command, rawArgs) {
386
427
  if (result.present) parsedOptions[field.name] = result.value;
387
428
  continue;
388
429
  }
389
- const result = await resolveMissingField(field, () => missingRequiredOption(field));
430
+ const result = await resolveMissingField(field, () => createMissingOptionError(field));
390
431
  if (!result.ok) return result;
391
432
  if (result.present) parsedOptions[field.name] = result.value;
433
+ else if (!isSchemaField(field) && field.type === "boolean") parsedOptions[field.name] = false;
392
434
  }
393
435
  return {
394
436
  ok: true,
@@ -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
  /**
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-B6XsJ6ON.mjs";
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
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 { n as defineCommand } from "./dist-FWLEc-op.mjs";
1
+ import { n as defineCommand } from "./dist-DuisScgY.mjs";
2
2
  export { defineCommand };
@@ -1,4 +1,4 @@
1
- import { a as isSchemaField, i as isDefinedCommand, o as parseCommand, r as executeCommand } from "./dist-FWLEc-op.mjs";
1
+ import { a as isSchemaField, i as isDefinedCommand, o as parseCommand, r as executeCommand } from "./dist-DuisScgY.mjs";
2
2
  import { pathToFileURL } from "node:url";
3
3
  //#region src/cli/flags.ts
4
4
  function isHelpFlag(token) {
@@ -1,4 +1,4 @@
1
- import { i as DefinedCommand, r as CommandOptionField, t as CommandArgField } from "./index-B6XsJ6ON.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[];
package/dist/runtime.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import "./dist-FWLEc-op.mjs";
2
- import { t as runManifestCommand } from "./run-manifest-command-BmeVwPA5.mjs";
1
+ import "./dist-DuisScgY.mjs";
2
+ import { t as runManifestCommand } from "./run-manifest-command-BphalAwU.mjs";
3
3
  export { runManifestCommand };
package/dist/test.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ExecuteCommandInput, i as DefinedCommand, o as InferExecutionFields, r as CommandOptionField, t as CommandArgField } from "./index-B6XsJ6ON.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>;
package/dist/test.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { r as executeCommand, t as captureProcessOutput } from "./dist-FWLEc-op.mjs";
1
+ import { r as executeCommand, t as captureProcessOutput } from "./dist-DuisScgY.mjs";
2
2
  //#region src/test.ts
3
3
  /**
4
4
  * Runs a command definition directly in-process for testing.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rune-cli/rune",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
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": {