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
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { commandSymbol } from '../command-utils.ts';
|
|
3
|
+
import type { AnyPadroneCommand, PadroneActionContext } from '../types.ts';
|
|
4
|
+
|
|
5
|
+
interface DoctorArgs {
|
|
6
|
+
entry: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type Severity = 'error' | 'warning';
|
|
10
|
+
|
|
11
|
+
interface Diagnostic {
|
|
12
|
+
severity: Severity;
|
|
13
|
+
command: string;
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function runDoctor(args: DoctorArgs, _ctx: PadroneActionContext) {
|
|
18
|
+
const entryPath = resolve(args.entry);
|
|
19
|
+
|
|
20
|
+
let mod: Record<string, unknown>;
|
|
21
|
+
try {
|
|
22
|
+
mod = (await import(entryPath)) as Record<string, unknown>;
|
|
23
|
+
} catch (err) {
|
|
24
|
+
console.error(`Failed to import entry file: ${entryPath}`);
|
|
25
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const program = findProgram(mod);
|
|
30
|
+
if (!program) {
|
|
31
|
+
console.error('No Padrone program found in the entry file.');
|
|
32
|
+
console.error('The entry file must export a Padrone program (default or named export).');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const cmd = (program as any)[commandSymbol] as AnyPadroneCommand;
|
|
37
|
+
const diagnostics: Diagnostic[] = [];
|
|
38
|
+
|
|
39
|
+
collectDiagnostics(cmd, diagnostics);
|
|
40
|
+
|
|
41
|
+
if (diagnostics.length === 0) {
|
|
42
|
+
console.log('No issues found.');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const errors = diagnostics.filter((d) => d.severity === 'error');
|
|
47
|
+
const warnings = diagnostics.filter((d) => d.severity === 'warning');
|
|
48
|
+
|
|
49
|
+
for (const d of diagnostics) {
|
|
50
|
+
const prefix = d.severity === 'error' ? 'error' : 'warning';
|
|
51
|
+
console.log(` ${prefix}: [${d.command}] ${d.message}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log();
|
|
55
|
+
const parts: string[] = [];
|
|
56
|
+
if (errors.length > 0) parts.push(`${errors.length} error(s)`);
|
|
57
|
+
if (warnings.length > 0) parts.push(`${warnings.length} warning(s)`);
|
|
58
|
+
console.log(`Found ${parts.join(' and ')}.`);
|
|
59
|
+
|
|
60
|
+
if (errors.length > 0) {
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function collectDiagnostics(cmd: AnyPadroneCommand, diagnostics: Diagnostic[]) {
|
|
66
|
+
const allCommands = flattenCommands(cmd);
|
|
67
|
+
|
|
68
|
+
checkDuplicateAliases(allCommands, diagnostics);
|
|
69
|
+
checkShadowedOptionNames(allCommands, diagnostics);
|
|
70
|
+
checkCommandsWithoutActions(allCommands, diagnostics);
|
|
71
|
+
checkSchemasWithoutDescriptions(allCommands, diagnostics);
|
|
72
|
+
checkConflictingPositionals(allCommands, diagnostics);
|
|
73
|
+
checkUnusedPlugins(allCommands, diagnostics);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function flattenCommands(cmd: AnyPadroneCommand): AnyPadroneCommand[] {
|
|
77
|
+
const result: AnyPadroneCommand[] = [cmd];
|
|
78
|
+
if (cmd.commands) {
|
|
79
|
+
for (const sub of cmd.commands) {
|
|
80
|
+
result.push(...flattenCommands(sub));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function commandDisplayName(cmd: AnyPadroneCommand): string {
|
|
87
|
+
return cmd.path || cmd.name || '<root>';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getJsonSchema(cmd: AnyPadroneCommand): Record<string, any> | null {
|
|
91
|
+
try {
|
|
92
|
+
if (!cmd.argsSchema) return null;
|
|
93
|
+
return cmd.argsSchema['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check for duplicate aliases across sibling commands.
|
|
101
|
+
*/
|
|
102
|
+
function checkDuplicateAliases(commands: AnyPadroneCommand[], diagnostics: Diagnostic[]) {
|
|
103
|
+
for (const cmd of commands) {
|
|
104
|
+
if (!cmd.commands || cmd.commands.length < 2) continue;
|
|
105
|
+
|
|
106
|
+
const seen = new Map<string, string>();
|
|
107
|
+
for (const sub of cmd.commands) {
|
|
108
|
+
const names = [sub.name, ...(sub.aliases || [])];
|
|
109
|
+
for (const name of names) {
|
|
110
|
+
if (!name) continue;
|
|
111
|
+
const existing = seen.get(name);
|
|
112
|
+
if (existing) {
|
|
113
|
+
diagnostics.push({
|
|
114
|
+
severity: 'error',
|
|
115
|
+
command: commandDisplayName(cmd),
|
|
116
|
+
message: `Duplicate alias "${name}" used by both "${existing}" and "${sub.name}".`,
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
seen.set(name, sub.name);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check for option names or subcommand names that shadow built-in flags/commands (help, version).
|
|
128
|
+
*/
|
|
129
|
+
function checkShadowedOptionNames(commands: AnyPadroneCommand[], diagnostics: Diagnostic[]) {
|
|
130
|
+
const builtins = new Set(['help', 'version']);
|
|
131
|
+
|
|
132
|
+
for (const cmd of commands) {
|
|
133
|
+
// Check subcommand names and aliases
|
|
134
|
+
if (cmd.commands) {
|
|
135
|
+
for (const sub of cmd.commands) {
|
|
136
|
+
const names = [sub.name, ...(sub.aliases || [])];
|
|
137
|
+
for (const name of names) {
|
|
138
|
+
if (name && builtins.has(name)) {
|
|
139
|
+
diagnostics.push({
|
|
140
|
+
severity: 'warning',
|
|
141
|
+
command: commandDisplayName(cmd),
|
|
142
|
+
message: `Subcommand "${sub.name}"${name !== sub.name ? ` (alias "${name}")` : ''} shadows the built-in "${name}" command.`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check option names in schema
|
|
150
|
+
const jsonSchema = getJsonSchema(cmd);
|
|
151
|
+
if (!jsonSchema?.properties) continue;
|
|
152
|
+
|
|
153
|
+
for (const propName of Object.keys(jsonSchema.properties)) {
|
|
154
|
+
if (builtins.has(propName)) {
|
|
155
|
+
diagnostics.push({
|
|
156
|
+
severity: 'warning',
|
|
157
|
+
command: commandDisplayName(cmd),
|
|
158
|
+
message: `Option "${propName}" shadows the built-in --${propName} flag.`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Also check field flags and aliases from meta
|
|
164
|
+
if (cmd.meta?.fields) {
|
|
165
|
+
for (const [fieldName, fieldMeta] of Object.entries(cmd.meta.fields)) {
|
|
166
|
+
if (!fieldMeta) continue;
|
|
167
|
+
|
|
168
|
+
// Check flags (single-char)
|
|
169
|
+
if (fieldMeta.flags) {
|
|
170
|
+
const flagList = typeof fieldMeta.flags === 'string' ? [fieldMeta.flags] : fieldMeta.flags;
|
|
171
|
+
for (const flag of flagList) {
|
|
172
|
+
if (builtins.has(flag)) {
|
|
173
|
+
diagnostics.push({
|
|
174
|
+
severity: 'warning',
|
|
175
|
+
command: commandDisplayName(cmd),
|
|
176
|
+
message: `Flag "${flag}" on field "${fieldName}" shadows the built-in --${flag} flag.`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check aliases (multi-char)
|
|
183
|
+
if (fieldMeta.alias) {
|
|
184
|
+
const aliasList = typeof fieldMeta.alias === 'string' ? [fieldMeta.alias] : fieldMeta.alias;
|
|
185
|
+
for (const alias of aliasList) {
|
|
186
|
+
if (builtins.has(alias)) {
|
|
187
|
+
diagnostics.push({
|
|
188
|
+
severity: 'warning',
|
|
189
|
+
command: commandDisplayName(cmd),
|
|
190
|
+
message: `Alias "${alias}" on field "${fieldName}" shadows the built-in --${alias} flag.`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check for leaf commands (no subcommands) that have no action defined.
|
|
202
|
+
*/
|
|
203
|
+
function checkCommandsWithoutActions(commands: AnyPadroneCommand[], diagnostics: Diagnostic[]) {
|
|
204
|
+
for (const cmd of commands) {
|
|
205
|
+
const isLeaf = !cmd.commands || cmd.commands.length === 0;
|
|
206
|
+
// Root without subcommands or leaf commands should have actions
|
|
207
|
+
const isRoot = !cmd.parent;
|
|
208
|
+
if (isLeaf && !isRoot && !cmd.action) {
|
|
209
|
+
diagnostics.push({
|
|
210
|
+
severity: 'warning',
|
|
211
|
+
command: commandDisplayName(cmd),
|
|
212
|
+
message: 'Command has no action defined.',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Check for schema properties without descriptions.
|
|
220
|
+
*/
|
|
221
|
+
function checkSchemasWithoutDescriptions(commands: AnyPadroneCommand[], diagnostics: Diagnostic[]) {
|
|
222
|
+
for (const cmd of commands) {
|
|
223
|
+
const jsonSchema = getJsonSchema(cmd);
|
|
224
|
+
if (!jsonSchema?.properties) continue;
|
|
225
|
+
|
|
226
|
+
for (const [propName, propSchema] of Object.entries(jsonSchema.properties as Record<string, any>)) {
|
|
227
|
+
if (!propSchema) continue;
|
|
228
|
+
|
|
229
|
+
const hasDescription = propSchema.description || cmd.meta?.fields?.[propName]?.description;
|
|
230
|
+
if (!hasDescription) {
|
|
231
|
+
diagnostics.push({
|
|
232
|
+
severity: 'warning',
|
|
233
|
+
command: commandDisplayName(cmd),
|
|
234
|
+
message: `Option "${propName}" has no description.`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check for conflicting positional argument configurations.
|
|
243
|
+
* e.g., variadic positional not at the end, or multiple variadics.
|
|
244
|
+
*/
|
|
245
|
+
function checkConflictingPositionals(commands: AnyPadroneCommand[], diagnostics: Diagnostic[]) {
|
|
246
|
+
for (const cmd of commands) {
|
|
247
|
+
const positional = cmd.meta?.positional;
|
|
248
|
+
if (!positional || positional.length === 0) continue;
|
|
249
|
+
|
|
250
|
+
let variadicCount = 0;
|
|
251
|
+
let variadicIndex = -1;
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < positional.length; i++) {
|
|
254
|
+
const p = positional[i] as string;
|
|
255
|
+
if (p.startsWith('...')) {
|
|
256
|
+
variadicCount++;
|
|
257
|
+
variadicIndex = i;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (variadicCount > 1) {
|
|
262
|
+
diagnostics.push({
|
|
263
|
+
severity: 'error',
|
|
264
|
+
command: commandDisplayName(cmd),
|
|
265
|
+
message: 'Multiple variadic positional arguments defined.',
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (variadicCount === 1 && variadicIndex !== positional.length - 1) {
|
|
270
|
+
diagnostics.push({
|
|
271
|
+
severity: 'warning',
|
|
272
|
+
command: commandDisplayName(cmd),
|
|
273
|
+
message: 'Variadic positional argument is not in the last position.',
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Check for positional names that don't exist in schema
|
|
278
|
+
const jsonSchema = getJsonSchema(cmd);
|
|
279
|
+
if (jsonSchema?.properties) {
|
|
280
|
+
for (const p of positional) {
|
|
281
|
+
const name = (p as string).replace(/^\.\.\./, '');
|
|
282
|
+
if (!(name in jsonSchema.properties)) {
|
|
283
|
+
diagnostics.push({
|
|
284
|
+
severity: 'error',
|
|
285
|
+
command: commandDisplayName(cmd),
|
|
286
|
+
message: `Positional "${name}" does not exist in the schema.`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Check for plugins that don't define any phase handlers.
|
|
296
|
+
*/
|
|
297
|
+
function checkUnusedPlugins(commands: AnyPadroneCommand[], diagnostics: Diagnostic[]) {
|
|
298
|
+
const phases = ['start', 'parse', 'validate', 'execute', 'error', 'shutdown'] as const;
|
|
299
|
+
|
|
300
|
+
for (const cmd of commands) {
|
|
301
|
+
if (!cmd.plugins) continue;
|
|
302
|
+
|
|
303
|
+
for (const plugin of cmd.plugins) {
|
|
304
|
+
const hasHandler = phases.some((phase) => typeof (plugin as any)[phase] === 'function');
|
|
305
|
+
if (!hasHandler) {
|
|
306
|
+
diagnostics.push({
|
|
307
|
+
severity: 'warning',
|
|
308
|
+
command: commandDisplayName(cmd),
|
|
309
|
+
message: `Plugin "${plugin.name}" has no phase handlers.`,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function findProgram(mod: Record<string, unknown>): unknown | null {
|
|
317
|
+
const defaultExport = mod.default;
|
|
318
|
+
if (isPadroneProgram(defaultExport)) return defaultExport;
|
|
319
|
+
|
|
320
|
+
for (const value of Object.values(mod)) {
|
|
321
|
+
if (isPadroneProgram(value)) return value;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function isPadroneProgram(value: unknown): boolean {
|
|
328
|
+
if (!value || typeof value !== 'object') return false;
|
|
329
|
+
return commandSymbol in value;
|
|
330
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createPadrone } from 'padrone';
|
|
2
|
+
import pkg from 'padrone/package.json' with { type: 'json' };
|
|
3
|
+
import * as z from 'zod/v4';
|
|
4
|
+
import { runCompletions } from './completions.ts';
|
|
5
|
+
import { runDocs } from './docs.ts';
|
|
6
|
+
import { runDoctor } from './doctor.ts';
|
|
7
|
+
import { runInit } from './init.ts';
|
|
8
|
+
import { runLink, runUnlink } from './link.ts';
|
|
9
|
+
import { runWrap } from './wrap.ts';
|
|
10
|
+
|
|
11
|
+
const PadroneCLI = createPadrone('padrone')
|
|
12
|
+
.configure({
|
|
13
|
+
version: pkg.version,
|
|
14
|
+
title: 'Padrone CLI',
|
|
15
|
+
description: 'The Padrone CLI',
|
|
16
|
+
})
|
|
17
|
+
.command('init', (cmd) =>
|
|
18
|
+
cmd
|
|
19
|
+
.configure({
|
|
20
|
+
description: 'Scaffold a new Padrone CLI project',
|
|
21
|
+
})
|
|
22
|
+
.arguments(
|
|
23
|
+
z.object({
|
|
24
|
+
name: z.string().optional().describe('Project name (defaults to directory name)'),
|
|
25
|
+
description: z.string().optional().describe('Project description'),
|
|
26
|
+
version: z.string().optional().default('0.1.0').describe('Initial version'),
|
|
27
|
+
dir: z.string().optional().describe('Target directory (defaults to current directory)'),
|
|
28
|
+
}),
|
|
29
|
+
{
|
|
30
|
+
positional: ['dir'],
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
.async()
|
|
34
|
+
.action(runInit),
|
|
35
|
+
)
|
|
36
|
+
.command('docs', (cmd) =>
|
|
37
|
+
cmd
|
|
38
|
+
.configure({
|
|
39
|
+
description: 'Generate documentation for a Padrone CLI program',
|
|
40
|
+
})
|
|
41
|
+
.arguments(
|
|
42
|
+
z.object({
|
|
43
|
+
entry: z.string().describe('Entry file that exports a Padrone program'),
|
|
44
|
+
output: z.string().optional().default('./docs/cli').describe('Output directory'),
|
|
45
|
+
format: z.enum(['markdown', 'html', 'man', 'json']).optional().default('markdown').describe('Output format'),
|
|
46
|
+
includeHidden: z.boolean().optional().default(false).describe('Include hidden commands and options'),
|
|
47
|
+
dryRun: z.boolean().optional().default(false).describe('Print what would be generated without writing'),
|
|
48
|
+
}),
|
|
49
|
+
{
|
|
50
|
+
positional: ['entry'],
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
.async()
|
|
54
|
+
.action(runDocs),
|
|
55
|
+
)
|
|
56
|
+
.command('doctor', (cmd) =>
|
|
57
|
+
cmd
|
|
58
|
+
.configure({
|
|
59
|
+
description: 'Lint and validate a Padrone CLI program definition',
|
|
60
|
+
})
|
|
61
|
+
.arguments(
|
|
62
|
+
z.object({
|
|
63
|
+
entry: z.string().describe('Entry file that exports a Padrone program'),
|
|
64
|
+
}),
|
|
65
|
+
{
|
|
66
|
+
positional: ['entry'],
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
.async()
|
|
70
|
+
.action(runDoctor),
|
|
71
|
+
)
|
|
72
|
+
.command('completions', (cmd) =>
|
|
73
|
+
cmd
|
|
74
|
+
.configure({
|
|
75
|
+
description: 'Show shell completion install instructions for a Padrone CLI program',
|
|
76
|
+
})
|
|
77
|
+
.arguments(
|
|
78
|
+
z.object({
|
|
79
|
+
appPath: z.string().optional().describe('Path or name of the CLI program (defaults to padrone)'),
|
|
80
|
+
for: z.enum(['bash', 'zsh', 'fish', 'powershell']).optional().describe('Target shell (auto-detected if omitted)'),
|
|
81
|
+
setup: z.boolean().optional().default(false).describe('Write completions to shell config file'),
|
|
82
|
+
}),
|
|
83
|
+
{
|
|
84
|
+
positional: ['appPath'],
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
.action(runCompletions),
|
|
88
|
+
)
|
|
89
|
+
.command('link', (cmd) =>
|
|
90
|
+
cmd
|
|
91
|
+
.configure({
|
|
92
|
+
description: 'Link a Padrone CLI program for global use during development',
|
|
93
|
+
})
|
|
94
|
+
.arguments(
|
|
95
|
+
z.object({
|
|
96
|
+
entry: z.string().optional().describe('Entry file (auto-detected from package.json bin field)'),
|
|
97
|
+
name: z.string().optional().describe('Command name (auto-detected from package.json)'),
|
|
98
|
+
list: z.boolean().optional().default(false).describe('List all linked programs'),
|
|
99
|
+
setup: z.boolean().optional().default(false).describe('Add ~/.padrone/bin to PATH in shell config'),
|
|
100
|
+
}),
|
|
101
|
+
{
|
|
102
|
+
positional: ['entry'],
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
.async()
|
|
106
|
+
.action(runLink),
|
|
107
|
+
)
|
|
108
|
+
.command('unlink', (cmd) =>
|
|
109
|
+
cmd
|
|
110
|
+
.configure({
|
|
111
|
+
description: 'Remove a previously linked Padrone CLI program',
|
|
112
|
+
})
|
|
113
|
+
.arguments(
|
|
114
|
+
z.object({
|
|
115
|
+
name: z.string().optional().describe('Program name to unlink (auto-detected from current directory)'),
|
|
116
|
+
}),
|
|
117
|
+
{
|
|
118
|
+
positional: ['name'],
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
.async()
|
|
122
|
+
.action(runUnlink),
|
|
123
|
+
)
|
|
124
|
+
.command('wrap', (cmd) =>
|
|
125
|
+
cmd
|
|
126
|
+
.configure({
|
|
127
|
+
description: 'Generate a Padrone wrapper for an existing CLI tool',
|
|
128
|
+
})
|
|
129
|
+
.arguments(
|
|
130
|
+
z.object({
|
|
131
|
+
command: z.string().describe('CLI command to wrap (e.g. gh, docker, kubectl)'),
|
|
132
|
+
source: z.enum(['help', 'fish', 'zsh']).optional().default('help').describe('Parsing source (default: help)'),
|
|
133
|
+
output: z.string().optional().describe('Output directory (default: ./src/<command>)'),
|
|
134
|
+
depth: z.number().default(4).optional().describe('Max subcommand depth'),
|
|
135
|
+
dryRun: z.boolean().optional().default(false).describe('Print what would be generated without writing'),
|
|
136
|
+
overwrite: z.boolean().optional().default(false).describe('Overwrite existing files'),
|
|
137
|
+
yes: z.boolean().optional().default(false).describe('Skip confirmation prompt'),
|
|
138
|
+
}),
|
|
139
|
+
{
|
|
140
|
+
positional: ['command'],
|
|
141
|
+
fields: { yes: { alias: 'y' } },
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
.async()
|
|
145
|
+
.action(runWrap),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
if (import.meta.main) {
|
|
149
|
+
try {
|
|
150
|
+
const cliRes = await PadroneCLI.cli();
|
|
151
|
+
await cliRes.result;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error('Error running Padrone CLI:', error);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export default PadroneCLI;
|
|
159
|
+
export { PadroneCLI };
|
package/src/cli/init.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { basename, resolve } from 'node:path';
|
|
3
|
+
import { createFileEmitter, template } from 'padrone/codegen';
|
|
4
|
+
import type { PadroneActionContext } from '../types.ts';
|
|
5
|
+
|
|
6
|
+
interface InitArgs {
|
|
7
|
+
name?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
version?: string;
|
|
10
|
+
dir?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const packageJsonTemplate = template(`{
|
|
14
|
+
"name": "{{name}}",
|
|
15
|
+
"version": "{{version}}",
|
|
16
|
+
"private": true,
|
|
17
|
+
"type": "module",
|
|
18
|
+
"module": "src/index.ts",
|
|
19
|
+
"bin": {
|
|
20
|
+
"{{name}}": "src/index.ts"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"start": "bun src/index.ts",
|
|
24
|
+
"dev": "bun --watch src/index.ts",
|
|
25
|
+
"link": "padrone link",
|
|
26
|
+
"unlink": "padrone unlink"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"padrone": "^{{padroneVersion}}",
|
|
30
|
+
"zod": "^4.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
`);
|
|
34
|
+
|
|
35
|
+
const tsconfigTemplate = template(`{
|
|
36
|
+
"compilerOptions": {
|
|
37
|
+
"target": "ESNext",
|
|
38
|
+
"module": "nodenext",
|
|
39
|
+
"moduleResolution": "nodenext",
|
|
40
|
+
"strict": true,
|
|
41
|
+
"esModuleInterop": true,
|
|
42
|
+
"skipLibCheck": true,
|
|
43
|
+
"declaration": true,
|
|
44
|
+
"outDir": "dist",
|
|
45
|
+
"rootDir": "src"
|
|
46
|
+
},
|
|
47
|
+
"include": ["src"]
|
|
48
|
+
}
|
|
49
|
+
`);
|
|
50
|
+
|
|
51
|
+
const programTemplate = template(`import { createPadrone } from 'padrone'
|
|
52
|
+
import * as z from 'zod/v4'
|
|
53
|
+
|
|
54
|
+
const program = createPadrone('{{name}}')
|
|
55
|
+
.configure({
|
|
56
|
+
version: '{{version}}',
|
|
57
|
+
description: '{{description}}',
|
|
58
|
+
})
|
|
59
|
+
.command('hello', (cmd) => cmd
|
|
60
|
+
.configure({ description: 'Say hello' })
|
|
61
|
+
.arguments(z.object({
|
|
62
|
+
name: z.string().default('world').describe('Name to greet'),
|
|
63
|
+
}), {
|
|
64
|
+
positional: ['name'],
|
|
65
|
+
})
|
|
66
|
+
.action((args) => {
|
|
67
|
+
return \`Hello, \${args.name}!\`
|
|
68
|
+
})
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if (import.meta.main) {
|
|
72
|
+
try {
|
|
73
|
+
const cliRes = await program.cli();
|
|
74
|
+
await cliRes.result;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('Error running program:', error);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default program;
|
|
82
|
+
export { program };
|
|
83
|
+
`);
|
|
84
|
+
|
|
85
|
+
export async function runInit(args: InitArgs, ctx: PadroneActionContext) {
|
|
86
|
+
const { output, error } = ctx.runtime;
|
|
87
|
+
const dir = resolve(args.dir || '.');
|
|
88
|
+
const name = args.name || basename(dir);
|
|
89
|
+
const description = args.description || `${name} CLI`;
|
|
90
|
+
const version = args.version || '0.1.0';
|
|
91
|
+
|
|
92
|
+
if (existsSync(resolve(dir, 'package.json'))) {
|
|
93
|
+
error(`A package.json already exists in ${dir}. Aborting.`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let padroneVersion = '1.0.0';
|
|
98
|
+
try {
|
|
99
|
+
const pkg = await import('padrone/package.json', { with: { type: 'json' } });
|
|
100
|
+
padroneVersion = (pkg.default as any).version || padroneVersion;
|
|
101
|
+
} catch {
|
|
102
|
+
// Use fallback version
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const data = { name, description, version, padroneVersion };
|
|
106
|
+
|
|
107
|
+
const emitter = createFileEmitter({ outDir: dir });
|
|
108
|
+
|
|
109
|
+
emitter.addFile('package.json', packageJsonTemplate(data));
|
|
110
|
+
emitter.addFile('tsconfig.json', tsconfigTemplate(data));
|
|
111
|
+
emitter.addFile('src/index.ts', programTemplate(data));
|
|
112
|
+
|
|
113
|
+
const result = await emitter.emit();
|
|
114
|
+
|
|
115
|
+
if (result.errors.length > 0) {
|
|
116
|
+
for (const err of result.errors) {
|
|
117
|
+
error(`Failed to write ${err.file}: ${err.error.message}`);
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
output(`Created ${name} CLI project in ${dir}`);
|
|
123
|
+
output('');
|
|
124
|
+
output('Files written:');
|
|
125
|
+
for (const file of result.written) {
|
|
126
|
+
output(` ${file}`);
|
|
127
|
+
}
|
|
128
|
+
output('');
|
|
129
|
+
output('Next steps:');
|
|
130
|
+
if (dir !== process.cwd()) {
|
|
131
|
+
output(` cd ${dir}`);
|
|
132
|
+
}
|
|
133
|
+
output(' bun install');
|
|
134
|
+
output(' bun run dev');
|
|
135
|
+
}
|