@rune-cli/rune 0.0.10 → 0.0.11

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,83 +1,111 @@
1
1
  #!/usr/bin/env node
2
- import "./dist-CSbOseWZ.mjs";
3
- import { n as isHelpFlag, r as isVersionFlag, t as runManifestCommand } from "./run-manifest-command-CSsdj02B.mjs";
2
+ import "./dist-Bpf2xVvb.mjs";
3
+ import { n as isHelpFlag, r as isVersionFlag, t as runManifestCommand } from "./run-manifest-command-DepwxrFI.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.10";
10
+ var version = "0.0.11";
11
11
  //#endregion
12
- //#region src/manifest/generate-manifest.ts
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
- ];
21
- function comparePathSegments(left, right) {
22
- const length = Math.min(left.length, right.length);
23
- for (let index = 0; index < length; index += 1) {
24
- const comparison = left[index].localeCompare(right[index]);
25
- if (comparison !== 0) return comparison;
26
- }
27
- return left.length - right.length;
28
- }
12
+ //#region src/manifest/generate/extract-description.ts
29
13
  function getPropertyNameText(name) {
30
14
  if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
31
15
  }
32
- function getStaticDescriptionValue(expression) {
16
+ function getStaticStringValue(expression) {
33
17
  if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
34
18
  }
19
+ function getStaticStringArrayValue(expression) {
20
+ if (!ts.isArrayLiteralExpression(expression)) return;
21
+ const values = [];
22
+ for (const element of expression.elements) {
23
+ const value = getStaticStringValue(element);
24
+ if (value === void 0) return;
25
+ values.push(value);
26
+ }
27
+ return values;
28
+ }
29
+ function resolveExpression(expression, sourceFile) {
30
+ if (ts.isIdentifier(expression)) return findVariableInitializer(sourceFile, expression.text) ?? expression;
31
+ return expression;
32
+ }
35
33
  function isDefineCommandExpression(expression) {
36
34
  return isNamedCallExpression(expression, "defineCommand");
37
35
  }
38
36
  function isDefineGroupExpression(expression) {
39
37
  return isNamedCallExpression(expression, "defineGroup");
40
38
  }
41
- function isNamedCallExpression(expression, name) {
42
- if (ts.isIdentifier(expression)) return expression.text === name;
43
- if (ts.isPropertyAccessExpression(expression)) return expression.name.text === name;
44
- return false;
45
- }
46
39
  function isDefineCallExpression(expression) {
47
40
  return ts.isCallExpression(expression) && (isDefineCommandExpression(expression.expression) || isDefineGroupExpression(expression.expression));
48
41
  }
49
- function extractDescriptionFromCommandDefinition(expression, knownDescriptions) {
50
- if (ts.isIdentifier(expression)) return knownDescriptions.get(expression.text);
42
+ function extractMetadataFromCommandDefinition(expression, sourceFile, knownMetadata) {
43
+ if (ts.isIdentifier(expression)) return knownMetadata.get(expression.text);
51
44
  if (!ts.isCallExpression(expression) || !isDefineCallExpression(expression)) return;
52
45
  const [definition] = expression.arguments;
53
46
  if (!definition || !ts.isObjectLiteralExpression(definition)) return;
47
+ let description;
48
+ let aliases = [];
54
49
  for (const property of definition.properties) {
50
+ if (ts.isShorthandPropertyAssignment(property)) {
51
+ const propertyName = property.name.text;
52
+ const resolved = resolveExpression(property.name, sourceFile);
53
+ if (propertyName === "description") description = getStaticStringValue(resolved);
54
+ else if (propertyName === "aliases") {
55
+ const extracted = getStaticStringArrayValue(resolved);
56
+ if (extracted === void 0) throw new Error("Could not statically analyze aliases. Aliases must be an inline array of string literals (e.g. aliases: [\"d\"]).");
57
+ aliases = extracted;
58
+ }
59
+ continue;
60
+ }
55
61
  if (!ts.isPropertyAssignment(property)) continue;
56
- if (getPropertyNameText(property.name) !== "description") continue;
57
- return getStaticDescriptionValue(property.initializer);
62
+ const propertyName = getPropertyNameText(property.name);
63
+ const resolved = resolveExpression(property.initializer, sourceFile);
64
+ if (propertyName === "description") description = getStaticStringValue(resolved);
65
+ else if (propertyName === "aliases") {
66
+ const extracted = getStaticStringArrayValue(resolved);
67
+ if (extracted === void 0) throw new Error("Could not statically analyze aliases. Aliases must be an inline array of string literals (e.g. aliases: [\"d\"]).");
68
+ aliases = extracted;
69
+ }
58
70
  }
71
+ return {
72
+ description,
73
+ aliases
74
+ };
75
+ }
76
+ function findVariableInitializer(sourceFile, name) {
77
+ for (const statement of sourceFile.statements) {
78
+ if (!ts.isVariableStatement(statement)) continue;
79
+ for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name) && declaration.name.text === name && declaration.initializer) return declaration.initializer;
80
+ }
81
+ }
82
+ function isNamedCallExpression(expression, name) {
83
+ if (ts.isIdentifier(expression)) return expression.text === name;
84
+ if (ts.isPropertyAccessExpression(expression)) return expression.name.text === name;
85
+ return false;
59
86
  }
60
- async function extractDescriptionFromSourceFile(sourceFilePath) {
87
+ async function extractMetadataFromSourceFile(sourceFilePath) {
61
88
  const sourceText = await readFile(sourceFilePath, "utf8");
62
89
  const sourceFile = ts.createSourceFile(sourceFilePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
63
- const knownDescriptions = /* @__PURE__ */ new Map();
90
+ const knownMetadata = /* @__PURE__ */ new Map();
64
91
  for (const statement of sourceFile.statements) {
65
92
  if (ts.isVariableStatement(statement)) {
66
93
  for (const declaration of statement.declarationList.declarations) {
67
94
  if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
68
- knownDescriptions.set(declaration.name.text, extractDescriptionFromCommandDefinition(declaration.initializer, knownDescriptions));
95
+ knownMetadata.set(declaration.name.text, extractMetadataFromCommandDefinition(declaration.initializer, sourceFile, knownMetadata));
69
96
  }
70
97
  continue;
71
98
  }
72
- if (ts.isExportAssignment(statement)) return extractDescriptionFromCommandDefinition(statement.expression, knownDescriptions);
99
+ if (ts.isExportAssignment(statement)) return extractMetadataFromCommandDefinition(statement.expression, sourceFile, knownMetadata);
73
100
  }
74
101
  }
102
+ //#endregion
103
+ //#region src/manifest/generate/validate-group-meta.ts
75
104
  async function validateGroupMetaFile(sourceFilePath) {
76
105
  const sourceText = await readFile(sourceFilePath, "utf8");
77
106
  const sourceFile = ts.createSourceFile(sourceFilePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
78
107
  for (const statement of sourceFile.statements) {
79
- if (ts.isVariableStatement(statement)) continue;
80
- if (ts.isImportDeclaration(statement)) continue;
108
+ if (ts.isVariableStatement(statement) || ts.isImportDeclaration(statement)) continue;
81
109
  if (ts.isExportAssignment(statement)) {
82
110
  const expression = ts.isIdentifier(statement.expression) ? findVariableInitializer(sourceFile, statement.expression.text) : statement.expression;
83
111
  if (expression && ts.isCallExpression(expression) && isDefineGroupExpression(expression.expression)) return;
@@ -86,19 +114,45 @@ async function validateGroupMetaFile(sourceFilePath) {
86
114
  }
87
115
  throw new Error(`${sourceFilePath}: _group.ts must have a default export using defineGroup().`);
88
116
  }
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;
117
+ //#endregion
118
+ //#region src/manifest/generate/walk-commands-directory.ts
119
+ const COMMAND_ENTRY_FILE = "index.ts";
120
+ const GROUP_META_FILE = "_group.ts";
121
+ const BARE_COMMAND_EXTENSION = ".ts";
122
+ const DECLARATION_FILE_SUFFIXES = [
123
+ ".d.ts",
124
+ ".d.mts",
125
+ ".d.cts"
126
+ ];
127
+ function comparePathSegments(left, right) {
128
+ const length = Math.min(left.length, right.length);
129
+ for (let index = 0; index < length; index += 1) {
130
+ const comparison = left[index].localeCompare(right[index]);
131
+ if (comparison !== 0) return comparison;
132
+ }
133
+ return left.length - right.length;
134
+ }
135
+ function validateSiblingAliases(siblings) {
136
+ const seen = /* @__PURE__ */ new Map();
137
+ for (const sibling of siblings) {
138
+ const existing = seen.get(sibling.name);
139
+ if (existing !== void 0) throw new Error(`Command name conflict: "${sibling.name}" is already used by "${existing}".`);
140
+ seen.set(sibling.name, sibling.name);
141
+ for (const alias of sibling.aliases) {
142
+ if (alias === sibling.name) throw new Error(`Command alias "${alias}" for "${sibling.name}" is the same as its canonical name.`);
143
+ const conflicting = seen.get(alias);
144
+ if (conflicting !== void 0) throw new Error(`Command alias conflict: alias "${alias}" for "${sibling.name}" conflicts with "${conflicting}".`);
145
+ seen.set(alias, sibling.name);
146
+ }
93
147
  }
94
148
  }
95
- async function walkCommandsDirectory(absoluteDirectoryPath, pathSegments, extractDescription) {
149
+ async function walkCommandsDirectory(absoluteDirectoryPath, pathSegments, extractMetadata) {
96
150
  const entries = await readdir(absoluteDirectoryPath, { withFileTypes: true });
97
151
  const childDirectoryNames = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
98
152
  const childResults = await Promise.all(childDirectoryNames.map(async (directoryName) => {
99
153
  return {
100
154
  directoryName,
101
- result: await walkCommandsDirectory(path.join(absoluteDirectoryPath, directoryName), [...pathSegments, directoryName], extractDescription)
155
+ result: await walkCommandsDirectory(path.join(absoluteDirectoryPath, directoryName), [...pathSegments, directoryName], extractMetadata)
102
156
  };
103
157
  }));
104
158
  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));
@@ -107,58 +161,86 @@ async function walkCommandsDirectory(absoluteDirectoryPath, pathSegments, extrac
107
161
  const commandName = fileName.slice(0, -3);
108
162
  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
163
  const sourceFilePath = path.join(absoluteDirectoryPath, fileName);
164
+ const metadata = await extractMetadata(sourceFilePath);
110
165
  return {
111
166
  pathSegments: [...pathSegments, commandName],
112
167
  kind: "command",
113
168
  sourceFilePath,
114
169
  childNames: [],
115
- description: await extractDescription(sourceFilePath)
170
+ aliases: metadata?.aliases ?? [],
171
+ description: metadata?.description
116
172
  };
117
173
  }));
118
- const childNodes = childResults.flatMap(({ result }) => result.nodes);
174
+ const descendantNodes = childResults.flatMap(({ result }) => result.nodes);
119
175
  const childNames = [...childResults.filter(({ result }) => result.hasNode).map(({ directoryName }) => directoryName), ...bareCommandFiles.map((fileName) => fileName.slice(0, -3))].sort((left, right) => left.localeCompare(right));
120
176
  const hasCommandEntry = entries.some((entry) => entry.isFile() && entry.name === COMMAND_ENTRY_FILE);
121
177
  const hasGroupMeta = entries.some((entry) => entry.isFile() && entry.name === GROUP_META_FILE);
122
178
  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
179
  if (hasGroupMeta && childNames.length === 0) throw new Error(`${path.join(absoluteDirectoryPath, GROUP_META_FILE)}: _group.ts exists but the directory has no subcommands.`);
124
180
  if (!hasCommandEntry && !hasGroupMeta && childNames.length === 0) return {
125
- nodes: [...childNodes, ...bareCommandNodes],
181
+ nodes: [...descendantNodes, ...bareCommandNodes],
126
182
  hasNode: false
127
183
  };
128
184
  let node;
129
185
  if (hasCommandEntry) {
130
186
  const sourceFilePath = path.join(absoluteDirectoryPath, COMMAND_ENTRY_FILE);
187
+ const metadata = await extractMetadata(sourceFilePath);
188
+ const aliases = metadata?.aliases ?? [];
189
+ if (aliases.length > 0 && pathSegments.length === 0) throw new Error("Aliases on the root command are not supported. The root command has no parent to resolve aliases against.");
131
190
  node = {
132
191
  pathSegments,
133
192
  kind: "command",
134
193
  sourceFilePath,
135
194
  childNames,
136
- description: await extractDescription(sourceFilePath)
195
+ aliases,
196
+ description: metadata?.description
137
197
  };
138
198
  } else {
139
199
  const groupMetaPath = path.join(absoluteDirectoryPath, GROUP_META_FILE);
140
200
  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.`);
201
+ const metadata = hasGroupMeta ? await extractMetadata(groupMetaPath) : void 0;
202
+ if (hasGroupMeta && !metadata?.description) throw new Error(`${groupMetaPath}: _group.ts must export a defineGroup() call with a non-empty "description" string.`);
203
+ const aliases = metadata?.aliases ?? [];
204
+ if (aliases.length > 0 && pathSegments.length === 0) throw new Error("Aliases on the root group are not supported. The root group has no parent to resolve aliases against.");
143
205
  node = {
144
206
  pathSegments,
145
207
  kind: "group",
146
208
  childNames,
147
- ...description !== void 0 ? { description } : {}
209
+ aliases,
210
+ ...metadata?.description !== void 0 ? { description: metadata.description } : {}
148
211
  };
149
212
  }
213
+ const siblingEntries = [];
214
+ for (const childResult of childResults) {
215
+ if (!childResult.result.hasNode) continue;
216
+ const childNode = childResult.result.nodes.find((n) => n.pathSegments.length === pathSegments.length + 1 && n.pathSegments[pathSegments.length] === childResult.directoryName);
217
+ siblingEntries.push({
218
+ name: childResult.directoryName,
219
+ aliases: childNode?.aliases ?? []
220
+ });
221
+ }
222
+ for (const bareNode of bareCommandNodes) {
223
+ const name = bareNode.pathSegments[bareNode.pathSegments.length - 1];
224
+ siblingEntries.push({
225
+ name,
226
+ aliases: bareNode.aliases
227
+ });
228
+ }
229
+ validateSiblingAliases(siblingEntries);
150
230
  return {
151
231
  nodes: [
152
232
  node,
153
- ...childNodes,
233
+ ...descendantNodes,
154
234
  ...bareCommandNodes
155
235
  ],
156
236
  hasNode: true
157
237
  };
158
238
  }
239
+ //#endregion
240
+ //#region src/manifest/generate/generate-manifest.ts
159
241
  async function generateCommandManifest(options) {
160
- const extractDescription = options.extractDescription ?? extractDescriptionFromSourceFile;
161
- const walkResult = await walkCommandsDirectory(options.commandsDirectory, [], extractDescription);
242
+ const extractMetadata = options.extractMetadata ?? extractMetadataFromSourceFile;
243
+ const walkResult = await walkCommandsDirectory(options.commandsDirectory, [], extractMetadata);
162
244
  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
245
  return { nodes: [...walkResult.nodes].sort((left, right) => comparePathSegments(left.pathSegments, right.pathSegments)) };
164
246
  }
@@ -183,19 +265,20 @@ function resolveCommandsDirectory(projectRoot) {
183
265
  function resolveDistDirectory(projectRoot) {
184
266
  return path.join(projectRoot, DIST_DIRECTORY_NAME);
185
267
  }
268
+ function resolveCliNameFromPackageJson(packageJson) {
269
+ if (packageJson.bin && typeof packageJson.bin === "object") {
270
+ const binNames = Object.keys(packageJson.bin).sort((left, right) => left.localeCompare(right));
271
+ if (binNames.length > 0) return binNames[0];
272
+ }
273
+ if (packageJson.name && packageJson.name.length > 0) return packageJson.name.split("/").at(-1) ?? packageJson.name;
274
+ }
186
275
  async function readProjectCliInfo(projectRoot) {
187
276
  const packageJsonPath = path.join(projectRoot, "package.json");
188
277
  try {
189
278
  const packageJsonContents = await readFile(packageJsonPath, "utf8");
190
279
  const packageJson = JSON.parse(packageJsonContents);
191
- let name;
192
- if (packageJson.bin && typeof packageJson.bin === "object") {
193
- const binNames = Object.keys(packageJson.bin).sort((left, right) => left.localeCompare(right));
194
- if (binNames.length > 0) name = binNames[0];
195
- }
196
- if (!name && packageJson.name && packageJson.name.length > 0) name = packageJson.name.split("/").at(-1) ?? packageJson.name;
197
280
  return {
198
- name: name ?? path.basename(projectRoot),
281
+ name: resolveCliNameFromPackageJson(packageJson) ?? path.basename(projectRoot),
199
282
  version: packageJson.version
200
283
  };
201
284
  } catch (error) {
@@ -542,6 +625,19 @@ function tryParseProjectOption(argv, index) {
542
625
  function getRuneVersion() {
543
626
  return version;
544
627
  }
628
+ function renderRuneCliHelp() {
629
+ return `\
630
+ Usage: rune <command>
631
+
632
+ Commands:
633
+ build Build a Rune project into a distributable CLI
634
+ dev Run a Rune project in development mode
635
+
636
+ Options:
637
+ -h, --help Show this help message
638
+ -V, --version Show the version number
639
+ `;
640
+ }
545
641
  function parseDevArgs(argv) {
546
642
  const commandArgs = [];
547
643
  let projectPath;
@@ -610,18 +706,22 @@ function parseBuildArgs(argv) {
610
706
  projectPath
611
707
  };
612
708
  }
613
- function renderRuneCliHelp() {
614
- return `\
615
- Usage: rune <command>
616
-
617
- Commands:
618
- build Build a Rune project into a distributable CLI
619
- dev Run a Rune project in development mode
620
-
621
- Options:
622
- -h, --help Show this help message
623
- -V, --version Show the version number
624
- `;
709
+ async function runDevSubcommand(options, restArgs) {
710
+ const parsedDevArgs = parseDevArgs(restArgs);
711
+ if (!parsedDevArgs.ok) return writeEarlyExit(parsedDevArgs);
712
+ return runDevCommand({
713
+ rawArgs: parsedDevArgs.commandArgs,
714
+ projectPath: parsedDevArgs.projectPath,
715
+ cwd: options.cwd
716
+ });
717
+ }
718
+ async function runBuildSubcommand(options, restArgs) {
719
+ const parsedBuildArgs = parseBuildArgs(restArgs);
720
+ if (!parsedBuildArgs.ok) return writeEarlyExit(parsedBuildArgs);
721
+ return runBuildCommand({
722
+ projectPath: parsedBuildArgs.projectPath,
723
+ cwd: options.cwd
724
+ });
625
725
  }
626
726
  async function runRuneCli(options) {
627
727
  const [subcommand, ...restArgs] = options.argv;
@@ -633,23 +733,8 @@ async function runRuneCli(options) {
633
733
  await writeStdout(`rune v${getRuneVersion()}\n`);
634
734
  return 0;
635
735
  }
636
- if (subcommand === "dev") {
637
- const parsedDevArgs = parseDevArgs(restArgs);
638
- if (!parsedDevArgs.ok) return writeEarlyExit(parsedDevArgs);
639
- return runDevCommand({
640
- rawArgs: parsedDevArgs.commandArgs,
641
- projectPath: parsedDevArgs.projectPath,
642
- cwd: options.cwd
643
- });
644
- }
645
- if (subcommand === "build") {
646
- const parsedBuildArgs = parseBuildArgs(restArgs);
647
- if (!parsedBuildArgs.ok) return writeEarlyExit(parsedBuildArgs);
648
- return runBuildCommand({
649
- projectPath: parsedBuildArgs.projectPath,
650
- cwd: options.cwd
651
- });
652
- }
736
+ if (subcommand === "dev") return runDevSubcommand(options, restArgs);
737
+ if (subcommand === "build") return runBuildSubcommand(options, restArgs);
653
738
  await writeStderrLine(`Unknown command: ${subcommand}. Available commands: build, dev`);
654
739
  return 1;
655
740
  }
@@ -5,7 +5,8 @@ function isSchemaField(field) {
5
5
  }
6
6
  const DEFINED_COMMAND_BRAND = Symbol.for("@rune-cli/defined-command");
7
7
  const OPTION_NAME_RE = /^[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*$/;
8
- const ALIAS_RE = /^[a-zA-Z]$/;
8
+ const OPTION_SHORT_RE = /^[a-zA-Z]$/;
9
+ const COMMAND_ALIAS_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
9
10
  function validateFieldShape(fields, kind) {
10
11
  for (const field of fields) {
11
12
  const raw = field;
@@ -23,13 +24,21 @@ function validateUniqueFieldNames(fields, kind) {
23
24
  function validateOptionNames(options) {
24
25
  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
  }
26
- function validateOptionAliases(options) {
27
+ function validateOptionShortNames(options) {
27
28
  const seen = /* @__PURE__ */ new Set();
28
29
  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);
30
+ if (field.short === void 0) continue;
31
+ if (!OPTION_SHORT_RE.test(field.short)) throw new Error(`Invalid short name "${field.short}" for option "${field.name}". Short name must be a single letter.`);
32
+ if (seen.has(field.short)) throw new Error(`Duplicate short name "${field.short}" for option "${field.name}".`);
33
+ seen.add(field.short);
34
+ }
35
+ }
36
+ function validateCommandAliases(aliases) {
37
+ const seen = /* @__PURE__ */ new Set();
38
+ for (const alias of aliases) {
39
+ if (!COMMAND_ALIAS_RE.test(alias)) throw new Error(`Invalid command alias "${alias}". Aliases must be lowercase kebab-case (letters, digits, and internal hyphens).`);
40
+ if (seen.has(alias)) throw new Error(`Duplicate command alias "${alias}".`);
41
+ seen.add(alias);
33
42
  }
34
43
  }
35
44
  function isOptionalArg(field) {
@@ -59,7 +68,7 @@ function validateArgOrdering(args) {
59
68
  * { name: "name", type: "string", required: true },
60
69
  * ],
61
70
  * options: [
62
- * { name: "loud", type: "boolean", alias: "l" },
71
+ * { name: "loud", type: "boolean", short: "l" },
63
72
  * ],
64
73
  * run(ctx) {
65
74
  * const greeting = `Hello, ${ctx.args.name}!`;
@@ -98,6 +107,7 @@ function validateArgOrdering(args) {
98
107
  */
99
108
  function defineCommand(input) {
100
109
  if (typeof input.run !== "function") throw new Error("defineCommand() requires a \"run\" function.");
110
+ if (input.aliases) validateCommandAliases(input.aliases);
101
111
  if (input.args) {
102
112
  validateFieldShape(input.args, "argument");
103
113
  validateUniqueFieldNames(input.args, "argument");
@@ -107,10 +117,11 @@ function defineCommand(input) {
107
117
  validateFieldShape(input.options, "option");
108
118
  validateUniqueFieldNames(input.options, "option");
109
119
  validateOptionNames(input.options);
110
- validateOptionAliases(input.options);
120
+ validateOptionShortNames(input.options);
111
121
  }
112
122
  const command = {
113
123
  description: input.description,
124
+ aliases: input.aliases ?? [],
114
125
  args: input.args ?? [],
115
126
  options: input.options ?? [],
116
127
  run: input.run
@@ -142,7 +153,11 @@ const DEFINED_GROUP_BRAND = Symbol.for("@rune-cli/defined-group");
142
153
  */
143
154
  function defineGroup(input) {
144
155
  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 };
156
+ if (input.aliases) validateCommandAliases(input.aliases);
157
+ const group = {
158
+ description: input.description,
159
+ aliases: input.aliases ?? []
160
+ };
146
161
  Object.defineProperty(group, DEFINED_GROUP_BRAND, {
147
162
  value: true,
148
163
  enumerable: false
@@ -154,12 +169,15 @@ function formatExecutionError(error) {
154
169
  if (typeof error === "string") return error;
155
170
  return "Unknown error";
156
171
  }
172
+ function createExecutionOptions(command, input) {
173
+ const options = { ...input.options };
174
+ for (const field of command.options) if (options[field.name] === void 0 && !isSchemaField(field) && field.type === "boolean") options[field.name] = false;
175
+ return options;
176
+ }
157
177
  async function executeCommand(command, input = {}) {
158
178
  try {
159
- const options = { ...input.options };
160
- for (const field of command.options) if (options[field.name] === void 0 && !isSchemaField(field) && field.type === "boolean") options[field.name] = false;
161
179
  await command.run({
162
- options,
180
+ options: createExecutionOptions(command, input),
163
181
  args: input.args ?? {},
164
182
  cwd: input.cwd ?? process.cwd(),
165
183
  rawArgs: input.rawArgs ?? []
@@ -346,9 +364,9 @@ function getOptionParseType(field) {
346
364
  }
347
365
  function buildParseArgsOptions(options) {
348
366
  const config = {};
349
- for (const field of options) config[field.name] = field.alias ? {
367
+ for (const field of options) config[field.name] = field.short ? {
350
368
  type: getOptionParseType(field),
351
- short: field.alias
369
+ short: field.short
352
370
  } : { type: getOptionParseType(field) };
353
371
  return config;
354
372
  }
@@ -364,7 +382,7 @@ function detectDuplicateOption(options, tokens) {
364
382
  }
365
383
  }
366
384
  }
367
- async function parseCommand(command, rawArgs) {
385
+ async function parseCommandArgs(command, rawArgs) {
368
386
  let parsed;
369
387
  try {
370
388
  parsed = parseArgs({
@@ -411,4 +429,4 @@ async function parseCommand(command, rawArgs) {
411
429
  };
412
430
  }
413
431
  //#endregion
414
- export { isSchemaField as a, isDefinedCommand as i, defineGroup as n, parseCommand as o, executeCommand as r, defineCommand as t };
432
+ export { isSchemaField as a, isDefinedCommand as i, defineGroup as n, parseCommandArgs as o, executeCommand as r, defineCommand as t };
@@ -117,21 +117,21 @@ interface SchemaFieldBase<TName extends string, TSchema extends StandardSchemaV1
117
117
  readonly default?: never;
118
118
  }
119
119
  interface PrimitiveArgField<TName extends string = string, TType extends PrimitiveFieldType = PrimitiveFieldType> extends PrimitiveFieldBase<TName, TType> {
120
- readonly alias?: never;
120
+ readonly short?: never;
121
121
  readonly flag?: never;
122
122
  }
123
123
  interface SchemaArgField<TName extends string = string, TSchema extends StandardSchemaV1 = StandardSchemaV1> extends SchemaFieldBase<TName, TSchema> {
124
- readonly alias?: never;
124
+ readonly short?: never;
125
125
  readonly flag?: never;
126
126
  }
127
127
  interface PrimitiveOptionField<TName extends string = string, TType extends PrimitiveFieldType = PrimitiveFieldType> extends PrimitiveFieldBase<TName, TType> {
128
128
  /** Single-character shorthand (e.g. `"v"` for `--verbose` → `-v`). */
129
- readonly alias?: string | undefined;
129
+ readonly short?: string | undefined;
130
130
  readonly flag?: never;
131
131
  }
132
132
  interface SchemaOptionField<TName extends string = string, TSchema extends StandardSchemaV1 = StandardSchemaV1> extends SchemaFieldBase<TName, TSchema> {
133
133
  /** Single-character shorthand (e.g. `"v"` for `--verbose` → `-v`). */
134
- readonly alias?: string | undefined;
134
+ readonly short?: string | undefined;
135
135
  /**
136
136
  * When `true`, the option is parsed as a boolean flag (no value expected).
137
137
  * The schema receives `true` when the flag is present, `undefined` when absent.
@@ -201,6 +201,12 @@ interface CommandContext<TOptions, TArgs> {
201
201
  interface DefineCommandInput<TArgsFields extends readonly CommandArgField[] | undefined = undefined, TOptionsFields extends readonly CommandOptionField[] | undefined = undefined> {
202
202
  /** One-line summary shown in `--help` output. */
203
203
  readonly description?: string | undefined;
204
+ /**
205
+ * Alternative names for this command. Each alias is an additional path
206
+ * segment that routes to this command. Aliases must follow kebab-case
207
+ * rules (lowercase letters, digits, and internal hyphens).
208
+ */
209
+ readonly aliases?: readonly string[] | undefined;
204
210
  /**
205
211
  * Positional arguments declared in the order they appear on the command line.
206
212
  * Required arguments must come before optional ones.
@@ -211,7 +217,7 @@ interface DefineCommandInput<TArgsFields extends readonly CommandArgField[] | un
211
217
  */
212
218
  readonly args?: TArgsFields;
213
219
  /**
214
- * Options declared as `--name` flags, with optional single-character aliases.
220
+ * Options declared as `--name` flags, with optional single-character short forms.
215
221
  * Option names must be unique within the command, start with a letter, and
216
222
  * contain only letters, numbers, and internal hyphens.
217
223
  *
@@ -227,6 +233,7 @@ interface DefineCommandInput<TArgsFields extends readonly CommandArgField[] | un
227
233
  }
228
234
  interface DefinedCommand<TArgsFields extends readonly CommandArgField[] = readonly [], TOptionsFields extends readonly CommandOptionField[] = readonly []> {
229
235
  readonly description?: string | undefined;
236
+ readonly aliases: readonly string[];
230
237
  readonly args: TArgsFields;
231
238
  readonly options: TOptionsFields;
232
239
  readonly run: (ctx: CommandContext<InferNamedFields<TOptionsFields, true>, InferNamedFields<TArgsFields>>) => void | Promise<void>;
@@ -246,7 +253,7 @@ interface DefinedCommand<TArgsFields extends readonly CommandArgField[] = readon
246
253
  * { name: "name", type: "string", required: true },
247
254
  * ],
248
255
  * options: [
249
- * { name: "loud", type: "boolean", alias: "l" },
256
+ * { name: "loud", type: "boolean", short: "l" },
250
257
  * ],
251
258
  * run(ctx) {
252
259
  * const greeting = `Hello, ${ctx.args.name}!`;
@@ -290,10 +297,17 @@ declare function defineCommand<const TArgsFields extends readonly CommandArgFiel
290
297
  interface DefineGroupInput {
291
298
  /** One-line summary shown in `--help` output. */
292
299
  readonly description: string;
300
+ /**
301
+ * Alternative names for this command group. Each alias is an additional path
302
+ * segment that routes to this group. Aliases must follow kebab-case rules
303
+ * (lowercase letters, digits, and internal hyphens).
304
+ */
305
+ readonly aliases?: readonly string[] | undefined;
293
306
  }
294
307
  /** The normalized group object returned by `defineGroup`. */
295
308
  interface DefinedGroup {
296
309
  readonly description: string;
310
+ readonly aliases: readonly string[];
297
311
  }
298
312
  /**
299
313
  * Defines metadata for a command group (a directory that only groups
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
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";
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-C179V2IJ.mjs";
2
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 defineGroup, t as defineCommand } from "./dist-CSbOseWZ.mjs";
1
+ import { n as defineGroup, t as defineCommand } from "./dist-Bpf2xVvb.mjs";
2
2
  export { defineCommand, defineGroup };
@@ -1,4 +1,4 @@
1
- import { a as isSchemaField, i as isDefinedCommand, o as parseCommand, r as executeCommand } from "./dist-CSbOseWZ.mjs";
1
+ import { a as isSchemaField, i as isDefinedCommand, o as parseCommandArgs, r as executeCommand } from "./dist-Bpf2xVvb.mjs";
2
2
  import { pathToFileURL } from "node:url";
3
3
  //#region src/cli/flags.ts
4
4
  function isHelpFlag(token) {
@@ -8,7 +8,7 @@ function isVersionFlag(token) {
8
8
  return token === "--version" || token === "-V";
9
9
  }
10
10
  //#endregion
11
- //#region src/manifest/command-loader.ts
11
+ //#region src/manifest/runtime/command-loader.ts
12
12
  async function loadCommandFromModule(sourceFilePath) {
13
13
  const loadedModule = await import(pathToFileURL(sourceFilePath).href);
14
14
  if (loadedModule.default === void 0) throw new Error(`Command module did not export a default command: ${sourceFilePath}`);
@@ -29,7 +29,23 @@ function describeCommandModuleExport(value) {
29
29
  }
30
30
  const defaultLoadCommand = (node) => loadCommandFromModule(node.sourceFilePath);
31
31
  //#endregion
32
- //#region src/manifest/damerau-levenshtein.ts
32
+ //#region src/manifest/manifest-map.ts
33
+ function commandManifestPathToKey(pathSegments) {
34
+ return pathSegments.join(" ");
35
+ }
36
+ function createCommandManifestNodeMap(manifest) {
37
+ const entries = [];
38
+ for (const node of manifest.nodes) {
39
+ entries.push([commandManifestPathToKey(node.pathSegments), node]);
40
+ if (node.aliases.length > 0 && node.pathSegments.length > 0) {
41
+ const parentSegments = node.pathSegments.slice(0, -1);
42
+ for (const alias of node.aliases) entries.push([commandManifestPathToKey([...parentSegments, alias]), node]);
43
+ }
44
+ }
45
+ return Object.fromEntries(entries);
46
+ }
47
+ //#endregion
48
+ //#region src/manifest/runtime/damerau-levenshtein.ts
33
49
  function damerauLevenshteinDistance(left, right) {
34
50
  const rows = left.length + 1;
35
51
  const cols = right.length + 1;
@@ -44,15 +60,7 @@ function damerauLevenshteinDistance(left, right) {
44
60
  return matrix[left.length][right.length];
45
61
  }
46
62
  //#endregion
47
- //#region src/manifest/manifest-map.ts
48
- function commandManifestPathToKey(pathSegments) {
49
- return pathSegments.join(" ");
50
- }
51
- function createCommandManifestNodeMap(manifest) {
52
- return Object.fromEntries(manifest.nodes.map((node) => [commandManifestPathToKey(node.pathSegments), node]));
53
- }
54
- //#endregion
55
- //#region src/manifest/resolve-command-path.ts
63
+ //#region src/manifest/runtime/resolve-command-route.ts
56
64
  function isOptionLikeToken(token) {
57
65
  return token === "--" || token.startsWith("-");
58
66
  }
@@ -62,13 +70,35 @@ function getHelpRequested(args) {
62
70
  function getSuggestionThreshold(candidate) {
63
71
  return Math.max(2, Math.floor(candidate.length / 3));
64
72
  }
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);
73
+ function getSuggestedChildNames(unknownSegment, candidates) {
74
+ const scored = candidates.map((candidate) => ({
75
+ canonicalName: candidate.canonicalName,
76
+ distance: damerauLevenshteinDistance(unknownSegment, candidate.matchName),
77
+ threshold: getSuggestionThreshold(candidate.matchName)
78
+ })).filter(({ distance, threshold }) => distance <= threshold);
79
+ const bestByCanonical = /* @__PURE__ */ new Map();
80
+ for (const entry of scored) {
81
+ const existing = bestByCanonical.get(entry.canonicalName);
82
+ if (existing === void 0 || entry.distance < existing) bestByCanonical.set(entry.canonicalName, entry.distance);
83
+ }
84
+ return [...bestByCanonical.entries()].sort(([nameA, distA], [nameB, distB]) => distA - distB || nameA.localeCompare(nameB)).slice(0, 3).map(([name]) => name);
85
+ }
86
+ function collectSiblingCandidates(currentNode, nodeMap) {
87
+ const candidates = [];
88
+ for (const childName of currentNode.childNames) {
89
+ candidates.push({
90
+ canonicalName: childName,
91
+ matchName: childName
92
+ });
93
+ const childNode = nodeMap[commandManifestPathToKey([...currentNode.pathSegments, childName])];
94
+ if (childNode) for (const alias of childNode.aliases) candidates.push({
95
+ canonicalName: childName,
96
+ matchName: alias
97
+ });
98
+ }
99
+ return candidates;
70
100
  }
71
- function resolveCommandPath(manifest, rawArgs) {
101
+ function resolveCommandRoute(manifest, rawArgs) {
72
102
  const nodeMap = createCommandManifestNodeMap(manifest);
73
103
  const rootNode = nodeMap[""];
74
104
  if (rootNode === void 0) throw new Error("Manifest root node is missing");
@@ -79,7 +109,7 @@ function resolveCommandPath(manifest, rawArgs) {
79
109
  if (isOptionLikeToken(token)) break;
80
110
  const childNode = nodeMap[commandManifestPathToKey([...currentNode.pathSegments, token])];
81
111
  if (childNode === void 0) {
82
- const suggestions = getSuggestedChildNames(token, currentNode.childNames);
112
+ const suggestions = getSuggestedChildNames(token, collectSiblingCandidates(currentNode, nodeMap));
83
113
  if (currentNode.kind === "group" || suggestions.length > 0) return {
84
114
  kind: "unknown",
85
115
  attemptedPath: [...currentNode.pathSegments, token],
@@ -111,7 +141,7 @@ function resolveCommandPath(manifest, rawArgs) {
111
141
  };
112
142
  }
113
143
  //#endregion
114
- //#region src/manifest/render-help.ts
144
+ //#region src/manifest/runtime/render-help.ts
115
145
  function formatCommandName(cliName, pathSegments) {
116
146
  return pathSegments.length === 0 ? cliName : `${cliName} ${pathSegments.join(" ")}`;
117
147
  }
@@ -126,8 +156,8 @@ function formatArgumentLabel(field) {
126
156
  }
127
157
  function formatOptionLabel(field) {
128
158
  const longOptionLabel = `--${field.name}${formatTypeHint(field)}`;
129
- if (!field.alias) return longOptionLabel;
130
- return `-${field.alias}, ${longOptionLabel}`;
159
+ if (!field.short) return longOptionLabel;
160
+ return `-${field.short}, ${longOptionLabel}`;
131
161
  }
132
162
  async function isFieldRequired(field) {
133
163
  if (!isSchemaField(field)) return field.required === true && field.default === void 0;
@@ -148,9 +178,10 @@ function renderGroupHelp(options) {
148
178
  const { manifest, node, cliName, version } = options;
149
179
  const nodeMap = createCommandManifestNodeMap(manifest);
150
180
  const entries = node.childNames.map((childName) => {
181
+ const childNode = nodeMap[commandManifestPathToKey([...node.pathSegments, childName])];
151
182
  return {
152
- label: childName,
153
- description: nodeMap[commandManifestPathToKey([...node.pathSegments, childName])]?.description
183
+ label: `${childName}${childNode && childNode.aliases.length > 0 ? ` (${childNode.aliases.join(", ")})` : ""}`,
184
+ description: childNode?.description
154
185
  };
155
186
  });
156
187
  const commandName = formatCommandName(cliName, node.pathSegments);
@@ -197,7 +228,7 @@ function renderUnknownCommandMessage(route, cliName) {
197
228
  return `${parts.join("\n\n")}\n`;
198
229
  }
199
230
  //#endregion
200
- //#region src/manifest/resolve-help.ts
231
+ //#region src/manifest/runtime/resolve-help.ts
201
232
  async function renderResolvedHelp(options) {
202
233
  if (options.route.kind === "unknown") return renderUnknownCommandMessage(options.route, options.cliName);
203
234
  if (options.route.kind === "group") return renderGroupHelp({
@@ -209,7 +240,7 @@ async function renderResolvedHelp(options) {
209
240
  return renderCommandHelp(await (options.loadCommand ?? defaultLoadCommand)(options.route.node), options.route.matchedPath, options.cliName);
210
241
  }
211
242
  //#endregion
212
- //#region src/manifest/run-manifest-command.ts
243
+ //#region src/manifest/runtime/run-manifest-command.ts
213
244
  function ensureTrailingNewline(text) {
214
245
  return text.endsWith("\n") ? text : `${text}\n`;
215
246
  }
@@ -224,7 +255,7 @@ async function runManifestCommand(options) {
224
255
  process.stdout.write(`${options.cliName} v${options.version}\n`);
225
256
  return 0;
226
257
  }
227
- const route = resolveCommandPath(options.manifest, options.rawArgs);
258
+ const route = resolveCommandRoute(options.manifest, options.rawArgs);
228
259
  if (route.kind === "unknown" || route.kind === "group" || route.helpRequested) {
229
260
  const output = await renderResolvedHelp({
230
261
  manifest: options.manifest,
@@ -241,16 +272,16 @@ async function runManifestCommand(options) {
241
272
  return 0;
242
273
  }
243
274
  const command = await (options.loadCommand ?? defaultLoadCommand)(route.node);
244
- const parsed = await parseCommand(command, route.remainingArgs);
245
- if (!parsed.ok) {
246
- process.stderr.write(ensureTrailingNewline(parsed.error.message));
275
+ const commandInput = await parseCommandArgs(command, route.remainingArgs);
276
+ if (!commandInput.ok) {
277
+ process.stderr.write(ensureTrailingNewline(commandInput.error.message));
247
278
  return 1;
248
279
  }
249
280
  const result = await executeCommand(command, {
250
- options: parsed.value.options,
251
- args: parsed.value.args,
281
+ options: commandInput.value.options,
282
+ args: commandInput.value.args,
252
283
  cwd: options.cwd,
253
- rawArgs: parsed.value.rawArgs
284
+ rawArgs: commandInput.value.rawArgs
254
285
  });
255
286
  if (result.errorMessage) process.stderr.write(ensureTrailingNewline(result.errorMessage));
256
287
  return result.exitCode;
@@ -1,4 +1,4 @@
1
- import { a as DefinedCommand, r as CommandOptionField, t as CommandArgField } from "./index-CHUchkja.mjs";
1
+ import { a as DefinedCommand, r as CommandOptionField, t as CommandArgField } from "./index-C179V2IJ.mjs";
2
2
 
3
3
  //#region src/manifest/manifest-types.d.ts
4
4
  type CommandManifestPath = readonly string[];
@@ -7,6 +7,7 @@ interface CommandManifestNodeBase {
7
7
  readonly pathSegments: CommandManifestPath;
8
8
  readonly kind: CommandManifestNodeKind;
9
9
  readonly childNames: readonly string[];
10
+ readonly aliases: readonly string[];
10
11
  readonly description?: string | undefined;
11
12
  }
12
13
  interface CommandManifestCommandNode extends CommandManifestNodeBase {
@@ -22,10 +23,10 @@ interface CommandManifest {
22
23
  readonly nodes: readonly CommandManifestNode[];
23
24
  }
24
25
  //#endregion
25
- //#region src/manifest/command-loader.d.ts
26
+ //#region src/manifest/runtime/command-loader.d.ts
26
27
  type LoadCommandFn = (node: CommandManifestCommandNode) => Promise<DefinedCommand<readonly CommandArgField[], readonly CommandOptionField[]>>;
27
28
  //#endregion
28
- //#region src/manifest/run-manifest-command.d.ts
29
+ //#region src/manifest/runtime/run-manifest-command.d.ts
29
30
  interface RunManifestCommandOptions {
30
31
  readonly manifest: CommandManifest;
31
32
  readonly rawArgs: readonly string[];
package/dist/runtime.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import "./dist-CSbOseWZ.mjs";
2
- import { t as runManifestCommand } from "./run-manifest-command-CSsdj02B.mjs";
1
+ import "./dist-Bpf2xVvb.mjs";
2
+ import { t as runManifestCommand } from "./run-manifest-command-DepwxrFI.mjs";
3
3
  export { runManifestCommand };
package/dist/test.d.mts CHANGED
@@ -119,21 +119,21 @@ interface SchemaFieldBase<TName extends string, TSchema extends StandardSchemaV1
119
119
  readonly default?: never;
120
120
  }
121
121
  interface PrimitiveArgField<TName extends string = string, TType extends PrimitiveFieldType = PrimitiveFieldType> extends PrimitiveFieldBase<TName, TType> {
122
- readonly alias?: never;
122
+ readonly short?: never;
123
123
  readonly flag?: never;
124
124
  }
125
125
  interface SchemaArgField<TName extends string = string, TSchema extends StandardSchemaV1 = StandardSchemaV1> extends SchemaFieldBase<TName, TSchema> {
126
- readonly alias?: never;
126
+ readonly short?: never;
127
127
  readonly flag?: never;
128
128
  }
129
129
  interface PrimitiveOptionField<TName extends string = string, TType extends PrimitiveFieldType = PrimitiveFieldType> extends PrimitiveFieldBase<TName, TType> {
130
130
  /** Single-character shorthand (e.g. `"v"` for `--verbose` → `-v`). */
131
- readonly alias?: string | undefined;
131
+ readonly short?: string | undefined;
132
132
  readonly flag?: never;
133
133
  }
134
134
  interface SchemaOptionField<TName extends string = string, TSchema extends StandardSchemaV1 = StandardSchemaV1> extends SchemaFieldBase<TName, TSchema> {
135
135
  /** Single-character shorthand (e.g. `"v"` for `--verbose` → `-v`). */
136
- readonly alias?: string | undefined;
136
+ readonly short?: string | undefined;
137
137
  /**
138
138
  * When `true`, the option is parsed as a boolean flag (no value expected).
139
139
  * The schema receives `true` when the flag is present, `undefined` when absent.
@@ -190,62 +190,12 @@ interface CommandContext<TOptions, TArgs> {
190
190
  /** The command definition object accepted by {@link defineCommand}. */
191
191
  interface DefinedCommand<TArgsFields extends readonly CommandArgField[] = readonly [], TOptionsFields extends readonly CommandOptionField[] = readonly []> {
192
192
  readonly description?: string | undefined;
193
+ readonly aliases: readonly string[];
193
194
  readonly args: TArgsFields;
194
195
  readonly options: TOptionsFields;
195
196
  readonly run: (ctx: CommandContext<InferNamedFields<TOptionsFields, true>, InferNamedFields<TArgsFields>>) => void | Promise<void>;
196
197
  } //#endregion
197
198
  //#region src/define-command.d.ts
198
- /**
199
- * Defines a CLI command with a description, positional arguments, options,
200
- * and a function to execute when the command is invoked.
201
- *
202
- * The command module's default export should be the return value of this function.
203
- *
204
- * @example
205
- * ```ts
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
- * ],
214
- * run(ctx) {
215
- * const greeting = `Hello, ${ctx.args.name}!`;
216
- * console.log(ctx.options.loud ? greeting.toUpperCase() : greeting);
217
- * },
218
- * });
219
- * ```
220
- *
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:
224
- *
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() {},
242
- * });
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.
248
- */
249
199
  //#endregion
250
200
  //#region src/execute-command.d.ts
251
201
  interface ExecuteCommandInput<TOptions, TArgs> {
package/dist/test.mjs CHANGED
@@ -64,12 +64,15 @@ function formatExecutionError(error) {
64
64
  if (typeof error === "string") return error;
65
65
  return "Unknown error";
66
66
  }
67
+ function createExecutionOptions(command, input) {
68
+ const options = { ...input.options };
69
+ for (const field of command.options) if (options[field.name] === void 0 && !isSchemaField(field) && field.type === "boolean") options[field.name] = false;
70
+ return options;
71
+ }
67
72
  async function executeCommand(command, input = {}) {
68
73
  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
74
  await command.run({
72
- options,
75
+ options: createExecutionOptions(command, input),
73
76
  args: input.args ?? {},
74
77
  cwd: input.cwd ?? process.cwd(),
75
78
  rawArgs: input.rawArgs ?? []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rune-cli/rune",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
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": {