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.
- package/CHANGELOG.md +36 -0
- package/README.md +15 -11
- package/dist/{args-D5PNDyNu.mjs → args-Cnq0nwSM.mjs} +91 -41
- package/dist/args-Cnq0nwSM.mjs.map +1 -0
- package/dist/codegen/index.mjs +4 -4
- package/dist/codegen/index.mjs.map +1 -1
- package/dist/commands-B_gufyR9.mjs +514 -0
- package/dist/commands-B_gufyR9.mjs.map +1 -0
- package/dist/{completion.mjs → completion-BEuflbDO.mjs} +12 -82
- package/dist/completion-BEuflbDO.mjs.map +1 -0
- package/dist/docs/index.d.mts +4 -4
- package/dist/docs/index.d.mts.map +1 -1
- package/dist/docs/index.mjs +10 -12
- package/dist/docs/index.mjs.map +1 -1
- package/dist/{errors-BiVrBgi6.mjs → errors-CL63UOzt.mjs} +26 -3
- package/dist/errors-CL63UOzt.mjs.map +1 -0
- package/dist/{formatter-DtHzbP22.d.mts → formatter-DrvhDMrq.d.mts} +3 -3
- package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
- package/dist/{help-bbmu9-qd.mjs → help-B5Kk83of.mjs} +151 -37
- package/dist/help-B5Kk83of.mjs.map +1 -0
- package/dist/{types-Ch8Mk6Qb.d.mts → index-BaU3X6dY.d.mts} +621 -750
- package/dist/index-BaU3X6dY.d.mts.map +1 -0
- package/dist/index.d.mts +735 -37
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3409 -1563
- package/dist/index.mjs.map +1 -1
- package/dist/{mcp-mLWIdUIu.mjs → mcp-BM-d0nZi.mjs} +13 -15
- package/dist/mcp-BM-d0nZi.mjs.map +1 -0
- package/dist/{serve-B0u43DK7.mjs → serve-Bk0JUlCj.mjs} +12 -14
- package/dist/serve-Bk0JUlCj.mjs.map +1 -0
- package/dist/{stream-BcC146Ud.mjs → stream-DC4H8YTx.mjs} +24 -3
- package/dist/stream-DC4H8YTx.mjs.map +1 -0
- package/dist/test.d.mts +5 -8
- package/dist/test.d.mts.map +1 -1
- package/dist/test.mjs +2 -13
- package/dist/test.mjs.map +1 -1
- package/dist/{update-check-CFX1FV3v.mjs → update-check-CZ2VqjnV.mjs} +16 -17
- package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
- package/dist/zod.d.mts +2 -2
- package/dist/zod.d.mts.map +1 -1
- package/dist/zod.mjs +2 -2
- package/dist/zod.mjs.map +1 -1
- package/package.json +15 -12
- package/src/cli/completions.ts +14 -11
- package/src/cli/docs.ts +13 -10
- package/src/cli/doctor.ts +22 -18
- package/src/cli/index.ts +28 -82
- package/src/cli/init.ts +10 -7
- package/src/cli/link.ts +20 -16
- package/src/cli/wrap.ts +14 -11
- package/src/codegen/schema-to-code.ts +2 -2
- package/src/{args.ts → core/args.ts} +32 -225
- package/src/core/commands.ts +373 -0
- package/src/core/create.ts +268 -0
- package/src/core/default-runtime.ts +239 -0
- package/src/{errors.ts → core/errors.ts} +22 -0
- package/src/core/exec.ts +259 -0
- package/src/core/interceptors.ts +302 -0
- package/src/{parse.ts → core/parse.ts} +36 -89
- package/src/core/program-methods.ts +301 -0
- package/src/core/results.ts +229 -0
- package/src/core/runtime.ts +246 -0
- package/src/core/validate.ts +247 -0
- package/src/docs/index.ts +12 -13
- package/src/extension/auto-output.ts +95 -0
- package/src/extension/color.ts +38 -0
- package/src/extension/completion.ts +49 -0
- package/src/extension/config.ts +262 -0
- package/src/extension/env.ts +101 -0
- package/src/extension/help.ts +192 -0
- package/src/extension/index.ts +43 -0
- package/src/extension/ink.ts +93 -0
- package/src/extension/interactive.ts +106 -0
- package/src/extension/logger.ts +214 -0
- package/src/extension/man.ts +51 -0
- package/src/extension/mcp.ts +52 -0
- package/src/extension/progress-renderer.ts +338 -0
- package/src/extension/progress.ts +299 -0
- package/src/extension/repl.ts +94 -0
- package/src/extension/serve.ts +48 -0
- package/src/extension/signal.ts +87 -0
- package/src/extension/stdin.ts +62 -0
- package/src/extension/suggestions.ts +114 -0
- package/src/extension/timing.ts +81 -0
- package/src/extension/tracing.ts +175 -0
- package/src/extension/update-check.ts +77 -0
- package/src/extension/utils.ts +51 -0
- package/src/extension/version.ts +63 -0
- package/src/{completion.ts → feature/completion.ts} +12 -12
- package/src/{interactive.ts → feature/interactive.ts} +4 -4
- package/src/{mcp.ts → feature/mcp.ts} +12 -15
- package/src/{repl-loop.ts → feature/repl-loop.ts} +10 -13
- package/src/{serve.ts → feature/serve.ts} +11 -15
- package/src/feature/test.ts +262 -0
- package/src/{update-check.ts → feature/update-check.ts} +16 -16
- package/src/{wrap.ts → feature/wrap.ts} +10 -8
- package/src/index.ts +111 -30
- package/src/{formatter.ts → output/formatter.ts} +131 -31
- package/src/{help.ts → output/help.ts} +22 -8
- package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
- package/src/schema/zod.ts +50 -0
- package/src/test.ts +2 -276
- package/src/types/args-meta.ts +151 -0
- package/src/types/builder.ts +697 -0
- package/src/types/command.ts +157 -0
- package/src/types/index.ts +59 -0
- package/src/types/interceptor.ts +296 -0
- package/src/types/preferences.ts +83 -0
- package/src/types/result.ts +71 -0
- package/src/types/schema.ts +19 -0
- package/src/util/dotenv.ts +244 -0
- package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
- package/src/{stream.ts → util/stream.ts} +27 -1
- package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
- package/src/{type-utils.ts → util/type-utils.ts} +71 -33
- package/src/util/utils.ts +51 -0
- package/src/zod.ts +1 -50
- package/dist/args-D5PNDyNu.mjs.map +0 -1
- package/dist/chunk-CjcI7cDX.mjs +0 -15
- package/dist/command-utils-B1D-HqCd.mjs +0 -1117
- package/dist/command-utils-B1D-HqCd.mjs.map +0 -1
- package/dist/completion.d.mts +0 -64
- package/dist/completion.d.mts.map +0 -1
- package/dist/completion.mjs.map +0 -1
- package/dist/errors-BiVrBgi6.mjs.map +0 -1
- package/dist/formatter-DtHzbP22.d.mts.map +0 -1
- package/dist/help-bbmu9-qd.mjs.map +0 -1
- package/dist/mcp-mLWIdUIu.mjs.map +0 -1
- package/dist/serve-B0u43DK7.mjs.map +0 -1
- package/dist/stream-BcC146Ud.mjs.map +0 -1
- package/dist/types-Ch8Mk6Qb.d.mts.map +0 -1
- package/dist/update-check-CFX1FV3v.mjs.map +0 -1
- package/src/command-utils.ts +0 -882
- package/src/create.ts +0 -1829
- package/src/runtime.ts +0 -497
- package/src/types.ts +0 -1291
- package/src/utils.ts +0 -140
- /package/src/{colorizer.ts → output/colorizer.ts} +0 -0
|
@@ -1,6 +1,25 @@
|
|
|
1
|
-
import { camelToKebab } from '
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
if (
|
|
454
|
-
|
|
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
|
-
//
|
|
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(
|
|
560
|
+
if (line3Parts.length > 0) lines.push(contPad + join(line3Parts));
|
|
464
561
|
|
|
465
|
-
//
|
|
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(
|
|
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 (
|
|
641
|
-
if (
|
|
642
|
-
if (
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
}
|
|
@@ -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
|
-
|
|
2
|
-
|
|
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';
|