@kidd-cli/core 0.1.0
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/README.md +214 -0
- package/dist/config-BvGapuFJ.js +282 -0
- package/dist/config-BvGapuFJ.js.map +1 -0
- package/dist/create-store-BQUX0tAn.js +197 -0
- package/dist/create-store-BQUX0tAn.js.map +1 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1034 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/config.d.ts +64 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +4 -0
- package/dist/lib/logger.d.ts +2 -0
- package/dist/lib/logger.js +55 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/output.d.ts +62 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +276 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/project.d.ts +59 -0
- package/dist/lib/project.d.ts.map +1 -0
- package/dist/lib/project.js +3 -0
- package/dist/lib/prompts.d.ts +24 -0
- package/dist/lib/prompts.d.ts.map +1 -0
- package/dist/lib/prompts.js +3 -0
- package/dist/lib/store.d.ts +56 -0
- package/dist/lib/store.d.ts.map +1 -0
- package/dist/lib/store.js +4 -0
- package/dist/logger-BkQQej8h.d.ts +76 -0
- package/dist/logger-BkQQej8h.d.ts.map +1 -0
- package/dist/middleware/auth.d.ts +22 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +759 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/http.d.ts +87 -0
- package/dist/middleware/http.d.ts.map +1 -0
- package/dist/middleware/http.js +255 -0
- package/dist/middleware/http.js.map +1 -0
- package/dist/middleware-D3psyhYo.js +54 -0
- package/dist/middleware-D3psyhYo.js.map +1 -0
- package/dist/project-NPtYX2ZX.js +181 -0
- package/dist/project-NPtYX2ZX.js.map +1 -0
- package/dist/prompts-lLfUSgd6.js +63 -0
- package/dist/prompts-lLfUSgd6.js.map +1 -0
- package/dist/types-CqKJhsYk.d.ts +135 -0
- package/dist/types-CqKJhsYk.d.ts.map +1 -0
- package/dist/types-Cz9h927W.d.ts +23 -0
- package/dist/types-Cz9h927W.d.ts.map +1 -0
- package/dist/types-DFtYg5uZ.d.ts +26 -0
- package/dist/types-DFtYg5uZ.d.ts.map +1 -0
- package/dist/types-kjpRau0U.d.ts +382 -0
- package/dist/types-kjpRau0U.d.ts.map +1 -0
- package/package.json +94 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1034 @@
|
|
|
1
|
+
import { createCliLogger } from "./lib/logger.js";
|
|
2
|
+
import { i as createSpinner, r as createPromptUtils } from "./prompts-lLfUSgd6.js";
|
|
3
|
+
import { n as DEFAULT_EXIT_CODE, t as createConfigClient } from "./config-BvGapuFJ.js";
|
|
4
|
+
import { n as decorateContext, t as middleware } from "./middleware-D3psyhYo.js";
|
|
5
|
+
import "./project-NPtYX2ZX.js";
|
|
6
|
+
import { basename, extname, join, resolve } from "node:path";
|
|
7
|
+
import { loadConfig } from "@kidd-cli/config/loader";
|
|
8
|
+
import { attemptAsync, err, isPlainObject, isString, ok } from "@kidd-cli/utils/fp";
|
|
9
|
+
import yargs from "yargs";
|
|
10
|
+
import { TAG, hasTag, withTag } from "@kidd-cli/utils/tag";
|
|
11
|
+
import { jsonStringify } from "@kidd-cli/utils/json";
|
|
12
|
+
import { readdir } from "node:fs/promises";
|
|
13
|
+
import { formatZodIssues } from "@kidd-cli/utils/validate";
|
|
14
|
+
import { match as match$1 } from "ts-pattern";
|
|
15
|
+
import { defineConfig } from "@kidd-cli/config";
|
|
16
|
+
|
|
17
|
+
//#region src/context/error.ts
|
|
18
|
+
/**
|
|
19
|
+
* Create a ContextError with an exit code and optional error code.
|
|
20
|
+
*
|
|
21
|
+
* Used to surface user-facing CLI errors with clean messages.
|
|
22
|
+
* The error carries a Symbol-based tag for reliable type-narrowing
|
|
23
|
+
* via {@link isContextError}.
|
|
24
|
+
*
|
|
25
|
+
* @param message - Human-readable error message.
|
|
26
|
+
* @param options - Optional error code and exit code overrides.
|
|
27
|
+
* @returns A ContextError instance.
|
|
28
|
+
*/
|
|
29
|
+
function createContextError(message, options) {
|
|
30
|
+
const data = createContextErrorData(message, options);
|
|
31
|
+
const error = new Error(data.message);
|
|
32
|
+
error.name = "ContextError";
|
|
33
|
+
Object.defineProperty(error, TAG, {
|
|
34
|
+
enumerable: false,
|
|
35
|
+
value: "ContextError",
|
|
36
|
+
writable: false
|
|
37
|
+
});
|
|
38
|
+
Object.defineProperty(error, "code", {
|
|
39
|
+
enumerable: true,
|
|
40
|
+
value: data.code,
|
|
41
|
+
writable: false
|
|
42
|
+
});
|
|
43
|
+
Object.defineProperty(error, "exitCode", {
|
|
44
|
+
enumerable: true,
|
|
45
|
+
value: data.exitCode,
|
|
46
|
+
writable: false
|
|
47
|
+
});
|
|
48
|
+
return error;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Type guard that narrows an unknown value to {@link ContextError}.
|
|
52
|
+
*
|
|
53
|
+
* Checks that the value is an Error instance whose `[TAG]` property
|
|
54
|
+
* equals `'ContextError'`, which distinguishes CLI-layer errors from
|
|
55
|
+
* unexpected exceptions.
|
|
56
|
+
*
|
|
57
|
+
* @param error - The value to check.
|
|
58
|
+
* @returns `true` when the value is a ContextError.
|
|
59
|
+
*/
|
|
60
|
+
function isContextError(error) {
|
|
61
|
+
if (error instanceof Error) return hasTag(error, "ContextError");
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
function resolveExitCode(options) {
|
|
65
|
+
if (options && options.exitCode !== void 0) return options.exitCode;
|
|
66
|
+
return DEFAULT_EXIT_CODE;
|
|
67
|
+
}
|
|
68
|
+
function resolveCode(options) {
|
|
69
|
+
if (options && options.code !== void 0) return options.code;
|
|
70
|
+
}
|
|
71
|
+
function createContextErrorData(message, options) {
|
|
72
|
+
return withTag({
|
|
73
|
+
code: resolveCode(options),
|
|
74
|
+
exitCode: resolveExitCode(options),
|
|
75
|
+
message
|
|
76
|
+
}, "ContextError");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/context/output.ts
|
|
81
|
+
/**
|
|
82
|
+
* Create the structured output methods for a context.
|
|
83
|
+
*
|
|
84
|
+
* @private
|
|
85
|
+
* @param stream - The writable stream to write output to.
|
|
86
|
+
* @returns An Output instance backed by the given stream.
|
|
87
|
+
*/
|
|
88
|
+
function createContextOutput(stream) {
|
|
89
|
+
return {
|
|
90
|
+
markdown(content) {
|
|
91
|
+
stream.write(`${content}\n`);
|
|
92
|
+
},
|
|
93
|
+
raw(content) {
|
|
94
|
+
stream.write(content);
|
|
95
|
+
},
|
|
96
|
+
table(rows, options) {
|
|
97
|
+
if (options && options.json) {
|
|
98
|
+
const [, json] = jsonStringify(rows, { pretty: true });
|
|
99
|
+
stream.write(`${json}\n`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (rows.length === 0) return;
|
|
103
|
+
const [firstRow] = rows;
|
|
104
|
+
if (!firstRow) return;
|
|
105
|
+
writeTableToStream(stream, rows, Object.keys(firstRow));
|
|
106
|
+
},
|
|
107
|
+
write(data, options) {
|
|
108
|
+
if (options && options.json || typeof data === "object" && data !== null) {
|
|
109
|
+
const [, json] = jsonStringify(data, { pretty: true });
|
|
110
|
+
stream.write(`${json}\n`);
|
|
111
|
+
} else stream.write(`${String(data)}\n`);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Format an unknown value as a string for table cell display.
|
|
117
|
+
*
|
|
118
|
+
* @private
|
|
119
|
+
* @param val - The value to format.
|
|
120
|
+
* @returns The stringified value, or empty string for undefined.
|
|
121
|
+
*/
|
|
122
|
+
function formatStringValue(val) {
|
|
123
|
+
if (val === void 0) return "";
|
|
124
|
+
return String(val);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Create a padded header row string from column keys and widths.
|
|
128
|
+
*
|
|
129
|
+
* @private
|
|
130
|
+
* @param options - The keys and column widths.
|
|
131
|
+
* @returns A formatted header string.
|
|
132
|
+
*/
|
|
133
|
+
function createTableHeader(options) {
|
|
134
|
+
const { keys, widths } = options;
|
|
135
|
+
return keys.map((key, idx) => {
|
|
136
|
+
const width = widths[idx];
|
|
137
|
+
if (width === void 0) return key;
|
|
138
|
+
return key.padEnd(width);
|
|
139
|
+
}).join(" ");
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Create a padded row string from a data record, column keys, and widths.
|
|
143
|
+
*
|
|
144
|
+
* @private
|
|
145
|
+
* @param options - The row data, keys, and column widths.
|
|
146
|
+
* @returns A formatted row string.
|
|
147
|
+
*/
|
|
148
|
+
function createTableRow(options) {
|
|
149
|
+
const { row, keys, widths } = options;
|
|
150
|
+
return keys.map((key, idx) => {
|
|
151
|
+
const width = widths[idx];
|
|
152
|
+
const val = formatStringValue(row[key]);
|
|
153
|
+
if (width === void 0) return val;
|
|
154
|
+
return val.padEnd(width);
|
|
155
|
+
}).join(" ");
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Compute the maximum column width for each key across all rows.
|
|
159
|
+
*
|
|
160
|
+
* @private
|
|
161
|
+
* @param rows - The data rows.
|
|
162
|
+
* @param keys - The column keys.
|
|
163
|
+
* @returns An array of column widths.
|
|
164
|
+
*/
|
|
165
|
+
function computeColumnWidths(rows, keys) {
|
|
166
|
+
return keys.map((key) => {
|
|
167
|
+
const values = rows.map((row) => formatStringValue(row[key]));
|
|
168
|
+
return Math.max(key.length, ...values.map((val) => val.length));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Write a formatted table (header, separator, rows) to a writable stream.
|
|
173
|
+
*
|
|
174
|
+
* @private
|
|
175
|
+
* @param stream - The writable stream.
|
|
176
|
+
* @param rows - The data rows.
|
|
177
|
+
* @param keys - The column keys.
|
|
178
|
+
*/
|
|
179
|
+
function writeTableToStream(stream, rows, keys) {
|
|
180
|
+
const widths = computeColumnWidths(rows, keys);
|
|
181
|
+
const content = [
|
|
182
|
+
createTableHeader({
|
|
183
|
+
keys,
|
|
184
|
+
widths
|
|
185
|
+
}),
|
|
186
|
+
widths.map((width) => "-".repeat(width)).join(" "),
|
|
187
|
+
...rows.map((row) => createTableRow({
|
|
188
|
+
keys,
|
|
189
|
+
row,
|
|
190
|
+
widths
|
|
191
|
+
}))
|
|
192
|
+
].join("\n");
|
|
193
|
+
stream.write(`${content}\n`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/context/prompts.ts
|
|
198
|
+
/**
|
|
199
|
+
* Create the interactive prompt methods for a context.
|
|
200
|
+
*
|
|
201
|
+
* @private
|
|
202
|
+
* @returns A Prompts instance backed by clack.
|
|
203
|
+
*/
|
|
204
|
+
function createContextPrompts() {
|
|
205
|
+
const utils = createPromptUtils();
|
|
206
|
+
return {
|
|
207
|
+
async confirm(opts) {
|
|
208
|
+
return unwrapCancelSignal(utils, await utils.confirm(opts));
|
|
209
|
+
},
|
|
210
|
+
async multiselect(opts) {
|
|
211
|
+
return unwrapCancelSignal(utils, await utils.multiselect(opts));
|
|
212
|
+
},
|
|
213
|
+
async password(opts) {
|
|
214
|
+
return unwrapCancelSignal(utils, await utils.password(opts));
|
|
215
|
+
},
|
|
216
|
+
async select(opts) {
|
|
217
|
+
return unwrapCancelSignal(utils, await utils.select(opts));
|
|
218
|
+
},
|
|
219
|
+
async text(opts) {
|
|
220
|
+
return unwrapCancelSignal(utils, await utils.text(opts));
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Unwrap a prompt result that may be a cancel symbol.
|
|
226
|
+
*
|
|
227
|
+
* If the user cancelled (Ctrl-C), throws a ContextError. Otherwise returns
|
|
228
|
+
* the typed result value.
|
|
229
|
+
*
|
|
230
|
+
* @private
|
|
231
|
+
* @param utils - The prompt utils instance (for isCancel and cancel).
|
|
232
|
+
* @param result - The raw prompt result (value or cancel symbol).
|
|
233
|
+
* @returns The unwrapped typed value.
|
|
234
|
+
*/
|
|
235
|
+
function unwrapCancelSignal(utils, result) {
|
|
236
|
+
if (utils.isCancel(result)) {
|
|
237
|
+
utils.cancel("Operation cancelled.");
|
|
238
|
+
throw createContextError("Prompt cancelled by user", {
|
|
239
|
+
code: "PROMPT_CANCELLED",
|
|
240
|
+
exitCode: DEFAULT_EXIT_CODE
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
//#endregion
|
|
247
|
+
//#region src/context/store.ts
|
|
248
|
+
/**
|
|
249
|
+
* Create an in-memory key-value store.
|
|
250
|
+
*
|
|
251
|
+
* @private
|
|
252
|
+
* @returns A Store instance backed by a Map.
|
|
253
|
+
*/
|
|
254
|
+
function createMemoryStore() {
|
|
255
|
+
const map = /* @__PURE__ */ new Map();
|
|
256
|
+
return {
|
|
257
|
+
clear() {
|
|
258
|
+
map.clear();
|
|
259
|
+
},
|
|
260
|
+
delete(key) {
|
|
261
|
+
return map.delete(key);
|
|
262
|
+
},
|
|
263
|
+
get(key) {
|
|
264
|
+
return map.get(key);
|
|
265
|
+
},
|
|
266
|
+
has(key) {
|
|
267
|
+
return map.has(key);
|
|
268
|
+
},
|
|
269
|
+
set(key, value) {
|
|
270
|
+
map.set(key, value);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/context/create-context.ts
|
|
277
|
+
/**
|
|
278
|
+
* Create the {@link Context} object threaded through middleware and command handlers.
|
|
279
|
+
*
|
|
280
|
+
* Assembles logger, spinner, output, store, prompts, and meta from
|
|
281
|
+
* the provided options into a single immutable context. Each sub-system is
|
|
282
|
+
* constructed via its own factory so this function remains a lean orchestrator.
|
|
283
|
+
*
|
|
284
|
+
* @param options - Args, config, and meta for the current invocation.
|
|
285
|
+
* @returns A fully constructed Context.
|
|
286
|
+
*/
|
|
287
|
+
function createContext(options) {
|
|
288
|
+
const ctxLogger = options.logger ?? createCliLogger();
|
|
289
|
+
const ctxSpinner = createSpinner();
|
|
290
|
+
const ctxOutput = createContextOutput(options.output ?? process.stdout);
|
|
291
|
+
const ctxStore = createMemoryStore();
|
|
292
|
+
const ctxPrompts = createContextPrompts();
|
|
293
|
+
const ctxMeta = {
|
|
294
|
+
command: options.meta.command,
|
|
295
|
+
name: options.meta.name,
|
|
296
|
+
version: options.meta.version
|
|
297
|
+
};
|
|
298
|
+
return {
|
|
299
|
+
args: options.args,
|
|
300
|
+
config: options.config,
|
|
301
|
+
fail(message, failOptions) {
|
|
302
|
+
throw createContextError(message, failOptions);
|
|
303
|
+
},
|
|
304
|
+
logger: ctxLogger,
|
|
305
|
+
meta: ctxMeta,
|
|
306
|
+
output: ctxOutput,
|
|
307
|
+
prompts: ctxPrompts,
|
|
308
|
+
spinner: ctxSpinner,
|
|
309
|
+
store: ctxStore
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region src/autoloader.ts
|
|
315
|
+
const VALID_EXTENSIONS = new Set([
|
|
316
|
+
".ts",
|
|
317
|
+
".js",
|
|
318
|
+
".mjs"
|
|
319
|
+
]);
|
|
320
|
+
const INDEX_NAME = "index";
|
|
321
|
+
/**
|
|
322
|
+
* Scan a directory for command files and produce a CommandMap.
|
|
323
|
+
*
|
|
324
|
+
* @param options - Autoload configuration (directory override, etc.).
|
|
325
|
+
* @returns A promise resolving to a CommandMap built from the directory tree.
|
|
326
|
+
*/
|
|
327
|
+
async function autoload(options) {
|
|
328
|
+
const dir = resolveDir(options);
|
|
329
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
330
|
+
const fileEntries = entries.filter(isCommandFile);
|
|
331
|
+
const dirEntries = entries.filter(isCommandDir);
|
|
332
|
+
const fileResults = await Promise.all(fileEntries.map(async (entry) => {
|
|
333
|
+
const cmd = await importCommand(join(dir, entry.name));
|
|
334
|
+
if (!cmd) return;
|
|
335
|
+
return [deriveCommandName(entry), cmd];
|
|
336
|
+
}));
|
|
337
|
+
const dirResults = await Promise.all(dirEntries.map((entry) => buildDirCommand(join(dir, entry.name))));
|
|
338
|
+
const validPairs = [...fileResults, ...dirResults].filter((pair) => pair !== void 0);
|
|
339
|
+
return Object.fromEntries(validPairs);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Resolve the target directory from autoload options.
|
|
343
|
+
*
|
|
344
|
+
* @private
|
|
345
|
+
* @param options - Optional autoload configuration.
|
|
346
|
+
* @returns The resolved absolute directory path.
|
|
347
|
+
*/
|
|
348
|
+
function resolveDir(options) {
|
|
349
|
+
if (options && isString(options.dir)) return resolve(options.dir);
|
|
350
|
+
return resolve("./commands");
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Scan a subdirectory and assemble it as a parent command with subcommands.
|
|
354
|
+
*
|
|
355
|
+
* If the directory contains an `index.ts`/`index.js`, that becomes the parent
|
|
356
|
+
* handler. Otherwise a handler-less group command is created that demands a
|
|
357
|
+
* subcommand.
|
|
358
|
+
*
|
|
359
|
+
* @private
|
|
360
|
+
* @param dir - Absolute path to the subdirectory.
|
|
361
|
+
* @returns A tuple of [name, Command] or undefined if the directory is empty.
|
|
362
|
+
*/
|
|
363
|
+
async function buildDirCommand(dir) {
|
|
364
|
+
const name = basename(dir);
|
|
365
|
+
const dirEntries = await readdir(dir, { withFileTypes: true });
|
|
366
|
+
const subCommands = await buildSubCommands(dir, dirEntries);
|
|
367
|
+
const indexFile = findIndexInEntries(dirEntries);
|
|
368
|
+
if (indexFile) {
|
|
369
|
+
const parentCommand = await importCommand(join(dir, indexFile.name));
|
|
370
|
+
if (parentCommand) return [name, withTag({
|
|
371
|
+
...parentCommand,
|
|
372
|
+
commands: subCommands
|
|
373
|
+
}, "Command")];
|
|
374
|
+
}
|
|
375
|
+
if (Object.keys(subCommands).length === 0) return;
|
|
376
|
+
return [name, withTag({ commands: subCommands }, "Command")];
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Build subcommands from already-read directory entries, avoiding a redundant readdir call.
|
|
380
|
+
*
|
|
381
|
+
* @private
|
|
382
|
+
* @param dir - Absolute path to the directory.
|
|
383
|
+
* @param entries - Pre-read directory entries.
|
|
384
|
+
* @returns A CommandMap built from the entries.
|
|
385
|
+
*/
|
|
386
|
+
async function buildSubCommands(dir, entries) {
|
|
387
|
+
const fileEntries = entries.filter(isCommandFile);
|
|
388
|
+
const dirEntries = entries.filter(isCommandDir);
|
|
389
|
+
const fileResults = await Promise.all(fileEntries.map(async (entry) => {
|
|
390
|
+
const cmd = await importCommand(join(dir, entry.name));
|
|
391
|
+
if (!cmd) return;
|
|
392
|
+
return [deriveCommandName(entry), cmd];
|
|
393
|
+
}));
|
|
394
|
+
const dirResults = await Promise.all(dirEntries.map((entry) => buildDirCommand(join(dir, entry.name))));
|
|
395
|
+
const validPairs = [...fileResults, ...dirResults].filter((pair) => pair !== void 0);
|
|
396
|
+
return Object.fromEntries(validPairs);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Find the index file (index.ts or index.js) in pre-read directory entries.
|
|
400
|
+
*
|
|
401
|
+
* @private
|
|
402
|
+
* @param entries - Pre-read directory entries.
|
|
403
|
+
* @returns The index file's Dirent or undefined.
|
|
404
|
+
*/
|
|
405
|
+
function findIndexInEntries(entries) {
|
|
406
|
+
return entries.find((entry) => entry.isFile() && VALID_EXTENSIONS.has(extname(entry.name)) && basename(entry.name, extname(entry.name)) === INDEX_NAME);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Dynamically import a file and validate that its default export is a Command.
|
|
410
|
+
*
|
|
411
|
+
* @private
|
|
412
|
+
* @param filePath - Absolute path to the file to import.
|
|
413
|
+
* @returns The Command if valid, or undefined.
|
|
414
|
+
*/
|
|
415
|
+
async function importCommand(filePath) {
|
|
416
|
+
const mod = await import(filePath);
|
|
417
|
+
if (isCommandExport(mod)) return mod.default;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Check whether a module's default export is a Command object.
|
|
421
|
+
*
|
|
422
|
+
* ES module namespace objects have a null prototype, so isPlainObject
|
|
423
|
+
* rejects them. We only need to verify the namespace is a non-null
|
|
424
|
+
* object with a default export that is a plain Command object.
|
|
425
|
+
*
|
|
426
|
+
* @private
|
|
427
|
+
* @param mod - The imported module to inspect.
|
|
428
|
+
* @returns True when the module has a Command as its default export.
|
|
429
|
+
*/
|
|
430
|
+
function isCommandExport(mod) {
|
|
431
|
+
if (typeof mod !== "object" || mod === null) return false;
|
|
432
|
+
const def = mod["default"];
|
|
433
|
+
if (!isPlainObject(def)) return false;
|
|
434
|
+
return hasTag(def, "Command");
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Derive a command name from a directory entry by stripping its extension.
|
|
438
|
+
*
|
|
439
|
+
* @private
|
|
440
|
+
* @param entry - The directory entry to derive the name from.
|
|
441
|
+
* @returns The file name without its extension.
|
|
442
|
+
*/
|
|
443
|
+
function deriveCommandName(entry) {
|
|
444
|
+
return basename(entry.name, extname(entry.name));
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Predicate: entry is a command file (.ts/.js, not index, not _/. prefixed).
|
|
448
|
+
*
|
|
449
|
+
* @private
|
|
450
|
+
* @param entry - The directory entry to check.
|
|
451
|
+
* @returns True when the entry is a valid command file.
|
|
452
|
+
*/
|
|
453
|
+
function isCommandFile(entry) {
|
|
454
|
+
if (!entry.isFile()) return false;
|
|
455
|
+
if (entry.name.startsWith("_") || entry.name.startsWith(".")) return false;
|
|
456
|
+
if (!VALID_EXTENSIONS.has(extname(entry.name))) return false;
|
|
457
|
+
return deriveCommandName(entry) !== INDEX_NAME;
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Predicate: entry is a scannable command directory (not _/. prefixed).
|
|
461
|
+
*
|
|
462
|
+
* @private
|
|
463
|
+
* @param entry - The directory entry to check.
|
|
464
|
+
* @returns True when the entry is a valid command directory.
|
|
465
|
+
*/
|
|
466
|
+
function isCommandDir(entry) {
|
|
467
|
+
if (!entry.isDirectory()) return false;
|
|
468
|
+
return !entry.name.startsWith("_") && !entry.name.startsWith(".");
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
//#endregion
|
|
472
|
+
//#region src/runtime/args/zod.ts
|
|
473
|
+
/**
|
|
474
|
+
* Type guard that checks whether a value is a zod object schema.
|
|
475
|
+
*
|
|
476
|
+
* @param args - The value to check.
|
|
477
|
+
* @returns True when args is a ZodObject.
|
|
478
|
+
*/
|
|
479
|
+
function isZodSchema(args) {
|
|
480
|
+
return typeof args === "object" && args !== null && "_def" in args && typeof args._def === "object" && args._def !== null && args._def.type === "object";
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Convert an entire zod object schema into a record of yargs options.
|
|
484
|
+
*
|
|
485
|
+
* @param schema - The zod object schema.
|
|
486
|
+
* @returns A record mapping field names to yargs option definitions.
|
|
487
|
+
*/
|
|
488
|
+
function zodSchemaToYargsOptions(schema) {
|
|
489
|
+
const shape = schema.shape;
|
|
490
|
+
return Object.fromEntries(Object.entries(shape).map(([key, fieldSchema]) => [key, getZodTypeOption(fieldSchema)]));
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Extract a default value from a zod definition, falling back to the provided value.
|
|
494
|
+
*
|
|
495
|
+
* @private
|
|
496
|
+
* @param def - The zod definition to inspect.
|
|
497
|
+
* @param fallback - Value to return when no default is defined.
|
|
498
|
+
* @returns The resolved default value.
|
|
499
|
+
*/
|
|
500
|
+
function resolveDefaultValue(def, fallback) {
|
|
501
|
+
if (def.defaultValue !== void 0) return def.defaultValue;
|
|
502
|
+
return fallback;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Unwrap a ZodOptional type, recursing into the inner type.
|
|
506
|
+
*
|
|
507
|
+
* @private
|
|
508
|
+
* @param options - The unwrap options containing def, current type, and default value.
|
|
509
|
+
* @returns Unwrapped type information.
|
|
510
|
+
*/
|
|
511
|
+
function unwrapOptional(options) {
|
|
512
|
+
const { def, current, defaultValue } = options;
|
|
513
|
+
if (def.innerType) return unwrapZodTypeRecursive({
|
|
514
|
+
current: def.innerType,
|
|
515
|
+
defaultValue,
|
|
516
|
+
isOptional: true
|
|
517
|
+
});
|
|
518
|
+
return {
|
|
519
|
+
defaultValue,
|
|
520
|
+
inner: current,
|
|
521
|
+
isOptional: true
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Unwrap a ZodDefault type, resolving its default value and recursing.
|
|
526
|
+
*
|
|
527
|
+
* @private
|
|
528
|
+
* @param options - The unwrap options containing def, current type, and default value.
|
|
529
|
+
* @returns Unwrapped type information with the resolved default.
|
|
530
|
+
*/
|
|
531
|
+
function unwrapDefault(options) {
|
|
532
|
+
const { def, current, defaultValue } = options;
|
|
533
|
+
const newDefault = resolveDefaultValue(def, defaultValue);
|
|
534
|
+
if (def.innerType) return unwrapZodTypeRecursive({
|
|
535
|
+
current: def.innerType,
|
|
536
|
+
defaultValue: newDefault,
|
|
537
|
+
isOptional: true
|
|
538
|
+
});
|
|
539
|
+
return {
|
|
540
|
+
defaultValue: newDefault,
|
|
541
|
+
inner: current,
|
|
542
|
+
isOptional: true
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Recursively unwrap optional and default wrappers from a zod type.
|
|
547
|
+
*
|
|
548
|
+
* @private
|
|
549
|
+
* @param options - The recursive unwrap options containing current type, optionality flag, and default value.
|
|
550
|
+
* @returns The fully unwrapped type information.
|
|
551
|
+
*/
|
|
552
|
+
function unwrapZodTypeRecursive(options) {
|
|
553
|
+
const { current, isOptional, defaultValue } = options;
|
|
554
|
+
const def = current._def;
|
|
555
|
+
if (def.type === "optional") return unwrapOptional({
|
|
556
|
+
current,
|
|
557
|
+
def,
|
|
558
|
+
defaultValue
|
|
559
|
+
});
|
|
560
|
+
if (def.type === "default") return unwrapDefault({
|
|
561
|
+
current,
|
|
562
|
+
def,
|
|
563
|
+
defaultValue
|
|
564
|
+
});
|
|
565
|
+
return {
|
|
566
|
+
defaultValue,
|
|
567
|
+
inner: current,
|
|
568
|
+
isOptional
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Unwrap a zod schema to extract its base type, optionality, and default value.
|
|
573
|
+
*
|
|
574
|
+
* @private
|
|
575
|
+
* @param schema - The zod type to unwrap.
|
|
576
|
+
* @returns The unwrapped type information.
|
|
577
|
+
*/
|
|
578
|
+
function unwrapZodType(schema) {
|
|
579
|
+
return unwrapZodTypeRecursive({
|
|
580
|
+
current: schema,
|
|
581
|
+
defaultValue: void 0,
|
|
582
|
+
isOptional: false
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Map a zod type name to a yargs option type string.
|
|
587
|
+
*
|
|
588
|
+
* @private
|
|
589
|
+
* @param typeName - The zod type name (e.g. 'string', 'number').
|
|
590
|
+
* @returns The corresponding yargs type.
|
|
591
|
+
*/
|
|
592
|
+
function resolveZodYargsType(typeName) {
|
|
593
|
+
return match$1(typeName).with("string", () => "string").with("number", () => "number").with("boolean", () => "boolean").with("array", () => "array").otherwise(() => "string");
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Build a base yargs option from a zod schema's description and default.
|
|
597
|
+
*
|
|
598
|
+
* @private
|
|
599
|
+
* @param inner - The unwrapped zod schema instance.
|
|
600
|
+
* @param defaultValue - The resolved default value.
|
|
601
|
+
* @returns A partial yargs option object.
|
|
602
|
+
*/
|
|
603
|
+
function buildBaseOption(inner, defaultValue) {
|
|
604
|
+
const base = {};
|
|
605
|
+
const { description } = inner;
|
|
606
|
+
if (description) base.describe = description;
|
|
607
|
+
if (defaultValue !== void 0) base.default = defaultValue;
|
|
608
|
+
return base;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Convert a single zod field schema into a complete yargs option definition.
|
|
612
|
+
*
|
|
613
|
+
* @private
|
|
614
|
+
* @param schema - A single zod field type.
|
|
615
|
+
* @returns A complete yargs option object.
|
|
616
|
+
*/
|
|
617
|
+
function getZodTypeOption(schema) {
|
|
618
|
+
const { inner, isOptional, defaultValue } = unwrapZodType(schema);
|
|
619
|
+
const innerDef = inner._def;
|
|
620
|
+
const base = {
|
|
621
|
+
...buildBaseOption(inner, defaultValue),
|
|
622
|
+
type: resolveZodYargsType(innerDef.type)
|
|
623
|
+
};
|
|
624
|
+
if (!isOptional) return {
|
|
625
|
+
...base,
|
|
626
|
+
demandOption: true
|
|
627
|
+
};
|
|
628
|
+
return base;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
//#endregion
|
|
632
|
+
//#region src/runtime/args/parser.ts
|
|
633
|
+
/**
|
|
634
|
+
* Create an args parser that cleans and validates raw parsed arguments.
|
|
635
|
+
*
|
|
636
|
+
* Captures the argument definition in a closure and returns an ArgsParser
|
|
637
|
+
* whose `parse` method strips yargs-internal keys and validates against
|
|
638
|
+
* a zod schema when one is defined.
|
|
639
|
+
*
|
|
640
|
+
* @param argsDef - The argument definition from the command.
|
|
641
|
+
* @returns An ArgsParser with a parse method.
|
|
642
|
+
*/
|
|
643
|
+
function createArgsParser(argsDef) {
|
|
644
|
+
return { parse(rawArgs) {
|
|
645
|
+
return validateArgs(argsDef, cleanParsedArgs(rawArgs));
|
|
646
|
+
} };
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Strip yargs-internal keys (`_`, `$0`) and camelCase-duplicated hyphenated keys
|
|
650
|
+
* from a parsed argv record, returning only user-defined arguments.
|
|
651
|
+
*
|
|
652
|
+
* @private
|
|
653
|
+
* @param argv - Raw parsed argv from yargs.
|
|
654
|
+
* @returns A cleaned record containing only user-defined arguments.
|
|
655
|
+
*/
|
|
656
|
+
function cleanParsedArgs(argv) {
|
|
657
|
+
return Object.fromEntries(Object.entries(argv).filter(([key]) => key !== "_" && key !== "$0" && !key.includes("-")));
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Validate parsed arguments against a zod schema when one is defined.
|
|
661
|
+
*
|
|
662
|
+
* If the command uses yargs-native args (no zod schema), the parsed args are
|
|
663
|
+
* returned as-is. When a zod schema is present, validation is performed and
|
|
664
|
+
* a Result error is returned on failure.
|
|
665
|
+
*
|
|
666
|
+
* @private
|
|
667
|
+
* @param argsDef - The argument definition from the command.
|
|
668
|
+
* @param parsedArgs - The cleaned parsed arguments.
|
|
669
|
+
* @returns A Result containing validated arguments (zod-parsed when applicable).
|
|
670
|
+
*/
|
|
671
|
+
function validateArgs(argsDef, parsedArgs) {
|
|
672
|
+
if (!argsDef || !isZodSchema(argsDef)) return ok(parsedArgs);
|
|
673
|
+
const result = argsDef.safeParse(parsedArgs);
|
|
674
|
+
if (!result.success) return err(/* @__PURE__ */ new Error(`Invalid arguments:\n ${formatZodIssues(result.error.issues).message}`));
|
|
675
|
+
return ok(result.data);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
//#endregion
|
|
679
|
+
//#region src/runtime/args/register.ts
|
|
680
|
+
/**
|
|
681
|
+
* Register argument definitions on a yargs builder.
|
|
682
|
+
*
|
|
683
|
+
* Accepts either a zod object schema or a record of yargs-native arg definitions
|
|
684
|
+
* and wires them as yargs options on the given builder instance.
|
|
685
|
+
*
|
|
686
|
+
* @param builder - The yargs Argv instance to register options on.
|
|
687
|
+
* @param args - Argument definitions from a Command.
|
|
688
|
+
*/
|
|
689
|
+
function registerCommandArgs(builder, args) {
|
|
690
|
+
if (!args) return;
|
|
691
|
+
if (isZodSchema(args)) {
|
|
692
|
+
const options = zodSchemaToYargsOptions(args);
|
|
693
|
+
for (const [key, opt] of Object.entries(options)) builder.option(key, opt);
|
|
694
|
+
} else {
|
|
695
|
+
const argsDef = args;
|
|
696
|
+
for (const [key, def] of Object.entries(argsDef)) builder.option(key, yargsArgDefToOption(def));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Convert a yargs-native arg definition into a yargs option object.
|
|
701
|
+
*
|
|
702
|
+
* @private
|
|
703
|
+
* @param def - The yargs arg definition.
|
|
704
|
+
* @returns A yargs option object.
|
|
705
|
+
*/
|
|
706
|
+
function yargsArgDefToOption(def) {
|
|
707
|
+
return {
|
|
708
|
+
alias: def.alias,
|
|
709
|
+
choices: def.choices,
|
|
710
|
+
default: def.default,
|
|
711
|
+
demandOption: def.required ?? false,
|
|
712
|
+
describe: def.description,
|
|
713
|
+
type: def.type
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
//#endregion
|
|
718
|
+
//#region src/runtime/register.ts
|
|
719
|
+
/**
|
|
720
|
+
* Type guard that checks whether a value is a Command object.
|
|
721
|
+
*
|
|
722
|
+
* @param value - The value to test.
|
|
723
|
+
* @returns True when the value has `[TAG] === 'Command'`.
|
|
724
|
+
*/
|
|
725
|
+
function isCommand(value) {
|
|
726
|
+
return hasTag(value, "Command");
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Register all commands from a CommandMap on a yargs instance.
|
|
730
|
+
*
|
|
731
|
+
* Iterates over the command map, filters for valid Command objects,
|
|
732
|
+
* and recursively registers each command (including subcommands) on
|
|
733
|
+
* the provided yargs Argv instance.
|
|
734
|
+
*
|
|
735
|
+
* @param options - Registration options including the command map, yargs instance, and resolution ref.
|
|
736
|
+
*/
|
|
737
|
+
function registerCommands(options) {
|
|
738
|
+
const { instance, commands, resolved, parentPath } = options;
|
|
739
|
+
const commandEntries = Object.entries(commands).filter(([, entry]) => isCommand(entry));
|
|
740
|
+
for (const [name, entry] of commandEntries) registerResolvedCommand({
|
|
741
|
+
builder: instance,
|
|
742
|
+
cmd: entry,
|
|
743
|
+
instance,
|
|
744
|
+
name,
|
|
745
|
+
parentPath,
|
|
746
|
+
resolved
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Register a single resolved command (and its subcommands) with yargs.
|
|
751
|
+
*
|
|
752
|
+
* Sets up the yargs command handler, wires argument definitions, and
|
|
753
|
+
* recursively registers any nested subcommands. On match, stores the
|
|
754
|
+
* resolved handler and command path in the shared ref.
|
|
755
|
+
*
|
|
756
|
+
* @private
|
|
757
|
+
* @param options - Command registration context.
|
|
758
|
+
*/
|
|
759
|
+
function registerResolvedCommand(options) {
|
|
760
|
+
const { instance, name, cmd, resolved, parentPath } = options;
|
|
761
|
+
const description = cmd.description ?? "";
|
|
762
|
+
instance.command(name, description, (builder) => {
|
|
763
|
+
registerCommandArgs(builder, cmd.args);
|
|
764
|
+
if (cmd.commands) {
|
|
765
|
+
const subCommands = Object.entries(cmd.commands).filter(([, entry]) => isCommand(entry));
|
|
766
|
+
for (const [subName, subEntry] of subCommands) registerResolvedCommand({
|
|
767
|
+
builder,
|
|
768
|
+
cmd: subEntry,
|
|
769
|
+
instance: builder,
|
|
770
|
+
name: subName,
|
|
771
|
+
parentPath: [...parentPath, name],
|
|
772
|
+
resolved
|
|
773
|
+
});
|
|
774
|
+
if (cmd.handler) builder.demandCommand(0);
|
|
775
|
+
else builder.demandCommand(1, "You must specify a subcommand.");
|
|
776
|
+
}
|
|
777
|
+
return builder;
|
|
778
|
+
}, () => {
|
|
779
|
+
resolved.ref = {
|
|
780
|
+
args: cmd.args,
|
|
781
|
+
commandPath: [...parentPath, name],
|
|
782
|
+
handler: cmd.handler,
|
|
783
|
+
middleware: cmd.middleware ?? []
|
|
784
|
+
};
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
//#endregion
|
|
789
|
+
//#region src/runtime/runner.ts
|
|
790
|
+
/**
|
|
791
|
+
* Create a runner that executes root and command middleware chains.
|
|
792
|
+
*
|
|
793
|
+
* Root middleware wraps the command middleware chain, which in turn wraps
|
|
794
|
+
* the command handler — producing a nested onion lifecycle:
|
|
795
|
+
*
|
|
796
|
+
* ```
|
|
797
|
+
* root middleware start →
|
|
798
|
+
* command middleware start →
|
|
799
|
+
* handler
|
|
800
|
+
* command middleware end
|
|
801
|
+
* root middleware end
|
|
802
|
+
* ```
|
|
803
|
+
*
|
|
804
|
+
* @param rootMiddleware - Root-level middleware from `cli({ middleware })`.
|
|
805
|
+
* @returns A Runner with an execute method.
|
|
806
|
+
*/
|
|
807
|
+
function createRunner(rootMiddleware) {
|
|
808
|
+
return { async execute({ ctx, handler, middleware }) {
|
|
809
|
+
const commandHandler = async (innerCtx) => {
|
|
810
|
+
await runMiddlewareChain(middleware, innerCtx, handler);
|
|
811
|
+
};
|
|
812
|
+
await runMiddlewareChain(rootMiddleware, ctx, commandHandler);
|
|
813
|
+
} };
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Execute a middleware chain followed by a final handler.
|
|
817
|
+
*
|
|
818
|
+
* Runs each middleware in order, passing `ctx` and a `next` callback.
|
|
819
|
+
* When all middleware have called `next()`, the final handler is invoked.
|
|
820
|
+
* A middleware can short-circuit by not calling `next()`.
|
|
821
|
+
*
|
|
822
|
+
* @private
|
|
823
|
+
* @param middlewares - Ordered array of middleware to execute.
|
|
824
|
+
* @param ctx - The context object threaded through middleware and handler.
|
|
825
|
+
* @param finalHandler - The command handler to invoke after all middleware.
|
|
826
|
+
*/
|
|
827
|
+
async function runMiddlewareChain(middlewares, ctx, finalHandler) {
|
|
828
|
+
async function executeChain(index) {
|
|
829
|
+
if (index >= middlewares.length) {
|
|
830
|
+
await finalHandler(ctx);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const mw = middlewares[index];
|
|
834
|
+
if (mw) await mw.handler(ctx, () => executeChain(index + 1));
|
|
835
|
+
}
|
|
836
|
+
await executeChain(0);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
//#endregion
|
|
840
|
+
//#region src/runtime/runtime.ts
|
|
841
|
+
/**
|
|
842
|
+
* Create a runtime that orchestrates config loading and middleware execution.
|
|
843
|
+
*
|
|
844
|
+
* Loads config up front, then captures it in a closure alongside a runner.
|
|
845
|
+
* The returned `runtime.execute` method handles arg parsing, context creation,
|
|
846
|
+
* and middleware chain execution for each command invocation.
|
|
847
|
+
*
|
|
848
|
+
* @param options - Runtime configuration including name, version, config, and middleware.
|
|
849
|
+
* @returns An AsyncResult containing the runtime or an error.
|
|
850
|
+
*/
|
|
851
|
+
async function createRuntime(options) {
|
|
852
|
+
const config = await resolveConfig(options.config, options.name);
|
|
853
|
+
const runner = createRunner(options.middleware ?? []);
|
|
854
|
+
return ok({ async execute(command) {
|
|
855
|
+
const [argsError, validatedArgs] = createArgsParser(command.args).parse(command.rawArgs);
|
|
856
|
+
if (argsError) return err(argsError);
|
|
857
|
+
const ctx = createContext({
|
|
858
|
+
args: validatedArgs,
|
|
859
|
+
config,
|
|
860
|
+
meta: {
|
|
861
|
+
command: command.commandPath,
|
|
862
|
+
name: options.name,
|
|
863
|
+
version: options.version
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
const finalHandler = command.handler ?? (async () => {});
|
|
867
|
+
const [execError] = await attemptAsync(() => runner.execute({
|
|
868
|
+
ctx,
|
|
869
|
+
handler: finalHandler,
|
|
870
|
+
middleware: command.middleware
|
|
871
|
+
}));
|
|
872
|
+
if (execError) return err(execError);
|
|
873
|
+
return ok();
|
|
874
|
+
} });
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Load and validate a config file via the config client.
|
|
878
|
+
*
|
|
879
|
+
* Returns the validated config record or an empty object when no config
|
|
880
|
+
* options are provided or when loading fails.
|
|
881
|
+
*
|
|
882
|
+
* @private
|
|
883
|
+
* @param configOptions - Config loading options with schema and optional name override.
|
|
884
|
+
* @param defaultName - Fallback config file name derived from the CLI name.
|
|
885
|
+
* @returns The loaded config record or an empty object.
|
|
886
|
+
*/
|
|
887
|
+
async function resolveConfig(configOptions, defaultName) {
|
|
888
|
+
if (!configOptions || !configOptions.schema) return {};
|
|
889
|
+
const [configError, configResult] = await createConfigClient({
|
|
890
|
+
name: configOptions.name ?? defaultName,
|
|
891
|
+
schema: configOptions.schema
|
|
892
|
+
}).load();
|
|
893
|
+
if (configError || !configResult) return {};
|
|
894
|
+
return configResult.config;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
//#endregion
|
|
898
|
+
//#region src/cli.ts
|
|
899
|
+
const ARGV_SLICE_START = 2;
|
|
900
|
+
/**
|
|
901
|
+
* Bootstrap and run the CLI application.
|
|
902
|
+
*
|
|
903
|
+
* Parses argv, resolves the matched command, loads config, runs the
|
|
904
|
+
* middleware chain, and invokes the command handler.
|
|
905
|
+
*
|
|
906
|
+
* @param options - CLI configuration including name, version, commands, and middleware.
|
|
907
|
+
*/
|
|
908
|
+
async function cli(options) {
|
|
909
|
+
const logger = createCliLogger();
|
|
910
|
+
const [uncaughtError, result] = await attemptAsync(async () => {
|
|
911
|
+
const program = yargs(process.argv.slice(ARGV_SLICE_START)).scriptName(options.name).version(options.version).strict().help().option("cwd", {
|
|
912
|
+
describe: "Set the working directory",
|
|
913
|
+
global: true,
|
|
914
|
+
type: "string"
|
|
915
|
+
});
|
|
916
|
+
if (options.description) program.usage(options.description);
|
|
917
|
+
const resolved = { ref: void 0 };
|
|
918
|
+
const commands = await resolveCommands(options.commands);
|
|
919
|
+
if (commands) {
|
|
920
|
+
registerCommands({
|
|
921
|
+
commands,
|
|
922
|
+
instance: program,
|
|
923
|
+
parentPath: [],
|
|
924
|
+
resolved
|
|
925
|
+
});
|
|
926
|
+
program.demandCommand(1, "You must specify a command.");
|
|
927
|
+
}
|
|
928
|
+
const argv = await program.parseAsync();
|
|
929
|
+
applyCwd(argv);
|
|
930
|
+
if (!resolved.ref) return;
|
|
931
|
+
const [runtimeError, runtime] = await createRuntime({
|
|
932
|
+
config: options.config,
|
|
933
|
+
middleware: options.middleware,
|
|
934
|
+
name: options.name,
|
|
935
|
+
version: options.version
|
|
936
|
+
});
|
|
937
|
+
if (runtimeError) return runtimeError;
|
|
938
|
+
const [executeError] = await runtime.execute({
|
|
939
|
+
args: resolved.ref.args,
|
|
940
|
+
commandPath: resolved.ref.commandPath,
|
|
941
|
+
handler: resolved.ref.handler,
|
|
942
|
+
middleware: resolved.ref.middleware,
|
|
943
|
+
rawArgs: argv
|
|
944
|
+
});
|
|
945
|
+
return executeError;
|
|
946
|
+
});
|
|
947
|
+
if (uncaughtError) {
|
|
948
|
+
exitOnError(uncaughtError, logger);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (result) exitOnError(result, logger);
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Resolve the commands option to a CommandMap.
|
|
955
|
+
*
|
|
956
|
+
* Accepts a directory string (triggers autoload), a static CommandMap,
|
|
957
|
+
* a Promise<CommandMap> (from autoload() called at the call site),
|
|
958
|
+
* or undefined (loads `kidd.config.ts` and autoloads from its `commands` field,
|
|
959
|
+
* falling back to `'./commands'`).
|
|
960
|
+
*
|
|
961
|
+
* @private
|
|
962
|
+
* @param commands - The commands option from CliOptions.
|
|
963
|
+
* @returns A CommandMap or undefined.
|
|
964
|
+
*/
|
|
965
|
+
async function resolveCommands(commands) {
|
|
966
|
+
if (isString(commands)) return autoload({ dir: commands });
|
|
967
|
+
if (commands instanceof Promise) return commands;
|
|
968
|
+
if (isPlainObject(commands)) return commands;
|
|
969
|
+
return resolveCommandsFromConfig();
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Load `kidd.config.ts` and autoload commands from its `commands` field.
|
|
973
|
+
*
|
|
974
|
+
* Falls back to `'./commands'` when the config file is missing, fails to load,
|
|
975
|
+
* or does not specify a `commands` field.
|
|
976
|
+
*
|
|
977
|
+
* @private
|
|
978
|
+
* @returns A CommandMap autoloaded from the configured commands directory.
|
|
979
|
+
*/
|
|
980
|
+
async function resolveCommandsFromConfig() {
|
|
981
|
+
const DEFAULT_COMMANDS_DIR = "./commands";
|
|
982
|
+
const [configError, configResult] = await loadConfig();
|
|
983
|
+
if (configError || !configResult) return autoload({ dir: DEFAULT_COMMANDS_DIR });
|
|
984
|
+
return autoload({ dir: configResult.config.commands ?? DEFAULT_COMMANDS_DIR });
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Change the process working directory when `--cwd` is provided.
|
|
988
|
+
*
|
|
989
|
+
* Resolves the value to an absolute path and calls `process.chdir()` so
|
|
990
|
+
* that all downstream `process.cwd()` calls reflect the override.
|
|
991
|
+
*
|
|
992
|
+
* @private
|
|
993
|
+
* @param argv - The parsed argv record from yargs.
|
|
994
|
+
*/
|
|
995
|
+
function applyCwd(argv) {
|
|
996
|
+
if (isString(argv.cwd)) process.chdir(resolve(argv.cwd));
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* Handle a CLI error by logging the message and exiting with the appropriate code.
|
|
1000
|
+
*
|
|
1001
|
+
* ContextErrors carry a custom exit code; all other errors exit with code 1.
|
|
1002
|
+
*
|
|
1003
|
+
* @private
|
|
1004
|
+
* @param error - The caught error value.
|
|
1005
|
+
* @param logger - Logger with an error method for output.
|
|
1006
|
+
*/
|
|
1007
|
+
function exitOnError(error, logger) {
|
|
1008
|
+
if (isContextError(error)) {
|
|
1009
|
+
logger.error(error.message);
|
|
1010
|
+
process.exit(error.exitCode);
|
|
1011
|
+
} else if (error instanceof Error) {
|
|
1012
|
+
logger.error(error.message);
|
|
1013
|
+
process.exit(DEFAULT_EXIT_CODE);
|
|
1014
|
+
} else {
|
|
1015
|
+
logger.error(String(error));
|
|
1016
|
+
process.exit(DEFAULT_EXIT_CODE);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
//#endregion
|
|
1021
|
+
//#region src/command.ts
|
|
1022
|
+
/**
|
|
1023
|
+
* Define a CLI command with typed args, config, and handler.
|
|
1024
|
+
*
|
|
1025
|
+
* @param def - Command definition including description, args schema, and handler.
|
|
1026
|
+
* @returns A resolved Command object for registration in the command map.
|
|
1027
|
+
*/
|
|
1028
|
+
function command(def) {
|
|
1029
|
+
return withTag({ ...def }, "Command");
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
//#endregion
|
|
1033
|
+
export { autoload, cli, command, decorateContext, defineConfig, middleware };
|
|
1034
|
+
//# sourceMappingURL=index.js.map
|