padrone 1.5.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 (138) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +15 -11
  3. package/dist/{args-D5PNDyNu.mjs → args-Cnq0nwSM.mjs} +91 -41
  4. package/dist/args-Cnq0nwSM.mjs.map +1 -0
  5. package/dist/codegen/index.mjs +4 -4
  6. package/dist/codegen/index.mjs.map +1 -1
  7. package/dist/commands-B_gufyR9.mjs +514 -0
  8. package/dist/commands-B_gufyR9.mjs.map +1 -0
  9. package/dist/{completion.mjs → completion-BEuflbDO.mjs} +12 -82
  10. package/dist/completion-BEuflbDO.mjs.map +1 -0
  11. package/dist/docs/index.d.mts +4 -4
  12. package/dist/docs/index.d.mts.map +1 -1
  13. package/dist/docs/index.mjs +10 -12
  14. package/dist/docs/index.mjs.map +1 -1
  15. package/dist/{errors-BiVrBgi6.mjs → errors-CL63UOzt.mjs} +26 -3
  16. package/dist/errors-CL63UOzt.mjs.map +1 -0
  17. package/dist/{formatter-DtHzbP22.d.mts → formatter-DrvhDMrq.d.mts} +3 -3
  18. package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
  19. package/dist/{help-bbmu9-qd.mjs → help-B5Kk83of.mjs} +151 -37
  20. package/dist/help-B5Kk83of.mjs.map +1 -0
  21. package/dist/{types-Ch8Mk6Qb.d.mts → index-BaU3X6dY.d.mts} +621 -750
  22. package/dist/index-BaU3X6dY.d.mts.map +1 -0
  23. package/dist/index.d.mts +735 -37
  24. package/dist/index.d.mts.map +1 -1
  25. package/dist/index.mjs +3409 -1563
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/{mcp-mLWIdUIu.mjs → mcp-BM-d0nZi.mjs} +13 -15
  28. package/dist/mcp-BM-d0nZi.mjs.map +1 -0
  29. package/dist/{serve-B0u43DK7.mjs → serve-Bk0JUlCj.mjs} +12 -14
  30. package/dist/serve-Bk0JUlCj.mjs.map +1 -0
  31. package/dist/{stream-BcC146Ud.mjs → stream-DC4H8YTx.mjs} +24 -3
  32. package/dist/stream-DC4H8YTx.mjs.map +1 -0
  33. package/dist/test.d.mts +5 -8
  34. package/dist/test.d.mts.map +1 -1
  35. package/dist/test.mjs +2 -13
  36. package/dist/test.mjs.map +1 -1
  37. package/dist/{update-check-CFX1FV3v.mjs → update-check-CZ2VqjnV.mjs} +16 -17
  38. package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
  39. package/dist/zod.d.mts +2 -2
  40. package/dist/zod.d.mts.map +1 -1
  41. package/dist/zod.mjs +2 -2
  42. package/dist/zod.mjs.map +1 -1
  43. package/package.json +15 -12
  44. package/src/cli/completions.ts +14 -11
  45. package/src/cli/docs.ts +13 -10
  46. package/src/cli/doctor.ts +22 -18
  47. package/src/cli/index.ts +28 -82
  48. package/src/cli/init.ts +10 -7
  49. package/src/cli/link.ts +20 -16
  50. package/src/cli/wrap.ts +14 -11
  51. package/src/codegen/schema-to-code.ts +2 -2
  52. package/src/{args.ts → core/args.ts} +32 -225
  53. package/src/core/commands.ts +373 -0
  54. package/src/core/create.ts +268 -0
  55. package/src/core/default-runtime.ts +239 -0
  56. package/src/{errors.ts → core/errors.ts} +22 -0
  57. package/src/core/exec.ts +259 -0
  58. package/src/core/interceptors.ts +302 -0
  59. package/src/{parse.ts → core/parse.ts} +36 -89
  60. package/src/core/program-methods.ts +301 -0
  61. package/src/core/results.ts +229 -0
  62. package/src/core/runtime.ts +246 -0
  63. package/src/core/validate.ts +247 -0
  64. package/src/docs/index.ts +12 -13
  65. package/src/extension/auto-output.ts +95 -0
  66. package/src/extension/color.ts +38 -0
  67. package/src/extension/completion.ts +49 -0
  68. package/src/extension/config.ts +262 -0
  69. package/src/extension/env.ts +101 -0
  70. package/src/extension/help.ts +192 -0
  71. package/src/extension/index.ts +43 -0
  72. package/src/extension/ink.ts +93 -0
  73. package/src/extension/interactive.ts +106 -0
  74. package/src/extension/logger.ts +214 -0
  75. package/src/extension/man.ts +51 -0
  76. package/src/extension/mcp.ts +52 -0
  77. package/src/extension/progress-renderer.ts +338 -0
  78. package/src/extension/progress.ts +299 -0
  79. package/src/extension/repl.ts +94 -0
  80. package/src/extension/serve.ts +48 -0
  81. package/src/extension/signal.ts +87 -0
  82. package/src/extension/stdin.ts +62 -0
  83. package/src/extension/suggestions.ts +114 -0
  84. package/src/extension/timing.ts +81 -0
  85. package/src/extension/tracing.ts +175 -0
  86. package/src/extension/update-check.ts +77 -0
  87. package/src/extension/utils.ts +51 -0
  88. package/src/extension/version.ts +63 -0
  89. package/src/{completion.ts → feature/completion.ts} +12 -12
  90. package/src/{interactive.ts → feature/interactive.ts} +4 -4
  91. package/src/{mcp.ts → feature/mcp.ts} +12 -15
  92. package/src/{repl-loop.ts → feature/repl-loop.ts} +10 -13
  93. package/src/{serve.ts → feature/serve.ts} +11 -15
  94. package/src/feature/test.ts +262 -0
  95. package/src/{update-check.ts → feature/update-check.ts} +16 -16
  96. package/src/{wrap.ts → feature/wrap.ts} +10 -8
  97. package/src/index.ts +111 -30
  98. package/src/{formatter.ts → output/formatter.ts} +131 -31
  99. package/src/{help.ts → output/help.ts} +22 -8
  100. package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
  101. package/src/schema/zod.ts +50 -0
  102. package/src/test.ts +2 -276
  103. package/src/types/args-meta.ts +151 -0
  104. package/src/types/builder.ts +697 -0
  105. package/src/types/command.ts +157 -0
  106. package/src/types/index.ts +59 -0
  107. package/src/types/interceptor.ts +296 -0
  108. package/src/types/preferences.ts +83 -0
  109. package/src/types/result.ts +71 -0
  110. package/src/types/schema.ts +19 -0
  111. package/src/util/dotenv.ts +244 -0
  112. package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
  113. package/src/{stream.ts → util/stream.ts} +27 -1
  114. package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
  115. package/src/{type-utils.ts → util/type-utils.ts} +71 -33
  116. package/src/util/utils.ts +51 -0
  117. package/src/zod.ts +1 -50
  118. package/dist/args-D5PNDyNu.mjs.map +0 -1
  119. package/dist/chunk-CjcI7cDX.mjs +0 -15
  120. package/dist/command-utils-B1D-HqCd.mjs +0 -1117
  121. package/dist/command-utils-B1D-HqCd.mjs.map +0 -1
  122. package/dist/completion.d.mts +0 -64
  123. package/dist/completion.d.mts.map +0 -1
  124. package/dist/completion.mjs.map +0 -1
  125. package/dist/errors-BiVrBgi6.mjs.map +0 -1
  126. package/dist/formatter-DtHzbP22.d.mts.map +0 -1
  127. package/dist/help-bbmu9-qd.mjs.map +0 -1
  128. package/dist/mcp-mLWIdUIu.mjs.map +0 -1
  129. package/dist/serve-B0u43DK7.mjs.map +0 -1
  130. package/dist/stream-BcC146Ud.mjs.map +0 -1
  131. package/dist/types-Ch8Mk6Qb.d.mts.map +0 -1
  132. package/dist/update-check-CFX1FV3v.mjs.map +0 -1
  133. package/src/command-utils.ts +0 -882
  134. package/src/create.ts +0 -1829
  135. package/src/runtime.ts +0 -497
  136. package/src/types.ts +0 -1291
  137. package/src/utils.ts +0 -140
  138. /package/src/{colorizer.ts → output/colorizer.ts} +0 -0
@@ -1,6 +1,25 @@
1
- import { camelToKebab } from './args.ts';
1
+ import { camelToKebab } from '../util/shell-utils.ts';
2
2
  import { type ColorConfig, type ColorTheme, createColorizer } from './colorizer.ts';
3
3
 
4
+ const DEFAULT_TERMINAL_WIDTH = 80;
5
+
6
+ function wrapText(text: string, maxWidth: number): string[] {
7
+ if (maxWidth <= 0 || text.length <= maxWidth) return [text];
8
+ const words = text.split(' ');
9
+ const lines: string[] = [];
10
+ let current = '';
11
+ for (const word of words) {
12
+ if (current && current.length + 1 + word.length > maxWidth) {
13
+ lines.push(current);
14
+ current = word;
15
+ } else {
16
+ current = current ? `${current} ${word}` : word;
17
+ }
18
+ }
19
+ if (current) lines.push(current);
20
+ return lines.length > 0 ? lines : [text];
21
+ }
22
+
4
23
  export type HelpFormat = 'text' | 'ansi' | 'console' | 'markdown' | 'html' | 'json';
5
24
  export type HelpDetail = 'minimal' | 'standard' | 'full';
6
25
 
@@ -269,7 +288,7 @@ function createHtmlLayout(): LayoutConfig {
269
288
  /**
270
289
  * Creates a formatter that uses the given styler and layout configuration.
271
290
  */
272
- function createGenericFormatter(styler: Styler, layout: LayoutConfig, showAllBuiltins?: boolean): Formatter {
291
+ function createGenericFormatter(styler: Styler, layout: LayoutConfig, showAllBuiltins?: boolean, terminalWidth?: number): Formatter {
273
292
  const { newline, indent, join, wrapDocument } = layout;
274
293
 
275
294
  function formatUsageSection(info: HelpInfo): string[] {
@@ -366,15 +385,53 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig, showAllBui
366
385
  lines.push(styler.section('Arguments:'));
367
386
 
368
387
  const maxNameLength = Math.min(32, Math.max(...args.map((a) => a.name.length)));
388
+ const descCol = 2 + maxNameLength + 2;
389
+ const posAvailWidth = terminalWidth ? terminalWidth - descCol : undefined;
390
+ const descColPad = ' '.repeat(descCol);
369
391
 
370
392
  for (const arg of args) {
371
393
  const padding = ' '.repeat(Math.max(2, maxNameLength - arg.name.length + 2));
372
- const descParts: string[] = [];
373
- if (arg.description) descParts.push(styler.description(arg.description));
374
- if (info.usage.stdinField === arg.name) descParts.push(styler.meta('(stdin)'));
375
- if (arg.enum) descParts.push(styler.meta(`(choices: ${arg.enum.join(', ')})`));
376
- if (arg.default !== undefined) descParts.push(styler.meta(`(default: ${String(arg.default)})`));
377
- lines.push(indent(1) + styler.arg(arg.name) + padding + join(descParts));
394
+ const prefix = indent(1) + styler.arg(arg.name) + padding;
395
+
396
+ const descPlain = arg.description ?? '';
397
+ const styledDesc = descPlain ? styler.description(descPlain) : '';
398
+
399
+ const metaParts: string[] = [];
400
+ const styledMetaParts: string[] = [];
401
+ if (info.usage.stdinField === arg.name) {
402
+ metaParts.push('(stdin)');
403
+ styledMetaParts.push(styler.meta('(stdin)'));
404
+ }
405
+ if (arg.enum) {
406
+ const text = `(choices: ${arg.enum.join(', ')})`;
407
+ metaParts.push(text);
408
+ styledMetaParts.push(styler.meta(text));
409
+ }
410
+ if (arg.default !== undefined) {
411
+ const text = `(default: ${String(arg.default)})`;
412
+ metaParts.push(text);
413
+ styledMetaParts.push(styler.meta(text));
414
+ }
415
+
416
+ const metaStyled = join(styledMetaParts);
417
+
418
+ if (posAvailWidth && posAvailWidth > 0) {
419
+ const metaPlain = metaParts.join(' ');
420
+ const fullPlain = [descPlain, metaPlain].filter(Boolean).join(' ');
421
+ if (fullPlain.length <= posAvailWidth) {
422
+ lines.push(prefix + [styledDesc, metaStyled].filter(Boolean).join(' '));
423
+ } else if (!descPlain || descPlain.length <= posAvailWidth) {
424
+ lines.push(prefix + styledDesc);
425
+ if (metaStyled) lines.push(descColPad + metaStyled);
426
+ } else {
427
+ const wrapped = wrapText(descPlain, posAvailWidth);
428
+ lines.push(prefix + styler.description(wrapped[0]!));
429
+ for (const wline of wrapped.slice(1)) lines.push(descColPad + styler.description(wline));
430
+ if (metaStyled) lines.push(descColPad + metaStyled);
431
+ }
432
+ } else {
433
+ lines.push(prefix + join([styledDesc, metaStyled]));
434
+ }
378
435
  }
379
436
 
380
437
  return lines;
@@ -413,6 +470,9 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig, showAllBui
413
470
  const maxNamesWidth = Math.min(32, Math.max(0, ...argColumns.map((c) => c.namesPlain.length)));
414
471
  const maxTypeWidth = Math.min(16, Math.max(0, ...argColumns.map((c) => c.typePlain.length)));
415
472
  const hasAnyFlags = maxFlagsWidth > 0;
473
+ const descCol = 2 + (hasAnyFlags ? maxFlagsWidth + 2 : 0) + maxNamesWidth + 2 + (maxTypeWidth > 0 ? maxTypeWidth + 2 : 0);
474
+ const argAvailWidth = terminalWidth ? terminalWidth - descCol : undefined;
475
+ const descColPad = ' '.repeat(descCol);
416
476
 
417
477
  // Split into ordered groups: ungrouped first as "Options:", then each group in first-seen order
418
478
  const grouped = Object.groupBy(argColumns, (c) => c.arg.group ?? '');
@@ -441,28 +501,65 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig, showAllBui
441
501
  parts.push(styledType + typePadding);
442
502
  }
443
503
 
444
- // Line 1: description
445
- const descParts: string[] = [];
446
- if (arg.description) descParts.push(isDeprecated ? styler.deprecated(arg.description) : styler.description(arg.description));
447
- lines.push(indent(1) + parts.join('') + join(descParts));
504
+ const prefix = indent(1) + parts.join('');
505
+ const contPad = argAvailWidth ? descColPad : indent(3);
506
+
507
+ // Build inline meta (deprecated no-reason, default, choices)
508
+ const inlineMeta: string[] = [];
509
+ const styledInlineMeta: string[] = [];
510
+ if (isDeprecated && typeof arg.deprecated !== 'string') {
511
+ inlineMeta.push('(deprecated)');
512
+ styledInlineMeta.push(styler.meta('(deprecated)'));
513
+ }
514
+ if (hasDefault(arg.default)) {
515
+ const text = `(default: ${String(arg.default)})`;
516
+ inlineMeta.push(text);
517
+ styledInlineMeta.push(styler.meta(text));
518
+ }
519
+ if (arg.enum) {
520
+ const text = `(choices: ${arg.enum.join(', ')})`;
521
+ inlineMeta.push(text);
522
+ styledInlineMeta.push(styler.meta(text));
523
+ }
448
524
 
449
- // Line 2: deprecated (no reason), default, choices
450
- const line2Parts: string[] = [];
451
- if (isDeprecated && typeof arg.deprecated !== 'string') line2Parts.push(styler.meta('(deprecated)'));
452
- if (hasDefault(arg.default)) line2Parts.push(styler.meta(`(default: ${String(arg.default)})`));
453
- if (arg.enum) line2Parts.push(styler.meta(`(choices: ${arg.enum.join(', ')})`));
454
- if (line2Parts.length > 0) lines.push(indent(3) + join(line2Parts));
525
+ const descPlain = arg.description ?? '';
526
+ const styledDesc = descPlain ? (isDeprecated ? styler.deprecated(descPlain) : styler.description(descPlain)) : '';
527
+ const metaStyled = join(styledInlineMeta);
528
+
529
+ if (argAvailWidth && argAvailWidth > 0) {
530
+ // Terminal-width-aware: try to fit description + meta on one line
531
+ const metaPlain = inlineMeta.join(' ');
532
+ const fullPlain = [descPlain, metaPlain].filter(Boolean).join(' ');
533
+ if (fullPlain.length <= argAvailWidth) {
534
+ lines.push(prefix + [styledDesc, metaStyled].filter(Boolean).join(' '));
535
+ } else if (!descPlain || descPlain.length <= argAvailWidth) {
536
+ lines.push(prefix + styledDesc);
537
+ if (metaStyled) lines.push(descColPad + metaStyled);
538
+ } else {
539
+ const wrapped = wrapText(descPlain, argAvailWidth);
540
+ const styleFn = isDeprecated ? styler.deprecated : styler.description;
541
+ lines.push(prefix + styleFn(wrapped[0]!));
542
+ for (const wline of wrapped.slice(1)) lines.push(descColPad + styleFn(wline));
543
+ if (metaStyled) lines.push(descColPad + metaStyled);
544
+ }
545
+ } else {
546
+ // No terminal width (markdown/html): description on line 1, meta on line 2
547
+ const descParts: string[] = [];
548
+ if (styledDesc) descParts.push(styledDesc);
549
+ lines.push(prefix + join(descParts));
550
+ if (styledInlineMeta.length > 0) lines.push(indent(3) + metaStyled);
551
+ }
455
552
 
456
- // Line 3: deprecated (with reason), examples
553
+ // Deprecated (with reason), examples — always on separate line
457
554
  const line3Parts: string[] = [];
458
555
  if (isDeprecated && typeof arg.deprecated === 'string') line3Parts.push(styler.meta(`(deprecated: ${arg.deprecated})`));
459
556
  if (arg.examples && arg.examples.length > 0) {
460
557
  const exampleValues = arg.examples.map((example) => (typeof example === 'string' ? example : JSON.stringify(example))).join(', ');
461
558
  line3Parts.push(styler.example('Example:'), styler.exampleValue(exampleValues));
462
559
  }
463
- if (line3Parts.length > 0) lines.push(indent(3) + join(line3Parts));
560
+ if (line3Parts.length > 0) lines.push(contPad + join(line3Parts));
464
561
 
465
- // Line 4: stdin, env, config
562
+ // stdin, env, config — always on separate line
466
563
  const line4Parts: string[] = [];
467
564
  if (info.usage.stdinField === arg.name) line4Parts.push(styler.meta('(stdin)'));
468
565
  if (arg.env) {
@@ -472,7 +569,7 @@ function createGenericFormatter(styler: Styler, layout: LayoutConfig, showAllBui
472
569
  if (arg.configKey) {
473
570
  line4Parts.push(styler.example('Config:'), styler.exampleValue(arg.configKey));
474
571
  }
475
- if (line4Parts.length > 0) lines.push(indent(3) + join(line4Parts));
572
+ if (line4Parts.length > 0) lines.push(contPad + join(line4Parts));
476
573
  }
477
574
  };
478
575
 
@@ -636,11 +733,10 @@ function createJsonFormatter(): Formatter {
636
733
  // Formatter Factory
637
734
  // ============================================================================
638
735
 
639
- function shouldUseAnsi(): boolean {
640
- if (typeof process === 'undefined') return false;
641
- if (process.env.NO_COLOR) return false;
642
- if (process.env.CI) return false;
643
- if (process.stdout && typeof process.stdout.isTTY === 'boolean') return process.stdout.isTTY;
736
+ function shouldUseAnsi(env?: Record<string, string | undefined>, isTTY?: boolean): boolean {
737
+ if (env?.NO_COLOR) return false;
738
+ if (env?.CI) return false;
739
+ if (typeof isTTY === 'boolean') return isTTY;
644
740
  return false;
645
741
  }
646
742
 
@@ -673,13 +769,17 @@ export function createFormatter(
673
769
  detail: HelpDetail = 'standard',
674
770
  theme?: ColorTheme | ColorConfig,
675
771
  all?: boolean,
772
+ width?: number,
773
+ terminal?: { columns?: number; isTTY?: boolean },
774
+ env?: Record<string, string | undefined>,
676
775
  ): Formatter {
677
776
  if (detail === 'minimal') return createMinimalFormatter();
678
777
  if (format === 'json') return createJsonFormatter();
679
- if (format === 'ansi' || (format === 'auto' && shouldUseAnsi()))
680
- return createGenericFormatter(createAnsiStyler(theme), createTextLayout(), all);
681
- if (format === 'console') return createGenericFormatter(createConsoleStyler(theme), createTextLayout(), all);
778
+ const tw = format === 'markdown' || format === 'html' ? undefined : (width ?? terminal?.columns ?? DEFAULT_TERMINAL_WIDTH);
779
+ if (format === 'ansi' || (format === 'auto' && shouldUseAnsi(env, terminal?.isTTY)))
780
+ return createGenericFormatter(createAnsiStyler(theme), createTextLayout(), all, tw);
781
+ if (format === 'console') return createGenericFormatter(createConsoleStyler(theme), createTextLayout(), all, tw);
682
782
  if (format === 'markdown') return createGenericFormatter(createMarkdownStyler(), createMarkdownLayout(), all);
683
783
  if (format === 'html') return createGenericFormatter(createHtmlStyler(), createHtmlLayout(), all);
684
- return createGenericFormatter(createTextStyler(), createTextLayout(), all);
784
+ return createGenericFormatter(createTextStyler(), createTextLayout(), all, tw);
685
785
  }
@@ -1,7 +1,9 @@
1
1
  import type { StandardJSONSchemaV1 } from '@standard-schema/spec';
2
- import { extractSchemaMetadata, JSON_SCHEMA_OPTS, type PadroneArgsSchemaMeta, parsePositionalConfig, parseStdinConfig } from './args.ts';
2
+ import { extractSchemaMetadata, getJsonSchema, type PadroneArgsSchemaMeta, parsePositionalConfig } from '../core/args.ts';
3
+ import { findCommandByName } from '../core/commands.ts';
4
+ import type { AnyPadroneCommand } from '../types/index.ts';
5
+ import { getRootCommand } from '../util/utils.ts';
3
6
  import type { ColorConfig, ColorTheme } from './colorizer.ts';
4
- import { findCommandByName } from './command-utils.ts';
5
7
  import {
6
8
  createFormatter,
7
9
  type HelpArgumentInfo,
@@ -11,8 +13,6 @@ import {
11
13
  type HelpPositionalInfo,
12
14
  type HelpSubcommandInfo,
13
15
  } from './formatter.ts';
14
- import type { AnyPadroneCommand } from './types.ts';
15
- import { getRootCommand } from './utils.ts';
16
16
 
17
17
  export type HelpPreferences = {
18
18
  format?: HelpFormat | 'auto';
@@ -20,6 +20,12 @@ export type HelpPreferences = {
20
20
  theme?: ColorTheme | ColorConfig;
21
21
  /** Show all global commands and flags in full detail */
22
22
  all?: boolean;
23
+ /** Terminal width for text wrapping. Defaults to terminal columns or 80. */
24
+ width?: number;
25
+ /** Terminal capabilities for auto-detection of ANSI and width. */
26
+ terminal?: { columns?: number; isTTY?: boolean };
27
+ /** Environment variables for auto-detection (e.g., NO_COLOR, CI). */
28
+ env?: Record<string, string | undefined>;
23
29
  };
24
30
 
25
31
  /**
@@ -39,7 +45,7 @@ function extractPositionalArgsInfo(
39
45
  const positionalConfig = parsePositionalConfig(meta.positional);
40
46
 
41
47
  try {
42
- const jsonSchema = schema['~standard'].jsonSchema.input(JSON_SCHEMA_OPTS) as Record<string, any>;
48
+ const jsonSchema = getJsonSchema(schema) as Record<string, any>;
43
49
 
44
50
  if (jsonSchema.type === 'object' && jsonSchema.properties) {
45
51
  const properties = jsonSchema.properties as Record<string, any>;
@@ -79,7 +85,7 @@ function extractArgsInfo(schema: StandardJSONSchemaV1, meta?: PadroneArgsSchemaM
79
85
  const argsMeta = meta?.fields;
80
86
 
81
87
  try {
82
- const jsonSchema = schema['~standard'].jsonSchema.input(JSON_SCHEMA_OPTS) as Record<string, any>;
88
+ const jsonSchema = getJsonSchema(schema) as Record<string, any>;
83
89
 
84
90
  // Handle object: z.object({ key: z.string(), ... })
85
91
  if (jsonSchema.type === 'object' && jsonSchema.properties) {
@@ -190,7 +196,7 @@ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['det
190
196
  hasSubcommands: !!(cmd.commands && cmd.commands.length > 0),
191
197
  hasPositionals,
192
198
  hasArguments: false, // updated below after extracting arguments
193
- stdinField: cmd.meta?.stdin ? parseStdinConfig(cmd.meta.stdin) : undefined,
199
+ stdinField: cmd.meta?.stdin,
194
200
  },
195
201
  };
196
202
 
@@ -366,6 +372,14 @@ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['det
366
372
 
367
373
  export function generateHelp(rootCommand: AnyPadroneCommand, commandObj: AnyPadroneCommand = rootCommand, prefs?: HelpPreferences): string {
368
374
  const helpInfo = getHelpInfo(commandObj, prefs?.detail, prefs?.all);
369
- const formatter = createFormatter(prefs?.format ?? 'auto', prefs?.detail, prefs?.theme, prefs?.all);
375
+ const formatter = createFormatter(
376
+ prefs?.format ?? 'auto',
377
+ prefs?.detail,
378
+ prefs?.theme,
379
+ prefs?.all,
380
+ prefs?.width,
381
+ prefs?.terminal,
382
+ prefs?.env,
383
+ );
370
384
  return formatter.format(helpInfo);
371
385
  }
@@ -1,4 +1,4 @@
1
- import type { PadroneFieldMeta } from './args.ts';
1
+ import type { PadroneFieldMeta } from '../core/args.ts';
2
2
 
3
3
  declare module 'zod/v4/core' {
4
4
  export interface GlobalMeta extends PadroneFieldMeta {}
@@ -0,0 +1,50 @@
1
+ import * as z from 'zod/v4';
2
+ import type { PadroneSchema } from '../types/index.ts';
3
+ import { asyncStream } from '../util/stream.ts';
4
+
5
+ /**
6
+ * Creates a Zod schema for an async stream field, ready to use in `.arguments()`.
7
+ * Wraps `z.custom<AsyncIterable<T>>()` with the `asyncStream()` metadata automatically.
8
+ *
9
+ * @param itemSchema - Optional item schema for per-item validation.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { zodAsyncStream } from 'padrone/zod';
14
+ *
15
+ * // String lines
16
+ * z.object({ lines: zodAsyncStream() })
17
+ *
18
+ * // Typed items — each line JSON.parse'd and validated
19
+ * const recordSchema = z.object({ name: z.string(), age: z.number() });
20
+ * z.object({ records: zodAsyncStream(jsonCodec(recordSchema)) })
21
+ * ```
22
+ */
23
+ export function zodAsyncStream<T = string>(itemSchema?: PadroneSchema<unknown, T>) {
24
+ return z.custom<AsyncIterable<T>>().meta(asyncStream(itemSchema));
25
+ }
26
+
27
+ /**
28
+ * JSON codec for Zod schemas
29
+ * @see https://zod.dev/codecs?id=jsonschema
30
+ * Unlike the example in the docs, this codec also handles the case where the input is already an object
31
+ */
32
+ export const jsonCodec = <T extends z.ZodType>(schema: T) =>
33
+ z.codec(z.union([z.string(), z.unknown()]), schema, {
34
+ decode: (jsonString, ctx) => {
35
+ try {
36
+ // HACK: in some cases the object is already deserialized, we just need to validate it
37
+ if (typeof jsonString !== 'string') return jsonString as z.input<T>;
38
+ return JSON.parse(jsonString) as z.input<T>;
39
+ } catch (err: any) {
40
+ ctx.issues.push({
41
+ code: 'invalid_format',
42
+ format: 'json',
43
+ input: typeof jsonString === 'string' ? jsonString : JSON.stringify(jsonString),
44
+ message: err.message,
45
+ });
46
+ return z.NEVER;
47
+ }
48
+ },
49
+ encode: (value) => JSON.stringify(value),
50
+ });
package/src/test.ts CHANGED
@@ -1,276 +1,2 @@
1
- import type { InteractivePromptConfig, PadroneRuntime } from './runtime.ts';
2
- import type { AnyPadroneCommand, PadroneCommandResult } from './types.ts';
3
-
4
- /**
5
- * Result from a single command execution in test mode.
6
- * Extends the standard PadroneCommandResult with captured I/O.
7
- */
8
- export type TestCliResult = {
9
- /** The matched command. */
10
- command: AnyPadroneCommand;
11
- /** Validated arguments (undefined if validation failed). */
12
- args: unknown;
13
- /** Action handler return value (undefined if validation failed or no action). */
14
- result: unknown;
15
- /** Validation issues, if any. */
16
- issues: { message: string; path?: PropertyKey[] }[] | undefined;
17
- /** All values passed to `runtime.output()`. */
18
- stdout: unknown[];
19
- /** All strings passed to `runtime.error()`. */
20
- stderr: string[];
21
- /** The thrown error, if the command threw (routing error, action error, etc.). */
22
- error?: unknown;
23
- };
24
-
25
- /**
26
- * Result from a REPL test session.
27
- */
28
- export type TestReplResult = {
29
- /** One entry per successfully executed command (validation errors are captured in stderr, not here). */
30
- results: Omit<TestCliResult, 'stdout' | 'stderr'>[];
31
- /** All output from the entire REPL session. */
32
- stdout: unknown[];
33
- /** All errors from the entire REPL session. */
34
- stderr: string[];
35
- };
36
-
37
- /**
38
- * Fluent builder for setting up CLI test scenarios.
39
- */
40
- export type TestCliBuilder = {
41
- /** Set the CLI input string (e.g. `'deploy --env production'`). */
42
- args(input: string): TestCliBuilder;
43
- /** Set environment variables visible to the command. */
44
- env(vars: Record<string, string | undefined>): TestCliBuilder;
45
- /** Provide mock answers for interactive prompts. Keys are field names. */
46
- prompt(answers: Record<string, unknown>): TestCliBuilder;
47
- /** Provide mock config files. Keys are file paths, values are parsed config objects. */
48
- config(files: Record<string, Record<string, unknown>>): TestCliBuilder;
49
- /** Provide mock stdin data (simulates piped input). */
50
- stdin(data: string): TestCliBuilder;
51
- /**
52
- * Execute a single command via `eval()` and return the result with captured I/O.
53
- * @param input - Optional CLI input string. Overrides `.args()` if provided.
54
- */
55
- run(input?: string): Promise<TestCliResult>;
56
- /**
57
- * Run a REPL session with the given sequence of inputs.
58
- * Each string in the array is fed as one line of input.
59
- * The session ends after all inputs are consumed (EOF).
60
- */
61
- repl(inputs: string[]): Promise<TestReplResult>;
62
- };
63
-
64
- /**
65
- * Creates a fluent test builder for a Padrone program.
66
- * Captures all I/O and provides a clean interface for assertions.
67
- *
68
- * Works with any test framework (bun:test, vitest, jest, node:test, etc.).
69
- *
70
- * @example
71
- * ```ts
72
- * import { testCli } from 'padrone/test'
73
- *
74
- * const result = await testCli(myProgram)
75
- * .args('deploy --env production')
76
- * .env({ API_KEY: 'xxx' })
77
- * .run()
78
- *
79
- * expect(result.result).toBe('Deployed')
80
- * expect(result.stdout).toContain('Deploying...')
81
- * ```
82
- *
83
- * @example
84
- * ```ts
85
- * // Shorthand: pass input directly to run()
86
- * const result = await testCli(myProgram).run('deploy --env production')
87
- * ```
88
- *
89
- * @example
90
- * ```ts
91
- * // Test interactive prompts
92
- * const result = await testCli(myProgram)
93
- * .args('init')
94
- * .prompt({ name: 'myapp', template: 'react' })
95
- * .run()
96
- *
97
- * expect(result.args).toEqual({ name: 'myapp', template: 'react' })
98
- * ```
99
- *
100
- * @example
101
- * ```ts
102
- * // Test REPL sessions
103
- * const { results } = await testCli(myProgram)
104
- * .repl(['greet World', 'add --a=2 --b=3'])
105
- *
106
- * expect(results[0].result).toBe('Hello, World!')
107
- * expect(results[1].result).toBe(5)
108
- * ```
109
- */
110
- /**
111
- * Any program-like object that has `eval`, `runtime`, and `repl` methods.
112
- * Avoids strict variance issues with `AnyPadroneProgram`.
113
- */
114
- type TestableProgram = {
115
- eval: (input: string, prefs?: { autoOutput?: boolean }) => any;
116
- runtime: (runtime: PadroneRuntime) => TestableProgram;
117
- repl: (options?: { greeting?: false; hint?: false }) => AsyncIterable<any>;
118
- };
119
-
120
- export function testCli(program: TestableProgram): TestCliBuilder {
121
- let input: string | undefined;
122
- let envVars: Record<string, string | undefined> | undefined;
123
- let promptAnswers: Record<string, unknown> | undefined;
124
- let configFiles: Record<string, Record<string, unknown>> | undefined;
125
- let stdinData: string | undefined;
126
-
127
- const builder: TestCliBuilder = {
128
- args(args: string) {
129
- input = args;
130
- return builder;
131
- },
132
- env(vars) {
133
- envVars = vars;
134
- return builder;
135
- },
136
- prompt(answers) {
137
- promptAnswers = answers;
138
- return builder;
139
- },
140
- config(files) {
141
- configFiles = files;
142
- return builder;
143
- },
144
- stdin(data: string) {
145
- stdinData = data;
146
- return builder;
147
- },
148
-
149
- async run(runInput?: string) {
150
- const stdout: unknown[] = [];
151
- const stderr: string[] = [];
152
-
153
- const runtime = buildRuntime(stdout, stderr, { envVars, promptAnswers, configFiles, stdinData });
154
- const testProgram = program.runtime(runtime);
155
-
156
- const evalResult = await testProgram.eval(runInput ?? input ?? '', { autoOutput: false });
157
- if (evalResult.error) {
158
- stderr.push(evalResult.error instanceof Error ? evalResult.error.message : String(evalResult.error));
159
- }
160
- return toTestResult(evalResult, stdout, stderr);
161
- },
162
-
163
- async repl(inputs: string[]) {
164
- const stdout: unknown[] = [];
165
- const stderr: string[] = [];
166
-
167
- const runtime = buildRuntime(stdout, stderr, {
168
- envVars,
169
- promptAnswers,
170
- configFiles,
171
- readLine: createMockReadLine(inputs),
172
- });
173
-
174
- const testProgram = program.runtime(runtime);
175
- const results: Omit<TestCliResult, 'stdout' | 'stderr'>[] = [];
176
-
177
- for await (const r of testProgram.repl({ greeting: false, hint: false })) {
178
- results.push({
179
- command: r.command!,
180
- args: r.args,
181
- result: r.result,
182
- issues: r.argsResult?.issues as TestCliResult['issues'],
183
- });
184
- }
185
-
186
- return { results, stdout, stderr };
187
- },
188
- };
189
-
190
- return builder;
191
- }
192
-
193
- function toTestResult(evalResult: PadroneCommandResult, stdout: unknown[], stderr: string[]): TestCliResult {
194
- return {
195
- command: evalResult.command!,
196
- args: evalResult.args,
197
- result: evalResult.result,
198
- error: evalResult.error,
199
- issues: evalResult.argsResult?.issues as TestCliResult['issues'],
200
- stdout,
201
- stderr,
202
- };
203
- }
204
-
205
- function buildRuntime(
206
- stdout: unknown[],
207
- stderr: string[],
208
- opts: {
209
- envVars?: Record<string, string | undefined>;
210
- promptAnswers?: Record<string, unknown>;
211
- configFiles?: Record<string, Record<string, unknown>>;
212
- readLine?: (prompt: string) => Promise<string | null>;
213
- stdinData?: string;
214
- },
215
- ): PadroneRuntime {
216
- const runtime: PadroneRuntime = {
217
- output: (...args: unknown[]) => stdout.push(...args),
218
- error: (text: string) => stderr.push(text),
219
- };
220
-
221
- if (opts.envVars) {
222
- runtime.env = () => opts.envVars!;
223
- }
224
-
225
- if (opts.promptAnswers) {
226
- runtime.interactive = 'supported';
227
- runtime.prompt = async (config: InteractivePromptConfig) => opts.promptAnswers![config.name];
228
- }
229
-
230
- if (opts.configFiles) {
231
- runtime.loadConfigFile = (path: string) => opts.configFiles![path];
232
- runtime.findFile = (names: string[]) => names.find((n) => n in opts.configFiles!);
233
- }
234
-
235
- if (opts.readLine) {
236
- runtime.readLine = opts.readLine;
237
- }
238
-
239
- if (opts.stdinData !== undefined) {
240
- runtime.stdin = {
241
- isTTY: false,
242
- async text() {
243
- return opts.stdinData!;
244
- },
245
- async *lines() {
246
- const lines = opts.stdinData!.split('\n');
247
- // Remove trailing empty line from final newline (matches readline behavior)
248
- if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
249
- for (const line of lines) {
250
- yield line;
251
- }
252
- },
253
- };
254
- } else {
255
- // No stdin data: simulate a TTY (no piped input) to avoid reading from process.stdin
256
- runtime.stdin = {
257
- isTTY: true,
258
- async text() {
259
- return '';
260
- },
261
- async *lines() {
262
- // no lines
263
- },
264
- };
265
- }
266
-
267
- return runtime;
268
- }
269
-
270
- function createMockReadLine(inputs: string[]): (prompt: string) => Promise<string | null> {
271
- let index = 0;
272
- return async (_prompt: string): Promise<string | null> => {
273
- if (index >= inputs.length) return null;
274
- return inputs[index++] ?? null;
275
- };
276
- }
1
+ export type { TestCliBuilder, TestCliResult, TestReplResult } from './feature/test.ts';
2
+ export { testCli } from './feature/test.ts';