@rune-cli/rune 0.0.1
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/LICENSE +21 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +492 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +2 -0
- package/dist/runtime.d.mts +41 -0
- package/dist/runtime.mjs +2 -0
- package/dist/test.d.mts +7 -0
- package/dist/test.mjs +7 -0
- package/dist/write-result-C0wgFsjj.mjs +225 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shinya Fujino
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { i as successResult, n as runManifestCommand, r as failureResult, t as writeCommandExecutionResult } from "./write-result-C0wgFsjj.mjs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { build } from "esbuild";
|
|
5
|
+
import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import ts from "typescript";
|
|
8
|
+
//#region src/manifest/generate-manifest.ts
|
|
9
|
+
const COMMAND_ENTRY_FILE = "index.ts";
|
|
10
|
+
function comparePathSegments(left, right) {
|
|
11
|
+
const length = Math.min(left.length, right.length);
|
|
12
|
+
for (let index = 0; index < length; index += 1) {
|
|
13
|
+
const comparison = left[index].localeCompare(right[index]);
|
|
14
|
+
if (comparison !== 0) return comparison;
|
|
15
|
+
}
|
|
16
|
+
return left.length - right.length;
|
|
17
|
+
}
|
|
18
|
+
function getPropertyNameText(name) {
|
|
19
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
|
|
20
|
+
}
|
|
21
|
+
function getStaticDescriptionValue(expression) {
|
|
22
|
+
if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) return expression.text;
|
|
23
|
+
}
|
|
24
|
+
function isDefineCommandExpression(expression) {
|
|
25
|
+
if (ts.isIdentifier(expression)) return expression.text === "defineCommand";
|
|
26
|
+
if (ts.isPropertyAccessExpression(expression)) return expression.name.text === "defineCommand";
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
function extractDescriptionFromCommandDefinition(expression, knownDescriptions) {
|
|
30
|
+
if (ts.isIdentifier(expression)) return knownDescriptions.get(expression.text);
|
|
31
|
+
if (!ts.isCallExpression(expression) || !isDefineCommandExpression(expression.expression)) return;
|
|
32
|
+
const [definition] = expression.arguments;
|
|
33
|
+
if (!definition || !ts.isObjectLiteralExpression(definition)) return;
|
|
34
|
+
for (const property of definition.properties) {
|
|
35
|
+
if (!ts.isPropertyAssignment(property)) continue;
|
|
36
|
+
if (getPropertyNameText(property.name) !== "description") continue;
|
|
37
|
+
return getStaticDescriptionValue(property.initializer);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function extractDescriptionFromSourceFile(sourceFilePath) {
|
|
41
|
+
const sourceText = await readFile(sourceFilePath, "utf8");
|
|
42
|
+
const sourceFile = ts.createSourceFile(sourceFilePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
43
|
+
const knownDescriptions = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const statement of sourceFile.statements) {
|
|
45
|
+
if (ts.isVariableStatement(statement)) {
|
|
46
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
47
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
|
|
48
|
+
knownDescriptions.set(declaration.name.text, extractDescriptionFromCommandDefinition(declaration.initializer, knownDescriptions));
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (ts.isExportAssignment(statement)) return extractDescriptionFromCommandDefinition(statement.expression, knownDescriptions);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function walkCommandsDirectory(absoluteDirectoryPath, pathSegments, extractDescription) {
|
|
56
|
+
const entries = await readdir(absoluteDirectoryPath, { withFileTypes: true });
|
|
57
|
+
const childDirectoryNames = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
|
|
58
|
+
const childResults = await Promise.all(childDirectoryNames.map(async (directoryName) => {
|
|
59
|
+
return {
|
|
60
|
+
directoryName,
|
|
61
|
+
result: await walkCommandsDirectory(path.join(absoluteDirectoryPath, directoryName), [...pathSegments, directoryName], extractDescription)
|
|
62
|
+
};
|
|
63
|
+
}));
|
|
64
|
+
const childNodes = childResults.flatMap(({ result }) => result.nodes);
|
|
65
|
+
const childNames = childResults.filter(({ result }) => result.hasNode).map(({ directoryName }) => directoryName);
|
|
66
|
+
const hasCommandEntry = entries.some((entry) => entry.isFile() && entry.name === COMMAND_ENTRY_FILE);
|
|
67
|
+
if (!hasCommandEntry && childNames.length === 0) return {
|
|
68
|
+
nodes: childNodes,
|
|
69
|
+
hasNode: false
|
|
70
|
+
};
|
|
71
|
+
let node;
|
|
72
|
+
if (hasCommandEntry) {
|
|
73
|
+
const sourceFilePath = path.join(absoluteDirectoryPath, COMMAND_ENTRY_FILE);
|
|
74
|
+
node = {
|
|
75
|
+
pathSegments,
|
|
76
|
+
kind: "command",
|
|
77
|
+
sourceFilePath,
|
|
78
|
+
childNames,
|
|
79
|
+
description: await extractDescription(sourceFilePath)
|
|
80
|
+
};
|
|
81
|
+
} else node = {
|
|
82
|
+
pathSegments,
|
|
83
|
+
kind: "group",
|
|
84
|
+
childNames
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
nodes: [node, ...childNodes],
|
|
88
|
+
hasNode: true
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async function generateCommandManifest(options) {
|
|
92
|
+
const extractDescription = options.extractDescription ?? extractDescriptionFromSourceFile;
|
|
93
|
+
return { nodes: [...(await walkCommandsDirectory(options.commandsDirectory, [], extractDescription)).nodes].sort((left, right) => comparePathSegments(left.pathSegments, right.pathSegments)) };
|
|
94
|
+
}
|
|
95
|
+
function serializeCommandManifest(manifest) {
|
|
96
|
+
return JSON.stringify(manifest, null, 2);
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/project/project-files.ts
|
|
100
|
+
const SOURCE_DIRECTORY_NAME = "src";
|
|
101
|
+
const COMMANDS_DIRECTORY_NAME = path.join(SOURCE_DIRECTORY_NAME, "commands");
|
|
102
|
+
const DIST_DIRECTORY_NAME = "dist";
|
|
103
|
+
function resolveProjectPath(options) {
|
|
104
|
+
const baseDirectory = options.cwd ?? process.cwd();
|
|
105
|
+
return path.resolve(baseDirectory, options.projectPath ?? ".");
|
|
106
|
+
}
|
|
107
|
+
function resolveSourceDirectory(projectRoot) {
|
|
108
|
+
return path.join(projectRoot, SOURCE_DIRECTORY_NAME);
|
|
109
|
+
}
|
|
110
|
+
function resolveCommandsDirectory(projectRoot) {
|
|
111
|
+
return path.join(projectRoot, COMMANDS_DIRECTORY_NAME);
|
|
112
|
+
}
|
|
113
|
+
function resolveDistDirectory(projectRoot) {
|
|
114
|
+
return path.join(projectRoot, DIST_DIRECTORY_NAME);
|
|
115
|
+
}
|
|
116
|
+
async function readProjectCliName(projectRoot) {
|
|
117
|
+
const packageJsonPath = path.join(projectRoot, "package.json");
|
|
118
|
+
try {
|
|
119
|
+
const packageJsonContents = await readFile(packageJsonPath, "utf8");
|
|
120
|
+
const packageJson = JSON.parse(packageJsonContents);
|
|
121
|
+
if (packageJson.bin && typeof packageJson.bin === "object") {
|
|
122
|
+
const binNames = Object.keys(packageJson.bin).sort((left, right) => left.localeCompare(right));
|
|
123
|
+
if (binNames.length > 0) return binNames[0];
|
|
124
|
+
}
|
|
125
|
+
if (packageJson.name && packageJson.name.length > 0) return packageJson.name.split("/").at(-1) ?? packageJson.name;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error.code !== "ENOENT") throw error;
|
|
128
|
+
}
|
|
129
|
+
return path.basename(projectRoot);
|
|
130
|
+
}
|
|
131
|
+
async function assertCommandsDirectoryExists(commandsDirectory) {
|
|
132
|
+
if (!(await stat(commandsDirectory).catch((error) => {
|
|
133
|
+
if (error.code === "ENOENT") return;
|
|
134
|
+
throw error;
|
|
135
|
+
}))?.isDirectory()) throw new Error(`Commands directory not found: ${commandsDirectory}`);
|
|
136
|
+
}
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/cli/build-command.ts
|
|
139
|
+
const BUILD_CLI_FILENAME = "cli.mjs";
|
|
140
|
+
const BUILD_MANIFEST_FILENAME = "manifest.json";
|
|
141
|
+
const RUNE_PACKAGE_NAME = "@rune-cli/rune";
|
|
142
|
+
const CODE_SOURCE_EXTENSIONS = new Set([
|
|
143
|
+
".ts",
|
|
144
|
+
".tsx",
|
|
145
|
+
".mts",
|
|
146
|
+
".cts",
|
|
147
|
+
".js",
|
|
148
|
+
".jsx",
|
|
149
|
+
".mjs",
|
|
150
|
+
".cjs"
|
|
151
|
+
]);
|
|
152
|
+
const BUILD_TARGET = "node24";
|
|
153
|
+
function isCodeSourceFile(filePath) {
|
|
154
|
+
return CODE_SOURCE_EXTENSIONS.has(path.extname(filePath));
|
|
155
|
+
}
|
|
156
|
+
function isDeclarationFile(filePath) {
|
|
157
|
+
return filePath.endsWith(".d.ts") || filePath.endsWith(".d.mts") || filePath.endsWith(".d.cts");
|
|
158
|
+
}
|
|
159
|
+
function replaceFileExtension(filePath, extension) {
|
|
160
|
+
const parsedPath = path.parse(filePath);
|
|
161
|
+
return path.join(parsedPath.dir, `${parsedPath.name}${extension}`);
|
|
162
|
+
}
|
|
163
|
+
function toPosixPath(filePath) {
|
|
164
|
+
return filePath.split(path.sep).join(path.posix.sep);
|
|
165
|
+
}
|
|
166
|
+
function createBuiltManifest(manifest, sourceDirectory) {
|
|
167
|
+
return { nodes: manifest.nodes.map((node) => {
|
|
168
|
+
if (node.kind !== "command") return node;
|
|
169
|
+
const relativeSourceFilePath = path.relative(sourceDirectory, node.sourceFilePath);
|
|
170
|
+
return {
|
|
171
|
+
...node,
|
|
172
|
+
sourceFilePath: toPosixPath(replaceFileExtension(relativeSourceFilePath, ".mjs"))
|
|
173
|
+
};
|
|
174
|
+
}) };
|
|
175
|
+
}
|
|
176
|
+
function isBuildFailure(error) {
|
|
177
|
+
return typeof error === "object" && error !== null && "errors" in error && Array.isArray(error.errors);
|
|
178
|
+
}
|
|
179
|
+
function formatBuildFailure(projectRoot, error) {
|
|
180
|
+
const [firstError] = error.errors;
|
|
181
|
+
if (!firstError) return "Failed to build project";
|
|
182
|
+
if (!firstError.location) return `Failed to compile: ${firstError.text}`;
|
|
183
|
+
return `Failed to compile ${path.isAbsolute(firstError.location.file) ? path.relative(projectRoot, firstError.location.file) : firstError.location.file}:${firstError.location.line}:${firstError.location.column + 1}: ${firstError.text}`;
|
|
184
|
+
}
|
|
185
|
+
async function copyBuiltAssets(sourceDirectory, distDirectory) {
|
|
186
|
+
const entries = await readdir(sourceDirectory, { withFileTypes: true });
|
|
187
|
+
await Promise.all(entries.map(async (entry) => {
|
|
188
|
+
const sourceEntryPath = path.join(sourceDirectory, entry.name);
|
|
189
|
+
const distEntryPath = path.join(distDirectory, entry.name);
|
|
190
|
+
if (entry.isDirectory()) {
|
|
191
|
+
await copyBuiltAssets(sourceEntryPath, distEntryPath);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (isDeclarationFile(sourceEntryPath)) return;
|
|
195
|
+
if (isCodeSourceFile(sourceEntryPath)) return;
|
|
196
|
+
await mkdir(path.dirname(distEntryPath), { recursive: true });
|
|
197
|
+
await cp(sourceEntryPath, distEntryPath);
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
function renderBuiltCliEntry(cliName, runtimeImportPath) {
|
|
201
|
+
return `import { readFile } from "node:fs/promises";
|
|
202
|
+
import { fileURLToPath } from "node:url";
|
|
203
|
+
|
|
204
|
+
import { runManifestCommand, writeCommandExecutionResult } from ${JSON.stringify(runtimeImportPath)};
|
|
205
|
+
|
|
206
|
+
const cliName = ${JSON.stringify(cliName)};
|
|
207
|
+
const distDirectoryUrl = new URL("./", import.meta.url);
|
|
208
|
+
const manifestPath = fileURLToPath(new URL("./${BUILD_MANIFEST_FILENAME}", distDirectoryUrl));
|
|
209
|
+
const manifestContents = await readFile(manifestPath, "utf8");
|
|
210
|
+
const manifest = JSON.parse(manifestContents);
|
|
211
|
+
const runtimeManifest = {
|
|
212
|
+
...manifest,
|
|
213
|
+
nodes: manifest.nodes.map((node) =>
|
|
214
|
+
node.kind === "command"
|
|
215
|
+
? {
|
|
216
|
+
...node,
|
|
217
|
+
sourceFilePath: fileURLToPath(new URL(node.sourceFilePath, distDirectoryUrl)),
|
|
218
|
+
}
|
|
219
|
+
: node,
|
|
220
|
+
),
|
|
221
|
+
};
|
|
222
|
+
const result = await runManifestCommand({
|
|
223
|
+
manifest: runtimeManifest,
|
|
224
|
+
rawArgs: process.argv.slice(2),
|
|
225
|
+
cliName,
|
|
226
|
+
cwd: process.cwd(),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
await writeCommandExecutionResult(result);
|
|
230
|
+
`;
|
|
231
|
+
}
|
|
232
|
+
function collectCommandEntryPoints(manifest) {
|
|
233
|
+
return manifest.nodes.flatMap((node) => node.kind === "command" ? [node.sourceFilePath] : []);
|
|
234
|
+
}
|
|
235
|
+
async function pathExists(filePath) {
|
|
236
|
+
try {
|
|
237
|
+
return (await stat(filePath)).isFile();
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (error.code === "ENOENT") return false;
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async function resolveRunePackageRoot() {
|
|
244
|
+
let currentDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
245
|
+
while (true) {
|
|
246
|
+
const packageJsonPath = path.join(currentDirectory, "package.json");
|
|
247
|
+
try {
|
|
248
|
+
if (JSON.parse(await readFile(packageJsonPath, "utf8")).name === RUNE_PACKAGE_NAME) return currentDirectory;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (error.code !== "ENOENT") throw error;
|
|
251
|
+
}
|
|
252
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
253
|
+
if (parentDirectory === currentDirectory) throw new Error(`Could not locate package root for ${RUNE_PACKAGE_NAME}`);
|
|
254
|
+
currentDirectory = parentDirectory;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function resolveBuildTsconfig(projectRoot) {
|
|
258
|
+
const tsconfigPath = path.join(projectRoot, "tsconfig.json");
|
|
259
|
+
return await pathExists(tsconfigPath) ? tsconfigPath : void 0;
|
|
260
|
+
}
|
|
261
|
+
async function resolveRuntimeHelperEntryPath() {
|
|
262
|
+
const packageRoot = await resolveRunePackageRoot();
|
|
263
|
+
const sourceRuntimePath = path.join(packageRoot, "src", "runtime.ts");
|
|
264
|
+
if (await pathExists(sourceRuntimePath)) return sourceRuntimePath;
|
|
265
|
+
const distRuntimePath = path.join(packageRoot, "dist", "runtime.mjs");
|
|
266
|
+
if (await pathExists(distRuntimePath)) return distRuntimePath;
|
|
267
|
+
throw new Error("Could not locate Rune runtime helper entry");
|
|
268
|
+
}
|
|
269
|
+
async function buildCommandEntries(projectRoot, sourceDirectory, distDirectory, manifest) {
|
|
270
|
+
const entryPoints = [...collectCommandEntryPoints(manifest)];
|
|
271
|
+
const tsconfig = await resolveBuildTsconfig(projectRoot);
|
|
272
|
+
if (entryPoints.length === 0) return;
|
|
273
|
+
await build({
|
|
274
|
+
absWorkingDir: projectRoot,
|
|
275
|
+
entryPoints,
|
|
276
|
+
outdir: distDirectory,
|
|
277
|
+
outbase: sourceDirectory,
|
|
278
|
+
entryNames: "[dir]/[name]",
|
|
279
|
+
chunkNames: "chunks/[name]-[hash]",
|
|
280
|
+
assetNames: "assets/[name]-[hash]",
|
|
281
|
+
bundle: true,
|
|
282
|
+
format: "esm",
|
|
283
|
+
platform: "node",
|
|
284
|
+
target: BUILD_TARGET,
|
|
285
|
+
splitting: true,
|
|
286
|
+
tsconfig,
|
|
287
|
+
outExtension: { ".js": ".mjs" },
|
|
288
|
+
logLevel: "silent",
|
|
289
|
+
write: true
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
async function buildCliEntry(projectRoot, distDirectory, cliName) {
|
|
293
|
+
const runtimeHelperEntryPath = await resolveRuntimeHelperEntryPath();
|
|
294
|
+
await build({
|
|
295
|
+
absWorkingDir: projectRoot,
|
|
296
|
+
stdin: {
|
|
297
|
+
contents: renderBuiltCliEntry(cliName, `./${path.basename(runtimeHelperEntryPath)}`),
|
|
298
|
+
loader: "ts",
|
|
299
|
+
resolveDir: path.dirname(runtimeHelperEntryPath),
|
|
300
|
+
sourcefile: "rune-built-cli-entry.ts"
|
|
301
|
+
},
|
|
302
|
+
outfile: path.join(distDirectory, BUILD_CLI_FILENAME),
|
|
303
|
+
bundle: true,
|
|
304
|
+
format: "esm",
|
|
305
|
+
platform: "node",
|
|
306
|
+
target: BUILD_TARGET,
|
|
307
|
+
banner: { js: "#!/usr/bin/env node" },
|
|
308
|
+
logLevel: "silent",
|
|
309
|
+
write: true
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
async function writeBuiltRuntimeFiles(distDirectory, manifest) {
|
|
313
|
+
await mkdir(distDirectory, { recursive: true });
|
|
314
|
+
await writeFile(path.join(distDirectory, BUILD_MANIFEST_FILENAME), serializeCommandManifest(manifest));
|
|
315
|
+
}
|
|
316
|
+
async function runBuildCommand(options) {
|
|
317
|
+
let projectRoot = "";
|
|
318
|
+
try {
|
|
319
|
+
projectRoot = resolveProjectPath(options);
|
|
320
|
+
const sourceDirectory = resolveSourceDirectory(projectRoot);
|
|
321
|
+
const commandsDirectory = resolveCommandsDirectory(projectRoot);
|
|
322
|
+
const distDirectory = resolveDistDirectory(projectRoot);
|
|
323
|
+
await assertCommandsDirectoryExists(commandsDirectory);
|
|
324
|
+
const sourceManifest = await generateCommandManifest({ commandsDirectory });
|
|
325
|
+
const builtManifest = createBuiltManifest(sourceManifest, sourceDirectory);
|
|
326
|
+
const cliName = await readProjectCliName(projectRoot);
|
|
327
|
+
await rm(distDirectory, {
|
|
328
|
+
recursive: true,
|
|
329
|
+
force: true
|
|
330
|
+
});
|
|
331
|
+
await writeBuiltRuntimeFiles(distDirectory, builtManifest);
|
|
332
|
+
await Promise.all([
|
|
333
|
+
buildCommandEntries(projectRoot, sourceDirectory, distDirectory, sourceManifest),
|
|
334
|
+
buildCliEntry(projectRoot, distDirectory, cliName),
|
|
335
|
+
copyBuiltAssets(sourceDirectory, distDirectory)
|
|
336
|
+
]);
|
|
337
|
+
return successResult(`Built CLI to ${path.join(distDirectory, BUILD_CLI_FILENAME)}\n`);
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (isBuildFailure(error)) return failureResult(formatBuildFailure(projectRoot, error));
|
|
340
|
+
return failureResult(error instanceof Error ? error.message : "Failed to run rune build");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function renderRuneBuildHelp() {
|
|
344
|
+
return [
|
|
345
|
+
"Usage: rune build [--project <path>]",
|
|
346
|
+
"",
|
|
347
|
+
"Build a Rune project into a distributable CLI.",
|
|
348
|
+
""
|
|
349
|
+
].join("\n");
|
|
350
|
+
}
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/cli/dev-command.ts
|
|
353
|
+
const DEV_MANIFEST_DIRECTORY_PATH = ".rune";
|
|
354
|
+
const DEV_MANIFEST_FILENAME = "manifest.json";
|
|
355
|
+
async function writeDevManifest(projectRoot, manifestContents) {
|
|
356
|
+
const manifestDirectory = path.join(projectRoot, DEV_MANIFEST_DIRECTORY_PATH);
|
|
357
|
+
const manifestPath = path.join(manifestDirectory, DEV_MANIFEST_FILENAME);
|
|
358
|
+
await mkdir(manifestDirectory, { recursive: true });
|
|
359
|
+
await writeFile(manifestPath, manifestContents);
|
|
360
|
+
}
|
|
361
|
+
async function runDevCommand(options) {
|
|
362
|
+
try {
|
|
363
|
+
const projectRoot = resolveProjectPath(options);
|
|
364
|
+
const commandsDirectory = resolveCommandsDirectory(projectRoot);
|
|
365
|
+
await assertCommandsDirectoryExists(commandsDirectory);
|
|
366
|
+
const manifest = await generateCommandManifest({ commandsDirectory });
|
|
367
|
+
await writeDevManifest(projectRoot, serializeCommandManifest(manifest));
|
|
368
|
+
return runManifestCommand({
|
|
369
|
+
manifest,
|
|
370
|
+
rawArgs: options.rawArgs,
|
|
371
|
+
cliName: await readProjectCliName(projectRoot),
|
|
372
|
+
cwd: options.cwd
|
|
373
|
+
});
|
|
374
|
+
} catch (error) {
|
|
375
|
+
return failureResult(error instanceof Error ? error.message : "Failed to run rune dev");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function renderRuneDevHelp() {
|
|
379
|
+
return [
|
|
380
|
+
"Usage: rune dev [--project <path>] [--] [command...]",
|
|
381
|
+
"",
|
|
382
|
+
"Run a Rune project in development mode.",
|
|
383
|
+
""
|
|
384
|
+
].join("\n");
|
|
385
|
+
}
|
|
386
|
+
function renderRuneCliHelp() {
|
|
387
|
+
return [
|
|
388
|
+
"Usage: rune <command>",
|
|
389
|
+
"",
|
|
390
|
+
"Commands:",
|
|
391
|
+
" build Build a Rune project into a distributable CLI",
|
|
392
|
+
" dev Run a Rune project in development mode",
|
|
393
|
+
""
|
|
394
|
+
].join("\n");
|
|
395
|
+
}
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/cli/rune-cli.ts
|
|
398
|
+
function tryParseProjectOption(argv, index) {
|
|
399
|
+
const token = argv[index];
|
|
400
|
+
if (token.startsWith("--project=")) return {
|
|
401
|
+
projectPath: token.slice(10),
|
|
402
|
+
nextIndex: index + 1
|
|
403
|
+
};
|
|
404
|
+
if (token === "--project") {
|
|
405
|
+
const nextToken = argv[index + 1];
|
|
406
|
+
if (!nextToken) return failureResult("Missing value for --project");
|
|
407
|
+
return {
|
|
408
|
+
projectPath: nextToken,
|
|
409
|
+
nextIndex: index + 2
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function isHelpFlag(token) {
|
|
414
|
+
return token === "--help" || token === "-h";
|
|
415
|
+
}
|
|
416
|
+
function parseDevArgs(argv) {
|
|
417
|
+
const commandArgs = [];
|
|
418
|
+
let projectPath;
|
|
419
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
420
|
+
const token = argv[index];
|
|
421
|
+
if (token === "--") {
|
|
422
|
+
commandArgs.push(...argv.slice(index + 1));
|
|
423
|
+
return {
|
|
424
|
+
projectPath,
|
|
425
|
+
commandArgs
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
if (isHelpFlag(token)) return successResult(renderRuneDevHelp());
|
|
429
|
+
const projectResult = tryParseProjectOption(argv, index);
|
|
430
|
+
if (projectResult) {
|
|
431
|
+
if ("exitCode" in projectResult) return projectResult;
|
|
432
|
+
projectPath = projectResult.projectPath;
|
|
433
|
+
index = projectResult.nextIndex - 1;
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
commandArgs.push(token, ...argv.slice(index + 1));
|
|
437
|
+
return {
|
|
438
|
+
projectPath,
|
|
439
|
+
commandArgs
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
projectPath,
|
|
444
|
+
commandArgs
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function parseBuildArgs(argv) {
|
|
448
|
+
let projectPath;
|
|
449
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
450
|
+
const token = argv[index];
|
|
451
|
+
if (isHelpFlag(token)) return successResult(renderRuneBuildHelp());
|
|
452
|
+
const projectResult = tryParseProjectOption(argv, index);
|
|
453
|
+
if (projectResult) {
|
|
454
|
+
if ("exitCode" in projectResult) return projectResult;
|
|
455
|
+
projectPath = projectResult.projectPath;
|
|
456
|
+
index = projectResult.nextIndex - 1;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
return failureResult(`Unexpected argument for rune build: ${token}`);
|
|
460
|
+
}
|
|
461
|
+
return { projectPath };
|
|
462
|
+
}
|
|
463
|
+
async function runRuneCli(options) {
|
|
464
|
+
const [subcommand, ...restArgs] = options.argv;
|
|
465
|
+
if (!subcommand || isHelpFlag(subcommand)) return successResult(renderRuneCliHelp());
|
|
466
|
+
if (subcommand === "dev") {
|
|
467
|
+
const parsedDevArgs = parseDevArgs(restArgs);
|
|
468
|
+
if ("exitCode" in parsedDevArgs) return parsedDevArgs;
|
|
469
|
+
return runDevCommand({
|
|
470
|
+
rawArgs: parsedDevArgs.commandArgs,
|
|
471
|
+
projectPath: parsedDevArgs.projectPath,
|
|
472
|
+
cwd: options.cwd
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (subcommand === "build") {
|
|
476
|
+
const parsedBuildArgs = parseBuildArgs(restArgs);
|
|
477
|
+
if ("exitCode" in parsedBuildArgs) return parsedBuildArgs;
|
|
478
|
+
return runBuildCommand({
|
|
479
|
+
projectPath: parsedBuildArgs.projectPath,
|
|
480
|
+
cwd: options.cwd
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return failureResult(`Unknown rune command: ${subcommand}`);
|
|
484
|
+
}
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region src/cli.ts
|
|
487
|
+
await writeCommandExecutionResult(await runRuneCli({
|
|
488
|
+
argv: process.argv.slice(2),
|
|
489
|
+
cwd: process.cwd()
|
|
490
|
+
}));
|
|
491
|
+
//#endregion
|
|
492
|
+
export {};
|
package/dist/index.d.mts
ADDED
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CommandArgField, CommandExecutionResult, CommandOptionField, DefinedCommand } from "@rune-cli/core";
|
|
2
|
+
|
|
3
|
+
//#region src/manifest/manifest-types.d.ts
|
|
4
|
+
type CommandManifestPath = readonly string[];
|
|
5
|
+
type CommandManifestNodeKind = "command" | "group";
|
|
6
|
+
interface CommandManifestNodeBase {
|
|
7
|
+
readonly pathSegments: CommandManifestPath;
|
|
8
|
+
readonly kind: CommandManifestNodeKind;
|
|
9
|
+
readonly childNames: readonly string[];
|
|
10
|
+
readonly description?: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
interface CommandManifestCommandNode extends CommandManifestNodeBase {
|
|
13
|
+
readonly kind: "command";
|
|
14
|
+
readonly sourceFilePath: string;
|
|
15
|
+
}
|
|
16
|
+
interface CommandManifestGroupNode extends CommandManifestNodeBase {
|
|
17
|
+
readonly kind: "group";
|
|
18
|
+
readonly sourceFilePath?: undefined;
|
|
19
|
+
}
|
|
20
|
+
type CommandManifestNode = CommandManifestCommandNode | CommandManifestGroupNode;
|
|
21
|
+
interface CommandManifest {
|
|
22
|
+
readonly nodes: readonly CommandManifestNode[];
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/manifest/render-help.d.ts
|
|
26
|
+
type LoadCommandFn = (node: CommandManifestCommandNode) => Promise<DefinedCommand<readonly CommandArgField[], readonly CommandOptionField[]>>;
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/manifest/run-manifest-command.d.ts
|
|
29
|
+
interface RunManifestCommandOptions {
|
|
30
|
+
readonly manifest: CommandManifest;
|
|
31
|
+
readonly rawArgs: readonly string[];
|
|
32
|
+
readonly cliName: string;
|
|
33
|
+
readonly cwd?: string | undefined;
|
|
34
|
+
readonly loadCommand?: LoadCommandFn | undefined;
|
|
35
|
+
}
|
|
36
|
+
declare function runManifestCommand(options: RunManifestCommandOptions): Promise<CommandExecutionResult>;
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/cli/write-result.d.ts
|
|
39
|
+
declare function writeCommandExecutionResult(result: CommandExecutionResult): Promise<void>;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { runManifestCommand, writeCommandExecutionResult };
|
package/dist/runtime.mjs
ADDED
package/dist/test.d.mts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { CommandArgField, CommandExecutionResult, CommandOptionField, DefinedCommand, ExecuteCommandInput, InferExecutionFields } from "@rune-cli/core";
|
|
2
|
+
|
|
3
|
+
//#region src/test.d.ts
|
|
4
|
+
type RunCommandOptions<TOptions, TArgs> = ExecuteCommandInput<TOptions, TArgs>;
|
|
5
|
+
declare function runCommand<TArgsFields extends readonly CommandArgField[], TOptionsFields extends readonly CommandOptionField[]>(command: DefinedCommand<TArgsFields, TOptionsFields>, options?: RunCommandOptions<InferExecutionFields<TOptionsFields>, InferExecutionFields<TArgsFields>>): Promise<CommandExecutionResult>;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { RunCommandOptions, runCommand };
|
package/dist/test.mjs
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { executeCommand, formatFieldTypeHint, isSchemaField, parseCommand } from "@rune-cli/core";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
//#region src/cli/result.ts
|
|
4
|
+
function successResult(stdout) {
|
|
5
|
+
return {
|
|
6
|
+
exitCode: 0,
|
|
7
|
+
stdout,
|
|
8
|
+
stderr: ""
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function failureResult(stderr) {
|
|
12
|
+
return {
|
|
13
|
+
exitCode: 1,
|
|
14
|
+
stdout: "",
|
|
15
|
+
stderr: stderr.endsWith("\n") ? stderr : `${stderr}\n`
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/manifest/manifest-map.ts
|
|
20
|
+
function commandManifestPathToKey(pathSegments) {
|
|
21
|
+
return pathSegments.join(" ");
|
|
22
|
+
}
|
|
23
|
+
function createCommandManifestNodeMap(manifest) {
|
|
24
|
+
return Object.fromEntries(manifest.nodes.map((node) => [commandManifestPathToKey(node.pathSegments), node]));
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/manifest/render-help.ts
|
|
28
|
+
async function loadCommandFromModule(sourceFilePath) {
|
|
29
|
+
const loadedModule = await import(pathToFileURL(sourceFilePath).href);
|
|
30
|
+
if (loadedModule.default === void 0) throw new Error(`Command module did not export a default command: ${sourceFilePath}`);
|
|
31
|
+
return loadedModule.default;
|
|
32
|
+
}
|
|
33
|
+
const defaultLoadCommand = (node) => loadCommandFromModule(node.sourceFilePath);
|
|
34
|
+
function formatCommandName(cliName, pathSegments) {
|
|
35
|
+
return pathSegments.length === 0 ? cliName : `${cliName} ${pathSegments.join(" ")}`;
|
|
36
|
+
}
|
|
37
|
+
function formatSectionEntries(entries) {
|
|
38
|
+
return entries.map(({ label, description }) => ` ${label}${description ? ` ${description}` : ""}`).join("\n");
|
|
39
|
+
}
|
|
40
|
+
function formatArgumentLabel(field) {
|
|
41
|
+
return `${field.name}${formatFieldTypeHint(field)}`;
|
|
42
|
+
}
|
|
43
|
+
function formatOptionLabel(field) {
|
|
44
|
+
const longOptionLabel = `--${field.name}${formatFieldTypeHint(field)}`;
|
|
45
|
+
if (!field.alias) return longOptionLabel;
|
|
46
|
+
return `-${field.alias}, ${longOptionLabel}`;
|
|
47
|
+
}
|
|
48
|
+
async function isFieldRequired(field) {
|
|
49
|
+
if (!isSchemaField(field)) return field.required === true && field.default === void 0;
|
|
50
|
+
return !("value" in await field.schema["~standard"].validate(void 0));
|
|
51
|
+
}
|
|
52
|
+
async function formatUsageArguments(fields) {
|
|
53
|
+
const usageParts = [];
|
|
54
|
+
for (const field of fields) {
|
|
55
|
+
const required = await isFieldRequired(field);
|
|
56
|
+
usageParts.push(required ? `<${field.name}>` : `[${field.name}]`);
|
|
57
|
+
}
|
|
58
|
+
return usageParts.join(" ");
|
|
59
|
+
}
|
|
60
|
+
function getOptionUsageSuffix(fields) {
|
|
61
|
+
return fields.length === 0 ? "" : "[options]";
|
|
62
|
+
}
|
|
63
|
+
function renderGroupHelp(manifest, node, cliName) {
|
|
64
|
+
const nodeMap = createCommandManifestNodeMap(manifest);
|
|
65
|
+
const entries = node.childNames.map((childName) => {
|
|
66
|
+
return {
|
|
67
|
+
label: childName,
|
|
68
|
+
description: nodeMap[commandManifestPathToKey([...node.pathSegments, childName])]?.description
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
const parts = [`Usage: ${formatCommandName(cliName, node.pathSegments)} <command>`];
|
|
72
|
+
if (entries.length > 0) parts.push(`Subcommands:\n${formatSectionEntries(entries)}`);
|
|
73
|
+
return `${parts.join("\n\n")}\n`;
|
|
74
|
+
}
|
|
75
|
+
async function renderCommandHelp(command, pathSegments, cliName) {
|
|
76
|
+
const usageArguments = await formatUsageArguments(command.args);
|
|
77
|
+
const optionUsageSuffix = getOptionUsageSuffix(command.options);
|
|
78
|
+
const parts = [`Usage: ${[
|
|
79
|
+
formatCommandName(cliName, pathSegments),
|
|
80
|
+
usageArguments,
|
|
81
|
+
optionUsageSuffix
|
|
82
|
+
].filter((part) => part.length > 0).join(" ")}`];
|
|
83
|
+
if (command.description) parts.push(`Description:\n ${command.description}`);
|
|
84
|
+
if (command.args.length > 0) parts.push(`Arguments:\n${formatSectionEntries(command.args.map((field) => ({
|
|
85
|
+
label: formatArgumentLabel(field),
|
|
86
|
+
description: field.description
|
|
87
|
+
})))}`);
|
|
88
|
+
const optionEntries = [...command.options.map((field) => ({
|
|
89
|
+
label: formatOptionLabel(field),
|
|
90
|
+
description: field.description
|
|
91
|
+
})), {
|
|
92
|
+
label: "-h, --help",
|
|
93
|
+
description: "Show help"
|
|
94
|
+
}];
|
|
95
|
+
parts.push(`Options:\n${formatSectionEntries(optionEntries)}`);
|
|
96
|
+
return `${parts.join("\n\n")}\n`;
|
|
97
|
+
}
|
|
98
|
+
function renderUnknownCommandMessage(route, cliName) {
|
|
99
|
+
const parts = [`Unknown command: ${formatCommandName(cliName, route.attemptedPath)}`];
|
|
100
|
+
if (route.suggestions.length > 0) parts.push(`Did you mean?\n${route.suggestions.map((name) => ` ${name}`).join("\n")}`);
|
|
101
|
+
return `${parts.join("\n\n")}\n`;
|
|
102
|
+
}
|
|
103
|
+
async function renderResolvedHelp(options) {
|
|
104
|
+
if (options.route.kind === "unknown") return renderUnknownCommandMessage(options.route, options.cliName);
|
|
105
|
+
if (options.route.kind === "group") return renderGroupHelp(options.manifest, options.route.node, options.cliName);
|
|
106
|
+
return renderCommandHelp(await (options.loadCommand ?? defaultLoadCommand)(options.route.node), options.route.matchedPath, options.cliName);
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/manifest/damerau-levenshtein.ts
|
|
110
|
+
function damerauLevenshteinDistance(left, right) {
|
|
111
|
+
const rows = left.length + 1;
|
|
112
|
+
const cols = right.length + 1;
|
|
113
|
+
const matrix = Array.from({ length: rows }, () => Array(cols).fill(0));
|
|
114
|
+
for (let row = 0; row < rows; row += 1) matrix[row][0] = row;
|
|
115
|
+
for (let col = 0; col < cols; col += 1) matrix[0][col] = col;
|
|
116
|
+
for (let row = 1; row < rows; row += 1) for (let col = 1; col < cols; col += 1) {
|
|
117
|
+
const substitutionCost = left[row - 1] === right[col - 1] ? 0 : 1;
|
|
118
|
+
matrix[row][col] = Math.min(matrix[row - 1][col] + 1, matrix[row][col - 1] + 1, matrix[row - 1][col - 1] + substitutionCost);
|
|
119
|
+
if (row > 1 && col > 1 && left[row - 1] === right[col - 2] && left[row - 2] === right[col - 1]) matrix[row][col] = Math.min(matrix[row][col], matrix[row - 2][col - 2] + 1);
|
|
120
|
+
}
|
|
121
|
+
return matrix[left.length][right.length];
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/manifest/resolve-command-path.ts
|
|
125
|
+
function isOptionLikeToken(token) {
|
|
126
|
+
return token === "--" || token.startsWith("-");
|
|
127
|
+
}
|
|
128
|
+
function getHelpRequested(args) {
|
|
129
|
+
return args.includes("--help") || args.includes("-h");
|
|
130
|
+
}
|
|
131
|
+
function getSuggestionThreshold(candidate) {
|
|
132
|
+
return Math.max(2, Math.floor(candidate.length / 3));
|
|
133
|
+
}
|
|
134
|
+
function getSuggestedChildNames(unknownSegment, childNames) {
|
|
135
|
+
return [...childNames].map((childName) => ({
|
|
136
|
+
childName,
|
|
137
|
+
distance: damerauLevenshteinDistance(unknownSegment, childName)
|
|
138
|
+
})).filter(({ childName, distance }) => distance <= getSuggestionThreshold(childName)).sort((left, right) => left.distance - right.distance || left.childName.localeCompare(right.childName)).slice(0, 3).map(({ childName }) => childName);
|
|
139
|
+
}
|
|
140
|
+
function resolveCommandPath(manifest, rawArgs) {
|
|
141
|
+
const nodeMap = createCommandManifestNodeMap(manifest);
|
|
142
|
+
const rootNode = nodeMap[""];
|
|
143
|
+
if (rootNode === void 0) throw new Error("Manifest root node is missing");
|
|
144
|
+
let currentNode = rootNode;
|
|
145
|
+
let tokenIndex = 0;
|
|
146
|
+
while (tokenIndex < rawArgs.length) {
|
|
147
|
+
const token = rawArgs[tokenIndex];
|
|
148
|
+
if (isOptionLikeToken(token)) break;
|
|
149
|
+
const childNode = nodeMap[commandManifestPathToKey([...currentNode.pathSegments, token])];
|
|
150
|
+
if (childNode === void 0) {
|
|
151
|
+
const suggestions = getSuggestedChildNames(token, currentNode.childNames);
|
|
152
|
+
if (currentNode.kind === "group" || suggestions.length > 0) return {
|
|
153
|
+
kind: "unknown",
|
|
154
|
+
attemptedPath: [...currentNode.pathSegments, token],
|
|
155
|
+
matchedPath: currentNode.pathSegments,
|
|
156
|
+
unknownSegment: token,
|
|
157
|
+
availableChildNames: currentNode.childNames,
|
|
158
|
+
suggestions
|
|
159
|
+
};
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
currentNode = childNode;
|
|
163
|
+
tokenIndex += 1;
|
|
164
|
+
}
|
|
165
|
+
const remainingArgs = rawArgs.slice(tokenIndex);
|
|
166
|
+
const helpRequested = getHelpRequested(remainingArgs);
|
|
167
|
+
if (currentNode.kind === "group") return {
|
|
168
|
+
kind: "group",
|
|
169
|
+
node: currentNode,
|
|
170
|
+
matchedPath: currentNode.pathSegments,
|
|
171
|
+
remainingArgs,
|
|
172
|
+
helpRequested
|
|
173
|
+
};
|
|
174
|
+
return {
|
|
175
|
+
kind: "command",
|
|
176
|
+
node: currentNode,
|
|
177
|
+
matchedPath: currentNode.pathSegments,
|
|
178
|
+
remainingArgs,
|
|
179
|
+
helpRequested
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
//#endregion
|
|
183
|
+
//#region src/manifest/run-manifest-command.ts
|
|
184
|
+
async function runManifestCommand(options) {
|
|
185
|
+
const route = resolveCommandPath(options.manifest, options.rawArgs);
|
|
186
|
+
if (route.kind === "unknown" || route.kind === "group" || route.helpRequested) {
|
|
187
|
+
const output = await renderResolvedHelp({
|
|
188
|
+
manifest: options.manifest,
|
|
189
|
+
route,
|
|
190
|
+
cliName: options.cliName,
|
|
191
|
+
loadCommand: options.loadCommand
|
|
192
|
+
});
|
|
193
|
+
return route.kind === "unknown" ? failureResult(output) : successResult(output);
|
|
194
|
+
}
|
|
195
|
+
const command = await (options.loadCommand ?? defaultLoadCommand)(route.node);
|
|
196
|
+
const parsed = await parseCommand(command, route.remainingArgs);
|
|
197
|
+
if (!parsed.ok) return failureResult(parsed.error.message);
|
|
198
|
+
return executeCommand(command, {
|
|
199
|
+
options: parsed.value.options,
|
|
200
|
+
args: parsed.value.args,
|
|
201
|
+
cwd: options.cwd,
|
|
202
|
+
rawArgs: parsed.value.rawArgs
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/cli/write-result.ts
|
|
207
|
+
async function writeStream(stream, contents) {
|
|
208
|
+
if (contents.length === 0) return;
|
|
209
|
+
await new Promise((resolve, reject) => {
|
|
210
|
+
stream.write(contents, (error) => {
|
|
211
|
+
if (error) {
|
|
212
|
+
reject(error);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
resolve();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
async function writeCommandExecutionResult(result) {
|
|
220
|
+
await writeStream(process.stdout, result.stdout);
|
|
221
|
+
await writeStream(process.stderr, result.stderr);
|
|
222
|
+
process.exitCode = result.exitCode;
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
export { successResult as i, runManifestCommand as n, failureResult as r, writeCommandExecutionResult as t };
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rune-cli/rune",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Rune is a CLI framework built around the concept of file-based command routing.",
|
|
5
|
+
"homepage": "https://github.com/morinokami/rune#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/morinokami/rune/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"author": "Shinya Fujino <shf0811@gmail.com> (https://github.com/morinokami)",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/morinokami/rune.git",
|
|
14
|
+
"directory": "packages/rune"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"rune": "./dist/cli.mjs"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.mts",
|
|
26
|
+
"import": "./dist/index.mjs"
|
|
27
|
+
},
|
|
28
|
+
"./cli": {
|
|
29
|
+
"types": "./dist/cli.d.mts",
|
|
30
|
+
"import": "./dist/cli.mjs"
|
|
31
|
+
},
|
|
32
|
+
"./test": {
|
|
33
|
+
"types": "./dist/test.d.mts",
|
|
34
|
+
"import": "./dist/test.mjs"
|
|
35
|
+
},
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"esbuild": "0.27.4",
|
|
43
|
+
"@rune-cli/core": "0.0.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "24.12.0",
|
|
47
|
+
"@typescript/native-preview": "7.0.0-dev.20260317.1",
|
|
48
|
+
"bumpp": "11.0.1",
|
|
49
|
+
"typescript": "5.9.3",
|
|
50
|
+
"vite-plus": "v0.1.13"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"typescript": ">=5.0.0"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=24.12.0"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "vp pack",
|
|
60
|
+
"dev": "vp pack --watch",
|
|
61
|
+
"test": "vp test",
|
|
62
|
+
"check": "vp check"
|
|
63
|
+
}
|
|
64
|
+
}
|