padrone 1.4.0 → 1.6.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 (141) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +108 -283
  3. package/dist/args-Cnq0nwSM.mjs +272 -0
  4. package/dist/args-Cnq0nwSM.mjs.map +1 -0
  5. package/dist/codegen/index.d.mts +28 -3
  6. package/dist/codegen/index.d.mts.map +1 -1
  7. package/dist/codegen/index.mjs +169 -19
  8. package/dist/codegen/index.mjs.map +1 -1
  9. package/dist/commands-B_gufyR9.mjs +514 -0
  10. package/dist/commands-B_gufyR9.mjs.map +1 -0
  11. package/dist/{completion.mjs → completion-BEuflbDO.mjs} +86 -108
  12. package/dist/completion-BEuflbDO.mjs.map +1 -0
  13. package/dist/docs/index.d.mts +22 -2
  14. package/dist/docs/index.d.mts.map +1 -1
  15. package/dist/docs/index.mjs +92 -7
  16. package/dist/docs/index.mjs.map +1 -1
  17. package/dist/errors-CL63UOzt.mjs +137 -0
  18. package/dist/errors-CL63UOzt.mjs.map +1 -0
  19. package/dist/{formatter-ClUK5hcQ.d.mts → formatter-DrvhDMrq.d.mts} +35 -6
  20. package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
  21. package/dist/help-B5Kk83of.mjs +849 -0
  22. package/dist/help-B5Kk83of.mjs.map +1 -0
  23. package/dist/index-BaU3X6dY.d.mts +1178 -0
  24. package/dist/index-BaU3X6dY.d.mts.map +1 -0
  25. package/dist/index.d.mts +763 -36
  26. package/dist/index.d.mts.map +1 -1
  27. package/dist/index.mjs +3608 -1534
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/mcp-BM-d0nZi.mjs +377 -0
  30. package/dist/mcp-BM-d0nZi.mjs.map +1 -0
  31. package/dist/serve-Bk0JUlCj.mjs +402 -0
  32. package/dist/serve-Bk0JUlCj.mjs.map +1 -0
  33. package/dist/stream-DC4H8YTx.mjs +77 -0
  34. package/dist/stream-DC4H8YTx.mjs.map +1 -0
  35. package/dist/test.d.mts +5 -8
  36. package/dist/test.d.mts.map +1 -1
  37. package/dist/test.mjs +5 -27
  38. package/dist/test.mjs.map +1 -1
  39. package/dist/{update-check-EbNDkzyV.mjs → update-check-CZ2VqjnV.mjs} +16 -17
  40. package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
  41. package/dist/zod.d.mts +32 -0
  42. package/dist/zod.d.mts.map +1 -0
  43. package/dist/zod.mjs +50 -0
  44. package/dist/zod.mjs.map +1 -0
  45. package/package.json +20 -9
  46. package/src/cli/completions.ts +14 -11
  47. package/src/cli/docs.ts +13 -16
  48. package/src/cli/doctor.ts +213 -24
  49. package/src/cli/index.ts +28 -82
  50. package/src/cli/init.ts +12 -10
  51. package/src/cli/link.ts +22 -18
  52. package/src/cli/wrap.ts +14 -11
  53. package/src/codegen/discovery.ts +80 -28
  54. package/src/codegen/index.ts +2 -1
  55. package/src/codegen/parsers/bash.ts +179 -0
  56. package/src/codegen/schema-to-code.ts +2 -1
  57. package/src/core/args.ts +296 -0
  58. package/src/core/commands.ts +373 -0
  59. package/src/core/create.ts +268 -0
  60. package/src/{runtime.ts → core/default-runtime.ts} +70 -135
  61. package/src/{errors.ts → core/errors.ts} +22 -0
  62. package/src/core/exec.ts +259 -0
  63. package/src/core/interceptors.ts +302 -0
  64. package/src/{parse.ts → core/parse.ts} +36 -89
  65. package/src/core/program-methods.ts +301 -0
  66. package/src/core/results.ts +229 -0
  67. package/src/core/runtime.ts +246 -0
  68. package/src/core/validate.ts +247 -0
  69. package/src/docs/index.ts +124 -11
  70. package/src/extension/auto-output.ts +95 -0
  71. package/src/extension/color.ts +38 -0
  72. package/src/extension/completion.ts +49 -0
  73. package/src/extension/config.ts +262 -0
  74. package/src/extension/env.ts +101 -0
  75. package/src/extension/help.ts +192 -0
  76. package/src/extension/index.ts +43 -0
  77. package/src/extension/ink.ts +93 -0
  78. package/src/extension/interactive.ts +106 -0
  79. package/src/extension/logger.ts +214 -0
  80. package/src/extension/man.ts +51 -0
  81. package/src/extension/mcp.ts +52 -0
  82. package/src/extension/progress-renderer.ts +338 -0
  83. package/src/extension/progress.ts +299 -0
  84. package/src/extension/repl.ts +94 -0
  85. package/src/extension/serve.ts +48 -0
  86. package/src/extension/signal.ts +87 -0
  87. package/src/extension/stdin.ts +62 -0
  88. package/src/extension/suggestions.ts +114 -0
  89. package/src/extension/timing.ts +81 -0
  90. package/src/extension/tracing.ts +175 -0
  91. package/src/extension/update-check.ts +77 -0
  92. package/src/extension/utils.ts +51 -0
  93. package/src/extension/version.ts +63 -0
  94. package/src/{completion.ts → feature/completion.ts} +130 -57
  95. package/src/{interactive.ts → feature/interactive.ts} +47 -6
  96. package/src/feature/mcp.ts +387 -0
  97. package/src/{repl-loop.ts → feature/repl-loop.ts} +26 -16
  98. package/src/feature/serve.ts +438 -0
  99. package/src/feature/test.ts +262 -0
  100. package/src/{update-check.ts → feature/update-check.ts} +16 -16
  101. package/src/{wrap.ts → feature/wrap.ts} +27 -27
  102. package/src/index.ts +120 -11
  103. package/src/output/colorizer.ts +154 -0
  104. package/src/{formatter.ts → output/formatter.ts} +281 -135
  105. package/src/{help.ts → output/help.ts} +62 -15
  106. package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
  107. package/src/schema/zod.ts +50 -0
  108. package/src/test.ts +2 -285
  109. package/src/types/args-meta.ts +151 -0
  110. package/src/types/builder.ts +697 -0
  111. package/src/types/command.ts +157 -0
  112. package/src/types/index.ts +59 -0
  113. package/src/types/interceptor.ts +296 -0
  114. package/src/types/preferences.ts +83 -0
  115. package/src/types/result.ts +71 -0
  116. package/src/types/schema.ts +19 -0
  117. package/src/util/dotenv.ts +244 -0
  118. package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
  119. package/src/util/stream.ts +101 -0
  120. package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
  121. package/src/{type-utils.ts → util/type-utils.ts} +99 -37
  122. package/src/util/utils.ts +51 -0
  123. package/src/zod.ts +1 -0
  124. package/dist/args-CVDbyyzG.mjs +0 -199
  125. package/dist/args-CVDbyyzG.mjs.map +0 -1
  126. package/dist/chunk-y_GBKt04.mjs +0 -5
  127. package/dist/completion.d.mts +0 -64
  128. package/dist/completion.d.mts.map +0 -1
  129. package/dist/completion.mjs.map +0 -1
  130. package/dist/formatter-ClUK5hcQ.d.mts.map +0 -1
  131. package/dist/help-CcBe91bV.mjs +0 -1254
  132. package/dist/help-CcBe91bV.mjs.map +0 -1
  133. package/dist/types-DjIdJN5G.d.mts +0 -1059
  134. package/dist/types-DjIdJN5G.d.mts.map +0 -1
  135. package/dist/update-check-EbNDkzyV.mjs.map +0 -1
  136. package/src/args.ts +0 -461
  137. package/src/colorizer.ts +0 -41
  138. package/src/command-utils.ts +0 -532
  139. package/src/create.ts +0 -1477
  140. package/src/types.ts +0 -1109
  141. package/src/utils.ts +0 -140
package/src/cli/doctor.ts CHANGED
@@ -1,10 +1,15 @@
1
1
  import { resolve } from 'node:path';
2
- import { commandSymbol } from '../command-utils.ts';
3
- import type { AnyPadroneCommand, PadroneActionContext } from '../types.ts';
2
+ import * as z from 'zod/v4';
3
+ import { getJsonSchema } from '../core/args.ts';
4
+ import { getCommand, isPadroneProgram } from '../core/commands.ts';
5
+ import type { AnyPadroneCommand, PadroneActionContext } from '../types/index.ts';
6
+ import { detectEntry } from './link.ts';
4
7
 
5
- interface DoctorArgs {
6
- entry: string;
7
- }
8
+ export const doctorSchema = z.object({
9
+ entry: z.string().optional().describe('Entry file that exports a Padrone program (auto-detected from package.json if omitted)'),
10
+ });
11
+
12
+ type DoctorArgs = z.infer<typeof doctorSchema>;
8
13
 
9
14
  type Severity = 'error' | 'warning';
10
15
 
@@ -15,7 +20,18 @@ interface Diagnostic {
15
20
  }
16
21
 
17
22
  export async function runDoctor(args: DoctorArgs, _ctx: PadroneActionContext) {
18
- const entryPath = resolve(args.entry);
23
+ let entryPath: string;
24
+
25
+ if (args.entry) {
26
+ entryPath = resolve(args.entry);
27
+ } else {
28
+ const detected = detectEntry(process.cwd());
29
+ if (!detected) {
30
+ console.error('Could not detect entry point. Provide an entry file or add a "bin" field to package.json.');
31
+ process.exit(1);
32
+ }
33
+ entryPath = detected.entry;
34
+ }
19
35
 
20
36
  let mod: Record<string, unknown>;
21
37
  try {
@@ -33,7 +49,7 @@ export async function runDoctor(args: DoctorArgs, _ctx: PadroneActionContext) {
33
49
  process.exit(1);
34
50
  }
35
51
 
36
- const cmd = (program as any)[commandSymbol] as AnyPadroneCommand;
52
+ const cmd = getCommand(program as object);
37
53
  const diagnostics: Diagnostic[] = [];
38
54
 
39
55
  collectDiagnostics(cmd, diagnostics);
@@ -63,6 +79,8 @@ export async function runDoctor(args: DoctorArgs, _ctx: PadroneActionContext) {
63
79
  }
64
80
 
65
81
  function collectDiagnostics(cmd: AnyPadroneCommand, diagnostics: Diagnostic[]) {
82
+ checkCircularParentRefs(cmd, diagnostics);
83
+
66
84
  const allCommands = flattenCommands(cmd);
67
85
 
68
86
  checkDuplicateAliases(allCommands, diagnostics);
@@ -70,7 +88,12 @@ function collectDiagnostics(cmd: AnyPadroneCommand, diagnostics: Diagnostic[]) {
70
88
  checkCommandsWithoutActions(allCommands, diagnostics);
71
89
  checkSchemasWithoutDescriptions(allCommands, diagnostics);
72
90
  checkConflictingPositionals(allCommands, diagnostics);
73
- checkUnusedPlugins(allCommands, diagnostics);
91
+ checkUnusedInterceptors(allCommands, diagnostics);
92
+ checkDuplicateOptionFlagsAndAliases(allCommands, diagnostics);
93
+ checkUnreachableCommands(allCommands, diagnostics);
94
+ checkMissingCommandDescriptions(allCommands, diagnostics);
95
+ checkEmptyCommandGroups(allCommands, diagnostics);
96
+ checkDeprecatedWithoutHint(allCommands, diagnostics);
74
97
  }
75
98
 
76
99
  function flattenCommands(cmd: AnyPadroneCommand): AnyPadroneCommand[] {
@@ -87,10 +110,10 @@ function commandDisplayName(cmd: AnyPadroneCommand): string {
87
110
  return cmd.path || cmd.name || '<root>';
88
111
  }
89
112
 
90
- function getJsonSchema(cmd: AnyPadroneCommand): Record<string, any> | null {
113
+ function getCommandJsonSchema(cmd: AnyPadroneCommand): Record<string, any> | null {
91
114
  try {
92
115
  if (!cmd.argsSchema) return null;
93
- return cmd.argsSchema['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
116
+ return getJsonSchema(cmd.argsSchema) as Record<string, any>;
94
117
  } catch {
95
118
  return null;
96
119
  }
@@ -147,7 +170,7 @@ function checkShadowedOptionNames(commands: AnyPadroneCommand[], diagnostics: Di
147
170
  }
148
171
 
149
172
  // Check option names in schema
150
- const jsonSchema = getJsonSchema(cmd);
173
+ const jsonSchema = getCommandJsonSchema(cmd);
151
174
  if (!jsonSchema?.properties) continue;
152
175
 
153
176
  for (const propName of Object.keys(jsonSchema.properties)) {
@@ -220,7 +243,7 @@ function checkCommandsWithoutActions(commands: AnyPadroneCommand[], diagnostics:
220
243
  */
221
244
  function checkSchemasWithoutDescriptions(commands: AnyPadroneCommand[], diagnostics: Diagnostic[]) {
222
245
  for (const cmd of commands) {
223
- const jsonSchema = getJsonSchema(cmd);
246
+ const jsonSchema = getCommandJsonSchema(cmd);
224
247
  if (!jsonSchema?.properties) continue;
225
248
 
226
249
  for (const [propName, propSchema] of Object.entries(jsonSchema.properties as Record<string, any>)) {
@@ -275,7 +298,7 @@ function checkConflictingPositionals(commands: AnyPadroneCommand[], diagnostics:
275
298
  }
276
299
 
277
300
  // Check for positional names that don't exist in schema
278
- const jsonSchema = getJsonSchema(cmd);
301
+ const jsonSchema = getCommandJsonSchema(cmd);
279
302
  if (jsonSchema?.properties) {
280
303
  for (const p of positional) {
281
304
  const name = (p as string).replace(/^\.\.\./, '');
@@ -292,27 +315,198 @@ function checkConflictingPositionals(commands: AnyPadroneCommand[], diagnostics:
292
315
  }
293
316
 
294
317
  /**
295
- * Check for plugins that don't define any phase handlers.
318
+ * Check for interceptors that don't define any phase handlers.
296
319
  */
297
- function checkUnusedPlugins(commands: AnyPadroneCommand[], diagnostics: Diagnostic[]) {
320
+ function checkUnusedInterceptors(commands: AnyPadroneCommand[], diagnostics: Diagnostic[]) {
298
321
  const phases = ['start', 'parse', 'validate', 'execute', 'error', 'shutdown'] as const;
299
322
 
300
323
  for (const cmd of commands) {
301
- if (!cmd.plugins) continue;
324
+ if (!cmd.interceptors) continue;
302
325
 
303
- for (const plugin of cmd.plugins) {
304
- const hasHandler = phases.some((phase) => typeof (plugin as any)[phase] === 'function');
326
+ for (const interceptor of cmd.interceptors) {
327
+ const phases_obj = interceptor.factory();
328
+ const hasHandler = phases.some((phase) => typeof (phases_obj as any)[phase] === 'function');
305
329
  if (!hasHandler) {
306
330
  diagnostics.push({
307
331
  severity: 'warning',
308
332
  command: commandDisplayName(cmd),
309
- message: `Plugin "${plugin.name}" has no phase handlers.`,
333
+ message: `Interceptor "${interceptor.meta.name}" has no phase handlers.`,
310
334
  });
311
335
  }
312
336
  }
313
337
  }
314
338
  }
315
339
 
340
+ /**
341
+ * Check for circular parent references in the command tree.
342
+ * Uses a visited set to detect cycles before flattening.
343
+ */
344
+ function checkCircularParentRefs(cmd: AnyPadroneCommand, diagnostics: Diagnostic[]) {
345
+ const visited = new Set<AnyPadroneCommand>();
346
+
347
+ function walk(node: AnyPadroneCommand) {
348
+ if (visited.has(node)) {
349
+ diagnostics.push({
350
+ severity: 'error',
351
+ command: commandDisplayName(node),
352
+ message: 'Circular reference detected in command tree.',
353
+ });
354
+ return;
355
+ }
356
+ visited.add(node);
357
+ if (node.commands) {
358
+ for (const sub of node.commands) {
359
+ walk(sub);
360
+ }
361
+ }
362
+ }
363
+
364
+ walk(cmd);
365
+ }
366
+
367
+ /**
368
+ * Check for duplicate flags or aliases within a single command's meta fields.
369
+ * e.g., two different options both using `-v` or both aliased to `--output`.
370
+ */
371
+ function checkDuplicateOptionFlagsAndAliases(commands: AnyPadroneCommand[], diagnostics: Diagnostic[]) {
372
+ for (const cmd of commands) {
373
+ if (!cmd.meta?.fields) continue;
374
+
375
+ const seenFlags = new Map<string, string>();
376
+ const seenAliases = new Map<string, string>();
377
+
378
+ for (const [fieldName, fieldMeta] of Object.entries(cmd.meta.fields)) {
379
+ if (!fieldMeta) continue;
380
+
381
+ if (fieldMeta.flags) {
382
+ const flagList = typeof fieldMeta.flags === 'string' ? [fieldMeta.flags] : fieldMeta.flags;
383
+ for (const flag of flagList) {
384
+ const existing = seenFlags.get(flag);
385
+ if (existing) {
386
+ diagnostics.push({
387
+ severity: 'error',
388
+ command: commandDisplayName(cmd),
389
+ message: `Flag "-${flag}" is used by both "${existing}" and "${fieldName}".`,
390
+ });
391
+ } else {
392
+ seenFlags.set(flag, fieldName);
393
+ }
394
+ }
395
+ }
396
+
397
+ if (fieldMeta.alias) {
398
+ const aliasList = typeof fieldMeta.alias === 'string' ? [fieldMeta.alias] : fieldMeta.alias;
399
+ for (const alias of aliasList) {
400
+ const existing = seenAliases.get(alias);
401
+ if (existing) {
402
+ diagnostics.push({
403
+ severity: 'error',
404
+ command: commandDisplayName(cmd),
405
+ message: `Alias "--${alias}" is used by both "${existing}" and "${fieldName}".`,
406
+ });
407
+ } else {
408
+ seenAliases.set(alias, fieldName);
409
+ }
410
+ }
411
+ }
412
+ }
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Check for commands that are unreachable because a parent is hidden.
418
+ * A hidden parent makes all its children effectively invisible to users.
419
+ */
420
+ function checkUnreachableCommands(commands: AnyPadroneCommand[], diagnostics: Diagnostic[]) {
421
+ for (const cmd of commands) {
422
+ if (!cmd.parent || cmd.hidden) continue;
423
+
424
+ let ancestor: AnyPadroneCommand | undefined = cmd.parent;
425
+ while (ancestor) {
426
+ if (ancestor.hidden && ancestor.parent) {
427
+ diagnostics.push({
428
+ severity: 'warning',
429
+ command: commandDisplayName(cmd),
430
+ message: `Command is unreachable because parent "${commandDisplayName(ancestor)}" is hidden.`,
431
+ });
432
+ break;
433
+ }
434
+ ancestor = ancestor.parent;
435
+ }
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Check for non-root commands without a title or description.
441
+ */
442
+ function checkMissingCommandDescriptions(commands: AnyPadroneCommand[], diagnostics: Diagnostic[]) {
443
+ for (const cmd of commands) {
444
+ if (!cmd.parent) continue;
445
+ if (cmd.hidden) continue;
446
+
447
+ if (!cmd.title && !cmd.description) {
448
+ diagnostics.push({
449
+ severity: 'warning',
450
+ command: commandDisplayName(cmd),
451
+ message: 'Command has no title or description.',
452
+ });
453
+ }
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Check for branch commands (have subcommands) that have no leaf descendants.
459
+ * This catches empty command groups where all subcommands are also empty branches.
460
+ */
461
+ function checkEmptyCommandGroups(commands: AnyPadroneCommand[], diagnostics: Diagnostic[]) {
462
+ function hasLeafDescendant(cmd: AnyPadroneCommand): boolean {
463
+ if (!cmd.commands || cmd.commands.length === 0) return true;
464
+ return cmd.commands.some(hasLeafDescendant);
465
+ }
466
+
467
+ for (const cmd of commands) {
468
+ if (!cmd.commands || cmd.commands.length === 0) continue;
469
+
470
+ const allSubsEmpty = cmd.commands.every((sub) => sub.commands && sub.commands.length > 0 && !hasLeafDescendant(sub));
471
+
472
+ if (allSubsEmpty) {
473
+ diagnostics.push({
474
+ severity: 'warning',
475
+ command: commandDisplayName(cmd),
476
+ message: 'Command group has no reachable leaf commands.',
477
+ });
478
+ }
479
+ }
480
+ }
481
+
482
+ /**
483
+ * Check for deprecated commands that use `deprecated: true` without a hint message.
484
+ */
485
+ function checkDeprecatedWithoutHint(commands: AnyPadroneCommand[], diagnostics: Diagnostic[]) {
486
+ for (const cmd of commands) {
487
+ if (cmd.deprecated === true) {
488
+ diagnostics.push({
489
+ severity: 'warning',
490
+ command: commandDisplayName(cmd),
491
+ message: 'Command is deprecated without a replacement hint. Use a string to provide guidance (e.g., "Use \'new-cmd\' instead").',
492
+ });
493
+ }
494
+
495
+ // Also check deprecated option fields
496
+ if (cmd.meta?.fields) {
497
+ for (const [fieldName, fieldMeta] of Object.entries(cmd.meta.fields)) {
498
+ if (fieldMeta?.deprecated === true) {
499
+ diagnostics.push({
500
+ severity: 'warning',
501
+ command: commandDisplayName(cmd),
502
+ message: `Option "${fieldName}" is deprecated without a replacement hint.`,
503
+ });
504
+ }
505
+ }
506
+ }
507
+ }
508
+ }
509
+
316
510
  function findProgram(mod: Record<string, unknown>): unknown | null {
317
511
  const defaultExport = mod.default;
318
512
  if (isPadroneProgram(defaultExport)) return defaultExport;
@@ -323,8 +517,3 @@ function findProgram(mod: Record<string, unknown>): unknown | null {
323
517
 
324
518
  return null;
325
519
  }
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 CHANGED
@@ -1,12 +1,11 @@
1
1
  import { createPadrone } from 'padrone';
2
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';
3
+ import { completionsSchema, runCompletions } from './completions.ts';
4
+ import { docsSchema, runDocs } from './docs.ts';
5
+ import { doctorSchema, runDoctor } from './doctor.ts';
6
+ import { initSchema, runInit } from './init.ts';
7
+ import { linkSchema, runLink, runUnlink, unlinkSchema } from './link.ts';
8
+ import { runWrap, wrapSchema } from './wrap.ts';
10
9
 
11
10
  const PadroneCLI = createPadrone('padrone')
12
11
  .configure({
@@ -19,17 +18,9 @@ const PadroneCLI = createPadrone('padrone')
19
18
  .configure({
20
19
  description: 'Scaffold a new Padrone CLI project',
21
20
  })
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
- )
21
+ .arguments(initSchema, {
22
+ positional: ['dir'],
23
+ })
33
24
  .async()
34
25
  .action(runInit),
35
26
  )
@@ -38,18 +29,9 @@ const PadroneCLI = createPadrone('padrone')
38
29
  .configure({
39
30
  description: 'Generate documentation for a Padrone CLI program',
40
31
  })
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
- )
32
+ .arguments(docsSchema, {
33
+ positional: ['entry'],
34
+ })
53
35
  .async()
54
36
  .action(runDocs),
55
37
  )
@@ -58,14 +40,9 @@ const PadroneCLI = createPadrone('padrone')
58
40
  .configure({
59
41
  description: 'Lint and validate a Padrone CLI program definition',
60
42
  })
61
- .arguments(
62
- z.object({
63
- entry: z.string().describe('Entry file that exports a Padrone program'),
64
- }),
65
- {
66
- positional: ['entry'],
67
- },
68
- )
43
+ .arguments(doctorSchema, {
44
+ positional: ['entry'],
45
+ })
69
46
  .async()
70
47
  .action(runDoctor),
71
48
  )
@@ -74,16 +51,9 @@ const PadroneCLI = createPadrone('padrone')
74
51
  .configure({
75
52
  description: 'Show shell completion install instructions for a Padrone CLI program',
76
53
  })
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
- )
54
+ .arguments(completionsSchema, {
55
+ positional: ['appPath'],
56
+ })
87
57
  .action(runCompletions),
88
58
  )
89
59
  .command('link', (cmd) =>
@@ -91,17 +61,9 @@ const PadroneCLI = createPadrone('padrone')
91
61
  .configure({
92
62
  description: 'Link a Padrone CLI program for global use during development',
93
63
  })
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
- )
64
+ .arguments(linkSchema, {
65
+ positional: ['entry'],
66
+ })
105
67
  .async()
106
68
  .action(runLink),
107
69
  )
@@ -110,14 +72,9 @@ const PadroneCLI = createPadrone('padrone')
110
72
  .configure({
111
73
  description: 'Remove a previously linked Padrone CLI program',
112
74
  })
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
- )
75
+ .arguments(unlinkSchema, {
76
+ positional: ['name'],
77
+ })
121
78
  .async()
122
79
  .action(runUnlink),
123
80
  )
@@ -126,21 +83,10 @@ const PadroneCLI = createPadrone('padrone')
126
83
  .configure({
127
84
  description: 'Generate a Padrone wrapper for an existing CLI tool',
128
85
  })
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
- )
86
+ .arguments(wrapSchema, {
87
+ positional: ['command'],
88
+ fields: { yes: { alias: 'y' } },
89
+ })
144
90
  .async()
145
91
  .action(runWrap),
146
92
  );
package/src/cli/init.ts CHANGED
@@ -1,14 +1,17 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { basename, resolve } from 'node:path';
3
3
  import { createFileEmitter, template } from 'padrone/codegen';
4
- import type { PadroneActionContext } from '../types.ts';
4
+ import * as z from 'zod/v4';
5
+ import type { PadroneActionContext } from '../types/index.ts';
5
6
 
6
- interface InitArgs {
7
- name?: string;
8
- description?: string;
9
- version?: string;
10
- dir?: string;
11
- }
7
+ export const initSchema = z.object({
8
+ name: z.string().optional().describe('Project name (defaults to directory name)'),
9
+ description: z.string().optional().describe('Project description'),
10
+ version: z.string().optional().default('0.1.0').describe('Initial version'),
11
+ dir: z.string().optional().describe('Target directory (defaults to current directory)'),
12
+ });
13
+
14
+ type InitArgs = z.infer<typeof initSchema>;
12
15
 
13
16
  const packageJsonTemplate = template(`{
14
17
  "name": "{{name}}",
@@ -16,9 +19,7 @@ const packageJsonTemplate = template(`{
16
19
  "private": true,
17
20
  "type": "module",
18
21
  "module": "src/index.ts",
19
- "bin": {
20
- "{{name}}": "src/index.ts"
21
- },
22
+ "bin": "src/index.ts",
22
23
  "scripts": {
23
24
  "start": "bun src/index.ts",
24
25
  "dev": "bun --watch src/index.ts",
@@ -131,5 +132,6 @@ export async function runInit(args: InitArgs, ctx: PadroneActionContext) {
131
132
  output(` cd ${dir}`);
132
133
  }
133
134
  output(' bun install');
135
+ output(' bun update');
134
136
  output(' bun run dev');
135
137
  }
package/src/cli/link.ts CHANGED
@@ -1,19 +1,23 @@
1
1
  import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
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
- }
4
+ import * as z from 'zod/v4';
5
+ import type { PadroneActionContext } from '../types/index.ts';
6
+ import { detectShell, getRcFile, type ShellType, writeToRcFile } from '../util/shell-utils.ts';
13
7
 
14
- interface UnlinkArgs {
15
- name?: string;
16
- }
8
+ export const linkSchema = z.object({
9
+ entry: z.string().optional().describe('Entry file (auto-detected from package.json bin field)'),
10
+ name: z.string().optional().describe('Command name (auto-detected from package.json)'),
11
+ list: z.boolean().optional().default(false).describe('List all linked programs'),
12
+ setup: z.boolean().optional().default(false).describe('Add ~/.padrone/bin to PATH in shell config'),
13
+ });
14
+
15
+ export const unlinkSchema = z.object({
16
+ name: z.string().optional().describe('Program name to unlink (auto-detected from current directory)'),
17
+ });
18
+
19
+ type LinkArgs = z.infer<typeof linkSchema>;
20
+ type UnlinkArgs = z.infer<typeof unlinkSchema>;
17
21
 
18
22
  interface LinkEntry {
19
23
  name: string;
@@ -49,7 +53,7 @@ function writeLinks(links: LinksData) {
49
53
  writeFileSync(LINKS_FILE, `${JSON.stringify(links, null, 2)}\n`);
50
54
  }
51
55
 
52
- interface DetectedEntry {
56
+ export interface DetectedEntry {
53
57
  entry: string;
54
58
  name: string;
55
59
  /** Full run command prefix parsed from scripts (e.g. "bun --conditions=padrone@dev") */
@@ -70,7 +74,7 @@ function parseRunPrefix(script: string, entryRelative: string, dir: string): str
70
74
  return undefined;
71
75
  }
72
76
 
73
- function detectEntry(dir: string): DetectedEntry | undefined {
77
+ export function detectEntry(dir: string): DetectedEntry | undefined {
74
78
  const pkgPath = resolve(dir, 'package.json');
75
79
  if (!existsSync(pkgPath)) return undefined;
76
80
 
@@ -143,8 +147,8 @@ function buildPathSnippet(shell: ShellType, binDir: string): string {
143
147
  }
144
148
  }
145
149
 
146
- function setupPath(shell: ShellType): { file: string; updated: boolean } {
147
- const rcFile = getRcFile(shell);
150
+ async function setupPath(shell: ShellType): Promise<{ file: string; updated: boolean }> {
151
+ const rcFile = await getRcFile(shell);
148
152
  if (!rcFile) {
149
153
  throw new Error(`Could not determine config file for ${shell}.`);
150
154
  }
@@ -268,13 +272,13 @@ export async function runLink(args: LinkArgs, ctx: PadroneActionContext) {
268
272
 
269
273
  if (!isInPath(BIN_DIR)) {
270
274
  if (args.setup) {
271
- const shell = detectShell();
275
+ const shell = await detectShell();
272
276
  if (!shell) {
273
277
  error('Could not detect shell. Add the PATH manually:');
274
278
  error(` export PATH="${BIN_DIR}:$PATH"`);
275
279
  return;
276
280
  }
277
- const result = setupPath(shell);
281
+ const result = await setupPath(shell);
278
282
  const verb = result.updated ? 'Updated' : 'Added';
279
283
  output(`${verb} PATH in ${result.file}`);
280
284
  output('Restart your shell or run:');
package/src/cli/wrap.ts CHANGED
@@ -1,20 +1,23 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { createCodeBuilder, createFileEmitter, generateCommandTree } from 'padrone/codegen';
3
+ import * as z from 'zod/v4';
3
4
  import type { DiscoverySource } from '../codegen/discovery.ts';
4
5
  import { discoverCli } from '../codegen/discovery.ts';
5
6
  import { template } from '../codegen/template.ts';
6
7
  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
- }
8
+ import type { PadroneActionContext } from '../types/index.ts';
9
+
10
+ export const wrapSchema = z.object({
11
+ command: z.string().describe('CLI command to wrap (e.g. gh, docker, kubectl)'),
12
+ source: z.enum(['help', 'fish', 'zsh']).optional().default('help').describe('Parsing source (default: help)'),
13
+ output: z.string().optional().describe('Output directory (default: ./src/<command>)'),
14
+ depth: z.number().default(4).optional().describe('Max subcommand depth'),
15
+ dryRun: z.boolean().optional().default(false).describe('Print what would be generated without writing'),
16
+ overwrite: z.boolean().optional().default(false).describe('Overwrite existing files'),
17
+ yes: z.boolean().optional().default(false).describe('Skip confirmation prompt'),
18
+ });
19
+
20
+ type WrapArgs = z.infer<typeof wrapSchema>;
18
21
 
19
22
  export async function runWrap(args: WrapArgs, ctx: PadroneActionContext) {
20
23
  const { output, error } = ctx.runtime;