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.
Files changed (80) hide show
  1. package/CHANGELOG.md +97 -1
  2. package/LICENSE +1 -1
  3. package/README.md +60 -30
  4. package/dist/args-DFEI7_G_.mjs +197 -0
  5. package/dist/args-DFEI7_G_.mjs.map +1 -0
  6. package/dist/chunk-y_GBKt04.mjs +5 -0
  7. package/dist/codegen/index.d.mts +305 -0
  8. package/dist/codegen/index.d.mts.map +1 -0
  9. package/dist/codegen/index.mjs +1358 -0
  10. package/dist/codegen/index.mjs.map +1 -0
  11. package/dist/completion.d.mts +64 -0
  12. package/dist/completion.d.mts.map +1 -0
  13. package/dist/completion.mjs +417 -0
  14. package/dist/completion.mjs.map +1 -0
  15. package/dist/docs/index.d.mts +34 -0
  16. package/dist/docs/index.d.mts.map +1 -0
  17. package/dist/docs/index.mjs +405 -0
  18. package/dist/docs/index.mjs.map +1 -0
  19. package/dist/formatter-XroimS3Q.d.mts +83 -0
  20. package/dist/formatter-XroimS3Q.d.mts.map +1 -0
  21. package/dist/help-CgGP7hQU.mjs +1229 -0
  22. package/dist/help-CgGP7hQU.mjs.map +1 -0
  23. package/dist/index.d.mts +120 -546
  24. package/dist/index.d.mts.map +1 -1
  25. package/dist/index.mjs +1220 -1204
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/test.d.mts +112 -0
  28. package/dist/test.d.mts.map +1 -0
  29. package/dist/test.mjs +138 -0
  30. package/dist/test.mjs.map +1 -0
  31. package/dist/types-BS7RP5Ls.d.mts +1059 -0
  32. package/dist/types-BS7RP5Ls.d.mts.map +1 -0
  33. package/dist/update-check-EbNDkzyV.mjs +146 -0
  34. package/dist/update-check-EbNDkzyV.mjs.map +1 -0
  35. package/package.json +61 -21
  36. package/src/args.ts +457 -0
  37. package/src/cli/completions.ts +29 -0
  38. package/src/cli/docs.ts +86 -0
  39. package/src/cli/doctor.ts +330 -0
  40. package/src/cli/index.ts +159 -0
  41. package/src/cli/init.ts +135 -0
  42. package/src/cli/link.ts +320 -0
  43. package/src/cli/wrap.ts +152 -0
  44. package/src/codegen/README.md +118 -0
  45. package/src/codegen/code-builder.ts +226 -0
  46. package/src/codegen/discovery.ts +232 -0
  47. package/src/codegen/file-emitter.ts +73 -0
  48. package/src/codegen/generators/barrel-file.ts +16 -0
  49. package/src/codegen/generators/command-file.ts +197 -0
  50. package/src/codegen/generators/command-tree.ts +124 -0
  51. package/src/codegen/index.ts +33 -0
  52. package/src/codegen/parsers/fish.ts +163 -0
  53. package/src/codegen/parsers/help.ts +378 -0
  54. package/src/codegen/parsers/merge.ts +158 -0
  55. package/src/codegen/parsers/zsh.ts +221 -0
  56. package/src/codegen/schema-to-code.ts +199 -0
  57. package/src/codegen/template.ts +69 -0
  58. package/src/codegen/types.ts +143 -0
  59. package/src/colorizer.ts +2 -2
  60. package/src/command-utils.ts +504 -0
  61. package/src/completion.ts +110 -97
  62. package/src/create.ts +1048 -308
  63. package/src/docs/index.ts +607 -0
  64. package/src/errors.ts +131 -0
  65. package/src/formatter.ts +195 -73
  66. package/src/help.ts +159 -58
  67. package/src/index.ts +12 -15
  68. package/src/interactive.ts +169 -0
  69. package/src/parse.ts +52 -21
  70. package/src/repl-loop.ts +317 -0
  71. package/src/runtime.ts +304 -0
  72. package/src/shell-utils.ts +83 -0
  73. package/src/test.ts +285 -0
  74. package/src/type-helpers.ts +10 -10
  75. package/src/type-utils.ts +124 -14
  76. package/src/types.ts +752 -154
  77. package/src/update-check.ts +244 -0
  78. package/src/wrap.ts +44 -40
  79. package/src/zod.d.ts +2 -2
  80. package/src/options.ts +0 -180
@@ -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
+ }
@@ -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`).