padrone 1.1.0 → 1.2.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/CHANGELOG.md +38 -1
- package/LICENSE +1 -1
- package/README.md +60 -30
- package/dist/args-CKNh7Dm9.mjs +175 -0
- package/dist/args-CKNh7Dm9.mjs.map +1 -0
- package/dist/chunk-y_GBKt04.mjs +5 -0
- package/dist/codegen/index.d.mts +305 -0
- package/dist/codegen/index.d.mts.map +1 -0
- package/dist/codegen/index.mjs +1348 -0
- package/dist/codegen/index.mjs.map +1 -0
- package/dist/completion.d.mts +64 -0
- package/dist/completion.d.mts.map +1 -0
- package/dist/completion.mjs +417 -0
- package/dist/completion.mjs.map +1 -0
- package/dist/docs/index.d.mts +34 -0
- package/dist/docs/index.d.mts.map +1 -0
- package/dist/docs/index.mjs +404 -0
- package/dist/docs/index.mjs.map +1 -0
- package/dist/formatter-Dvx7jFXr.d.mts +82 -0
- package/dist/formatter-Dvx7jFXr.d.mts.map +1 -0
- package/dist/help-mUIX0T0V.mjs +1195 -0
- package/dist/help-mUIX0T0V.mjs.map +1 -0
- package/dist/index.d.mts +120 -546
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1180 -1197
- package/dist/index.mjs.map +1 -1
- package/dist/test.d.mts +112 -0
- package/dist/test.d.mts.map +1 -0
- package/dist/test.mjs +138 -0
- package/dist/test.mjs.map +1 -0
- package/dist/types-qrtt0135.d.mts +1037 -0
- package/dist/types-qrtt0135.d.mts.map +1 -0
- package/dist/update-check-EbNDkzyV.mjs +146 -0
- package/dist/update-check-EbNDkzyV.mjs.map +1 -0
- package/package.json +61 -21
- package/src/args.ts +365 -0
- package/src/cli/completions.ts +29 -0
- package/src/cli/docs.ts +86 -0
- package/src/cli/doctor.ts +312 -0
- package/src/cli/index.ts +159 -0
- package/src/cli/init.ts +135 -0
- package/src/cli/link.ts +320 -0
- package/src/cli/wrap.ts +152 -0
- package/src/codegen/README.md +118 -0
- package/src/codegen/code-builder.ts +226 -0
- package/src/codegen/discovery.ts +232 -0
- package/src/codegen/file-emitter.ts +73 -0
- package/src/codegen/generators/barrel-file.ts +16 -0
- package/src/codegen/generators/command-file.ts +184 -0
- package/src/codegen/generators/command-tree.ts +124 -0
- package/src/codegen/index.ts +33 -0
- package/src/codegen/parsers/fish.ts +163 -0
- package/src/codegen/parsers/help.ts +378 -0
- package/src/codegen/parsers/merge.ts +158 -0
- package/src/codegen/parsers/zsh.ts +221 -0
- package/src/codegen/schema-to-code.ts +199 -0
- package/src/codegen/template.ts +69 -0
- package/src/codegen/types.ts +143 -0
- package/src/colorizer.ts +2 -2
- package/src/command-utils.ts +501 -0
- package/src/completion.ts +110 -97
- package/src/create.ts +1036 -305
- package/src/docs/index.ts +607 -0
- package/src/errors.ts +131 -0
- package/src/formatter.ts +149 -63
- package/src/help.ts +151 -55
- package/src/index.ts +12 -15
- package/src/interactive.ts +169 -0
- package/src/parse.ts +31 -16
- package/src/repl-loop.ts +317 -0
- package/src/runtime.ts +304 -0
- package/src/shell-utils.ts +83 -0
- package/src/test.ts +285 -0
- package/src/type-helpers.ts +10 -10
- package/src/type-utils.ts +124 -14
- package/src/types.ts +752 -154
- package/src/update-check.ts +244 -0
- package/src/wrap.ts +44 -40
- package/src/zod.d.ts +2 -2
- package/src/options.ts +0 -180
|
@@ -0,0 +1,1348 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
//#region src/codegen/code-builder.ts
|
|
4
|
+
var CodeBuilderImpl = class CodeBuilderImpl {
|
|
5
|
+
imports = /* @__PURE__ */ new Map();
|
|
6
|
+
lines = [];
|
|
7
|
+
indent;
|
|
8
|
+
constructor(indent = 0) {
|
|
9
|
+
this.indent = indent;
|
|
10
|
+
}
|
|
11
|
+
import(specifier, source) {
|
|
12
|
+
const specs = Array.isArray(specifier) ? specifier : [specifier];
|
|
13
|
+
const existing = this.imports.get(source);
|
|
14
|
+
if (existing) {
|
|
15
|
+
for (const s of specs) existing.specifiers.add(s);
|
|
16
|
+
existing.typeOnly = false;
|
|
17
|
+
} else this.imports.set(source, {
|
|
18
|
+
specifiers: new Set(specs),
|
|
19
|
+
typeOnly: false
|
|
20
|
+
});
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
importDefault(name, source) {
|
|
24
|
+
const existing = this.imports.get(source);
|
|
25
|
+
if (existing) {
|
|
26
|
+
existing.defaultSpecifier = name;
|
|
27
|
+
existing.typeOnly = false;
|
|
28
|
+
} else this.imports.set(source, {
|
|
29
|
+
specifiers: /* @__PURE__ */ new Set(),
|
|
30
|
+
defaultSpecifier: name,
|
|
31
|
+
typeOnly: false
|
|
32
|
+
});
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
importType(specifier, source) {
|
|
36
|
+
const specs = Array.isArray(specifier) ? specifier : [specifier];
|
|
37
|
+
const existing = this.imports.get(source);
|
|
38
|
+
if (existing) for (const s of specs) existing.specifiers.add(s);
|
|
39
|
+
else this.imports.set(source, {
|
|
40
|
+
specifiers: new Set(specs),
|
|
41
|
+
typeOnly: true
|
|
42
|
+
});
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
line(code) {
|
|
46
|
+
this.lines.push({
|
|
47
|
+
type: "line",
|
|
48
|
+
content: code ?? ""
|
|
49
|
+
});
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
block(openOrBuilder, builderOrClose, closeOrBuilder) {
|
|
53
|
+
let open;
|
|
54
|
+
let close;
|
|
55
|
+
let builder;
|
|
56
|
+
if (typeof openOrBuilder === "function") builder = openOrBuilder;
|
|
57
|
+
else if (typeof builderOrClose === "function") {
|
|
58
|
+
open = openOrBuilder;
|
|
59
|
+
builder = builderOrClose;
|
|
60
|
+
close = typeof closeOrBuilder === "string" ? closeOrBuilder : void 0;
|
|
61
|
+
} else if (typeof closeOrBuilder === "function") {
|
|
62
|
+
open = openOrBuilder;
|
|
63
|
+
close = typeof builderOrClose === "string" ? builderOrClose : void 0;
|
|
64
|
+
builder = closeOrBuilder;
|
|
65
|
+
} else throw new Error("Invalid block() arguments");
|
|
66
|
+
this.lines.push({
|
|
67
|
+
type: "block-open",
|
|
68
|
+
content: open ?? ""
|
|
69
|
+
});
|
|
70
|
+
const inner = new CodeBuilderImpl(this.indent + 1);
|
|
71
|
+
builder(inner);
|
|
72
|
+
for (const [source, entry] of inner.imports) {
|
|
73
|
+
const existing = this.imports.get(source);
|
|
74
|
+
if (existing) {
|
|
75
|
+
for (const s of entry.specifiers) existing.specifiers.add(s);
|
|
76
|
+
if (entry.defaultSpecifier) existing.defaultSpecifier = entry.defaultSpecifier;
|
|
77
|
+
if (!entry.typeOnly) existing.typeOnly = false;
|
|
78
|
+
} else this.imports.set(source, {
|
|
79
|
+
specifiers: new Set(entry.specifiers),
|
|
80
|
+
defaultSpecifier: entry.defaultSpecifier,
|
|
81
|
+
typeOnly: entry.typeOnly
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
for (const line of inner.lines) this.lines.push(line);
|
|
85
|
+
this.lines.push({
|
|
86
|
+
type: "block-close",
|
|
87
|
+
content: close ?? ""
|
|
88
|
+
});
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
comment(text) {
|
|
92
|
+
this.lines.push({
|
|
93
|
+
type: "line",
|
|
94
|
+
content: `// ${text}`
|
|
95
|
+
});
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
docComment(text) {
|
|
99
|
+
const lines = text.split("\n");
|
|
100
|
+
if (lines.length === 1) this.lines.push({
|
|
101
|
+
type: "line",
|
|
102
|
+
content: `/** ${text} */`
|
|
103
|
+
});
|
|
104
|
+
else {
|
|
105
|
+
this.lines.push({
|
|
106
|
+
type: "line",
|
|
107
|
+
content: "/**"
|
|
108
|
+
});
|
|
109
|
+
for (const line of lines) this.lines.push({
|
|
110
|
+
type: "line",
|
|
111
|
+
content: ` * ${line}`
|
|
112
|
+
});
|
|
113
|
+
this.lines.push({
|
|
114
|
+
type: "line",
|
|
115
|
+
content: " */"
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
todoComment(text) {
|
|
121
|
+
this.lines.push({
|
|
122
|
+
type: "line",
|
|
123
|
+
content: `// TODO: ${text}`
|
|
124
|
+
});
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
raw(code) {
|
|
128
|
+
this.lines.push({
|
|
129
|
+
type: "raw",
|
|
130
|
+
content: code
|
|
131
|
+
});
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
build() {
|
|
135
|
+
const parts = [];
|
|
136
|
+
if (this.imports.size > 0) {
|
|
137
|
+
const typeImports = [];
|
|
138
|
+
const valueImports = [];
|
|
139
|
+
for (const [source, entry] of this.imports) {
|
|
140
|
+
const specs = [...entry.specifiers].sort();
|
|
141
|
+
const namedPart = specs.length > 0 ? specs.length === 1 && !specs[0].includes(" ") ? `{ ${specs[0]} }` : `{ ${specs.join(", ")} }` : null;
|
|
142
|
+
const specStr = entry.defaultSpecifier ? namedPart ? `${entry.defaultSpecifier}, ${namedPart}` : entry.defaultSpecifier : namedPart;
|
|
143
|
+
const line = entry.typeOnly ? `import type ${specStr} from '${source}'` : `import ${specStr} from '${source}'`;
|
|
144
|
+
if (entry.typeOnly) typeImports.push(line);
|
|
145
|
+
else valueImports.push(line);
|
|
146
|
+
}
|
|
147
|
+
parts.push([...valueImports, ...typeImports].join("\n"));
|
|
148
|
+
parts.push("");
|
|
149
|
+
}
|
|
150
|
+
let currentIndent = this.indent;
|
|
151
|
+
for (const line of this.lines) if (line.type === "raw") parts.push(line.content);
|
|
152
|
+
else if (line.type === "block-open") {
|
|
153
|
+
if (line.content) {
|
|
154
|
+
const indent = " ".repeat(currentIndent);
|
|
155
|
+
parts.push(`${indent}${line.content}`);
|
|
156
|
+
}
|
|
157
|
+
currentIndent++;
|
|
158
|
+
} else if (line.type === "block-close") {
|
|
159
|
+
currentIndent--;
|
|
160
|
+
if (line.content) {
|
|
161
|
+
const indent = " ".repeat(currentIndent);
|
|
162
|
+
parts.push(`${indent}${line.content}`);
|
|
163
|
+
}
|
|
164
|
+
} else if (line.content === "") parts.push("");
|
|
165
|
+
else {
|
|
166
|
+
const indent = " ".repeat(currentIndent);
|
|
167
|
+
parts.push(`${indent}${line.content}`);
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
text: parts.join("\n"),
|
|
171
|
+
imports: new Map([...this.imports].map(([source, entry]) => [source, {
|
|
172
|
+
specifiers: new Set(entry.specifiers),
|
|
173
|
+
typeOnly: entry.typeOnly
|
|
174
|
+
}]))
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Create a new CodeBuilder for constructing TypeScript source files.
|
|
180
|
+
*/
|
|
181
|
+
function createCodeBuilder() {
|
|
182
|
+
return new CodeBuilderImpl();
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/codegen/parsers/fish.ts
|
|
186
|
+
/**
|
|
187
|
+
* Parse fish shell completion scripts into CommandMeta.
|
|
188
|
+
*
|
|
189
|
+
* Fish completions use the `complete` builtin:
|
|
190
|
+
* complete -c <command> -s <short> -l <long> -d <description> -a <arguments> -r -f
|
|
191
|
+
*/
|
|
192
|
+
function parseFishCompletions(text) {
|
|
193
|
+
const lines = text.split("\n");
|
|
194
|
+
const result = {
|
|
195
|
+
name: "",
|
|
196
|
+
arguments: [],
|
|
197
|
+
subcommands: []
|
|
198
|
+
};
|
|
199
|
+
const subcommandMap = /* @__PURE__ */ new Map();
|
|
200
|
+
for (const line of lines) {
|
|
201
|
+
const trimmed = line.trim();
|
|
202
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
203
|
+
if (!trimmed.match(/^complete\s+/)) continue;
|
|
204
|
+
const parts = parseCompleteLine(trimmed);
|
|
205
|
+
if (!parts) continue;
|
|
206
|
+
if (!result.name && parts.command) result.name = parts.command;
|
|
207
|
+
const subcommandCondition = parts.condition?.match(/__fish_seen_subcommand_from\s+(\S+)/);
|
|
208
|
+
if (subcommandCondition) {
|
|
209
|
+
const subName = subcommandCondition[1];
|
|
210
|
+
let sub = subcommandMap.get(subName);
|
|
211
|
+
if (!sub) {
|
|
212
|
+
sub = {
|
|
213
|
+
name: subName,
|
|
214
|
+
arguments: []
|
|
215
|
+
};
|
|
216
|
+
subcommandMap.set(subName, sub);
|
|
217
|
+
}
|
|
218
|
+
if (parts.longFlag || parts.shortFlag) {
|
|
219
|
+
const field = completionToField(parts);
|
|
220
|
+
if (field) sub.arguments.push(field);
|
|
221
|
+
}
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (parts.arguments && !parts.longFlag && !parts.shortFlag) {
|
|
225
|
+
const names = parts.arguments.split(/\s+/);
|
|
226
|
+
for (const name of names) {
|
|
227
|
+
if (!name || name.startsWith("(")) continue;
|
|
228
|
+
if (!subcommandMap.has(name)) subcommandMap.set(name, {
|
|
229
|
+
name,
|
|
230
|
+
description: parts.description,
|
|
231
|
+
arguments: []
|
|
232
|
+
});
|
|
233
|
+
else if (parts.description) subcommandMap.get(name).description = parts.description;
|
|
234
|
+
}
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (parts.longFlag || parts.shortFlag) {
|
|
238
|
+
const field = completionToField(parts);
|
|
239
|
+
if (field) result.arguments.push(field);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
for (const sub of subcommandMap.values()) {
|
|
243
|
+
if (sub.arguments.length === 0) delete sub.arguments;
|
|
244
|
+
result.subcommands.push(sub);
|
|
245
|
+
}
|
|
246
|
+
if (result.arguments.length === 0) delete result.arguments;
|
|
247
|
+
if (result.subcommands.length === 0) delete result.subcommands;
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
function parseCompleteLine(line) {
|
|
251
|
+
const parts = {};
|
|
252
|
+
const cmdMatch = line.match(/-c\s+(\S+)/);
|
|
253
|
+
if (cmdMatch) parts.command = cmdMatch[1];
|
|
254
|
+
const shortMatch = line.match(/-s\s+(\S+)/);
|
|
255
|
+
if (shortMatch) parts.shortFlag = shortMatch[1];
|
|
256
|
+
const longMatch = line.match(/-l\s+(\S+)/);
|
|
257
|
+
if (longMatch) parts.longFlag = longMatch[1];
|
|
258
|
+
const descMatch = line.match(/-d\s+['"]([^'"]+)['"]/) || line.match(/-d\s+(\S+)/);
|
|
259
|
+
if (descMatch) parts.description = descMatch[1];
|
|
260
|
+
const argsMatch = line.match(/-a\s+['"]([^'"]+)['"]/) || line.match(/-a\s+(\S+)/);
|
|
261
|
+
if (argsMatch) parts.arguments = argsMatch[1];
|
|
262
|
+
const condMatch = line.match(/-n\s+['"]([^'"]+)['"]/) || line.match(/-n\s+(\S+)/);
|
|
263
|
+
if (condMatch) parts.condition = condMatch[1];
|
|
264
|
+
parts.requiresArg = /-r\b/.test(line);
|
|
265
|
+
parts.noFiles = /-f\b/.test(line);
|
|
266
|
+
return parts;
|
|
267
|
+
}
|
|
268
|
+
function completionToField(parts) {
|
|
269
|
+
const name = parts.longFlag ? parts.longFlag.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) : parts.shortFlag || "";
|
|
270
|
+
if (!name) return null;
|
|
271
|
+
let type = parts.requiresArg ? "string" : "boolean";
|
|
272
|
+
let enumValues;
|
|
273
|
+
if (parts.arguments) {
|
|
274
|
+
const values = parts.arguments.split(/\s+/).filter((v) => !v.startsWith("("));
|
|
275
|
+
if (values.length > 0 && values.length <= 20) {
|
|
276
|
+
enumValues = values;
|
|
277
|
+
type = "enum";
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const aliases = parts.shortFlag && parts.longFlag ? [`-${parts.shortFlag}`] : void 0;
|
|
281
|
+
return {
|
|
282
|
+
name,
|
|
283
|
+
type,
|
|
284
|
+
description: parts.description,
|
|
285
|
+
aliases,
|
|
286
|
+
enumValues
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/codegen/parsers/help.ts
|
|
291
|
+
/**
|
|
292
|
+
* Parse --help text output into CommandMeta.
|
|
293
|
+
* Handles common styles: GNU coreutils, Go cobra, Python argparse, Node commander/yargs, gh CLI.
|
|
294
|
+
*/
|
|
295
|
+
function parseHelpOutput(text, options) {
|
|
296
|
+
const lines = text.split("\n");
|
|
297
|
+
const result = {
|
|
298
|
+
name: options?.name || "",
|
|
299
|
+
arguments: [],
|
|
300
|
+
positionals: [],
|
|
301
|
+
subcommands: []
|
|
302
|
+
};
|
|
303
|
+
let section = "none";
|
|
304
|
+
const usageMatch = text.match(/^[Uu](?:SAGE|sage):?\s*(\S+)/m) || text.match(/^USAGE\n\s+(\S+)/m);
|
|
305
|
+
if (usageMatch && !result.name) result.name = usageMatch[1];
|
|
306
|
+
const descriptionLines = [];
|
|
307
|
+
for (const line of lines) {
|
|
308
|
+
const trimmed = line.trim();
|
|
309
|
+
if (!trimmed) {
|
|
310
|
+
if (descriptionLines.length > 0) break;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (isSectionHeader(trimmed)) break;
|
|
314
|
+
if (descriptionLines.length === 0 || descriptionLines.length > 0) descriptionLines.push(trimmed);
|
|
315
|
+
}
|
|
316
|
+
if (descriptionLines.length > 0) result.description = descriptionLines.join(" ");
|
|
317
|
+
for (let i = 0; i < lines.length; i++) {
|
|
318
|
+
const line = lines[i];
|
|
319
|
+
const trimmed = line.trim();
|
|
320
|
+
if (!trimmed) continue;
|
|
321
|
+
const sectionType = detectSection(trimmed);
|
|
322
|
+
if (sectionType) {
|
|
323
|
+
section = sectionType;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
switch (section) {
|
|
327
|
+
case "commands": {
|
|
328
|
+
const cmd = parseCommandLine(line);
|
|
329
|
+
if (cmd) result.subcommands.push(cmd);
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
case "options": {
|
|
333
|
+
const field = parseOptionLine(line);
|
|
334
|
+
if (field) result.arguments.push(field);
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
case "arguments":
|
|
338
|
+
case "positional": {
|
|
339
|
+
const field = parsePositionalLine(line);
|
|
340
|
+
if (field) result.positionals.push(field);
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case "aliases":
|
|
344
|
+
if (trimmed.match(/^\S+(?:\s+\S+)*$/)) {
|
|
345
|
+
const parts = trimmed.split(/\s+/);
|
|
346
|
+
if (parts.length >= 2) {
|
|
347
|
+
const alias = parts[parts.length - 1];
|
|
348
|
+
if (!result.aliases) result.aliases = [];
|
|
349
|
+
result.aliases.push(alias);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (result.arguments.length === 0) delete result.arguments;
|
|
356
|
+
if (result.positionals.length === 0) delete result.positionals;
|
|
357
|
+
if (result.subcommands.length === 0) delete result.subcommands;
|
|
358
|
+
if (result.aliases?.length === 0) delete result.aliases;
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
function isSectionHeader(line) {
|
|
362
|
+
return /^[A-Z][A-Za-z\s]*:?\s*$/i.test(line) || /^[A-Z][A-Z\s]+$/i.test(line);
|
|
363
|
+
}
|
|
364
|
+
function detectSection(line) {
|
|
365
|
+
const lower = line.toLowerCase().replace(/:$/, "").trim();
|
|
366
|
+
if (/^(?:available\s+)?(?:commands|subcommands)$/.test(lower)) return "commands";
|
|
367
|
+
if (/^(?:\w+\s+)*commands$/.test(lower) && !/alias/.test(lower)) return "commands";
|
|
368
|
+
if (/^(?:global\s+|inherited\s+)?(?:options|flags)$/.test(lower)) return "options";
|
|
369
|
+
if (/^(?:positional\s+)?(?:arguments|args|positionals)$/.test(lower)) return "positional";
|
|
370
|
+
if (/^alias(?:es)?(?:\s+commands)?$/.test(lower)) return "aliases";
|
|
371
|
+
if (lower === "usage") return "usage";
|
|
372
|
+
if (/^(?:help\s+topics|examples?|learn\s+more|json\s+fields|see\s+also|notes?)$/.test(lower)) return "skip";
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
function parseCommandLine(line) {
|
|
376
|
+
const colonMatch = line.match(/^\s{2,}([\w][\w-]*):?\s{2,}(.+)$/);
|
|
377
|
+
if (colonMatch) return {
|
|
378
|
+
name: colonMatch[1].replace(/:$/, ""),
|
|
379
|
+
description: colonMatch[2].trim()
|
|
380
|
+
};
|
|
381
|
+
const nameOnly = line.match(/^\s{2,}([\w][\w-]*):?\s*$/);
|
|
382
|
+
if (nameOnly) return { name: nameOnly[1].replace(/:$/, "") };
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
function parseOptionLine(line) {
|
|
386
|
+
const cobraMatch = line.match(/^\s{2,}(?:(-\w),\s+)?(-{1,2}[\w-]+)(?:\s+([^\s<>]+))?\s{2,}(.+)$/);
|
|
387
|
+
if (cobraMatch) {
|
|
388
|
+
const shortFlag = cobraMatch[1];
|
|
389
|
+
const longFlag = cobraMatch[2];
|
|
390
|
+
const typeHint = cobraMatch[3];
|
|
391
|
+
const description = cobraMatch[4]?.trim();
|
|
392
|
+
const name = normalizeOptionName(longFlag);
|
|
393
|
+
const aliases = shortFlag ? [normalizeAlias(shortFlag)] : void 0;
|
|
394
|
+
const { type, ambiguous } = resolveType(typeHint);
|
|
395
|
+
const { defaultValue, resolvedType } = extractDefault(description, type, ambiguous);
|
|
396
|
+
const { enumValues, resolvedType: finalType } = extractEnum(description, resolvedType);
|
|
397
|
+
return reconcileField({
|
|
398
|
+
name,
|
|
399
|
+
type: finalType,
|
|
400
|
+
description,
|
|
401
|
+
required: type === "boolean" ? void 0 : true,
|
|
402
|
+
aliases,
|
|
403
|
+
default: defaultValue,
|
|
404
|
+
enumValues,
|
|
405
|
+
ambiguous: ambiguous || void 0
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
const gnuMatch = line.match(/^\s{2,}(?:(-\w),?\s+)?(-{1,2}[\w-]+)(?:\s*[=\s]\s*(?:<([^>]+)>|\[([^\]]+)\]|(\w+)))?\s{2,}(.+)$/);
|
|
409
|
+
if (gnuMatch) {
|
|
410
|
+
const shortFlag = gnuMatch[1];
|
|
411
|
+
const longFlag = gnuMatch[2];
|
|
412
|
+
const valueName = gnuMatch[3] || gnuMatch[4] || gnuMatch[5];
|
|
413
|
+
const description = gnuMatch[6]?.trim();
|
|
414
|
+
const name = normalizeOptionName(longFlag);
|
|
415
|
+
const aliases = shortFlag ? [normalizeAlias(shortFlag)] : void 0;
|
|
416
|
+
const { type, ambiguous } = resolveType(valueName);
|
|
417
|
+
const { defaultValue, resolvedType } = extractDefault(description, type, ambiguous);
|
|
418
|
+
const { enumValues, resolvedType: finalType } = extractEnum(description, resolvedType);
|
|
419
|
+
const required = !gnuMatch[4];
|
|
420
|
+
return reconcileField({
|
|
421
|
+
name,
|
|
422
|
+
type: finalType,
|
|
423
|
+
description,
|
|
424
|
+
required: finalType === "boolean" ? void 0 : required,
|
|
425
|
+
aliases,
|
|
426
|
+
default: defaultValue,
|
|
427
|
+
enumValues,
|
|
428
|
+
ambiguous: ambiguous || void 0
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
const simple = line.match(/^\s{2,}(-{1,2}[\w-]+)\s{2,}(.+)$/);
|
|
432
|
+
if (simple) return {
|
|
433
|
+
name: normalizeOptionName(simple[1]),
|
|
434
|
+
type: "boolean",
|
|
435
|
+
description: simple[2].trim()
|
|
436
|
+
};
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Resolve a type hint string to a FieldMeta type.
|
|
441
|
+
*/
|
|
442
|
+
function resolveType(hint) {
|
|
443
|
+
if (!hint) return {
|
|
444
|
+
type: "boolean",
|
|
445
|
+
ambiguous: false
|
|
446
|
+
};
|
|
447
|
+
const lower = hint.toLowerCase();
|
|
448
|
+
if (/^(num|number|int|integer|port|count|float|duration)$/.test(lower)) return {
|
|
449
|
+
type: "number",
|
|
450
|
+
ambiguous: false
|
|
451
|
+
};
|
|
452
|
+
if (/^(str|string|text|name|path|file|dir|url|host|query|expression|template)$/.test(lower)) return {
|
|
453
|
+
type: "string",
|
|
454
|
+
ambiguous: false
|
|
455
|
+
};
|
|
456
|
+
if (/^(bool|boolean)$/.test(lower)) return {
|
|
457
|
+
type: "boolean",
|
|
458
|
+
ambiguous: false
|
|
459
|
+
};
|
|
460
|
+
if (/^(strings|fields)$/.test(lower)) return {
|
|
461
|
+
type: "array",
|
|
462
|
+
ambiguous: false
|
|
463
|
+
};
|
|
464
|
+
return {
|
|
465
|
+
type: "string",
|
|
466
|
+
ambiguous: true
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Extract default value from description text.
|
|
471
|
+
*/
|
|
472
|
+
function extractDefault(description, type, ambiguous) {
|
|
473
|
+
let resolvedType = type;
|
|
474
|
+
let defaultValue;
|
|
475
|
+
const defaultMatch = description?.match(/\(default[:\s]+([^)]+)\)/i) || description?.match(/\[default[:\s]+([^\]]+)\]/i);
|
|
476
|
+
if (defaultMatch) {
|
|
477
|
+
const raw = defaultMatch[1].trim();
|
|
478
|
+
if (raw === "true" || raw === "false") {
|
|
479
|
+
defaultValue = raw === "true";
|
|
480
|
+
resolvedType = "boolean";
|
|
481
|
+
} else if (/^\d+$/.test(raw)) {
|
|
482
|
+
defaultValue = parseInt(raw, 10);
|
|
483
|
+
if (resolvedType === "string" && !ambiguous) resolvedType = "number";
|
|
484
|
+
} else if (/^\d+\.\d+$/.test(raw)) {
|
|
485
|
+
defaultValue = parseFloat(raw);
|
|
486
|
+
if (resolvedType === "string" && !ambiguous) resolvedType = "number";
|
|
487
|
+
} else defaultValue = raw.replace(/^["']|["']$/g, "");
|
|
488
|
+
}
|
|
489
|
+
return {
|
|
490
|
+
defaultValue,
|
|
491
|
+
resolvedType
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Extract enum values from description text.
|
|
496
|
+
*/
|
|
497
|
+
function extractEnum(description, type) {
|
|
498
|
+
const choiceMatch = description?.match(/\((?:one of|choices?)[:\s]+([^)]+)\)/i);
|
|
499
|
+
if (choiceMatch) return {
|
|
500
|
+
enumValues: choiceMatch[1].split(/[,|]/).map((v) => v.trim().replace(/^["']|["']$/g, "")),
|
|
501
|
+
resolvedType: "enum"
|
|
502
|
+
};
|
|
503
|
+
const inlineMatch = description?.match(/\{(\w+(?:\|[\w-]+)+)\}/);
|
|
504
|
+
if (inlineMatch) return {
|
|
505
|
+
enumValues: inlineMatch[1].split("|"),
|
|
506
|
+
resolvedType: "enum"
|
|
507
|
+
};
|
|
508
|
+
return {
|
|
509
|
+
enumValues: void 0,
|
|
510
|
+
resolvedType: type
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Fix up a parsed field for edge cases:
|
|
515
|
+
* - Enum default not in the listed values → add it
|
|
516
|
+
* - Default value type doesn't match declared type → reset to type default + mark ambiguous
|
|
517
|
+
*/
|
|
518
|
+
function reconcileField(field) {
|
|
519
|
+
if (field.type === "enum" && field.enumValues && field.default !== void 0) {
|
|
520
|
+
const defStr = String(field.default);
|
|
521
|
+
if (!field.enumValues.includes(defStr)) field.enumValues = [...field.enumValues, defStr];
|
|
522
|
+
}
|
|
523
|
+
if (field.default !== void 0) {
|
|
524
|
+
if (field.type === "boolean" && typeof field.default !== "boolean") {
|
|
525
|
+
field.default = false;
|
|
526
|
+
field.ambiguous = true;
|
|
527
|
+
} else if (field.type === "number" && typeof field.default !== "number") {
|
|
528
|
+
field.default = 0;
|
|
529
|
+
field.ambiguous = true;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return field;
|
|
533
|
+
}
|
|
534
|
+
function parsePositionalLine(line) {
|
|
535
|
+
const match = line.match(/^\s{2,}<?(\w[\w-]*)>?\s{2,}(.+)$/);
|
|
536
|
+
if (!match) return null;
|
|
537
|
+
return {
|
|
538
|
+
name: match[1],
|
|
539
|
+
type: "string",
|
|
540
|
+
description: match[2].trim(),
|
|
541
|
+
positional: true,
|
|
542
|
+
ambiguous: true
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Strip leading dashes from a flag name, preserving kebab-case.
|
|
547
|
+
*/
|
|
548
|
+
function normalizeOptionName(flag) {
|
|
549
|
+
return flag.replace(/^-+/, "");
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Strip leading dash from a short alias flag (e.g. '-v' → 'v').
|
|
553
|
+
*/
|
|
554
|
+
function normalizeAlias(alias) {
|
|
555
|
+
return alias.replace(/^-/, "");
|
|
556
|
+
}
|
|
557
|
+
//#endregion
|
|
558
|
+
//#region src/codegen/parsers/merge.ts
|
|
559
|
+
/**
|
|
560
|
+
* Deep-merge multiple CommandMeta from different sources.
|
|
561
|
+
* Deduplicates fields, resolves conflicts, and combines subcommands.
|
|
562
|
+
*
|
|
563
|
+
* Later sources take precedence for descriptions and types,
|
|
564
|
+
* unless the earlier source was more specific (non-ambiguous).
|
|
565
|
+
*/
|
|
566
|
+
function mergeCommandMeta(...sources) {
|
|
567
|
+
if (sources.length === 0) return { name: "" };
|
|
568
|
+
if (sources.length === 1) return sources[0];
|
|
569
|
+
const result = { name: "" };
|
|
570
|
+
for (const source of sources) {
|
|
571
|
+
if (source.name && !result.name) result.name = source.name;
|
|
572
|
+
if (source.description) result.description = source.description;
|
|
573
|
+
if (source.aliases) result.aliases = [...new Set([...result.aliases || [], ...source.aliases])];
|
|
574
|
+
if (source.examples) result.examples = [...new Set([...result.examples || [], ...source.examples])];
|
|
575
|
+
if (source.deprecated !== void 0) result.deprecated = source.deprecated;
|
|
576
|
+
if (source.arguments) result.arguments = mergeFields(result.arguments || [], source.arguments);
|
|
577
|
+
if (source.positionals) result.positionals = mergeFields(result.positionals || [], source.positionals);
|
|
578
|
+
if (source.subcommands) result.subcommands = mergeSubcommands(result.subcommands || [], source.subcommands);
|
|
579
|
+
}
|
|
580
|
+
if (result.aliases?.length === 0) delete result.aliases;
|
|
581
|
+
if (result.examples?.length === 0) delete result.examples;
|
|
582
|
+
if (result.arguments?.length === 0) delete result.arguments;
|
|
583
|
+
if (result.positionals?.length === 0) delete result.positionals;
|
|
584
|
+
if (result.subcommands?.length === 0) delete result.subcommands;
|
|
585
|
+
return result;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Merge two arrays of FieldMeta by name.
|
|
589
|
+
* Later fields take precedence unless earlier was non-ambiguous.
|
|
590
|
+
*/
|
|
591
|
+
function mergeFields(existing, incoming) {
|
|
592
|
+
const map = /* @__PURE__ */ new Map();
|
|
593
|
+
for (const field of existing) map.set(field.name, { ...field });
|
|
594
|
+
for (const field of incoming) {
|
|
595
|
+
const prev = map.get(field.name);
|
|
596
|
+
if (!prev) {
|
|
597
|
+
map.set(field.name, { ...field });
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
const merged = { ...prev };
|
|
601
|
+
if (field.type !== "unknown") {
|
|
602
|
+
if (prev.ambiguous || !field.ambiguous) {
|
|
603
|
+
merged.type = field.type;
|
|
604
|
+
merged.ambiguous = field.ambiguous;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (field.description) merged.description = field.description;
|
|
608
|
+
if (field.default !== void 0) merged.default = field.default;
|
|
609
|
+
if (field.required !== void 0) merged.required = field.required;
|
|
610
|
+
if (field.aliases) merged.aliases = [...new Set([...prev.aliases || [], ...field.aliases])];
|
|
611
|
+
if (field.enumValues) merged.enumValues = [...new Set([...prev.enumValues || [], ...field.enumValues])];
|
|
612
|
+
if (field.items) merged.items = field.items;
|
|
613
|
+
map.set(field.name, merged);
|
|
614
|
+
}
|
|
615
|
+
return [...map.values()];
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Merge two arrays of CommandMeta by name, recursively.
|
|
619
|
+
*/
|
|
620
|
+
function mergeSubcommands(existing, incoming) {
|
|
621
|
+
const map = /* @__PURE__ */ new Map();
|
|
622
|
+
for (const cmd of existing) map.set(cmd.name, cmd);
|
|
623
|
+
for (const cmd of incoming) {
|
|
624
|
+
const prev = map.get(cmd.name);
|
|
625
|
+
if (!prev) map.set(cmd.name, cmd);
|
|
626
|
+
else map.set(cmd.name, mergeCommandMeta(prev, cmd));
|
|
627
|
+
}
|
|
628
|
+
return [...map.values()];
|
|
629
|
+
}
|
|
630
|
+
//#endregion
|
|
631
|
+
//#region src/codegen/parsers/zsh.ts
|
|
632
|
+
/**
|
|
633
|
+
* Parse zsh completion function definitions into CommandMeta.
|
|
634
|
+
*
|
|
635
|
+
* Zsh completions typically use _arguments or compadd:
|
|
636
|
+
* _arguments \
|
|
637
|
+
* '-v[verbose mode]' \
|
|
638
|
+
* '--output=[output file]:filename:_files' \
|
|
639
|
+
* '1:command:(start stop restart)'
|
|
640
|
+
*/
|
|
641
|
+
function parseZshCompletions(text) {
|
|
642
|
+
const result = {
|
|
643
|
+
name: "",
|
|
644
|
+
arguments: [],
|
|
645
|
+
positionals: [],
|
|
646
|
+
subcommands: []
|
|
647
|
+
};
|
|
648
|
+
const compdefMatch = text.match(/#compdef\s+(\S+)/);
|
|
649
|
+
if (compdefMatch) result.name = compdefMatch[1];
|
|
650
|
+
else {
|
|
651
|
+
const funcMatch = text.match(/^_(\w+)\s*\(\)/m);
|
|
652
|
+
if (funcMatch) result.name = funcMatch[1];
|
|
653
|
+
}
|
|
654
|
+
const argumentsBlocks = findArgumentsBlocks(text);
|
|
655
|
+
for (const block of argumentsBlocks) {
|
|
656
|
+
const specs = parseArgumentSpecs(block);
|
|
657
|
+
for (const spec of specs) if (spec.positional) result.positionals.push(spec);
|
|
658
|
+
else result.arguments.push(spec);
|
|
659
|
+
}
|
|
660
|
+
const subcommands = findSubcommands(text);
|
|
661
|
+
result.subcommands.push(...subcommands);
|
|
662
|
+
if (result.arguments.length === 0) delete result.arguments;
|
|
663
|
+
if (result.positionals.length === 0) delete result.positionals;
|
|
664
|
+
if (result.subcommands.length === 0) delete result.subcommands;
|
|
665
|
+
return result;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Extract _arguments blocks from zsh completion text.
|
|
669
|
+
* Handles backslash continuation lines.
|
|
670
|
+
*/
|
|
671
|
+
function findArgumentsBlocks(text) {
|
|
672
|
+
const blocks = [];
|
|
673
|
+
const lines = text.replace(/\\\n\s*/g, " ").split("\n");
|
|
674
|
+
for (const line of lines) {
|
|
675
|
+
const match = line.match(/_arguments\s+(.+)/);
|
|
676
|
+
if (match) blocks.push(match[1]);
|
|
677
|
+
}
|
|
678
|
+
return blocks;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Parse individual argument specs from an _arguments line.
|
|
682
|
+
*/
|
|
683
|
+
function parseArgumentSpecs(block) {
|
|
684
|
+
const results = [];
|
|
685
|
+
const specs = extractQuotedStrings(block);
|
|
686
|
+
for (const spec of specs) {
|
|
687
|
+
const field = parseZshSpec(spec);
|
|
688
|
+
if (field) results.push(field);
|
|
689
|
+
}
|
|
690
|
+
return results;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Extract single-quoted strings from an _arguments block.
|
|
694
|
+
*/
|
|
695
|
+
function extractQuotedStrings(text) {
|
|
696
|
+
const strings = [];
|
|
697
|
+
const regex = /'([^']+)'/g;
|
|
698
|
+
let match;
|
|
699
|
+
while ((match = regex.exec(text)) !== null) strings.push(match[1]);
|
|
700
|
+
return strings;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Parse a single zsh argument spec.
|
|
704
|
+
*
|
|
705
|
+
* Formats:
|
|
706
|
+
* '-v[verbose mode]' → boolean flag
|
|
707
|
+
* '--output=[output file]:filename:_files' → string with value
|
|
708
|
+
* '(-v --verbose)'{-v,--verbose}'[verbose mode]' → flag with aliases
|
|
709
|
+
* '*--flag[repeatable]' → array
|
|
710
|
+
* '1:command:(start stop restart)' → positional enum
|
|
711
|
+
* ':filename:_files' → positional
|
|
712
|
+
*/
|
|
713
|
+
function parseZshSpec(spec) {
|
|
714
|
+
const positionalMatch = spec.match(/^(\d+)?:([^:]*):(.*)$/);
|
|
715
|
+
if (positionalMatch) {
|
|
716
|
+
const description = positionalMatch[2] || void 0;
|
|
717
|
+
const enumMatch = positionalMatch[3]?.match(/^\(([^)]+)\)$/);
|
|
718
|
+
if (enumMatch) {
|
|
719
|
+
const values = enumMatch[1].split(/\s+/);
|
|
720
|
+
return {
|
|
721
|
+
name: description?.toLowerCase().replace(/\s+/g, "_") || `arg${positionalMatch[1] || "0"}`,
|
|
722
|
+
type: "enum",
|
|
723
|
+
enumValues: values,
|
|
724
|
+
description,
|
|
725
|
+
positional: true
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
return {
|
|
729
|
+
name: description?.toLowerCase().replace(/\s+/g, "_") || `arg${positionalMatch[1] || "0"}`,
|
|
730
|
+
type: "string",
|
|
731
|
+
description,
|
|
732
|
+
positional: true,
|
|
733
|
+
ambiguous: true
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
const repeatable = spec.startsWith("*");
|
|
737
|
+
const flagMatch = (repeatable ? spec.slice(1) : spec).match(/^(-{1,2}[\w-]+)(?:=?)(?:\[([^\]]*)\])?(?::([^:]*):?(.*))?$/);
|
|
738
|
+
if (!flagMatch) return null;
|
|
739
|
+
const rawName = flagMatch[1];
|
|
740
|
+
const description = flagMatch[2] || void 0;
|
|
741
|
+
const valueName = flagMatch[3] || void 0;
|
|
742
|
+
const name = rawName.replace(/^-+/, "").replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
743
|
+
let type = rawName.includes("=") || !!valueName ? "string" : "boolean";
|
|
744
|
+
if (repeatable) type = "array";
|
|
745
|
+
let enumValues;
|
|
746
|
+
if (flagMatch[4]) {
|
|
747
|
+
const enumMatch = flagMatch[4].match(/^\(([^)]+)\)$/);
|
|
748
|
+
if (enumMatch) {
|
|
749
|
+
enumValues = enumMatch[1].split(/\s+/);
|
|
750
|
+
type = "enum";
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
name,
|
|
755
|
+
type,
|
|
756
|
+
description,
|
|
757
|
+
enumValues
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Find subcommand definitions from _describe calls or case statements.
|
|
762
|
+
*/
|
|
763
|
+
function findSubcommands(text) {
|
|
764
|
+
const subcommands = [];
|
|
765
|
+
const seen = /* @__PURE__ */ new Set();
|
|
766
|
+
const describeRegex = /\(([^)]+)\)/g;
|
|
767
|
+
const joined = text.replace(/\\\n\s*/g, " ");
|
|
768
|
+
let match;
|
|
769
|
+
while ((match = describeRegex.exec(joined)) !== null) {
|
|
770
|
+
const content = match[1];
|
|
771
|
+
const entries = content.match(/'([^']+)'/g) || content.match(/"([^"]+)"/g);
|
|
772
|
+
if (!entries) continue;
|
|
773
|
+
for (const entry of entries) {
|
|
774
|
+
const parts = entry.replace(/^['"]|['"]$/g, "").split(":");
|
|
775
|
+
if (parts.length >= 2 && /^[\w-]+$/.test(parts[0])) {
|
|
776
|
+
const name = parts[0];
|
|
777
|
+
if (seen.has(name)) continue;
|
|
778
|
+
seen.add(name);
|
|
779
|
+
subcommands.push({
|
|
780
|
+
name,
|
|
781
|
+
description: parts.slice(1).join(":")
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return subcommands;
|
|
787
|
+
}
|
|
788
|
+
//#endregion
|
|
789
|
+
//#region src/codegen/discovery.ts
|
|
790
|
+
/**
|
|
791
|
+
* Discover CLI structure by running --help recursively and optionally
|
|
792
|
+
* parsing shell completion scripts.
|
|
793
|
+
*/
|
|
794
|
+
async function discoverCli(options) {
|
|
795
|
+
const { command, sources = ["help"], depth, delay = 50, log, timeout = 1e4 } = options;
|
|
796
|
+
const warnings = [];
|
|
797
|
+
let invocations = 0;
|
|
798
|
+
const results = [];
|
|
799
|
+
if (sources.includes("help")) {
|
|
800
|
+
log?.info(`Discovering ${command} via --help...`);
|
|
801
|
+
const helpResult = await crawlHelp(command, [], {
|
|
802
|
+
depth,
|
|
803
|
+
delay,
|
|
804
|
+
timeout,
|
|
805
|
+
log,
|
|
806
|
+
onInvocation: () => {
|
|
807
|
+
invocations++;
|
|
808
|
+
},
|
|
809
|
+
onWarning: (msg) => {
|
|
810
|
+
warnings.push(msg);
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
results.push(helpResult);
|
|
814
|
+
}
|
|
815
|
+
if (sources.includes("fish")) {
|
|
816
|
+
log?.info(`Parsing fish completions for ${command}...`);
|
|
817
|
+
const fishText = await getCompletionScript(command, "fish", timeout);
|
|
818
|
+
if (fishText) results.push(parseFishCompletions(fishText));
|
|
819
|
+
else warnings.push("Could not obtain fish completion script");
|
|
820
|
+
}
|
|
821
|
+
if (sources.includes("zsh")) {
|
|
822
|
+
log?.info(`Parsing zsh completions for ${command}...`);
|
|
823
|
+
const zshText = await getCompletionScript(command, "zsh", timeout);
|
|
824
|
+
if (zshText) results.push(parseZshCompletions(zshText));
|
|
825
|
+
else warnings.push("Could not obtain zsh completion script");
|
|
826
|
+
}
|
|
827
|
+
const merged = results.length > 0 ? mergeCommandMeta(...results) : { name: command };
|
|
828
|
+
if (!merged.name) merged.name = command;
|
|
829
|
+
return {
|
|
830
|
+
command: merged,
|
|
831
|
+
invocations,
|
|
832
|
+
warnings
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Breadth-first crawl of --help output.
|
|
837
|
+
*/
|
|
838
|
+
async function crawlHelp(command, prefixArgs, options) {
|
|
839
|
+
const fullCmd = [command, ...prefixArgs].join(" ");
|
|
840
|
+
options.onInvocation();
|
|
841
|
+
const helpText = await runHelp(command, prefixArgs, options.timeout);
|
|
842
|
+
if (!helpText) {
|
|
843
|
+
options.onWarning(`No help output from: ${fullCmd} --help`);
|
|
844
|
+
return { name: prefixArgs[prefixArgs.length - 1] || command };
|
|
845
|
+
}
|
|
846
|
+
const parsed = parseHelpOutput(helpText, { name: prefixArgs[prefixArgs.length - 1] || command });
|
|
847
|
+
options.log?.info(` ${fullCmd}: ${parsed.subcommands?.length || 0} subcommands, ${parsed.arguments?.length || 0} options`);
|
|
848
|
+
const currentDepth = prefixArgs.length;
|
|
849
|
+
if (parsed.subcommands && parsed.subcommands.length > 0 && (options.depth === void 0 || currentDepth < options.depth)) {
|
|
850
|
+
const resolvedSubs = [];
|
|
851
|
+
for (const sub of parsed.subcommands) {
|
|
852
|
+
if (options.delay > 0) await sleep(options.delay);
|
|
853
|
+
const resolved = await crawlHelp(command, [...prefixArgs, sub.name], options);
|
|
854
|
+
if (!resolved.description && sub.description) resolved.description = sub.description;
|
|
855
|
+
resolvedSubs.push(resolved);
|
|
856
|
+
}
|
|
857
|
+
parsed.subcommands = resolvedSubs;
|
|
858
|
+
}
|
|
859
|
+
return parsed;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Run `<cmd> --help` or `<cmd> help` and return combined stdout+stderr.
|
|
863
|
+
*/
|
|
864
|
+
async function runHelp(command, args, timeout) {
|
|
865
|
+
let result = await runCommand(command, [...args, "--help"], timeout);
|
|
866
|
+
if (result) return result;
|
|
867
|
+
if (args.length > 0) {
|
|
868
|
+
result = await runCommand(command, ["help", ...args], timeout);
|
|
869
|
+
if (result) return result;
|
|
870
|
+
}
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Run a command and return its combined output, or null on failure.
|
|
875
|
+
*/
|
|
876
|
+
async function runCommand(command, args, timeout) {
|
|
877
|
+
try {
|
|
878
|
+
const proc = Bun.spawn([command, ...args], {
|
|
879
|
+
stdout: "pipe",
|
|
880
|
+
stderr: "pipe",
|
|
881
|
+
stdin: "ignore"
|
|
882
|
+
});
|
|
883
|
+
const timer = setTimeout(() => proc.kill(), timeout);
|
|
884
|
+
const [_exitCode, stdoutBuf, stderrBuf] = await Promise.all([
|
|
885
|
+
proc.exited,
|
|
886
|
+
new Response(proc.stdout).arrayBuffer(),
|
|
887
|
+
new Response(proc.stderr).arrayBuffer()
|
|
888
|
+
]);
|
|
889
|
+
clearTimeout(timer);
|
|
890
|
+
const stdout = new TextDecoder().decode(stdoutBuf).trim();
|
|
891
|
+
const stderr = new TextDecoder().decode(stderrBuf).trim();
|
|
892
|
+
const combined = stdout || stderr;
|
|
893
|
+
if (!combined) return null;
|
|
894
|
+
if (combined.length < 10) return null;
|
|
895
|
+
return combined;
|
|
896
|
+
} catch {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Try to get a shell completion script for a command.
|
|
902
|
+
* Checks both `<cmd> completion <shell>` and well-known file paths.
|
|
903
|
+
*/
|
|
904
|
+
async function getCompletionScript(command, shell, timeout) {
|
|
905
|
+
let result = await runCommand(command, ["completion", shell], timeout);
|
|
906
|
+
if (result) return result;
|
|
907
|
+
result = await runCommand(command, ["completions", shell], timeout);
|
|
908
|
+
if (result) return result;
|
|
909
|
+
const paths = shell === "fish" ? [`/usr/share/fish/vendor_completions.d/${command}.fish`, `/usr/local/share/fish/vendor_completions.d/${command}.fish`] : [`/usr/share/zsh/site-functions/_${command}`, `/usr/local/share/zsh/site-functions/_${command}`];
|
|
910
|
+
for (const path of paths) try {
|
|
911
|
+
const file = Bun.file(path);
|
|
912
|
+
if (await file.exists()) return await file.text();
|
|
913
|
+
} catch {}
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
function sleep(ms) {
|
|
917
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
918
|
+
}
|
|
919
|
+
//#endregion
|
|
920
|
+
//#region src/codegen/file-emitter.ts
|
|
921
|
+
var FileEmitterImpl = class {
|
|
922
|
+
files = [];
|
|
923
|
+
options;
|
|
924
|
+
constructor(options) {
|
|
925
|
+
this.options = options;
|
|
926
|
+
}
|
|
927
|
+
addFile(path, content) {
|
|
928
|
+
const text = typeof content === "string" ? content : content.text;
|
|
929
|
+
const fullContent = this.options.header ? `${this.options.header}\n\n${text}` : text;
|
|
930
|
+
this.files.push({
|
|
931
|
+
path,
|
|
932
|
+
content: fullContent
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
async emit() {
|
|
936
|
+
const result = {
|
|
937
|
+
written: [],
|
|
938
|
+
skipped: [],
|
|
939
|
+
errors: []
|
|
940
|
+
};
|
|
941
|
+
const outDir = resolve(this.options.outDir);
|
|
942
|
+
for (const file of this.files) {
|
|
943
|
+
const fullPath = join(outDir, file.path);
|
|
944
|
+
try {
|
|
945
|
+
if (existsSync(fullPath) && !this.options.overwrite) {
|
|
946
|
+
result.skipped.push(file.path);
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (this.options.dryRun) {
|
|
950
|
+
result.written.push(file.path);
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
954
|
+
writeFileSync(fullPath, file.content, "utf-8");
|
|
955
|
+
result.written.push(file.path);
|
|
956
|
+
} catch (err) {
|
|
957
|
+
result.errors.push({
|
|
958
|
+
file: file.path,
|
|
959
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return result;
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
/**
|
|
967
|
+
* Create a FileEmitter for writing multiple generated files to disk.
|
|
968
|
+
*/
|
|
969
|
+
function createFileEmitter(options) {
|
|
970
|
+
return new FileEmitterImpl(options);
|
|
971
|
+
}
|
|
972
|
+
//#endregion
|
|
973
|
+
//#region src/codegen/generators/barrel-file.ts
|
|
974
|
+
/**
|
|
975
|
+
* Generate an index.ts barrel file that re-exports all given files.
|
|
976
|
+
*/
|
|
977
|
+
function generateBarrelFile(files, ctx) {
|
|
978
|
+
const code = ctx.createCodeBuilder();
|
|
979
|
+
for (const file of files) {
|
|
980
|
+
const importPath = file.startsWith("./") ? file : `./${file}`;
|
|
981
|
+
code.line(`export * from '${importPath}'`);
|
|
982
|
+
}
|
|
983
|
+
return code;
|
|
984
|
+
}
|
|
985
|
+
//#endregion
|
|
986
|
+
//#region src/codegen/schema-to-code.ts
|
|
987
|
+
/**
|
|
988
|
+
* Convert a JSON Schema property to Zod code.
|
|
989
|
+
*/
|
|
990
|
+
function jsonSchemaPropertyToZod(prop, required, ambiguous) {
|
|
991
|
+
let code;
|
|
992
|
+
const type = prop.type;
|
|
993
|
+
const enumValues = prop.enum;
|
|
994
|
+
if (enumValues && enumValues.length > 0) code = `z.enum([${enumValues.map((v) => JSON.stringify(v)).join(", ")}])`;
|
|
995
|
+
else if (type === "string") code = "z.string()";
|
|
996
|
+
else if (type === "number" || type === "integer") code = "z.number()";
|
|
997
|
+
else if (type === "boolean") code = "z.boolean()";
|
|
998
|
+
else if (type === "array") {
|
|
999
|
+
const items = prop.items;
|
|
1000
|
+
code = `${items ? jsonSchemaPropertyToZod(items, true) : "z.unknown()"}.array()`;
|
|
1001
|
+
} else code = "z.unknown()";
|
|
1002
|
+
if (prop.default !== void 0) code += `.default(${JSON.stringify(prop.default)})`;
|
|
1003
|
+
else if (!required) code += ".optional()";
|
|
1004
|
+
if (prop.description) code += `.describe(${JSON.stringify(prop.description)})`;
|
|
1005
|
+
if (ambiguous) code += " /* TODO: verify type */";
|
|
1006
|
+
return code;
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Generate Zod source code from a Standard Schema instance by introspecting
|
|
1010
|
+
* its `~standard.jsonSchema` interface.
|
|
1011
|
+
*/
|
|
1012
|
+
function schemaToCode(schema) {
|
|
1013
|
+
try {
|
|
1014
|
+
return jsonSchemaToCode(schema["~standard"].jsonSchema.input({ target: "draft-2020-12" }));
|
|
1015
|
+
} catch {
|
|
1016
|
+
return {
|
|
1017
|
+
code: "z.unknown()",
|
|
1018
|
+
imports: ["z"]
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
function jsonSchemaToCode(jsonSchema) {
|
|
1023
|
+
if (jsonSchema.type === "object" && jsonSchema.properties) {
|
|
1024
|
+
const properties = jsonSchema.properties;
|
|
1025
|
+
const required = new Set(jsonSchema.required || []);
|
|
1026
|
+
return {
|
|
1027
|
+
code: `z.object({\n${Object.entries(properties).map(([key, prop]) => {
|
|
1028
|
+
return ` ${key}: ${jsonSchemaPropertyToZod(prop, required.has(key))},`;
|
|
1029
|
+
}).join("\n")}\n})`,
|
|
1030
|
+
imports: ["z"]
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
code: jsonSchemaPropertyToZod(jsonSchema, true),
|
|
1035
|
+
imports: ["z"]
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Build a real Zod schema from FieldMeta objects.
|
|
1040
|
+
* Returns the schema as Zod source code, since we can't dynamically import Zod here
|
|
1041
|
+
* (codegen has no runtime dependency on padrone's main entry point).
|
|
1042
|
+
*/
|
|
1043
|
+
function fieldMetaToCode(fields) {
|
|
1044
|
+
return {
|
|
1045
|
+
code: `z.object({\n${fields.map((field) => {
|
|
1046
|
+
let code;
|
|
1047
|
+
switch (field.type) {
|
|
1048
|
+
case "string":
|
|
1049
|
+
code = "z.string()";
|
|
1050
|
+
break;
|
|
1051
|
+
case "number":
|
|
1052
|
+
code = "z.number()";
|
|
1053
|
+
break;
|
|
1054
|
+
case "boolean":
|
|
1055
|
+
code = "z.boolean()";
|
|
1056
|
+
break;
|
|
1057
|
+
case "array":
|
|
1058
|
+
code = `${(field.items || "string") === "number" ? "z.number()" : "z.string()"}.array()`;
|
|
1059
|
+
break;
|
|
1060
|
+
case "enum":
|
|
1061
|
+
if (field.enumValues && field.enumValues.length > 0) code = `z.enum([${field.enumValues.map((v) => JSON.stringify(v)).join(", ")}])`;
|
|
1062
|
+
else code = "z.string()";
|
|
1063
|
+
break;
|
|
1064
|
+
default:
|
|
1065
|
+
code = "z.unknown()";
|
|
1066
|
+
break;
|
|
1067
|
+
}
|
|
1068
|
+
if (field.default !== void 0) code += `.default(${JSON.stringify(field.default)})`;
|
|
1069
|
+
else if (!field.required) code += ".optional()";
|
|
1070
|
+
if (field.description) code += `.describe(${JSON.stringify(field.description)})`;
|
|
1071
|
+
if (field.ambiguous) code += " /* TODO: verify type */";
|
|
1072
|
+
return ` ${needsQuoting(field.name) ? JSON.stringify(field.name) : field.name}: ${code},`;
|
|
1073
|
+
}).join("\n")}\n})`,
|
|
1074
|
+
imports: ["z"]
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
const JS_RESERVED$1 = new Set([
|
|
1078
|
+
"break",
|
|
1079
|
+
"case",
|
|
1080
|
+
"catch",
|
|
1081
|
+
"continue",
|
|
1082
|
+
"debugger",
|
|
1083
|
+
"default",
|
|
1084
|
+
"delete",
|
|
1085
|
+
"do",
|
|
1086
|
+
"else",
|
|
1087
|
+
"export",
|
|
1088
|
+
"extends",
|
|
1089
|
+
"finally",
|
|
1090
|
+
"for",
|
|
1091
|
+
"function",
|
|
1092
|
+
"if",
|
|
1093
|
+
"import",
|
|
1094
|
+
"in",
|
|
1095
|
+
"instanceof",
|
|
1096
|
+
"new",
|
|
1097
|
+
"return",
|
|
1098
|
+
"super",
|
|
1099
|
+
"switch",
|
|
1100
|
+
"this",
|
|
1101
|
+
"throw",
|
|
1102
|
+
"try",
|
|
1103
|
+
"typeof",
|
|
1104
|
+
"var",
|
|
1105
|
+
"void",
|
|
1106
|
+
"while",
|
|
1107
|
+
"with",
|
|
1108
|
+
"yield",
|
|
1109
|
+
"class",
|
|
1110
|
+
"const",
|
|
1111
|
+
"enum",
|
|
1112
|
+
"let",
|
|
1113
|
+
"static",
|
|
1114
|
+
"implements",
|
|
1115
|
+
"interface",
|
|
1116
|
+
"package",
|
|
1117
|
+
"private",
|
|
1118
|
+
"protected",
|
|
1119
|
+
"public",
|
|
1120
|
+
"await",
|
|
1121
|
+
"async"
|
|
1122
|
+
]);
|
|
1123
|
+
/** Returns true if the name needs quoting to be a valid JS object key. */
|
|
1124
|
+
function needsQuoting(name) {
|
|
1125
|
+
if (JS_RESERVED$1.has(name)) return true;
|
|
1126
|
+
return !/^[a-zA-Z_$][\w$]*$/.test(name);
|
|
1127
|
+
}
|
|
1128
|
+
//#endregion
|
|
1129
|
+
//#region src/codegen/generators/command-file.ts
|
|
1130
|
+
const JS_RESERVED = new Set([
|
|
1131
|
+
"break",
|
|
1132
|
+
"case",
|
|
1133
|
+
"catch",
|
|
1134
|
+
"continue",
|
|
1135
|
+
"debugger",
|
|
1136
|
+
"default",
|
|
1137
|
+
"delete",
|
|
1138
|
+
"do",
|
|
1139
|
+
"else",
|
|
1140
|
+
"export",
|
|
1141
|
+
"extends",
|
|
1142
|
+
"finally",
|
|
1143
|
+
"for",
|
|
1144
|
+
"function",
|
|
1145
|
+
"if",
|
|
1146
|
+
"import",
|
|
1147
|
+
"in",
|
|
1148
|
+
"instanceof",
|
|
1149
|
+
"new",
|
|
1150
|
+
"return",
|
|
1151
|
+
"super",
|
|
1152
|
+
"switch",
|
|
1153
|
+
"this",
|
|
1154
|
+
"throw",
|
|
1155
|
+
"try",
|
|
1156
|
+
"typeof",
|
|
1157
|
+
"var",
|
|
1158
|
+
"void",
|
|
1159
|
+
"while",
|
|
1160
|
+
"with",
|
|
1161
|
+
"yield",
|
|
1162
|
+
"class",
|
|
1163
|
+
"const",
|
|
1164
|
+
"enum",
|
|
1165
|
+
"let",
|
|
1166
|
+
"static",
|
|
1167
|
+
"implements",
|
|
1168
|
+
"interface",
|
|
1169
|
+
"package",
|
|
1170
|
+
"private",
|
|
1171
|
+
"protected",
|
|
1172
|
+
"public",
|
|
1173
|
+
"await",
|
|
1174
|
+
"async"
|
|
1175
|
+
]);
|
|
1176
|
+
/** Convert a command name to a safe JS identifier (camelCase, reserved-word-safe). */
|
|
1177
|
+
function toSafeIdentifier(name) {
|
|
1178
|
+
const camel = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1179
|
+
if (/^\d/.test(camel)) return `_${camel}`;
|
|
1180
|
+
if (JS_RESERVED.has(camel)) return `_${camel}`;
|
|
1181
|
+
return camel;
|
|
1182
|
+
}
|
|
1183
|
+
/** Build the exported function name for a command (e.g. 'repo' → 'repoCommand'). */
|
|
1184
|
+
function toCommandFunctionName(name) {
|
|
1185
|
+
return `${toSafeIdentifier(name)}Command`;
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Generate a single Padrone command file from a CommandMeta.
|
|
1189
|
+
* Produces a named function that chains .configure(), .arguments(), and .wrap() or .action().
|
|
1190
|
+
*/
|
|
1191
|
+
function generateCommandFile(command, ctx, options) {
|
|
1192
|
+
const code = ctx.createCodeBuilder();
|
|
1193
|
+
const hasArgs = command.arguments && command.arguments.length > 0 || command.positionals && command.positionals.length > 0;
|
|
1194
|
+
if (hasArgs) code.import("z", "zod/v4");
|
|
1195
|
+
code.importType("AnyPadroneBuilder", "padrone");
|
|
1196
|
+
if (options?.subcommands) for (const sub of options.subcommands) code.import([sub.varName], sub.importPath);
|
|
1197
|
+
code.line();
|
|
1198
|
+
if (command.deprecated) {
|
|
1199
|
+
const msg = typeof command.deprecated === "string" ? command.deprecated : "This command is deprecated";
|
|
1200
|
+
code.comment(`@deprecated ${msg}`);
|
|
1201
|
+
}
|
|
1202
|
+
const fnName = toCommandFunctionName(command.name);
|
|
1203
|
+
code.line(`export function ${fnName}<T extends AnyPadroneBuilder>(cmd: T) {`);
|
|
1204
|
+
code.line(` return cmd`);
|
|
1205
|
+
const configParts = [];
|
|
1206
|
+
if (command.description) configParts.push(`description: ${JSON.stringify(command.description)}`);
|
|
1207
|
+
if (command.deprecated) configParts.push(`deprecated: ${typeof command.deprecated === "string" ? JSON.stringify(command.deprecated) : "true"}`);
|
|
1208
|
+
if (configParts.length > 0) code.line(` .configure({ ${configParts.join(", ")} })`);
|
|
1209
|
+
if (hasArgs) {
|
|
1210
|
+
const allFields = [...command.arguments || [], ...command.positionals || []];
|
|
1211
|
+
const schemaCode = fieldMetaToCode(allFields);
|
|
1212
|
+
const positionalNames = (command.positionals || []).map((p) => p.type === "array" ? `'...${p.name}'` : `'${p.name}'`);
|
|
1213
|
+
const fieldsMap = buildFieldsMap(allFields);
|
|
1214
|
+
if (positionalNames.length > 0 || fieldsMap) {
|
|
1215
|
+
code.line(` .arguments(${schemaCode.code}, {`);
|
|
1216
|
+
if (positionalNames.length > 0) code.line(` positional: [${positionalNames.join(", ")}],`);
|
|
1217
|
+
if (fieldsMap) code.line(` fields: ${fieldsMap},`);
|
|
1218
|
+
code.line(` })`);
|
|
1219
|
+
} else code.line(` .arguments(${schemaCode.code})`);
|
|
1220
|
+
}
|
|
1221
|
+
if (options?.subcommands) for (const sub of options.subcommands) {
|
|
1222
|
+
const nameArg = sub.aliases && sub.aliases.length > 0 ? `[${JSON.stringify(sub.name)}, ${sub.aliases.map((a) => JSON.stringify(a)).join(", ")}]` : JSON.stringify(sub.name);
|
|
1223
|
+
code.line(` .command(${nameArg}, ${sub.varName})`);
|
|
1224
|
+
}
|
|
1225
|
+
if (options?.wrap) {
|
|
1226
|
+
const wrapParts = [];
|
|
1227
|
+
wrapParts.push(`command: ${JSON.stringify(options.wrap.command)}`);
|
|
1228
|
+
if (options.wrap.args && options.wrap.args.length > 0) wrapParts.push(`args: [${options.wrap.args.map((a) => JSON.stringify(a)).join(", ")}]`);
|
|
1229
|
+
code.line(` .wrap({ ${wrapParts.join(", ")} })`);
|
|
1230
|
+
} else code.line(` .action((args) => { /* TODO */ })`);
|
|
1231
|
+
code.line(`}`);
|
|
1232
|
+
return code;
|
|
1233
|
+
}
|
|
1234
|
+
function buildFieldsMap(fields) {
|
|
1235
|
+
const entries = [];
|
|
1236
|
+
for (const field of fields) if (field.aliases && field.aliases.length > 0) {
|
|
1237
|
+
const alias = field.aliases.length === 1 ? JSON.stringify(field.aliases[0]) : `[${field.aliases.map((a) => JSON.stringify(a)).join(", ")}]`;
|
|
1238
|
+
const key = /^[a-zA-Z_$][\w$]*$/.test(field.name) ? field.name : JSON.stringify(field.name);
|
|
1239
|
+
entries.push(`${key}: { alias: ${alias} }`);
|
|
1240
|
+
}
|
|
1241
|
+
if (entries.length === 0) return null;
|
|
1242
|
+
return `{ ${entries.join(", ")} }`;
|
|
1243
|
+
}
|
|
1244
|
+
//#endregion
|
|
1245
|
+
//#region src/codegen/generators/command-tree.ts
|
|
1246
|
+
/**
|
|
1247
|
+
* Walk a CommandMeta tree and emit one file per command plus a root program file.
|
|
1248
|
+
* Maps nested subcommands to a directory structure.
|
|
1249
|
+
*/
|
|
1250
|
+
function generateCommandTree(root, ctx, options) {
|
|
1251
|
+
const rootImports = [];
|
|
1252
|
+
function walkCommands(cmd, dirPath, parentArgs) {
|
|
1253
|
+
if (cmd === root) {
|
|
1254
|
+
for (const sub of cmd.subcommands || []) walkCommands(sub, "commands", []);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
const filePath = `${dirPath}/${cmd.name}.ts`;
|
|
1258
|
+
const childRefs = [];
|
|
1259
|
+
if (cmd.subcommands && cmd.subcommands.length > 0) for (const sub of cmd.subcommands) {
|
|
1260
|
+
walkCommands(sub, `${dirPath}/${cmd.name}`, [...parentArgs, cmd.name]);
|
|
1261
|
+
childRefs.push({
|
|
1262
|
+
name: sub.name,
|
|
1263
|
+
varName: toCommandFunctionName(sub.name),
|
|
1264
|
+
importPath: `./${cmd.name}/${sub.name}.ts`,
|
|
1265
|
+
aliases: sub.aliases
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
const fileOptions = {};
|
|
1269
|
+
if (options?.wrap) fileOptions.wrap = {
|
|
1270
|
+
command: options.wrap.command,
|
|
1271
|
+
args: [...parentArgs, cmd.name]
|
|
1272
|
+
};
|
|
1273
|
+
if (childRefs.length > 0) fileOptions.subcommands = childRefs;
|
|
1274
|
+
const code = generateCommandFile(cmd, ctx, Object.keys(fileOptions).length > 0 ? fileOptions : void 0);
|
|
1275
|
+
ctx.emitter.addFile(filePath, code.build());
|
|
1276
|
+
rootImports.push({
|
|
1277
|
+
name: cmd.name,
|
|
1278
|
+
varName: toCommandFunctionName(cmd.name),
|
|
1279
|
+
path: `./${filePath.replace(/\.ts$/, ".ts")}`,
|
|
1280
|
+
aliases: cmd.aliases
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
walkCommands(root, "", []);
|
|
1284
|
+
const program = ctx.createCodeBuilder();
|
|
1285
|
+
const rootHasArgs = root.arguments && root.arguments.length > 0 || root.positionals && root.positionals.length > 0;
|
|
1286
|
+
if (rootHasArgs) program.import("z", "zod/v4");
|
|
1287
|
+
program.import(["createPadrone"], "padrone");
|
|
1288
|
+
const directChildren = rootImports.filter((imp) => imp.path.split("/").length <= 3);
|
|
1289
|
+
for (const imp of directChildren) program.import([imp.varName], imp.path);
|
|
1290
|
+
program.line();
|
|
1291
|
+
program.line(`const program = createPadrone(${JSON.stringify(root.name)})`);
|
|
1292
|
+
const configParts = [];
|
|
1293
|
+
if (root.description) configParts.push(`description: ${JSON.stringify(root.description)}`);
|
|
1294
|
+
if (configParts.length > 0) program.line(` .configure({ ${configParts.join(", ")} })`);
|
|
1295
|
+
if (rootHasArgs) {
|
|
1296
|
+
const schemaCode = fieldMetaToCode([...root.arguments || [], ...root.positionals || []]);
|
|
1297
|
+
program.line(` .arguments(${schemaCode.code})`);
|
|
1298
|
+
}
|
|
1299
|
+
for (const imp of directChildren) {
|
|
1300
|
+
const nameArg = imp.aliases && imp.aliases.length > 0 ? `[${JSON.stringify(imp.name)}, ${imp.aliases.map((a) => JSON.stringify(a)).join(", ")}]` : JSON.stringify(imp.name);
|
|
1301
|
+
program.line(` .command(${nameArg}, ${imp.varName})`);
|
|
1302
|
+
}
|
|
1303
|
+
if (directChildren.length === 0 && options?.wrap) program.line(` .wrap({ command: ${JSON.stringify(options.wrap.command)} })`);
|
|
1304
|
+
program.line();
|
|
1305
|
+
program.line(`export default program`);
|
|
1306
|
+
ctx.emitter.addFile("program.ts", program.build());
|
|
1307
|
+
const index = ctx.createCodeBuilder();
|
|
1308
|
+
index.line(`export { default } from './program.ts'`);
|
|
1309
|
+
ctx.emitter.addFile("index.ts", index.build());
|
|
1310
|
+
}
|
|
1311
|
+
//#endregion
|
|
1312
|
+
//#region src/codegen/template.ts
|
|
1313
|
+
/**
|
|
1314
|
+
* Compile a template string into a reusable render function.
|
|
1315
|
+
*/
|
|
1316
|
+
function template(text) {
|
|
1317
|
+
return (data, partials) => {
|
|
1318
|
+
return render(text, data, partials);
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
function render(text, data, partials) {
|
|
1322
|
+
let result = text;
|
|
1323
|
+
result = result.replace(/\{\{#(\w+)\}\}([\s\S]*?)\{\{\/\1\}\}/g, (_match, key, body) => {
|
|
1324
|
+
const value = data[key];
|
|
1325
|
+
if (value === void 0 || value === null || value === false) return "";
|
|
1326
|
+
if (Array.isArray(value)) return value.map((item) => {
|
|
1327
|
+
if (typeof item === "object" && item !== null) return render(body, item, partials);
|
|
1328
|
+
return body.replace(/\{\{\.\}\}/g, String(item));
|
|
1329
|
+
}).join("");
|
|
1330
|
+
if (typeof value === "object") return render(body, value, partials);
|
|
1331
|
+
return render(body, data, partials);
|
|
1332
|
+
});
|
|
1333
|
+
if (partials) result = result.replace(/\{\{>(\w+)\}\}/g, (_match, name) => {
|
|
1334
|
+
const partialText = partials[name];
|
|
1335
|
+
if (!partialText) return "";
|
|
1336
|
+
return render(partialText, data, partials);
|
|
1337
|
+
});
|
|
1338
|
+
result = result.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
|
1339
|
+
const value = data[key];
|
|
1340
|
+
if (value === void 0 || value === null) return "";
|
|
1341
|
+
return String(value);
|
|
1342
|
+
});
|
|
1343
|
+
return result;
|
|
1344
|
+
}
|
|
1345
|
+
//#endregion
|
|
1346
|
+
export { createCodeBuilder, createFileEmitter, discoverCli, fieldMetaToCode, generateBarrelFile, generateCommandFile, generateCommandTree, mergeCommandMeta, parseFishCompletions, parseHelpOutput, parseZshCompletions, schemaToCode, template };
|
|
1347
|
+
|
|
1348
|
+
//# sourceMappingURL=index.mjs.map
|