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
@@ -1,6 +1,9 @@
1
1
  import type { StandardJSONSchemaV1 } from '@standard-schema/spec';
2
- import { extractSchemaMetadata, type PadroneArgsSchemaMeta, parsePositionalConfig, parseStdinConfig } from './args.ts';
3
- import { findCommandByName } from './command-utils.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';
6
+ import type { ColorConfig, ColorTheme } from './colorizer.ts';
4
7
  import {
5
8
  createFormatter,
6
9
  type HelpArgumentInfo,
@@ -10,12 +13,19 @@ import {
10
13
  type HelpPositionalInfo,
11
14
  type HelpSubcommandInfo,
12
15
  } from './formatter.ts';
13
- import type { AnyPadroneCommand } from './types.ts';
14
- import { getRootCommand } from './utils.ts';
15
16
 
16
17
  export type HelpPreferences = {
17
18
  format?: HelpFormat | 'auto';
18
19
  detail?: HelpDetail;
20
+ theme?: ColorTheme | ColorConfig;
21
+ /** Show all global commands and flags in full detail */
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>;
19
29
  };
20
30
 
21
31
  /**
@@ -35,7 +45,7 @@ function extractPositionalArgsInfo(
35
45
  const positionalConfig = parsePositionalConfig(meta.positional);
36
46
 
37
47
  try {
38
- const jsonSchema = schema['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
48
+ const jsonSchema = getJsonSchema(schema) as Record<string, any>;
39
49
 
40
50
  if (jsonSchema.type === 'object' && jsonSchema.properties) {
41
51
  const properties = jsonSchema.properties as Record<string, any>;
@@ -75,7 +85,7 @@ function extractArgsInfo(schema: StandardJSONSchemaV1, meta?: PadroneArgsSchemaM
75
85
  const argsMeta = meta?.fields;
76
86
 
77
87
  try {
78
- const jsonSchema = schema['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
88
+ const jsonSchema = getJsonSchema(schema) as Record<string, any>;
79
89
 
80
90
  // Handle object: z.object({ key: z.string(), ... })
81
91
  if (jsonSchema.type === 'object' && jsonSchema.properties) {
@@ -134,6 +144,7 @@ function extractArgsInfo(schema: StandardJSONSchemaV1, meta?: PadroneArgsSchemaM
134
144
  examples: optMeta?.examples ?? prop?.examples,
135
145
  variadic: propType === 'array',
136
146
  negatable: isNegatable,
147
+ group: optMeta?.group,
137
148
  });
138
149
  }
139
150
  }
@@ -154,7 +165,7 @@ function extractArgsInfo(schema: StandardJSONSchemaV1, meta?: PadroneArgsSchemaM
154
165
  * @param cmd - The command to build help info for
155
166
  * @param detail - The level of detail ('minimal', 'standard', or 'full')
156
167
  */
157
- export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['detail'] = 'standard'): HelpInfo {
168
+ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['detail'] = 'standard', all?: boolean): HelpInfo {
158
169
  const rootCmd = getRootCommand(cmd);
159
170
  // A command is a "default" command if its name is '' or it has '' as an alias
160
171
  const isDefaultCommand = cmd.parent && (!cmd.name || cmd.aliases?.includes(''));
@@ -176,6 +187,7 @@ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['det
176
187
  name: commandName,
177
188
  title: cmd.title,
178
189
  description: cmd.description,
190
+ examples: cmd.examples,
179
191
  aliases: displayAliases,
180
192
  deprecated: cmd.deprecated,
181
193
  hidden: cmd.hidden,
@@ -184,7 +196,7 @@ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['det
184
196
  hasSubcommands: !!(cmd.commands && cmd.commands.length > 0),
185
197
  hasPositionals,
186
198
  hasArguments: false, // updated below after extracting arguments
187
- stdinField: cmd.meta?.stdin ? parseStdinConfig(cmd.meta.stdin).field : undefined,
199
+ stdinField: cmd.meta?.stdin,
188
200
  },
189
201
  };
190
202
 
@@ -222,6 +234,7 @@ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['det
222
234
  aliases: displayAliases?.length ? displayAliases : undefined,
223
235
  deprecated: c.deprecated,
224
236
  hidden: c.hidden,
237
+ group: c.group,
225
238
  },
226
239
  {
227
240
  name: displayName,
@@ -230,6 +243,7 @@ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['det
230
243
  deprecated: c.deprecated,
231
244
  hidden: c.hidden,
232
245
  hasSubcommands: true,
246
+ group: c.group,
233
247
  },
234
248
  ];
235
249
  }
@@ -243,6 +257,7 @@ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['det
243
257
  deprecated: c.deprecated,
244
258
  hidden: c.hidden,
245
259
  hasSubcommands,
260
+ group: c.group,
246
261
  },
247
262
  ];
248
263
  }),
@@ -285,40 +300,64 @@ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['det
285
300
  }
286
301
  }
287
302
 
288
- // Add built-in commands/flags for root command only
289
- if (!cmd.parent) {
303
+ // Add global commands/flags (root command by default, all commands when --all is passed)
304
+ if (!cmd.parent || all) {
290
305
  const builtins: HelpInfo['builtins'] = [];
291
306
 
292
- if (!findCommandByName('help', cmd.commands)) {
307
+ if (!findCommandByName('help', rootCmd.commands)) {
293
308
  builtins.push({
294
309
  name: 'help [command], -h, --help',
295
310
  description: 'Show help for a command',
296
311
  sub: [
312
+ { name: '--all', description: 'Show all global commands and flags' },
297
313
  { name: '--detail <level>', description: 'Detail level (minimal, standard, full)' },
298
314
  { name: '--format <format>', description: 'Output format (text, ansi, json, markdown, html)' },
299
315
  ],
300
316
  });
301
317
  }
302
318
 
303
- if (!findCommandByName('version', cmd.commands)) {
319
+ if (!findCommandByName('version', rootCmd.commands)) {
304
320
  builtins.push({
305
321
  name: 'version, -v, --version',
306
322
  description: 'Show version information',
307
323
  });
308
324
  }
309
325
 
310
- if (!findCommandByName('completion', cmd.commands)) {
326
+ if (!findCommandByName('completion', rootCmd.commands)) {
311
327
  builtins.push({
312
328
  name: 'completion [shell]',
313
329
  description: 'Generate shell completions (bash, zsh, fish, powershell)',
314
330
  });
315
331
  }
316
332
 
333
+ if (!findCommandByName('man', rootCmd.commands)) {
334
+ builtins.push({
335
+ name: 'man',
336
+ description: 'Show or install man pages (--setup to install, --remove to uninstall) (experimental)',
337
+ });
338
+ }
339
+
317
340
  builtins.push({
318
341
  name: '[command] --repl',
319
342
  description: 'Start interactive REPL scoped to a command',
320
343
  });
321
344
 
345
+ if (!findCommandByName('mcp', rootCmd.commands)) {
346
+ builtins.push({
347
+ name: 'mcp [http|stdio]',
348
+ description: 'Start a Model Context Protocol server to expose commands as AI tools (experimental)',
349
+ sub: [
350
+ { name: '--port <port>', description: 'HTTP port (default: 3000)' },
351
+ { name: '--host <host>', description: 'HTTP host (default: 127.0.0.1)' },
352
+ ],
353
+ });
354
+ }
355
+
356
+ builtins.push({
357
+ name: '--color [theme], --no-color',
358
+ description: 'Set color theme (default, ocean, warm, monochrome) or disable colors',
359
+ });
360
+
322
361
  if (builtins.length > 0) {
323
362
  helpInfo.builtins = builtins;
324
363
  }
@@ -332,7 +371,15 @@ export function getHelpInfo(cmd: AnyPadroneCommand, detail: HelpPreferences['det
332
371
  // ============================================================================
333
372
 
334
373
  export function generateHelp(rootCommand: AnyPadroneCommand, commandObj: AnyPadroneCommand = rootCommand, prefs?: HelpPreferences): string {
335
- const helpInfo = getHelpInfo(commandObj, prefs?.detail);
336
- const formatter = createFormatter(prefs?.format ?? 'auto', prefs?.detail);
374
+ const helpInfo = getHelpInfo(commandObj, prefs?.detail, 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
+ );
337
384
  return formatter.format(helpInfo);
338
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,285 +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
- try {
157
- const evalResult = await testProgram.eval(runInput ?? input ?? '', { autoOutput: false });
158
- return toTestResult(evalResult, stdout, stderr);
159
- } catch (err) {
160
- stderr.push(err instanceof Error ? err.message : String(err));
161
- return {
162
- command: undefined as unknown as AnyPadroneCommand,
163
- args: undefined,
164
- result: undefined,
165
- issues: undefined,
166
- stdout,
167
- stderr,
168
- error: err,
169
- };
170
- }
171
- },
172
-
173
- async repl(inputs: string[]) {
174
- const stdout: unknown[] = [];
175
- const stderr: string[] = [];
176
-
177
- const runtime = buildRuntime(stdout, stderr, {
178
- envVars,
179
- promptAnswers,
180
- configFiles,
181
- readLine: createMockReadLine(inputs),
182
- });
183
-
184
- const testProgram = program.runtime(runtime);
185
- const results: Omit<TestCliResult, 'stdout' | 'stderr'>[] = [];
186
-
187
- for await (const r of testProgram.repl({ greeting: false, hint: false })) {
188
- results.push({
189
- command: r.command,
190
- args: r.args,
191
- result: r.result,
192
- issues: r.argsResult?.issues as TestCliResult['issues'],
193
- });
194
- }
195
-
196
- return { results, stdout, stderr };
197
- },
198
- };
199
-
200
- return builder;
201
- }
202
-
203
- function toTestResult(evalResult: PadroneCommandResult, stdout: unknown[], stderr: string[]): TestCliResult {
204
- return {
205
- command: evalResult.command,
206
- args: evalResult.args,
207
- result: evalResult.result,
208
- issues: evalResult.argsResult?.issues as TestCliResult['issues'],
209
- stdout,
210
- stderr,
211
- };
212
- }
213
-
214
- function buildRuntime(
215
- stdout: unknown[],
216
- stderr: string[],
217
- opts: {
218
- envVars?: Record<string, string | undefined>;
219
- promptAnswers?: Record<string, unknown>;
220
- configFiles?: Record<string, Record<string, unknown>>;
221
- readLine?: (prompt: string) => Promise<string | null>;
222
- stdinData?: string;
223
- },
224
- ): PadroneRuntime {
225
- const runtime: PadroneRuntime = {
226
- output: (...args: unknown[]) => stdout.push(...args),
227
- error: (text: string) => stderr.push(text),
228
- };
229
-
230
- if (opts.envVars) {
231
- runtime.env = () => opts.envVars!;
232
- }
233
-
234
- if (opts.promptAnswers) {
235
- runtime.interactive = 'supported';
236
- runtime.prompt = async (config: InteractivePromptConfig) => opts.promptAnswers![config.name];
237
- }
238
-
239
- if (opts.configFiles) {
240
- runtime.loadConfigFile = (path: string) => opts.configFiles![path];
241
- runtime.findFile = (names: string[]) => names.find((n) => n in opts.configFiles!);
242
- }
243
-
244
- if (opts.readLine) {
245
- runtime.readLine = opts.readLine;
246
- }
247
-
248
- if (opts.stdinData !== undefined) {
249
- runtime.stdin = {
250
- isTTY: false,
251
- async text() {
252
- return opts.stdinData!;
253
- },
254
- async *lines() {
255
- const lines = opts.stdinData!.split('\n');
256
- // Remove trailing empty line from final newline (matches readline behavior)
257
- if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
258
- for (const line of lines) {
259
- yield line;
260
- }
261
- },
262
- };
263
- } else {
264
- // No stdin data: simulate a TTY (no piped input) to avoid reading from process.stdin
265
- runtime.stdin = {
266
- isTTY: true,
267
- async text() {
268
- return '';
269
- },
270
- async *lines() {
271
- // no lines
272
- },
273
- };
274
- }
275
-
276
- return runtime;
277
- }
278
-
279
- function createMockReadLine(inputs: string[]): (prompt: string) => Promise<string | null> {
280
- let index = 0;
281
- return async (_prompt: string): Promise<string | null> => {
282
- if (index >= inputs.length) return null;
283
- return inputs[index++] ?? null;
284
- };
285
- }
1
+ export type { TestCliBuilder, TestCliResult, TestReplResult } from './feature/test.ts';
2
+ export { testCli } from './feature/test.ts';
@@ -0,0 +1,151 @@
1
+ type Letter =
2
+ | 'a'
3
+ | 'b'
4
+ | 'c'
5
+ | 'd'
6
+ | 'e'
7
+ | 'f'
8
+ | 'g'
9
+ | 'h'
10
+ | 'i'
11
+ | 'j'
12
+ | 'k'
13
+ | 'l'
14
+ | 'm'
15
+ | 'n'
16
+ | 'o'
17
+ | 'p'
18
+ | 'q'
19
+ | 'r'
20
+ | 's'
21
+ | 't'
22
+ | 'u'
23
+ | 'v'
24
+ | 'w'
25
+ | 'x'
26
+ | 'y'
27
+ | 'z';
28
+
29
+ /** A single letter character, valid as a short CLI flag (e.g. `'v'`, `'n'`, `'V'`). */
30
+ export type SingleChar = Letter | Uppercase<Letter>;
31
+
32
+ export interface PadroneFieldMeta {
33
+ description?: string;
34
+ /** Single-character short flags (stackable: `-abc` = `-a -b -c`). Used with single dash. */
35
+ flags?: readonly SingleChar[] | SingleChar;
36
+ /** Multi-character alternative long names. Used with double dash (e.g. `--dry-run` for `--dryRun`). */
37
+ alias?: readonly string[] | string;
38
+ deprecated?: boolean | string;
39
+ hidden?: boolean;
40
+ examples?: readonly unknown[];
41
+ /** Group name for organizing this option under a labeled section in help output. */
42
+ group?: string;
43
+ }
44
+
45
+ type PositionalArgs<TObj> =
46
+ TObj extends Record<string, any>
47
+ ? {
48
+ [K in keyof TObj]: NonNullable<TObj[K]> extends Array<any> ? `...${K & string}` | (K & string) : K & string;
49
+ }[keyof TObj]
50
+ : string;
51
+
52
+ /**
53
+ * Meta configuration for arguments, including positional arguments.
54
+ * The `positional` array defines which arguments are positional and their order.
55
+ * Use '...name' prefix to indicate variadic (rest) arguments, matching JS/TS rest syntax.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * .arguments(schema, {
60
+ * positional: ['source', '...files', 'dest'], // '...files' is variadic
61
+ * })
62
+ * ```
63
+ */
64
+ /**
65
+ * Configuration for reading from stdin and mapping it to an argument field.
66
+ * Simply specify the field name — the read mode is inferred from the schema:
67
+ * - `string` field → reads all stdin as text
68
+ * - `string[]` field → reads stdin line-by-line
69
+ */
70
+ export type StdinConfig<TObj = Record<string, any>> = keyof TObj & string;
71
+
72
+ export interface PadroneArgsSchemaMeta<TObj = Record<string, any>> {
73
+ /**
74
+ * Array of argument names that should be treated as positional arguments.
75
+ * Order in array determines position. Use '...name' prefix for variadic args.
76
+ * @example ['source', '...files', 'dest'] - 'files' captures multiple values
77
+ */
78
+ positional?: readonly PositionalArgs<TObj>[];
79
+ /**
80
+ * Per-argument metadata.
81
+ */
82
+ fields?: { [K in keyof TObj]?: PadroneFieldMeta };
83
+ /**
84
+ * Automatically generate kebab-case aliases for camelCase option names.
85
+ * For example, `dryRun` automatically gets `--dry-run` as an alias.
86
+ * Defaults to `true`. Set to `false` to disable.
87
+ *
88
+ * @default true
89
+ * @example
90
+ * ```ts
91
+ * // Auto-aliases enabled (default): --dry-run → dryRun
92
+ * .arguments(z.object({ dryRun: z.boolean() }))
93
+ *
94
+ * // Disable auto-aliases
95
+ * .arguments(z.object({ dryRun: z.boolean() }), { autoAlias: false })
96
+ * ```
97
+ */
98
+ autoAlias?: boolean;
99
+ /**
100
+ * Read from stdin and inject the data into the specified argument field.
101
+ * Only reads when stdin is piped (not a TTY) and the field wasn't already provided via CLI flags.
102
+ *
103
+ * The read mode is inferred from the schema type of the target field:
104
+ * - `string` field → reads all stdin as a single string
105
+ * - `string[]` field → reads stdin line-by-line into an array
106
+ *
107
+ * Precedence: CLI flags > stdin > env vars > config file > schema defaults.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * // Read all stdin as text into 'data' field
112
+ * .arguments(z.object({ data: z.string() }), { stdin: 'data' })
113
+ *
114
+ * // Read stdin lines into 'lines' field (inferred from array schema)
115
+ * .arguments(z.object({ lines: z.string().array() }), { stdin: 'lines' })
116
+ * ```
117
+ */
118
+ stdin?: StdinConfig<TObj>;
119
+ /**
120
+ * Fields to interactively prompt for when their values are missing after CLI/env/config resolution.
121
+ * - `true`: prompt for all required fields that are missing.
122
+ * - `string[]`: prompt for these specific fields if missing.
123
+ *
124
+ * Interactive prompting only occurs in `cli()` when the runtime has `interactive: true`.
125
+ * Setting this makes `parse()` and `cli()` return Promises.
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * .arguments(schema, {
130
+ * interactive: true, // prompt all missing required fields
131
+ * interactive: ['name', 'template'], // prompt only these fields
132
+ * })
133
+ * ```
134
+ */
135
+ interactive?: true | readonly (keyof TObj & string)[];
136
+ /**
137
+ * Optional fields offered after required interactive prompts.
138
+ * Users are shown a multi-select to choose which of these fields to configure.
139
+ * - `true`: offer all optional fields that are missing.
140
+ * - `string[]`: offer these specific fields.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * .arguments(schema, {
145
+ * interactive: ['name'],
146
+ * optionalInteractive: ['typescript', 'eslint', 'prettier'],
147
+ * })
148
+ * ```
149
+ */
150
+ optionalInteractive?: true | readonly (keyof TObj & string)[];
151
+ }