just-bash-util 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 +140 -0
- package/dist/chunk-DAO5RF73.js +153 -0
- package/dist/chunk-DLIJNNDI.js +221 -0
- package/dist/chunk-OLDCCFR6.js +793 -0
- package/dist/command/index.d.ts +304 -0
- package/dist/command/index.js +22 -0
- package/dist/config/index.d.ts +126 -0
- package/dist/config/index.js +11 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +56 -0
- package/dist/path/index.d.ts +39 -0
- package/dist/path/index.js +28 -0
- package/package.json +52 -0
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
// src/command/builders/option.ts
|
|
2
|
+
var OptionBuilder = class _OptionBuilder {
|
|
3
|
+
/** @internal */
|
|
4
|
+
_def;
|
|
5
|
+
constructor(def) {
|
|
6
|
+
this._def = def;
|
|
7
|
+
}
|
|
8
|
+
/** Add a description */
|
|
9
|
+
describe(text) {
|
|
10
|
+
return new _OptionBuilder({ ...this._def, description: text });
|
|
11
|
+
}
|
|
12
|
+
/** Set a short alias (single character) */
|
|
13
|
+
short(alias) {
|
|
14
|
+
return new _OptionBuilder({ ...this._def, short: alias });
|
|
15
|
+
}
|
|
16
|
+
/** Set an environment variable fallback */
|
|
17
|
+
env(name) {
|
|
18
|
+
return new _OptionBuilder({ ...this._def, env: name });
|
|
19
|
+
}
|
|
20
|
+
/** Mark as required — removes undefined from TOut */
|
|
21
|
+
required() {
|
|
22
|
+
return new _OptionBuilder({
|
|
23
|
+
...this._def,
|
|
24
|
+
required: true
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** Set a default value — removes undefined from TOut */
|
|
28
|
+
default(value) {
|
|
29
|
+
return new _OptionBuilder({
|
|
30
|
+
...this._def,
|
|
31
|
+
default: value
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
function createOption(type) {
|
|
36
|
+
return new OptionBuilder({
|
|
37
|
+
_kind: "option",
|
|
38
|
+
type
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function string() {
|
|
42
|
+
return createOption("string");
|
|
43
|
+
}
|
|
44
|
+
function number() {
|
|
45
|
+
return createOption("number");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/command/builders/flag.ts
|
|
49
|
+
var FlagBuilder = class _FlagBuilder {
|
|
50
|
+
/** @internal */
|
|
51
|
+
_def;
|
|
52
|
+
constructor(def = { _kind: "flag" }) {
|
|
53
|
+
this._def = def;
|
|
54
|
+
}
|
|
55
|
+
/** Add a description */
|
|
56
|
+
describe(text) {
|
|
57
|
+
return new _FlagBuilder({ ...this._def, description: text });
|
|
58
|
+
}
|
|
59
|
+
/** Set a short alias (single character) */
|
|
60
|
+
short(alias) {
|
|
61
|
+
return new _FlagBuilder({ ...this._def, short: alias });
|
|
62
|
+
}
|
|
63
|
+
/** Set a default value */
|
|
64
|
+
default(value) {
|
|
65
|
+
return new _FlagBuilder({ ...this._def, default: value });
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// src/command/builders/arg.ts
|
|
70
|
+
var ArgBuilder = class _ArgBuilder {
|
|
71
|
+
/** @internal */
|
|
72
|
+
_def;
|
|
73
|
+
constructor(def) {
|
|
74
|
+
this._def = def;
|
|
75
|
+
}
|
|
76
|
+
/** Set the positional arg name (used for named access in the handler) */
|
|
77
|
+
name(name) {
|
|
78
|
+
return new _ArgBuilder({
|
|
79
|
+
...this._def,
|
|
80
|
+
name
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Add a description */
|
|
84
|
+
describe(text) {
|
|
85
|
+
return new _ArgBuilder({ ...this._def, description: text });
|
|
86
|
+
}
|
|
87
|
+
/** Mark as optional — adds undefined to TOut */
|
|
88
|
+
optional() {
|
|
89
|
+
return new _ArgBuilder({
|
|
90
|
+
...this._def,
|
|
91
|
+
required: false
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
/** Mark as variadic — collects all remaining positionals into an array */
|
|
95
|
+
variadic() {
|
|
96
|
+
return new _ArgBuilder({
|
|
97
|
+
...this._def,
|
|
98
|
+
variadic: true
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** Set a default value (also makes the arg optional at parse time) */
|
|
102
|
+
default(value) {
|
|
103
|
+
return new _ArgBuilder({
|
|
104
|
+
...this._def,
|
|
105
|
+
required: false,
|
|
106
|
+
default: value
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
function createArg(type) {
|
|
111
|
+
return new ArgBuilder({
|
|
112
|
+
_kind: "arg",
|
|
113
|
+
type,
|
|
114
|
+
required: true
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function string2() {
|
|
118
|
+
return createArg("string");
|
|
119
|
+
}
|
|
120
|
+
function number2() {
|
|
121
|
+
return createArg("number");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/command/builders/index.ts
|
|
125
|
+
var o = {
|
|
126
|
+
string,
|
|
127
|
+
number
|
|
128
|
+
};
|
|
129
|
+
function f() {
|
|
130
|
+
return new FlagBuilder();
|
|
131
|
+
}
|
|
132
|
+
var a = {
|
|
133
|
+
string: string2,
|
|
134
|
+
number: number2
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// src/command/errors.ts
|
|
138
|
+
function formatError(error) {
|
|
139
|
+
switch (error.type) {
|
|
140
|
+
case "unknown_option": {
|
|
141
|
+
let msg = `Unknown option "${error.name}".`;
|
|
142
|
+
if (error.suggestions.length > 0) {
|
|
143
|
+
msg += ` Did you mean ${error.suggestions.map((s) => `"${s}"`).join(" or ")}?`;
|
|
144
|
+
}
|
|
145
|
+
return msg;
|
|
146
|
+
}
|
|
147
|
+
case "invalid_type":
|
|
148
|
+
return `Invalid value for "${error.name}": expected ${error.expected}, got "${error.received}".`;
|
|
149
|
+
case "missing_required":
|
|
150
|
+
return error.kind === "option" ? `Missing required option "--${error.name}".` : `Missing required argument <${error.name}>.`;
|
|
151
|
+
case "unexpected_positional":
|
|
152
|
+
return error.maxPositionals === 0 ? `Unexpected argument "${error.value}". This command takes no positional arguments.` : `Unexpected argument "${error.value}". Expected at most ${error.maxPositionals} positional argument${error.maxPositionals === 1 ? "" : "s"}.`;
|
|
153
|
+
case "missing_value":
|
|
154
|
+
return `Option "--${error.name}" requires a value.`;
|
|
155
|
+
case "unknown_command": {
|
|
156
|
+
let msg = `Unknown command "${error.path}".`;
|
|
157
|
+
if (error.suggestions.length > 0) {
|
|
158
|
+
msg += ` Did you mean ${error.suggestions.map((s) => `"${s}"`).join(" or ")}?`;
|
|
159
|
+
}
|
|
160
|
+
return msg;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function formatErrors(errors) {
|
|
165
|
+
return errors.map(formatError).join("\n");
|
|
166
|
+
}
|
|
167
|
+
function levenshtein(a2, b) {
|
|
168
|
+
const m = a2.length;
|
|
169
|
+
const n = b.length;
|
|
170
|
+
const dp = new Array((m + 1) * (n + 1));
|
|
171
|
+
for (let i = 0; i <= m; i++) dp[i * (n + 1)] = i;
|
|
172
|
+
for (let j = 0; j <= n; j++) dp[j] = j;
|
|
173
|
+
for (let i = 1; i <= m; i++) {
|
|
174
|
+
for (let j = 1; j <= n; j++) {
|
|
175
|
+
const cost = a2[i - 1] === b[j - 1] ? 0 : 1;
|
|
176
|
+
dp[i * (n + 1) + j] = Math.min(
|
|
177
|
+
dp[(i - 1) * (n + 1) + j] + 1,
|
|
178
|
+
dp[i * (n + 1) + (j - 1)] + 1,
|
|
179
|
+
dp[(i - 1) * (n + 1) + (j - 1)] + cost
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return dp[m * (n + 1) + n];
|
|
184
|
+
}
|
|
185
|
+
function findSuggestions(input, candidates, maxDistance = 3) {
|
|
186
|
+
const scored = candidates.map((c) => ({ candidate: c, distance: levenshtein(input, c) })).filter((x) => x.distance <= maxDistance && x.distance > 0).sort((a2, b) => a2.distance - b.distance);
|
|
187
|
+
return scored.slice(0, 2).map((x) => x.candidate);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/command/parser.ts
|
|
191
|
+
function parseArgs(options, argDefs, tokens, env) {
|
|
192
|
+
const errors = [];
|
|
193
|
+
const longMap = /* @__PURE__ */ new Map();
|
|
194
|
+
const shortMap = /* @__PURE__ */ new Map();
|
|
195
|
+
for (const [key, def] of Object.entries(options)) {
|
|
196
|
+
const longName = camelToKebab(key);
|
|
197
|
+
longMap.set(longName, { key, def });
|
|
198
|
+
if (def.short) {
|
|
199
|
+
shortMap.set(def.short, { key, def });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const result = {};
|
|
203
|
+
const positionals = [];
|
|
204
|
+
const passthrough = [];
|
|
205
|
+
let i = 0;
|
|
206
|
+
while (i < tokens.length) {
|
|
207
|
+
const token = tokens[i];
|
|
208
|
+
if (token === "--") {
|
|
209
|
+
i++;
|
|
210
|
+
while (i < tokens.length) {
|
|
211
|
+
passthrough.push(tokens[i]);
|
|
212
|
+
i++;
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
if (token.startsWith("--")) {
|
|
217
|
+
const eqIdx = token.indexOf("=");
|
|
218
|
+
let longName;
|
|
219
|
+
let inlineValue;
|
|
220
|
+
if (eqIdx !== -1) {
|
|
221
|
+
longName = token.slice(2, eqIdx);
|
|
222
|
+
inlineValue = token.slice(eqIdx + 1);
|
|
223
|
+
} else {
|
|
224
|
+
longName = token.slice(2);
|
|
225
|
+
}
|
|
226
|
+
const entry = longMap.get(longName);
|
|
227
|
+
if (!entry) {
|
|
228
|
+
if (longName.startsWith("no-")) {
|
|
229
|
+
const positiveEntry = longMap.get(longName.slice(3));
|
|
230
|
+
if (positiveEntry && positiveEntry.def._kind === "flag") {
|
|
231
|
+
result[positiveEntry.key] = false;
|
|
232
|
+
i++;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const allLongNames = [...longMap.keys()];
|
|
237
|
+
errors.push({
|
|
238
|
+
type: "unknown_option",
|
|
239
|
+
name: `--${longName}`,
|
|
240
|
+
suggestions: findSuggestions(longName, allLongNames).map((s) => `--${s}`)
|
|
241
|
+
});
|
|
242
|
+
i++;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (entry.def._kind === "flag") {
|
|
246
|
+
result[entry.key] = true;
|
|
247
|
+
i++;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const rawValue = inlineValue ?? tokens[++i];
|
|
251
|
+
if (rawValue === void 0) {
|
|
252
|
+
errors.push({ type: "missing_value", name: entry.key });
|
|
253
|
+
i++;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const parsed = coerce(rawValue, entry.def.type, entry.key, errors);
|
|
257
|
+
if (parsed !== void 0) {
|
|
258
|
+
result[entry.key] = parsed;
|
|
259
|
+
}
|
|
260
|
+
i++;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (token.startsWith("-") && token.length > 1) {
|
|
264
|
+
const chars = token.slice(1);
|
|
265
|
+
for (let j = 0; j < chars.length; j++) {
|
|
266
|
+
const ch = chars[j];
|
|
267
|
+
const entry = shortMap.get(ch);
|
|
268
|
+
if (!entry) {
|
|
269
|
+
errors.push({
|
|
270
|
+
type: "unknown_option",
|
|
271
|
+
name: `-${ch}`,
|
|
272
|
+
suggestions: []
|
|
273
|
+
});
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (entry.def._kind === "flag") {
|
|
277
|
+
result[entry.key] = true;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const restOfString = chars.slice(j + 1);
|
|
281
|
+
const rawValue = restOfString.length > 0 ? restOfString : tokens[++i];
|
|
282
|
+
if (rawValue === void 0) {
|
|
283
|
+
errors.push({ type: "missing_value", name: entry.key });
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
const parsed = coerce(rawValue, entry.def.type, entry.key, errors);
|
|
287
|
+
if (parsed !== void 0) {
|
|
288
|
+
result[entry.key] = parsed;
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
i++;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
positionals.push(token);
|
|
296
|
+
i++;
|
|
297
|
+
}
|
|
298
|
+
let posIdx = 0;
|
|
299
|
+
for (let idx = 0; idx < argDefs.length; idx++) {
|
|
300
|
+
const argDef = argDefs[idx];
|
|
301
|
+
const argName = argDef.name ?? `arg${idx}`;
|
|
302
|
+
if (argDef.variadic) {
|
|
303
|
+
const values = positionals.slice(posIdx);
|
|
304
|
+
if (values.length > 0) {
|
|
305
|
+
result[argName] = values.map((v) => coerce(v, argDef.type, argName, errors));
|
|
306
|
+
} else if (argDef.required) {
|
|
307
|
+
errors.push({ type: "missing_required", name: argName, kind: "arg" });
|
|
308
|
+
} else if (argDef.default !== void 0) {
|
|
309
|
+
result[argName] = argDef.default;
|
|
310
|
+
}
|
|
311
|
+
posIdx = positionals.length;
|
|
312
|
+
} else {
|
|
313
|
+
const value = positionals[posIdx];
|
|
314
|
+
if (value !== void 0) {
|
|
315
|
+
result[argName] = coerce(value, argDef.type, argName, errors);
|
|
316
|
+
posIdx++;
|
|
317
|
+
} else if (argDef.required) {
|
|
318
|
+
errors.push({ type: "missing_required", name: argName, kind: "arg" });
|
|
319
|
+
} else if (argDef.default !== void 0) {
|
|
320
|
+
result[argName] = argDef.default;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (posIdx < positionals.length) {
|
|
325
|
+
for (let j = posIdx; j < positionals.length; j++) {
|
|
326
|
+
errors.push({
|
|
327
|
+
type: "unexpected_positional",
|
|
328
|
+
value: positionals[j],
|
|
329
|
+
maxPositionals: argDefs.length
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
for (const [key, def] of Object.entries(options)) {
|
|
334
|
+
if (result[key] === void 0) {
|
|
335
|
+
if (def._kind === "flag") {
|
|
336
|
+
result[key] = def.default ?? false;
|
|
337
|
+
} else if (def._kind === "option") {
|
|
338
|
+
const opt = def;
|
|
339
|
+
if (opt.env && env?.[opt.env] !== void 0) {
|
|
340
|
+
const parsed = coerce(env[opt.env], opt.type, key, errors);
|
|
341
|
+
if (parsed !== void 0) {
|
|
342
|
+
result[key] = parsed;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (result[key] === void 0) {
|
|
346
|
+
if (opt.required && opt.default === void 0) {
|
|
347
|
+
errors.push({ type: "missing_required", name: key, kind: "option" });
|
|
348
|
+
} else if (opt.default !== void 0) {
|
|
349
|
+
result[key] = opt.default;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (errors.length > 0) {
|
|
356
|
+
return { ok: false, errors };
|
|
357
|
+
}
|
|
358
|
+
return { ok: true, args: result, passthrough };
|
|
359
|
+
}
|
|
360
|
+
function coerce(raw, type, key, errors) {
|
|
361
|
+
switch (type) {
|
|
362
|
+
case "string":
|
|
363
|
+
return raw;
|
|
364
|
+
case "number": {
|
|
365
|
+
const n = Number(raw);
|
|
366
|
+
if (isNaN(n)) {
|
|
367
|
+
errors.push({ type: "invalid_type", name: key, expected: "number", received: raw });
|
|
368
|
+
return void 0;
|
|
369
|
+
}
|
|
370
|
+
return n;
|
|
371
|
+
}
|
|
372
|
+
case "boolean": {
|
|
373
|
+
if (raw === "true" || raw === "1") return true;
|
|
374
|
+
if (raw === "false" || raw === "0") return false;
|
|
375
|
+
errors.push({ type: "invalid_type", name: key, expected: "boolean", received: raw });
|
|
376
|
+
return void 0;
|
|
377
|
+
}
|
|
378
|
+
default:
|
|
379
|
+
return raw;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function camelToKebab(str) {
|
|
383
|
+
return str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/command/help.ts
|
|
387
|
+
function generateHelp(cmd) {
|
|
388
|
+
const lines = [];
|
|
389
|
+
const hasSubcommands = cmd.children.size > 0;
|
|
390
|
+
if (cmd.description) {
|
|
391
|
+
lines.push(`${cmd.fullPath} - ${cmd.description}`);
|
|
392
|
+
} else {
|
|
393
|
+
lines.push(cmd.fullPath);
|
|
394
|
+
}
|
|
395
|
+
lines.push("");
|
|
396
|
+
const usageParts = [cmd.fullPath];
|
|
397
|
+
if (hasSubcommands) {
|
|
398
|
+
usageParts.push("<command>");
|
|
399
|
+
}
|
|
400
|
+
const allOptions = { ...cmd.inheritedOptions, ...cmd.options };
|
|
401
|
+
if (Object.keys(allOptions).length > 0) {
|
|
402
|
+
usageParts.push("[options]");
|
|
403
|
+
}
|
|
404
|
+
const argDefs = cmd.args;
|
|
405
|
+
for (const argDef of argDefs) {
|
|
406
|
+
const argName = argDef.name ?? "arg";
|
|
407
|
+
const label = argDef.variadic ? `${argName}...` : argName;
|
|
408
|
+
usageParts.push(argDef.required ? `<${label}>` : `[${label}]`);
|
|
409
|
+
}
|
|
410
|
+
lines.push("Usage:");
|
|
411
|
+
lines.push(` ${usageParts.join(" ")}`);
|
|
412
|
+
lines.push("");
|
|
413
|
+
if (hasSubcommands) {
|
|
414
|
+
lines.push("Commands:");
|
|
415
|
+
const entries = [];
|
|
416
|
+
for (const [name, child] of cmd.children) {
|
|
417
|
+
entries.push([name, child.description || ""]);
|
|
418
|
+
}
|
|
419
|
+
const maxNameLen = Math.max(...entries.map(([name]) => name.length));
|
|
420
|
+
for (const [name, desc] of entries) {
|
|
421
|
+
const padding = " ".repeat(maxNameLen - name.length + 2);
|
|
422
|
+
lines.push(` ${name}${padding}${desc}`);
|
|
423
|
+
}
|
|
424
|
+
lines.push("");
|
|
425
|
+
}
|
|
426
|
+
if (argDefs.length > 0) {
|
|
427
|
+
lines.push("Arguments:");
|
|
428
|
+
const argRows = [];
|
|
429
|
+
for (const argDef of argDefs) {
|
|
430
|
+
const rawName = argDef.name ?? "arg";
|
|
431
|
+
const label = argDef.variadic ? `${rawName}...` : rawName;
|
|
432
|
+
const parts = [];
|
|
433
|
+
if (argDef.description) parts.push(argDef.description);
|
|
434
|
+
if (argDef.required) parts.push("(required)");
|
|
435
|
+
if (argDef.default !== void 0) {
|
|
436
|
+
parts.push(`(default: ${JSON.stringify(argDef.default)})`);
|
|
437
|
+
}
|
|
438
|
+
argRows.push([label, parts.join(" ")]);
|
|
439
|
+
}
|
|
440
|
+
const maxLen = Math.max(...argRows.map(([label]) => label.length));
|
|
441
|
+
for (const [label, desc] of argRows) {
|
|
442
|
+
const padding = " ".repeat(maxLen - label.length + 2);
|
|
443
|
+
lines.push(` ${label}${padding}${desc}`);
|
|
444
|
+
}
|
|
445
|
+
lines.push("");
|
|
446
|
+
}
|
|
447
|
+
const ownOptLines = formatOptionsTable(cmd.options);
|
|
448
|
+
if (ownOptLines.length > 0) {
|
|
449
|
+
lines.push("Options:");
|
|
450
|
+
lines.push(...ownOptLines);
|
|
451
|
+
lines.push("");
|
|
452
|
+
}
|
|
453
|
+
const inheritedOptLines = formatOptionsTable(cmd.inheritedOptions, "Inherited Options:");
|
|
454
|
+
if (inheritedOptLines.length > 0) {
|
|
455
|
+
lines.push(...inheritedOptLines);
|
|
456
|
+
lines.push("");
|
|
457
|
+
}
|
|
458
|
+
if (cmd.examples.length > 0) {
|
|
459
|
+
lines.push("Examples:");
|
|
460
|
+
for (const ex of cmd.examples) {
|
|
461
|
+
lines.push(` ${ex}`);
|
|
462
|
+
}
|
|
463
|
+
lines.push("");
|
|
464
|
+
}
|
|
465
|
+
return lines.join("\n");
|
|
466
|
+
}
|
|
467
|
+
function formatOptionsTable(schema, header) {
|
|
468
|
+
const entries = Object.entries(schema);
|
|
469
|
+
if (entries.length === 0) return [];
|
|
470
|
+
const rows = [];
|
|
471
|
+
for (const [key, def] of entries) {
|
|
472
|
+
const longName = camelToKebab(key);
|
|
473
|
+
if (def._kind === "flag") {
|
|
474
|
+
const flag = def;
|
|
475
|
+
const parts = [];
|
|
476
|
+
if (flag.short) parts.push(`-${flag.short},`);
|
|
477
|
+
parts.push(`--${longName}`);
|
|
478
|
+
const descParts = [];
|
|
479
|
+
if (flag.description) descParts.push(flag.description);
|
|
480
|
+
if (flag.default !== void 0) descParts.push(`(default: ${flag.default})`);
|
|
481
|
+
rows.push([parts.join(" "), descParts.join(" ")]);
|
|
482
|
+
} else {
|
|
483
|
+
const opt = def;
|
|
484
|
+
const parts = [];
|
|
485
|
+
if (opt.short) parts.push(`-${opt.short},`);
|
|
486
|
+
parts.push(`--${longName} <${opt.type}>`);
|
|
487
|
+
const descParts = [];
|
|
488
|
+
if (opt.description) descParts.push(opt.description);
|
|
489
|
+
if (opt.required) descParts.push("(required)");
|
|
490
|
+
if (opt.default !== void 0) descParts.push(`(default: ${JSON.stringify(opt.default)})`);
|
|
491
|
+
if (opt.env) descParts.push(`[env: ${opt.env}]`);
|
|
492
|
+
rows.push([parts.join(" "), descParts.join(" ")]);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const maxFlagLen = Math.max(...rows.map(([flag]) => flag.length));
|
|
496
|
+
const lines = [];
|
|
497
|
+
if (header) {
|
|
498
|
+
lines.push(header);
|
|
499
|
+
}
|
|
500
|
+
for (const [flag, desc] of rows) {
|
|
501
|
+
const padding = " ".repeat(maxFlagLen - flag.length + 2);
|
|
502
|
+
lines.push(` ${flag}${padding}${desc}`);
|
|
503
|
+
}
|
|
504
|
+
return lines;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/command/command.ts
|
|
508
|
+
function resolveOptionsInput(input) {
|
|
509
|
+
if (!input) return {};
|
|
510
|
+
const result = {};
|
|
511
|
+
for (const [key, builder] of Object.entries(input)) {
|
|
512
|
+
result[key] = builder._def;
|
|
513
|
+
}
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
function resolveArgsInput(input) {
|
|
517
|
+
if (!input) return [];
|
|
518
|
+
return input.map((builder) => builder._def);
|
|
519
|
+
}
|
|
520
|
+
var Command = class _Command {
|
|
521
|
+
name;
|
|
522
|
+
description;
|
|
523
|
+
options;
|
|
524
|
+
args;
|
|
525
|
+
examples;
|
|
526
|
+
omitInherited;
|
|
527
|
+
handler;
|
|
528
|
+
children = /* @__PURE__ */ new Map();
|
|
529
|
+
parent;
|
|
530
|
+
/** @internal — accumulated builder types for generic inference */
|
|
531
|
+
_accOpts;
|
|
532
|
+
/** @internal — args builder types for generic inference */
|
|
533
|
+
_accArgs;
|
|
534
|
+
/** @internal */
|
|
535
|
+
constructor(name, description, options, args, examples, omitInherited, handler, accOpts, accArgs) {
|
|
536
|
+
this.name = name;
|
|
537
|
+
this.description = description;
|
|
538
|
+
this.options = options;
|
|
539
|
+
this.args = args;
|
|
540
|
+
this.examples = examples;
|
|
541
|
+
this.omitInherited = omitInherited;
|
|
542
|
+
this.handler = handler;
|
|
543
|
+
this._accOpts = accOpts;
|
|
544
|
+
this._accArgs = accArgs;
|
|
545
|
+
}
|
|
546
|
+
// --------------------------------------------------------------------------
|
|
547
|
+
// Tree building
|
|
548
|
+
// --------------------------------------------------------------------------
|
|
549
|
+
/** Add a subcommand. Returns the child command for further nesting. */
|
|
550
|
+
command(name, config) {
|
|
551
|
+
const omitSet = new Set(config.omitInherited ?? []);
|
|
552
|
+
const parentAcc = { ...this._accOpts };
|
|
553
|
+
for (const key of omitSet) delete parentAcc[key];
|
|
554
|
+
const accOpts = { ...parentAcc, ...config.options ?? {} };
|
|
555
|
+
const child = new _Command(
|
|
556
|
+
name,
|
|
557
|
+
config.description,
|
|
558
|
+
resolveOptionsInput(config.options),
|
|
559
|
+
resolveArgsInput(config.args),
|
|
560
|
+
config.examples ?? [],
|
|
561
|
+
omitSet,
|
|
562
|
+
config.handler,
|
|
563
|
+
accOpts,
|
|
564
|
+
config.args ?? []
|
|
565
|
+
);
|
|
566
|
+
child.parent = this;
|
|
567
|
+
this.children.set(name, child);
|
|
568
|
+
return child;
|
|
569
|
+
}
|
|
570
|
+
// --------------------------------------------------------------------------
|
|
571
|
+
// Computed properties
|
|
572
|
+
// --------------------------------------------------------------------------
|
|
573
|
+
/** Full path from root (e.g. "mycli db migrate") */
|
|
574
|
+
get fullPath() {
|
|
575
|
+
const segments = [];
|
|
576
|
+
let current = this;
|
|
577
|
+
while (current) {
|
|
578
|
+
segments.unshift(current.name);
|
|
579
|
+
current = current.parent;
|
|
580
|
+
}
|
|
581
|
+
return segments.join(" ");
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Return a plain `{ name, execute }` object compatible with just-bash's
|
|
585
|
+
* `CustomCommand` interface, with `execute` pre-bound to this command tree.
|
|
586
|
+
*
|
|
587
|
+
* @example
|
|
588
|
+
* ```ts
|
|
589
|
+
* const bash = new Bash({ customCommands: [mycli.toCommand()] });
|
|
590
|
+
* ```
|
|
591
|
+
*/
|
|
592
|
+
toCommand() {
|
|
593
|
+
return { name: this.name, execute: this.execute.bind(this) };
|
|
594
|
+
}
|
|
595
|
+
/** Options inherited from ancestor commands */
|
|
596
|
+
get inheritedOptions() {
|
|
597
|
+
const inherited = {};
|
|
598
|
+
const chain = [];
|
|
599
|
+
let current = this;
|
|
600
|
+
while (current) {
|
|
601
|
+
chain.unshift(current);
|
|
602
|
+
current = current.parent;
|
|
603
|
+
}
|
|
604
|
+
for (const cmd of chain) {
|
|
605
|
+
for (const key of cmd.omitInherited) {
|
|
606
|
+
delete inherited[key];
|
|
607
|
+
}
|
|
608
|
+
if (cmd !== this) {
|
|
609
|
+
Object.assign(inherited, cmd.options);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return inherited;
|
|
613
|
+
}
|
|
614
|
+
/** All options available to this command (inherited + own) */
|
|
615
|
+
get allOptions() {
|
|
616
|
+
return { ...this.inheritedOptions, ...this.options };
|
|
617
|
+
}
|
|
618
|
+
// --------------------------------------------------------------------------
|
|
619
|
+
// Programmatic invocation
|
|
620
|
+
// --------------------------------------------------------------------------
|
|
621
|
+
/**
|
|
622
|
+
* Serialize a typed args object into CLI tokens.
|
|
623
|
+
*
|
|
624
|
+
* Produces tokens that, when parsed, reproduce the given args.
|
|
625
|
+
* Useful for building commands to pass to `execute()` or composing
|
|
626
|
+
* with `fullPath` for string-based execution.
|
|
627
|
+
*
|
|
628
|
+
* Only explicitly-provided values are serialized — omit a key to let
|
|
629
|
+
* the parser apply its default or env fallback as usual.
|
|
630
|
+
*
|
|
631
|
+
* @example
|
|
632
|
+
* ```ts
|
|
633
|
+
* const tokens = serve.toTokens({ port: 8080, entry: "app.ts" });
|
|
634
|
+
* await cli.execute(["serve", ...tokens], ctx);
|
|
635
|
+
* ```
|
|
636
|
+
*/
|
|
637
|
+
toTokens(args) {
|
|
638
|
+
const tokens = [];
|
|
639
|
+
const allOpts = this.allOptions;
|
|
640
|
+
const input = args;
|
|
641
|
+
for (const [key, def] of Object.entries(allOpts)) {
|
|
642
|
+
const value = input[key];
|
|
643
|
+
const kebab = camelToKebab(key);
|
|
644
|
+
if (def._kind === "flag") {
|
|
645
|
+
if (value === true) {
|
|
646
|
+
tokens.push(`--${kebab}`);
|
|
647
|
+
} else if (value === false && def.default === true) {
|
|
648
|
+
tokens.push(`--no-${kebab}`);
|
|
649
|
+
}
|
|
650
|
+
} else if (def._kind === "option") {
|
|
651
|
+
if (value !== void 0) {
|
|
652
|
+
tokens.push(`--${kebab}`, String(value));
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
for (const argDef of this.args) {
|
|
657
|
+
const argName = argDef.name ?? "arg";
|
|
658
|
+
const value = input[argName];
|
|
659
|
+
if (value === void 0) continue;
|
|
660
|
+
if (argDef.variadic && Array.isArray(value)) {
|
|
661
|
+
for (const v of value) {
|
|
662
|
+
tokens.push(String(v));
|
|
663
|
+
}
|
|
664
|
+
} else {
|
|
665
|
+
tokens.push(String(value));
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return tokens;
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Call this command's handler directly with typed args.
|
|
672
|
+
*
|
|
673
|
+
* Required options (no default) and required positional args must be
|
|
674
|
+
* provided. Options with defaults, flags, and optional args can be
|
|
675
|
+
* omitted — invoke applies their defaults automatically.
|
|
676
|
+
*
|
|
677
|
+
* @example
|
|
678
|
+
* ```ts
|
|
679
|
+
* const result = await serve.invoke({ port: 8080, entry: "app.ts" }, ctx);
|
|
680
|
+
* ```
|
|
681
|
+
*/
|
|
682
|
+
async invoke(args, ctx) {
|
|
683
|
+
if (!this.handler) {
|
|
684
|
+
return {
|
|
685
|
+
stdout: "",
|
|
686
|
+
stderr: `Command "${this.fullPath}" has no handler`,
|
|
687
|
+
exitCode: 1
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
const resolved = { ...args };
|
|
691
|
+
const allOpts = this.allOptions;
|
|
692
|
+
for (const [key, def] of Object.entries(allOpts)) {
|
|
693
|
+
if (resolved[key] === void 0) {
|
|
694
|
+
if (def._kind === "flag") {
|
|
695
|
+
resolved[key] = def.default ?? false;
|
|
696
|
+
} else if (def._kind === "option") {
|
|
697
|
+
if (def.default !== void 0) {
|
|
698
|
+
resolved[key] = def.default;
|
|
699
|
+
} else if (def.required) {
|
|
700
|
+
return {
|
|
701
|
+
stdout: "",
|
|
702
|
+
stderr: `Missing required option "${key}"`,
|
|
703
|
+
exitCode: 1
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
for (const argDef of this.args) {
|
|
710
|
+
const argName = argDef.name ?? "arg";
|
|
711
|
+
if (resolved[argName] === void 0) {
|
|
712
|
+
if (argDef.default !== void 0) {
|
|
713
|
+
resolved[argName] = argDef.default;
|
|
714
|
+
} else if (argDef.required) {
|
|
715
|
+
return {
|
|
716
|
+
stdout: "",
|
|
717
|
+
stderr: `Missing required arg "${argName}"`,
|
|
718
|
+
exitCode: 1
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return this.handler(resolved, ctx, { passthrough: [] });
|
|
724
|
+
}
|
|
725
|
+
// --------------------------------------------------------------------------
|
|
726
|
+
// Execution
|
|
727
|
+
// --------------------------------------------------------------------------
|
|
728
|
+
/**
|
|
729
|
+
* Execute this command tree with the given tokens.
|
|
730
|
+
*
|
|
731
|
+
* Tokens flow through the tree: each level consumes the subcommand name
|
|
732
|
+
* and passes the rest deeper. When no subcommand matches, the current
|
|
733
|
+
* node either parses and runs its handler, or returns help/error.
|
|
734
|
+
*/
|
|
735
|
+
async execute(tokens, ctx) {
|
|
736
|
+
const env = ctx?.env ? Object.fromEntries(ctx.env) : {};
|
|
737
|
+
const firstToken = tokens[0];
|
|
738
|
+
if (firstToken && !firstToken.startsWith("-") && this.children.has(firstToken)) {
|
|
739
|
+
return this.children.get(firstToken).execute(tokens.slice(1), ctx);
|
|
740
|
+
}
|
|
741
|
+
if (hasHelpFlag(tokens)) {
|
|
742
|
+
return { stdout: generateHelp(this), stderr: "", exitCode: 0 };
|
|
743
|
+
}
|
|
744
|
+
if (this.handler) {
|
|
745
|
+
const parsed = parseArgs(this.allOptions, this.args, [...tokens], env);
|
|
746
|
+
if (!parsed.ok) {
|
|
747
|
+
return { stdout: "", stderr: formatErrors(parsed.errors), exitCode: 1 };
|
|
748
|
+
}
|
|
749
|
+
return this.handler(parsed.args, ctx, { passthrough: parsed.passthrough });
|
|
750
|
+
}
|
|
751
|
+
if (firstToken && !firstToken.startsWith("-")) {
|
|
752
|
+
const suggestions = findSuggestions(firstToken, [...this.children.keys()]);
|
|
753
|
+
return {
|
|
754
|
+
stdout: "",
|
|
755
|
+
stderr: formatErrors([{
|
|
756
|
+
type: "unknown_command",
|
|
757
|
+
path: `${this.fullPath} ${firstToken}`,
|
|
758
|
+
suggestions
|
|
759
|
+
}]),
|
|
760
|
+
exitCode: 1
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
return { stdout: generateHelp(this), stderr: "", exitCode: 0 };
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
function command(name, config) {
|
|
767
|
+
return new Command(
|
|
768
|
+
name,
|
|
769
|
+
config.description,
|
|
770
|
+
resolveOptionsInput(config.options),
|
|
771
|
+
resolveArgsInput(config.args),
|
|
772
|
+
config.examples ?? [],
|
|
773
|
+
/* @__PURE__ */ new Set(),
|
|
774
|
+
config.handler,
|
|
775
|
+
config.options ?? {},
|
|
776
|
+
config.args ?? []
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
function hasHelpFlag(tokens) {
|
|
780
|
+
return tokens.some((t) => t === "--help" || t === "-h");
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
export {
|
|
784
|
+
o,
|
|
785
|
+
f,
|
|
786
|
+
a,
|
|
787
|
+
formatError,
|
|
788
|
+
formatErrors,
|
|
789
|
+
parseArgs,
|
|
790
|
+
generateHelp,
|
|
791
|
+
Command,
|
|
792
|
+
command
|
|
793
|
+
};
|