@rune-cli/rune 0.0.9 → 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 +227 -78
- package/dist/{dist-DuisScgY.mjs → dist-Bpf2xVvb.mjs} +59 -72
- package/dist/{index-BWxfSwrT.d.mts → index-C179V2IJ.d.mts} +48 -7
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{run-manifest-command-BphalAwU.mjs → run-manifest-command-DepwxrFI.mjs} +68 -34
- package/dist/runtime.d.mts +4 -3
- package/dist/runtime.mjs +2 -2
- package/dist/test.d.mts +209 -42
- package/dist/test.mjs +88 -39
- package/package.json +3 -2
package/dist/cli.mjs
CHANGED
|
@@ -1,101 +1,247 @@
|
|
|
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
|
-
function comparePathSegments(left, right) {
|
|
15
|
-
const length = Math.min(left.length, right.length);
|
|
16
|
-
for (let index = 0; index < length; index += 1) {
|
|
17
|
-
const comparison = left[index].localeCompare(right[index]);
|
|
18
|
-
if (comparison !== 0) return comparison;
|
|
19
|
-
}
|
|
20
|
-
return left.length - right.length;
|
|
21
|
-
}
|
|
12
|
+
//#region src/manifest/generate/extract-description.ts
|
|
22
13
|
function getPropertyNameText(name) {
|
|
23
14
|
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
|
|
24
15
|
}
|
|
25
|
-
function
|
|
16
|
+
function getStaticStringValue(expression) {
|
|
26
17
|
if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
|
|
27
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
|
+
}
|
|
28
33
|
function isDefineCommandExpression(expression) {
|
|
29
|
-
|
|
30
|
-
if (ts.isPropertyAccessExpression(expression)) return expression.name.text === "defineCommand";
|
|
31
|
-
return false;
|
|
34
|
+
return isNamedCallExpression(expression, "defineCommand");
|
|
32
35
|
}
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
function isDefineGroupExpression(expression) {
|
|
37
|
+
return isNamedCallExpression(expression, "defineGroup");
|
|
38
|
+
}
|
|
39
|
+
function isDefineCallExpression(expression) {
|
|
40
|
+
return ts.isCallExpression(expression) && (isDefineCommandExpression(expression.expression) || isDefineGroupExpression(expression.expression));
|
|
41
|
+
}
|
|
42
|
+
function extractMetadataFromCommandDefinition(expression, sourceFile, knownMetadata) {
|
|
43
|
+
if (ts.isIdentifier(expression)) return knownMetadata.get(expression.text);
|
|
44
|
+
if (!ts.isCallExpression(expression) || !isDefineCallExpression(expression)) return;
|
|
36
45
|
const [definition] = expression.arguments;
|
|
37
46
|
if (!definition || !ts.isObjectLiteralExpression(definition)) return;
|
|
47
|
+
let description;
|
|
48
|
+
let aliases = [];
|
|
38
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
|
+
}
|
|
39
61
|
if (!ts.isPropertyAssignment(property)) continue;
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
}
|
|
42
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;
|
|
43
86
|
}
|
|
44
|
-
async function
|
|
87
|
+
async function extractMetadataFromSourceFile(sourceFilePath) {
|
|
45
88
|
const sourceText = await readFile(sourceFilePath, "utf8");
|
|
46
89
|
const sourceFile = ts.createSourceFile(sourceFilePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
47
|
-
const
|
|
90
|
+
const knownMetadata = /* @__PURE__ */ new Map();
|
|
48
91
|
for (const statement of sourceFile.statements) {
|
|
49
92
|
if (ts.isVariableStatement(statement)) {
|
|
50
93
|
for (const declaration of statement.declarationList.declarations) {
|
|
51
94
|
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
|
|
52
|
-
|
|
95
|
+
knownMetadata.set(declaration.name.text, extractMetadataFromCommandDefinition(declaration.initializer, sourceFile, knownMetadata));
|
|
53
96
|
}
|
|
54
97
|
continue;
|
|
55
98
|
}
|
|
56
|
-
if (ts.isExportAssignment(statement)) return
|
|
99
|
+
if (ts.isExportAssignment(statement)) return extractMetadataFromCommandDefinition(statement.expression, sourceFile, knownMetadata);
|
|
57
100
|
}
|
|
58
101
|
}
|
|
59
|
-
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/manifest/generate/validate-group-meta.ts
|
|
104
|
+
async function validateGroupMetaFile(sourceFilePath) {
|
|
105
|
+
const sourceText = await readFile(sourceFilePath, "utf8");
|
|
106
|
+
const sourceFile = ts.createSourceFile(sourceFilePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
107
|
+
for (const statement of sourceFile.statements) {
|
|
108
|
+
if (ts.isVariableStatement(statement) || ts.isImportDeclaration(statement)) continue;
|
|
109
|
+
if (ts.isExportAssignment(statement)) {
|
|
110
|
+
const expression = ts.isIdentifier(statement.expression) ? findVariableInitializer(sourceFile, statement.expression.text) : statement.expression;
|
|
111
|
+
if (expression && ts.isCallExpression(expression) && isDefineGroupExpression(expression.expression)) return;
|
|
112
|
+
throw new Error(`${sourceFilePath}: _group.ts must use "export default defineGroup(...)". Found a default export that is not a defineGroup() call.`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
throw new Error(`${sourceFilePath}: _group.ts must have a default export using defineGroup().`);
|
|
116
|
+
}
|
|
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
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function walkCommandsDirectory(absoluteDirectoryPath, pathSegments, extractMetadata) {
|
|
60
150
|
const entries = await readdir(absoluteDirectoryPath, { withFileTypes: true });
|
|
61
151
|
const childDirectoryNames = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
|
|
62
152
|
const childResults = await Promise.all(childDirectoryNames.map(async (directoryName) => {
|
|
63
153
|
return {
|
|
64
154
|
directoryName,
|
|
65
|
-
result: await walkCommandsDirectory(path.join(absoluteDirectoryPath, directoryName), [...pathSegments, directoryName],
|
|
155
|
+
result: await walkCommandsDirectory(path.join(absoluteDirectoryPath, directoryName), [...pathSegments, directoryName], extractMetadata)
|
|
66
156
|
};
|
|
67
157
|
}));
|
|
68
|
-
const
|
|
69
|
-
const
|
|
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));
|
|
159
|
+
const childDirectoriesWithNodes = new Set(childResults.filter(({ result }) => result.hasNode).map(({ directoryName }) => directoryName));
|
|
160
|
+
const bareCommandNodes = await Promise.all(bareCommandFiles.map(async (fileName) => {
|
|
161
|
+
const commandName = fileName.slice(0, -3);
|
|
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.`);
|
|
163
|
+
const sourceFilePath = path.join(absoluteDirectoryPath, fileName);
|
|
164
|
+
const metadata = await extractMetadata(sourceFilePath);
|
|
165
|
+
return {
|
|
166
|
+
pathSegments: [...pathSegments, commandName],
|
|
167
|
+
kind: "command",
|
|
168
|
+
sourceFilePath,
|
|
169
|
+
childNames: [],
|
|
170
|
+
aliases: metadata?.aliases ?? [],
|
|
171
|
+
description: metadata?.description
|
|
172
|
+
};
|
|
173
|
+
}));
|
|
174
|
+
const descendantNodes = childResults.flatMap(({ result }) => result.nodes);
|
|
175
|
+
const childNames = [...childResults.filter(({ result }) => result.hasNode).map(({ directoryName }) => directoryName), ...bareCommandFiles.map((fileName) => fileName.slice(0, -3))].sort((left, right) => left.localeCompare(right));
|
|
70
176
|
const hasCommandEntry = entries.some((entry) => entry.isFile() && entry.name === COMMAND_ENTRY_FILE);
|
|
71
|
-
|
|
72
|
-
|
|
177
|
+
const hasGroupMeta = entries.some((entry) => entry.isFile() && entry.name === GROUP_META_FILE);
|
|
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.`);
|
|
179
|
+
if (hasGroupMeta && childNames.length === 0) throw new Error(`${path.join(absoluteDirectoryPath, GROUP_META_FILE)}: _group.ts exists but the directory has no subcommands.`);
|
|
180
|
+
if (!hasCommandEntry && !hasGroupMeta && childNames.length === 0) return {
|
|
181
|
+
nodes: [...descendantNodes, ...bareCommandNodes],
|
|
73
182
|
hasNode: false
|
|
74
183
|
};
|
|
75
184
|
let node;
|
|
76
185
|
if (hasCommandEntry) {
|
|
77
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.");
|
|
78
190
|
node = {
|
|
79
191
|
pathSegments,
|
|
80
192
|
kind: "command",
|
|
81
193
|
sourceFilePath,
|
|
82
194
|
childNames,
|
|
83
|
-
|
|
195
|
+
aliases,
|
|
196
|
+
description: metadata?.description
|
|
84
197
|
};
|
|
85
|
-
} else
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
198
|
+
} else {
|
|
199
|
+
const groupMetaPath = path.join(absoluteDirectoryPath, GROUP_META_FILE);
|
|
200
|
+
if (hasGroupMeta) await validateGroupMetaFile(groupMetaPath);
|
|
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.");
|
|
205
|
+
node = {
|
|
206
|
+
pathSegments,
|
|
207
|
+
kind: "group",
|
|
208
|
+
childNames,
|
|
209
|
+
aliases,
|
|
210
|
+
...metadata?.description !== void 0 ? { description: metadata.description } : {}
|
|
211
|
+
};
|
|
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);
|
|
90
230
|
return {
|
|
91
|
-
nodes: [
|
|
231
|
+
nodes: [
|
|
232
|
+
node,
|
|
233
|
+
...descendantNodes,
|
|
234
|
+
...bareCommandNodes
|
|
235
|
+
],
|
|
92
236
|
hasNode: true
|
|
93
237
|
};
|
|
94
238
|
}
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/manifest/generate/generate-manifest.ts
|
|
95
241
|
async function generateCommandManifest(options) {
|
|
96
|
-
const
|
|
97
|
-
const walkResult = await walkCommandsDirectory(options.commandsDirectory, [],
|
|
98
|
-
if (walkResult.nodes.length === 0) throw new Error("No commands found in src/commands/. Create a command file like src/commands/hello/index.ts");
|
|
242
|
+
const extractMetadata = options.extractMetadata ?? extractMetadataFromSourceFile;
|
|
243
|
+
const walkResult = await walkCommandsDirectory(options.commandsDirectory, [], extractMetadata);
|
|
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");
|
|
99
245
|
return { nodes: [...walkResult.nodes].sort((left, right) => comparePathSegments(left.pathSegments, right.pathSegments)) };
|
|
100
246
|
}
|
|
101
247
|
function serializeCommandManifest(manifest) {
|
|
@@ -119,19 +265,20 @@ function resolveCommandsDirectory(projectRoot) {
|
|
|
119
265
|
function resolveDistDirectory(projectRoot) {
|
|
120
266
|
return path.join(projectRoot, DIST_DIRECTORY_NAME);
|
|
121
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
|
+
}
|
|
122
275
|
async function readProjectCliInfo(projectRoot) {
|
|
123
276
|
const packageJsonPath = path.join(projectRoot, "package.json");
|
|
124
277
|
try {
|
|
125
278
|
const packageJsonContents = await readFile(packageJsonPath, "utf8");
|
|
126
279
|
const packageJson = JSON.parse(packageJsonContents);
|
|
127
|
-
let name;
|
|
128
|
-
if (packageJson.bin && typeof packageJson.bin === "object") {
|
|
129
|
-
const binNames = Object.keys(packageJson.bin).sort((left, right) => left.localeCompare(right));
|
|
130
|
-
if (binNames.length > 0) name = binNames[0];
|
|
131
|
-
}
|
|
132
|
-
if (!name && packageJson.name && packageJson.name.length > 0) name = packageJson.name.split("/").at(-1) ?? packageJson.name;
|
|
133
280
|
return {
|
|
134
|
-
name:
|
|
281
|
+
name: resolveCliNameFromPackageJson(packageJson) ?? path.basename(projectRoot),
|
|
135
282
|
version: packageJson.version
|
|
136
283
|
};
|
|
137
284
|
} catch (error) {
|
|
@@ -478,6 +625,19 @@ function tryParseProjectOption(argv, index) {
|
|
|
478
625
|
function getRuneVersion() {
|
|
479
626
|
return version;
|
|
480
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
|
+
}
|
|
481
641
|
function parseDevArgs(argv) {
|
|
482
642
|
const commandArgs = [];
|
|
483
643
|
let projectPath;
|
|
@@ -546,18 +706,22 @@ function parseBuildArgs(argv) {
|
|
|
546
706
|
projectPath
|
|
547
707
|
};
|
|
548
708
|
}
|
|
549
|
-
function
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
+
});
|
|
561
725
|
}
|
|
562
726
|
async function runRuneCli(options) {
|
|
563
727
|
const [subcommand, ...restArgs] = options.argv;
|
|
@@ -569,23 +733,8 @@ async function runRuneCli(options) {
|
|
|
569
733
|
await writeStdout(`rune v${getRuneVersion()}\n`);
|
|
570
734
|
return 0;
|
|
571
735
|
}
|
|
572
|
-
if (subcommand === "dev")
|
|
573
|
-
|
|
574
|
-
if (!parsedDevArgs.ok) return writeEarlyExit(parsedDevArgs);
|
|
575
|
-
return runDevCommand({
|
|
576
|
-
rawArgs: parsedDevArgs.commandArgs,
|
|
577
|
-
projectPath: parsedDevArgs.projectPath,
|
|
578
|
-
cwd: options.cwd
|
|
579
|
-
});
|
|
580
|
-
}
|
|
581
|
-
if (subcommand === "build") {
|
|
582
|
-
const parsedBuildArgs = parseBuildArgs(restArgs);
|
|
583
|
-
if (!parsedBuildArgs.ok) return writeEarlyExit(parsedBuildArgs);
|
|
584
|
-
return runBuildCommand({
|
|
585
|
-
projectPath: parsedBuildArgs.projectPath,
|
|
586
|
-
cwd: options.cwd
|
|
587
|
-
});
|
|
588
|
-
}
|
|
736
|
+
if (subcommand === "dev") return runDevSubcommand(options, restArgs);
|
|
737
|
+
if (subcommand === "build") return runBuildSubcommand(options, restArgs);
|
|
589
738
|
await writeStderrLine(`Unknown command: ${subcommand}. Available commands: build, dev`);
|
|
590
739
|
return 1;
|
|
591
740
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
2
|
//#region ../core/dist/index.mjs
|
|
3
3
|
function isSchemaField(field) {
|
|
4
4
|
return "schema" in field && field.schema !== void 0;
|
|
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
|
|
@@ -124,17 +135,49 @@ function defineCommand(input) {
|
|
|
124
135
|
function isDefinedCommand(value) {
|
|
125
136
|
return typeof value === "object" && value !== null && value[DEFINED_COMMAND_BRAND] === true;
|
|
126
137
|
}
|
|
138
|
+
const DEFINED_GROUP_BRAND = Symbol.for("@rune-cli/defined-group");
|
|
139
|
+
/**
|
|
140
|
+
* Defines metadata for a command group (a directory that only groups
|
|
141
|
+
* subcommands without being executable itself).
|
|
142
|
+
*
|
|
143
|
+
* Place the default export of this function in a `_group.ts` file inside a
|
|
144
|
+
* command directory.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* // src/commands/project/_group.ts
|
|
149
|
+
* export default defineGroup({
|
|
150
|
+
* description: "Manage projects",
|
|
151
|
+
* });
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
function defineGroup(input) {
|
|
155
|
+
if (typeof input.description !== "string" || input.description.length === 0) throw new Error("defineGroup() requires a non-empty \"description\" string.");
|
|
156
|
+
if (input.aliases) validateCommandAliases(input.aliases);
|
|
157
|
+
const group = {
|
|
158
|
+
description: input.description,
|
|
159
|
+
aliases: input.aliases ?? []
|
|
160
|
+
};
|
|
161
|
+
Object.defineProperty(group, DEFINED_GROUP_BRAND, {
|
|
162
|
+
value: true,
|
|
163
|
+
enumerable: false
|
|
164
|
+
});
|
|
165
|
+
return group;
|
|
166
|
+
}
|
|
127
167
|
function formatExecutionError(error) {
|
|
128
168
|
if (error instanceof Error) return error.message === "" ? "" : error.message || error.name || "Unknown error";
|
|
129
169
|
if (typeof error === "string") return error;
|
|
130
170
|
return "Unknown error";
|
|
131
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
|
+
}
|
|
132
177
|
async function executeCommand(command, input = {}) {
|
|
133
178
|
try {
|
|
134
|
-
const options = { ...input.options };
|
|
135
|
-
for (const field of command.options) if (options[field.name] === void 0 && !isSchemaField(field) && field.type === "boolean") options[field.name] = false;
|
|
136
179
|
await command.run({
|
|
137
|
-
options,
|
|
180
|
+
options: createExecutionOptions(command, input),
|
|
138
181
|
args: input.args ?? {},
|
|
139
182
|
cwd: input.cwd ?? process.cwd(),
|
|
140
183
|
rawArgs: input.rawArgs ?? []
|
|
@@ -148,62 +191,6 @@ async function executeCommand(command, input = {}) {
|
|
|
148
191
|
} : { exitCode: 1 };
|
|
149
192
|
}
|
|
150
193
|
}
|
|
151
|
-
async function captureProcessOutput(action) {
|
|
152
|
-
const stdoutChunks = [];
|
|
153
|
-
const stderrChunks = [];
|
|
154
|
-
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
155
|
-
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
156
|
-
const originalConsoleMethods = {
|
|
157
|
-
log: console.log,
|
|
158
|
-
info: console.info,
|
|
159
|
-
debug: console.debug,
|
|
160
|
-
warn: console.warn,
|
|
161
|
-
error: console.error
|
|
162
|
-
};
|
|
163
|
-
const captureChunk = (chunks, chunk, encoding) => {
|
|
164
|
-
if (typeof chunk === "string") {
|
|
165
|
-
chunks.push(chunk);
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
chunks.push(Buffer.from(chunk).toString(encoding));
|
|
169
|
-
};
|
|
170
|
-
const captureConsole = (chunks, args) => {
|
|
171
|
-
chunks.push(`${format(...args)}\n`);
|
|
172
|
-
};
|
|
173
|
-
const createWriteCapture = (chunks) => ((chunk, encoding, cb) => {
|
|
174
|
-
captureChunk(chunks, chunk, typeof encoding === "string" ? encoding : void 0);
|
|
175
|
-
if (typeof encoding === "function") encoding(null);
|
|
176
|
-
else cb?.(null);
|
|
177
|
-
return true;
|
|
178
|
-
});
|
|
179
|
-
process.stdout.write = createWriteCapture(stdoutChunks);
|
|
180
|
-
process.stderr.write = createWriteCapture(stderrChunks);
|
|
181
|
-
for (const method of [
|
|
182
|
-
"log",
|
|
183
|
-
"info",
|
|
184
|
-
"debug"
|
|
185
|
-
]) console[method] = (...args) => captureConsole(stdoutChunks, args);
|
|
186
|
-
for (const method of ["warn", "error"]) console[method] = (...args) => captureConsole(stderrChunks, args);
|
|
187
|
-
try {
|
|
188
|
-
return {
|
|
189
|
-
ok: true,
|
|
190
|
-
value: await action(),
|
|
191
|
-
stdout: stdoutChunks.join(""),
|
|
192
|
-
stderr: stderrChunks.join("")
|
|
193
|
-
};
|
|
194
|
-
} catch (error) {
|
|
195
|
-
return {
|
|
196
|
-
ok: false,
|
|
197
|
-
error,
|
|
198
|
-
stdout: stdoutChunks.join(""),
|
|
199
|
-
stderr: stderrChunks.join("")
|
|
200
|
-
};
|
|
201
|
-
} finally {
|
|
202
|
-
process.stdout.write = originalStdoutWrite;
|
|
203
|
-
process.stderr.write = originalStderrWrite;
|
|
204
|
-
Object.assign(console, originalConsoleMethods);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
194
|
function formatTypeHint(field) {
|
|
208
195
|
return isSchemaField(field) ? "" : ` <${field.type}>`;
|
|
209
196
|
}
|
|
@@ -377,9 +364,9 @@ function getOptionParseType(field) {
|
|
|
377
364
|
}
|
|
378
365
|
function buildParseArgsOptions(options) {
|
|
379
366
|
const config = {};
|
|
380
|
-
for (const field of options) config[field.name] = field.
|
|
367
|
+
for (const field of options) config[field.name] = field.short ? {
|
|
381
368
|
type: getOptionParseType(field),
|
|
382
|
-
short: field.
|
|
369
|
+
short: field.short
|
|
383
370
|
} : { type: getOptionParseType(field) };
|
|
384
371
|
return config;
|
|
385
372
|
}
|
|
@@ -395,7 +382,7 @@ function detectDuplicateOption(options, tokens) {
|
|
|
395
382
|
}
|
|
396
383
|
}
|
|
397
384
|
}
|
|
398
|
-
async function
|
|
385
|
+
async function parseCommandArgs(command, rawArgs) {
|
|
399
386
|
let parsed;
|
|
400
387
|
try {
|
|
401
388
|
parsed = parseArgs({
|
|
@@ -442,4 +429,4 @@ async function parseCommand(command, rawArgs) {
|
|
|
442
429
|
};
|
|
443
430
|
}
|
|
444
431
|
//#endregion
|
|
445
|
-
export { isSchemaField as a, isDefinedCommand as i,
|
|
432
|
+
export { isSchemaField as a, isDefinedCommand as i, defineGroup as n, parseCommandArgs as o, executeCommand as r, defineCommand as t };
|