@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 +173 -88
- package/dist/{dist-CSbOseWZ.mjs → dist-Bpf2xVvb.mjs} +34 -16
- package/dist/{index-CHUchkja.d.mts → index-C179V2IJ.d.mts} +20 -6
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{run-manifest-command-CSsdj02B.mjs → run-manifest-command-DepwxrFI.mjs} +64 -33
- package/dist/runtime.d.mts +4 -3
- package/dist/runtime.mjs +2 -2
- package/dist/test.d.mts +5 -55
- package/dist/test.mjs +6 -3
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,83 +1,111 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "./dist-
|
|
3
|
-
import { n as isHelpFlag, r as isVersionFlag, t as runManifestCommand } from "./run-manifest-command-
|
|
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
|
+
var version = "0.0.11";
|
|
11
11
|
//#endregion
|
|
12
|
-
//#region src/manifest/generate-
|
|
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
|
|
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
|
|
50
|
-
if (ts.isIdentifier(expression)) return
|
|
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
|
-
|
|
57
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
95
|
+
knownMetadata.set(declaration.name.text, extractMetadataFromCommandDefinition(declaration.initializer, sourceFile, knownMetadata));
|
|
69
96
|
}
|
|
70
97
|
continue;
|
|
71
98
|
}
|
|
72
|
-
if (ts.isExportAssignment(statement)) return
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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,
|
|
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],
|
|
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
|
-
|
|
170
|
+
aliases: metadata?.aliases ?? [],
|
|
171
|
+
description: metadata?.description
|
|
116
172
|
};
|
|
117
173
|
}));
|
|
118
|
-
const
|
|
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: [...
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
|
161
|
-
const walkResult = await walkCommandsDirectory(options.commandsDirectory, [],
|
|
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:
|
|
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
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
27
|
+
function validateOptionShortNames(options) {
|
|
27
28
|
const seen = /* @__PURE__ */ new Set();
|
|
28
29
|
for (const field of options) {
|
|
29
|
-
if (field.
|
|
30
|
-
if (!
|
|
31
|
-
if (seen.has(field.
|
|
32
|
-
seen.add(field.
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
367
|
+
for (const field of options) config[field.name] = field.short ? {
|
|
350
368
|
type: getOptionParseType(field),
|
|
351
|
-
short: field.
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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",
|
|
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-
|
|
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-
|
|
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
|
|
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/
|
|
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/
|
|
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,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
distance: damerauLevenshteinDistance(unknownSegment,
|
|
69
|
-
|
|
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
|
|
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
|
|
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.
|
|
130
|
-
return `-${field.
|
|
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:
|
|
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 =
|
|
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
|
|
245
|
-
if (!
|
|
246
|
-
process.stderr.write(ensureTrailingNewline(
|
|
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:
|
|
251
|
-
args:
|
|
281
|
+
options: commandInput.value.options,
|
|
282
|
+
args: commandInput.value.args,
|
|
252
283
|
cwd: options.cwd,
|
|
253
|
-
rawArgs:
|
|
284
|
+
rawArgs: commandInput.value.rawArgs
|
|
254
285
|
});
|
|
255
286
|
if (result.errorMessage) process.stderr.write(ensureTrailingNewline(result.errorMessage));
|
|
256
287
|
return result.exitCode;
|
package/dist/runtime.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as DefinedCommand, r as CommandOptionField, t as CommandArgField } from "./index-
|
|
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-
|
|
2
|
-
import { t as runManifestCommand } from "./run-manifest-command-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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