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.
- package/CHANGELOG.md +115 -0
- package/README.md +108 -283
- package/dist/args-Cnq0nwSM.mjs +272 -0
- package/dist/args-Cnq0nwSM.mjs.map +1 -0
- package/dist/codegen/index.d.mts +28 -3
- package/dist/codegen/index.d.mts.map +1 -1
- package/dist/codegen/index.mjs +169 -19
- 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} +86 -108
- package/dist/completion-BEuflbDO.mjs.map +1 -0
- package/dist/docs/index.d.mts +22 -2
- package/dist/docs/index.d.mts.map +1 -1
- package/dist/docs/index.mjs +92 -7
- package/dist/docs/index.mjs.map +1 -1
- package/dist/errors-CL63UOzt.mjs +137 -0
- package/dist/errors-CL63UOzt.mjs.map +1 -0
- package/dist/{formatter-ClUK5hcQ.d.mts → formatter-DrvhDMrq.d.mts} +35 -6
- package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
- package/dist/help-B5Kk83of.mjs +849 -0
- package/dist/help-B5Kk83of.mjs.map +1 -0
- package/dist/index-BaU3X6dY.d.mts +1178 -0
- package/dist/index-BaU3X6dY.d.mts.map +1 -0
- package/dist/index.d.mts +763 -36
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3608 -1534
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-BM-d0nZi.mjs +377 -0
- package/dist/mcp-BM-d0nZi.mjs.map +1 -0
- package/dist/serve-Bk0JUlCj.mjs +402 -0
- package/dist/serve-Bk0JUlCj.mjs.map +1 -0
- package/dist/stream-DC4H8YTx.mjs +77 -0
- 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 +5 -27
- package/dist/test.mjs.map +1 -1
- package/dist/{update-check-EbNDkzyV.mjs → update-check-CZ2VqjnV.mjs} +16 -17
- package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
- package/dist/zod.d.mts +32 -0
- package/dist/zod.d.mts.map +1 -0
- package/dist/zod.mjs +50 -0
- package/dist/zod.mjs.map +1 -0
- package/package.json +20 -9
- package/src/cli/completions.ts +14 -11
- package/src/cli/docs.ts +13 -16
- package/src/cli/doctor.ts +213 -24
- package/src/cli/index.ts +28 -82
- package/src/cli/init.ts +12 -10
- package/src/cli/link.ts +22 -18
- package/src/cli/wrap.ts +14 -11
- package/src/codegen/discovery.ts +80 -28
- package/src/codegen/index.ts +2 -1
- package/src/codegen/parsers/bash.ts +179 -0
- package/src/codegen/schema-to-code.ts +2 -1
- package/src/core/args.ts +296 -0
- package/src/core/commands.ts +373 -0
- package/src/core/create.ts +268 -0
- package/src/{runtime.ts → core/default-runtime.ts} +70 -135
- 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 +124 -11
- 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} +130 -57
- package/src/{interactive.ts → feature/interactive.ts} +47 -6
- package/src/feature/mcp.ts +387 -0
- package/src/{repl-loop.ts → feature/repl-loop.ts} +26 -16
- package/src/feature/serve.ts +438 -0
- 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} +27 -27
- package/src/index.ts +120 -11
- package/src/output/colorizer.ts +154 -0
- package/src/{formatter.ts → output/formatter.ts} +281 -135
- package/src/{help.ts → output/help.ts} +62 -15
- package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
- package/src/schema/zod.ts +50 -0
- package/src/test.ts +2 -285
- 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/util/stream.ts +101 -0
- package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
- package/src/{type-utils.ts → util/type-utils.ts} +99 -37
- package/src/util/utils.ts +51 -0
- package/src/zod.ts +1 -0
- package/dist/args-CVDbyyzG.mjs +0 -199
- package/dist/args-CVDbyyzG.mjs.map +0 -1
- package/dist/chunk-y_GBKt04.mjs +0 -5
- package/dist/completion.d.mts +0 -64
- package/dist/completion.d.mts.map +0 -1
- package/dist/completion.mjs.map +0 -1
- package/dist/formatter-ClUK5hcQ.d.mts.map +0 -1
- package/dist/help-CcBe91bV.mjs +0 -1254
- package/dist/help-CcBe91bV.mjs.map +0 -1
- package/dist/types-DjIdJN5G.d.mts +0 -1059
- package/dist/types-DjIdJN5G.d.mts.map +0 -1
- package/dist/update-check-EbNDkzyV.mjs.map +0 -1
- package/src/args.ts +0 -461
- package/src/colorizer.ts +0 -41
- package/src/command-utils.ts +0 -532
- package/src/create.ts +0 -1477
- package/src/types.ts +0 -1109
- 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
|
|
3
|
-
import { findCommandByName } from '
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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',
|
|
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',
|
|
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',
|
|
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(
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
-
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
|
+
}
|