padrone 1.1.0 → 1.3.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 +97 -1
- package/LICENSE +1 -1
- package/README.md +60 -30
- package/dist/args-DFEI7_G_.mjs +197 -0
- package/dist/args-DFEI7_G_.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 +1358 -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 +405 -0
- package/dist/docs/index.mjs.map +1 -0
- package/dist/formatter-XroimS3Q.d.mts +83 -0
- package/dist/formatter-XroimS3Q.d.mts.map +1 -0
- package/dist/help-CgGP7hQU.mjs +1229 -0
- package/dist/help-CgGP7hQU.mjs.map +1 -0
- package/dist/index.d.mts +120 -546
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1220 -1204
- 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-BS7RP5Ls.d.mts +1059 -0
- package/dist/types-BS7RP5Ls.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 +457 -0
- package/src/cli/completions.ts +29 -0
- package/src/cli/docs.ts +86 -0
- package/src/cli/doctor.ts +330 -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 +197 -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 +504 -0
- package/src/completion.ts +110 -97
- package/src/create.ts +1048 -308
- package/src/docs/index.ts +607 -0
- package/src/errors.ts +131 -0
- package/src/formatter.ts +195 -73
- package/src/help.ts +159 -58
- package/src/index.ts +12 -15
- package/src/interactive.ts +169 -0
- package/src/parse.ts +52 -21
- 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
package/src/cli/link.ts
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { basename, dirname, resolve } from 'node:path';
|
|
4
|
+
import { detectShell, getRcFile, type ShellType, writeToRcFile } from '../shell-utils.ts';
|
|
5
|
+
import type { PadroneActionContext } from '../types.ts';
|
|
6
|
+
|
|
7
|
+
interface LinkArgs {
|
|
8
|
+
entry?: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
list?: boolean;
|
|
11
|
+
setup?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface UnlinkArgs {
|
|
15
|
+
name?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface LinkEntry {
|
|
19
|
+
name: string;
|
|
20
|
+
entry: string;
|
|
21
|
+
dir: string;
|
|
22
|
+
linkedAt: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type LinksData = Record<string, LinkEntry>;
|
|
26
|
+
|
|
27
|
+
function sanitizeBinName(name: string): string {
|
|
28
|
+
// Strip npm scope (@org/name → name)
|
|
29
|
+
const unscoped = name.startsWith('@') ? name.split('/').pop()! : name;
|
|
30
|
+
// Replace any remaining path-unsafe chars
|
|
31
|
+
return unscoped.replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const PADRONE_HOME = resolve(homedir(), '.padrone');
|
|
35
|
+
const BIN_DIR = resolve(PADRONE_HOME, 'bin');
|
|
36
|
+
const LINKS_FILE = resolve(PADRONE_HOME, 'links.json');
|
|
37
|
+
|
|
38
|
+
function readLinks(): LinksData {
|
|
39
|
+
if (!existsSync(LINKS_FILE)) return {};
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(readFileSync(LINKS_FILE, 'utf-8'));
|
|
42
|
+
} catch {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writeLinks(links: LinksData) {
|
|
48
|
+
mkdirSync(PADRONE_HOME, { recursive: true });
|
|
49
|
+
writeFileSync(LINKS_FILE, `${JSON.stringify(links, null, 2)}\n`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface DetectedEntry {
|
|
53
|
+
entry: string;
|
|
54
|
+
name: string;
|
|
55
|
+
/** Full run command prefix parsed from scripts (e.g. "bun --conditions=padrone@dev") */
|
|
56
|
+
runPrefix?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseRunPrefix(script: string, entryRelative: string, dir: string): string | undefined {
|
|
60
|
+
// Split script into tokens and find the one that resolves to the same path as the entry
|
|
61
|
+
const entryResolved = resolve(dir, entryRelative);
|
|
62
|
+
const tokens = script.split(/\s+/);
|
|
63
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
64
|
+
const token = tokens[i]!;
|
|
65
|
+
if (resolve(dir, token) === entryResolved) {
|
|
66
|
+
const prefix = tokens.slice(0, i).join(' ');
|
|
67
|
+
return prefix || undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function detectEntry(dir: string): DetectedEntry | undefined {
|
|
74
|
+
const pkgPath = resolve(dir, 'package.json');
|
|
75
|
+
if (!existsSync(pkgPath)) return undefined;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
79
|
+
|
|
80
|
+
let entryRelative: string | undefined;
|
|
81
|
+
let name: string | undefined;
|
|
82
|
+
|
|
83
|
+
// Try bin field first
|
|
84
|
+
if (pkg.bin) {
|
|
85
|
+
if (typeof pkg.bin === 'string') {
|
|
86
|
+
entryRelative = pkg.bin;
|
|
87
|
+
name = pkg.name || basename(dir);
|
|
88
|
+
} else {
|
|
89
|
+
const binEntries = Object.entries(pkg.bin);
|
|
90
|
+
if (binEntries.length > 0) {
|
|
91
|
+
[name, entryRelative] = binEntries[0] as [string, string];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Fallback to main/module
|
|
97
|
+
if (!entryRelative) {
|
|
98
|
+
const main = pkg.module || pkg.main;
|
|
99
|
+
if (main) {
|
|
100
|
+
entryRelative = main;
|
|
101
|
+
name = pkg.name || basename(dir);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!entryRelative || !name) return undefined;
|
|
106
|
+
|
|
107
|
+
// Check start/dev scripts for runtime flags
|
|
108
|
+
let runPrefix: string | undefined;
|
|
109
|
+
const scripts = pkg.scripts as Record<string, string> | undefined;
|
|
110
|
+
if (scripts) {
|
|
111
|
+
for (const key of ['start', 'dev']) {
|
|
112
|
+
if (scripts[key]) {
|
|
113
|
+
runPrefix = parseRunPrefix(scripts[key], entryRelative!, dir);
|
|
114
|
+
if (runPrefix) break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { entry: resolve(dir, entryRelative), name, runPrefix };
|
|
120
|
+
} catch {
|
|
121
|
+
// Invalid package.json
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isInPath(dir: string): boolean {
|
|
128
|
+
const pathEnv = process.env.PATH || '';
|
|
129
|
+
return pathEnv.split(':').includes(dir);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const PATH_BEGIN_MARKER = '###-begin-padrone-path-###';
|
|
133
|
+
const PATH_END_MARKER = '###-end-padrone-path-###';
|
|
134
|
+
|
|
135
|
+
function buildPathSnippet(shell: ShellType, binDir: string): string {
|
|
136
|
+
switch (shell) {
|
|
137
|
+
case 'fish':
|
|
138
|
+
return `${PATH_BEGIN_MARKER}\nfish_add_path "${binDir}"\n${PATH_END_MARKER}`;
|
|
139
|
+
case 'powershell':
|
|
140
|
+
return `${PATH_BEGIN_MARKER}\n$env:PATH = "${binDir}" + [IO.Path]::PathSeparator + $env:PATH\n${PATH_END_MARKER}`;
|
|
141
|
+
default:
|
|
142
|
+
return `${PATH_BEGIN_MARKER}\nexport PATH="${binDir}:$PATH"\n${PATH_END_MARKER}`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function setupPath(shell: ShellType): { file: string; updated: boolean } {
|
|
147
|
+
const rcFile = getRcFile(shell);
|
|
148
|
+
if (!rcFile) {
|
|
149
|
+
throw new Error(`Could not determine config file for ${shell}.`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const snippet = buildPathSnippet(shell, BIN_DIR);
|
|
153
|
+
return writeToRcFile(rcFile, snippet, PATH_BEGIN_MARKER, PATH_END_MARKER);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function detectRuntime(dir: string): string {
|
|
157
|
+
let current = dir;
|
|
158
|
+
while (true) {
|
|
159
|
+
if (existsSync(resolve(current, 'bun.lock')) || existsSync(resolve(current, 'bun.lockb'))) return 'bun';
|
|
160
|
+
if (
|
|
161
|
+
existsSync(resolve(current, 'package-lock.json')) ||
|
|
162
|
+
existsSync(resolve(current, 'yarn.lock')) ||
|
|
163
|
+
existsSync(resolve(current, 'pnpm-lock.yaml'))
|
|
164
|
+
)
|
|
165
|
+
return 'node';
|
|
166
|
+
const parent = dirname(current);
|
|
167
|
+
if (parent === current) break;
|
|
168
|
+
current = parent;
|
|
169
|
+
}
|
|
170
|
+
return 'node';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function createShim(name: string, entry: string, dir: string, runPrefix?: string) {
|
|
174
|
+
mkdirSync(BIN_DIR, { recursive: true });
|
|
175
|
+
const shimPath = resolve(BIN_DIR, sanitizeBinName(name));
|
|
176
|
+
|
|
177
|
+
const prefix = runPrefix ?? detectRuntime(dir);
|
|
178
|
+
|
|
179
|
+
const shim = ['#!/usr/bin/env sh', `# Linked by padrone — do not edit`, `${prefix} "${entry}" "$@"`, ''].join('\n');
|
|
180
|
+
|
|
181
|
+
writeFileSync(shimPath, shim);
|
|
182
|
+
chmodSync(shimPath, 0o755);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function removeShim(name: string) {
|
|
186
|
+
const shimPath = resolve(BIN_DIR, sanitizeBinName(name));
|
|
187
|
+
if (existsSync(shimPath)) {
|
|
188
|
+
rmSync(shimPath);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function runLink(args: LinkArgs, ctx: PadroneActionContext) {
|
|
193
|
+
const { output, error } = ctx.runtime;
|
|
194
|
+
|
|
195
|
+
if (args.list) {
|
|
196
|
+
const links = readLinks();
|
|
197
|
+
const entries = Object.values(links);
|
|
198
|
+
if (entries.length === 0) {
|
|
199
|
+
output('No linked programs.');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
output('Linked programs:');
|
|
203
|
+
for (const link of entries) {
|
|
204
|
+
output(` ${link.name} → ${link.entry}`);
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const dir = process.cwd();
|
|
210
|
+
|
|
211
|
+
let entry: string;
|
|
212
|
+
let name: string;
|
|
213
|
+
let runPrefix: string | undefined;
|
|
214
|
+
|
|
215
|
+
const resolvedArg = args.entry ? resolve(dir, args.entry) : undefined;
|
|
216
|
+
|
|
217
|
+
// Determine the target directory: if a folder or package.json was passed, use that
|
|
218
|
+
let targetDir: string | undefined;
|
|
219
|
+
if (resolvedArg) {
|
|
220
|
+
if (existsSync(resolvedArg) && statSync(resolvedArg).isDirectory()) {
|
|
221
|
+
targetDir = resolvedArg;
|
|
222
|
+
} else if (basename(resolvedArg) === 'package.json' && existsSync(resolvedArg)) {
|
|
223
|
+
targetDir = dirname(resolvedArg);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (targetDir || !resolvedArg) {
|
|
228
|
+
// Detect entry from the target directory's package.json
|
|
229
|
+
const detected = detectEntry(targetDir ?? dir);
|
|
230
|
+
if (!detected) {
|
|
231
|
+
error('Could not detect entry point. Provide an entry file or add a "bin" field to package.json.');
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
entry = detected.entry;
|
|
235
|
+
name = sanitizeBinName(args.name || detected.name);
|
|
236
|
+
runPrefix = detected.runPrefix;
|
|
237
|
+
} else {
|
|
238
|
+
// Explicit file path
|
|
239
|
+
entry = resolvedArg;
|
|
240
|
+
name = sanitizeBinName(args.name || basename(entry).replace(/\.[cm]?[jt]sx?$/, ''));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const GENERIC_NAMES = new Set(['cli', 'index', 'main', 'app', 'bin', 'start', 'src', 'run', 'server', 'program']);
|
|
244
|
+
if (GENERIC_NAMES.has(name)) {
|
|
245
|
+
error(`"${name}" is too generic to use as a command name. Use --name to specify one.`);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!existsSync(entry)) {
|
|
250
|
+
error(`Entry file not found: ${entry}`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const entryDir = targetDir ?? (existsSync(resolve(dirname(entry), 'package.json')) ? dirname(entry) : dir);
|
|
255
|
+
|
|
256
|
+
createShim(name, entry, entryDir, runPrefix);
|
|
257
|
+
|
|
258
|
+
const links = readLinks();
|
|
259
|
+
links[name] = {
|
|
260
|
+
name,
|
|
261
|
+
entry,
|
|
262
|
+
dir: entryDir,
|
|
263
|
+
linkedAt: new Date().toISOString(),
|
|
264
|
+
};
|
|
265
|
+
writeLinks(links);
|
|
266
|
+
|
|
267
|
+
output(`Linked ${name} → ${entry}`);
|
|
268
|
+
|
|
269
|
+
if (!isInPath(BIN_DIR)) {
|
|
270
|
+
if (args.setup) {
|
|
271
|
+
const shell = detectShell();
|
|
272
|
+
if (!shell) {
|
|
273
|
+
error('Could not detect shell. Add the PATH manually:');
|
|
274
|
+
error(` export PATH="${BIN_DIR}:$PATH"`);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const result = setupPath(shell);
|
|
278
|
+
const verb = result.updated ? 'Updated' : 'Added';
|
|
279
|
+
output(`${verb} PATH in ${result.file}`);
|
|
280
|
+
output('Restart your shell or run:');
|
|
281
|
+
output(` export PATH="${BIN_DIR}:$PATH"`);
|
|
282
|
+
} else {
|
|
283
|
+
output('');
|
|
284
|
+
output(`Add ${BIN_DIR} to your PATH to use "${name}" globally:`);
|
|
285
|
+
output(` export PATH="${BIN_DIR}:$PATH"`);
|
|
286
|
+
output('Or re-run with --setup to do it automatically.');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function runUnlink(args: UnlinkArgs, ctx: PadroneActionContext) {
|
|
292
|
+
const { output, error } = ctx.runtime;
|
|
293
|
+
const links = readLinks();
|
|
294
|
+
|
|
295
|
+
let name = args.name ? sanitizeBinName(args.name) : undefined;
|
|
296
|
+
|
|
297
|
+
if (!name) {
|
|
298
|
+
const dir = process.cwd();
|
|
299
|
+
const detected = detectEntry(dir);
|
|
300
|
+
if (detected) {
|
|
301
|
+
name = sanitizeBinName(detected.name);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!name) {
|
|
306
|
+
error('Could not detect program name. Provide the name to unlink.');
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!links[name]) {
|
|
311
|
+
error(`"${name}" is not linked.`);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
removeShim(name);
|
|
316
|
+
delete links[name];
|
|
317
|
+
writeLinks(links);
|
|
318
|
+
|
|
319
|
+
output(`Unlinked ${name}`);
|
|
320
|
+
}
|
package/src/cli/wrap.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { createCodeBuilder, createFileEmitter, generateCommandTree } from 'padrone/codegen';
|
|
3
|
+
import type { DiscoverySource } from '../codegen/discovery.ts';
|
|
4
|
+
import { discoverCli } from '../codegen/discovery.ts';
|
|
5
|
+
import { template } from '../codegen/template.ts';
|
|
6
|
+
import type { GeneratorContext } from '../codegen/types.ts';
|
|
7
|
+
import type { PadroneActionContext } from '../types.ts';
|
|
8
|
+
|
|
9
|
+
interface WrapArgs {
|
|
10
|
+
command: string;
|
|
11
|
+
source?: DiscoverySource;
|
|
12
|
+
output?: string;
|
|
13
|
+
depth?: number;
|
|
14
|
+
dryRun?: boolean;
|
|
15
|
+
overwrite?: boolean;
|
|
16
|
+
yes?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function runWrap(args: WrapArgs, ctx: PadroneActionContext) {
|
|
20
|
+
const { output, error } = ctx.runtime;
|
|
21
|
+
const command = args.command;
|
|
22
|
+
const sources: DiscoverySource[] = args.source ? [args.source] : ['help'];
|
|
23
|
+
const outDir = resolve(args.output || `./src/${command}`);
|
|
24
|
+
|
|
25
|
+
// Experimental warning — skip with -y
|
|
26
|
+
if (!args.yes) {
|
|
27
|
+
output('⚠ The `wrap` command is experimental. Generated code may require manual adjustments.');
|
|
28
|
+
output('');
|
|
29
|
+
|
|
30
|
+
if (ctx.runtime.prompt) {
|
|
31
|
+
const proceed = await ctx.runtime.prompt({
|
|
32
|
+
name: 'confirm',
|
|
33
|
+
message: 'Do you want to continue?',
|
|
34
|
+
type: 'confirm',
|
|
35
|
+
default: true,
|
|
36
|
+
});
|
|
37
|
+
if (!proceed) {
|
|
38
|
+
output('Aborted.');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
output('');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
output(`Discovering ${command} CLI structure...`);
|
|
46
|
+
|
|
47
|
+
const result = await discoverCli({
|
|
48
|
+
command,
|
|
49
|
+
sources,
|
|
50
|
+
depth: args.depth,
|
|
51
|
+
log: {
|
|
52
|
+
info: (msg) => output(msg),
|
|
53
|
+
warn: (msg) => output(` warn: ${msg}`),
|
|
54
|
+
error: (msg) => error(msg),
|
|
55
|
+
success: (msg) => output(msg),
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Error out if the root command returned nothing useful
|
|
60
|
+
const hasSubcommands = result.command.subcommands && result.command.subcommands.length > 0;
|
|
61
|
+
const hasArguments = result.command.arguments && result.command.arguments.length > 0;
|
|
62
|
+
if (!hasSubcommands && !hasArguments && !result.command.description) {
|
|
63
|
+
error(`Could not discover CLI structure for "${command}". Make sure the command exists and supports --help.`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (result.warnings.length > 0) {
|
|
68
|
+
output('');
|
|
69
|
+
output('Warnings:');
|
|
70
|
+
for (const warn of result.warnings) {
|
|
71
|
+
output(` - ${warn}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const subcommandCount = countSubcommands(result.command);
|
|
76
|
+
const optionCount = countOptions(result.command);
|
|
77
|
+
|
|
78
|
+
output('');
|
|
79
|
+
output(`Discovered: ${subcommandCount} commands, ${optionCount} options (${result.invocations} invocations)`);
|
|
80
|
+
output('');
|
|
81
|
+
|
|
82
|
+
// Generate the wrapper project
|
|
83
|
+
const emitter = createFileEmitter({
|
|
84
|
+
outDir,
|
|
85
|
+
header: `// Generated by \`padrone wrap ${command}\` — do not edit manually`,
|
|
86
|
+
overwrite: args.overwrite,
|
|
87
|
+
dryRun: args.dryRun,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const genCtx: GeneratorContext = {
|
|
91
|
+
outDir,
|
|
92
|
+
createCodeBuilder,
|
|
93
|
+
emitter,
|
|
94
|
+
template,
|
|
95
|
+
log: {
|
|
96
|
+
info: (msg) => output(msg),
|
|
97
|
+
warn: (msg) => output(` warn: ${msg}`),
|
|
98
|
+
error: (msg) => error(msg),
|
|
99
|
+
success: (msg) => output(msg),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
generateCommandTree(result.command, genCtx, { wrap: { command } });
|
|
104
|
+
|
|
105
|
+
const emitResult = await emitter.emit();
|
|
106
|
+
|
|
107
|
+
if (emitResult.errors.length > 0) {
|
|
108
|
+
for (const err of emitResult.errors) {
|
|
109
|
+
error(`Failed to write ${err.file}: ${err.error.message}`);
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (args.dryRun) {
|
|
115
|
+
output('Dry run — files that would be written:');
|
|
116
|
+
} else {
|
|
117
|
+
output('Files written:');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const file of emitResult.written) {
|
|
121
|
+
output(` ${file}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (emitResult.skipped.length > 0) {
|
|
125
|
+
output('');
|
|
126
|
+
output('Skipped (already exist, use --overwrite to replace):');
|
|
127
|
+
for (const file of emitResult.skipped) {
|
|
128
|
+
output(` ${file}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function countSubcommands(cmd: { subcommands?: { subcommands?: any[] }[] }): number {
|
|
134
|
+
let count = 0;
|
|
135
|
+
if (cmd.subcommands) {
|
|
136
|
+
count += cmd.subcommands.length;
|
|
137
|
+
for (const sub of cmd.subcommands) {
|
|
138
|
+
count += countSubcommands(sub);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return count;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function countOptions(cmd: { arguments?: unknown[]; subcommands?: any[] }): number {
|
|
145
|
+
let count = cmd.arguments?.length || 0;
|
|
146
|
+
if (cmd.subcommands) {
|
|
147
|
+
for (const sub of cmd.subcommands) {
|
|
148
|
+
count += countOptions(sub);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return count;
|
|
152
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# padrone/codegen
|
|
2
|
+
|
|
3
|
+
Code generation toolkit for Padrone CLI projects. Import from `padrone/codegen`.
|
|
4
|
+
|
|
5
|
+
## Core Concepts
|
|
6
|
+
|
|
7
|
+
All parsers produce `CommandMeta` / `FieldMeta` (intermediate representations). All generators consume them. This decouples input formats from output formats.
|
|
8
|
+
|
|
9
|
+
## API Reference
|
|
10
|
+
|
|
11
|
+
### Types
|
|
12
|
+
|
|
13
|
+
- **`FieldMeta`** — Metadata for a single option/flag/argument: `name`, `type` (`string | number | boolean | array | enum | unknown`), `description`, `default`, `required`, `aliases`, `positional`, `enumValues`, `ambiguous`.
|
|
14
|
+
- **`CommandMeta`** — Intermediate representation for a CLI command: `name`, `description`, `aliases`, `arguments` (named options), `positionals`, `subcommands` (recursive), `examples`, `deprecated`.
|
|
15
|
+
- **`CodeBuilder`** — Fluent interface for building TypeScript source with `.import()`, `.importType()`, `.line()`, `.block()`, `.comment()`, `.raw()`, `.build()`.
|
|
16
|
+
- **`FileEmitter`** — Multi-file output manager with `.addFile()` and `.emit()`.
|
|
17
|
+
- **`GeneratorContext`** — Shared context for generators: `outDir`, `createCodeBuilder`, `emitter`, `template`, `log`.
|
|
18
|
+
|
|
19
|
+
### Template Engine
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { template } from 'padrone/codegen'
|
|
23
|
+
|
|
24
|
+
const render = template(`Hello, {{name}}!`)
|
|
25
|
+
render({ name: 'world' }) // "Hello, world!"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Syntax: `{{var}}` interpolation, `{{#arr}}...{{/arr}}` iteration (`{{.}}` for current item), `{{#bool}}...{{/bool}}` conditionals, `{{>partial}}` partials.
|
|
29
|
+
|
|
30
|
+
### CodeBuilder
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { createCodeBuilder } from 'padrone/codegen'
|
|
34
|
+
|
|
35
|
+
const code = createCodeBuilder()
|
|
36
|
+
.import(['createPadrone'], 'padrone')
|
|
37
|
+
.import(['z'], 'zod/v4')
|
|
38
|
+
.line(`const program = createPadrone('my-cli')`)
|
|
39
|
+
.block('.configure({', (b) => b.line(`description: 'My CLI',`), '})')
|
|
40
|
+
.build()
|
|
41
|
+
|
|
42
|
+
// code.text contains the formatted source
|
|
43
|
+
// code.imports contains the deduped import map
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### FileEmitter
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { createFileEmitter } from 'padrone/codegen'
|
|
50
|
+
|
|
51
|
+
const emitter = createFileEmitter({
|
|
52
|
+
outDir: './output',
|
|
53
|
+
header: '// Auto-generated',
|
|
54
|
+
overwrite: false, // skip existing files
|
|
55
|
+
dryRun: false,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
emitter.addFile('src/index.ts', codeBuilder.build())
|
|
59
|
+
emitter.addFile('package.json', jsonString)
|
|
60
|
+
const result = await emitter.emit()
|
|
61
|
+
// result: { written: string[], skipped: string[], errors: [] }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Schema-to-Code
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { schemaToCode, fieldMetaToCode } from 'padrone/codegen'
|
|
68
|
+
|
|
69
|
+
// Convert a Standard Schema (e.g. Zod) to Zod source code
|
|
70
|
+
const result = schemaToCode(myZodSchema)
|
|
71
|
+
// result: { code: 'z.object({ ... })', imports: ['z'] }
|
|
72
|
+
|
|
73
|
+
// Convert FieldMeta[] to Zod z.object() source
|
|
74
|
+
const result2 = fieldMetaToCode(fields)
|
|
75
|
+
// result2: { code: 'z.object({ ... })', imports: ['z'] }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Generators
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { generateCommandFile, generateCommandTree, generateBarrelFile } from 'padrone/codegen'
|
|
82
|
+
|
|
83
|
+
// Generate a single command file from CommandMeta
|
|
84
|
+
const builder = generateCommandFile(commandMeta, generatorCtx)
|
|
85
|
+
|
|
86
|
+
// Walk a CommandMeta tree and emit one file per command + program.ts + index.ts
|
|
87
|
+
generateCommandTree(rootCommandMeta, generatorCtx)
|
|
88
|
+
|
|
89
|
+
// Generate a barrel (index.ts) re-exporting from given paths
|
|
90
|
+
const barrelCode = generateBarrelFile(['./commands/hello', './commands/deploy'])
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Parsers
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { parseHelpOutput, parseFishCompletions, parseZshCompletions, mergeCommandMeta } from 'padrone/codegen'
|
|
97
|
+
|
|
98
|
+
// Parse --help output (GNU, cobra, argparse, commander/yargs styles)
|
|
99
|
+
const meta = parseHelpOutput(helpText, { name: 'my-tool' })
|
|
100
|
+
|
|
101
|
+
// Parse fish shell completion scripts
|
|
102
|
+
const meta2 = parseFishCompletions(fishScript)
|
|
103
|
+
|
|
104
|
+
// Parse zsh _arguments completion definitions
|
|
105
|
+
const meta3 = parseZshCompletions(zshScript)
|
|
106
|
+
|
|
107
|
+
// Merge multiple CommandMeta from different sources (later sources take precedence unless ambiguous)
|
|
108
|
+
const merged = mergeCommandMeta(meta, meta2, meta3)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Typical Workflow
|
|
112
|
+
|
|
113
|
+
1. **Parse** existing CLI help/completions into `CommandMeta` using parsers
|
|
114
|
+
2. **Merge** multiple sources with `mergeCommandMeta()` for best coverage
|
|
115
|
+
3. **Generate** Padrone source files with `generateCommandTree()` or `generateCommandFile()`
|
|
116
|
+
4. **Emit** files to disk with `FileEmitter`
|
|
117
|
+
|
|
118
|
+
Or use `template()` and `createFileEmitter()` directly for custom scaffolding (see `padrone init` implementation in `src/cli/init.ts`).
|