@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 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 {};
@@ -0,0 +1,2 @@
1
+ import { defineCommand } from "@rune-cli/core";
2
+ export { defineCommand };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { defineCommand } from "@rune-cli/core";
2
+ export { defineCommand };
@@ -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 };
@@ -0,0 +1,2 @@
1
+ import { n as runManifestCommand, t as writeCommandExecutionResult } from "./write-result-C0wgFsjj.mjs";
2
+ export { runManifestCommand, writeCommandExecutionResult };
@@ -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,7 @@
1
+ import { executeCommand } from "@rune-cli/core";
2
+ //#region src/test.ts
3
+ async function runCommand(command, options = {}) {
4
+ return executeCommand(command, options);
5
+ }
6
+ //#endregion
7
+ export { runCommand };
@@ -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
+ }