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
package/dist/test.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test.mjs","names":[],"sources":["../src/test.ts"],"sourcesContent":["import type { InteractivePromptConfig, PadroneRuntime } from './runtime.ts';\nimport type { AnyPadroneCommand, PadroneCommandResult } from './types.ts';\n\n/**\n * Result from a single command execution in test mode.\n * Extends the standard PadroneCommandResult with captured I/O.\n */\nexport type TestCliResult = {\n /** The matched command. */\n command: AnyPadroneCommand;\n /** Validated arguments (undefined if validation failed). */\n args: unknown;\n /** Action handler return value (undefined if validation failed or no action). */\n result: unknown;\n /** Validation issues, if any. */\n issues: { message: string; path?: PropertyKey[] }[] | undefined;\n /** All values passed to `runtime.output()`. */\n stdout: unknown[];\n /** All strings passed to `runtime.error()`. */\n stderr: string[];\n /** The thrown error, if the command threw (routing error, action error, etc.). */\n error?: unknown;\n};\n\n/**\n * Result from a REPL test session.\n */\nexport type TestReplResult = {\n /** One entry per successfully executed command (validation errors are captured in stderr, not here). */\n results: Omit<TestCliResult, 'stdout' | 'stderr'>[];\n /** All output from the entire REPL session. */\n stdout: unknown[];\n /** All errors from the entire REPL session. */\n stderr: string[];\n};\n\n/**\n * Fluent builder for setting up CLI test scenarios.\n */\nexport type TestCliBuilder = {\n /** Set the CLI input string (e.g. `'deploy --env production'`). */\n args(input: string): TestCliBuilder;\n /** Set environment variables visible to the command. */\n env(vars: Record<string, string | undefined>): TestCliBuilder;\n /** Provide mock answers for interactive prompts. Keys are field names. */\n prompt(answers: Record<string, unknown>): TestCliBuilder;\n /** Provide mock config files. Keys are file paths, values are parsed config objects. */\n config(files: Record<string, Record<string, unknown>>): TestCliBuilder;\n /** Provide mock stdin data (simulates piped input). */\n stdin(data: string): TestCliBuilder;\n /**\n * Execute a single command via `eval()` and return the result with captured I/O.\n * @param input - Optional CLI input string. Overrides `.args()` if provided.\n */\n run(input?: string): Promise<TestCliResult>;\n /**\n * Run a REPL session with the given sequence of inputs.\n * Each string in the array is fed as one line of input.\n * The session ends after all inputs are consumed (EOF).\n */\n repl(inputs: string[]): Promise<TestReplResult>;\n};\n\n/**\n * Creates a fluent test builder for a Padrone program.\n * Captures all I/O and provides a clean interface for assertions.\n *\n * Works with any test framework (bun:test, vitest, jest, node:test, etc.).\n *\n * @example\n * ```ts\n * import { testCli } from 'padrone/test'\n *\n * const result = await testCli(myProgram)\n * .args('deploy --env production')\n * .env({ API_KEY: 'xxx' })\n * .run()\n *\n * expect(result.result).toBe('Deployed')\n * expect(result.stdout).toContain('Deploying...')\n * ```\n *\n * @example\n * ```ts\n * // Shorthand: pass input directly to run()\n * const result = await testCli(myProgram).run('deploy --env production')\n * ```\n *\n * @example\n * ```ts\n * // Test interactive prompts\n * const result = await testCli(myProgram)\n * .args('init')\n * .prompt({ name: 'myapp', template: 'react' })\n * .run()\n *\n * expect(result.args).toEqual({ name: 'myapp', template: 'react' })\n * ```\n *\n * @example\n * ```ts\n * // Test REPL sessions\n * const { results } = await testCli(myProgram)\n * .repl(['greet World', 'add --a=2 --b=3'])\n *\n * expect(results[0].result).toBe('Hello, World!')\n * expect(results[1].result).toBe(5)\n * ```\n */\n/**\n * Any program-like object that has `eval`, `runtime`, and `repl` methods.\n * Avoids strict variance issues with `AnyPadroneProgram`.\n */\ntype TestableProgram = {\n eval: (input: string, prefs?: { autoOutput?: boolean }) => any;\n runtime: (runtime: PadroneRuntime) => TestableProgram;\n repl: (options?: { greeting?: false; hint?: false }) => AsyncIterable<any>;\n};\n\nexport function testCli(program: TestableProgram): TestCliBuilder {\n let input: string | undefined;\n let envVars: Record<string, string | undefined> | undefined;\n let promptAnswers: Record<string, unknown> | undefined;\n let configFiles: Record<string, Record<string, unknown>> | undefined;\n let stdinData: string | undefined;\n\n const builder: TestCliBuilder = {\n args(args: string) {\n input = args;\n return builder;\n },\n env(vars) {\n envVars = vars;\n return builder;\n },\n prompt(answers) {\n promptAnswers = answers;\n return builder;\n },\n config(files) {\n configFiles = files;\n return builder;\n },\n stdin(data: string) {\n stdinData = data;\n return builder;\n },\n\n async run(runInput?: string) {\n const stdout: unknown[] = [];\n const stderr: string[] = [];\n\n const runtime = buildRuntime(stdout, stderr, { envVars, promptAnswers, configFiles, stdinData });\n const testProgram = program.runtime(runtime);\n\n try {\n const evalResult = await testProgram.eval(runInput ?? input ?? '', { autoOutput: false });\n return toTestResult(evalResult, stdout, stderr);\n } catch (err) {\n stderr.push(err instanceof Error ? err.message : String(err));\n return {\n command: undefined as unknown as AnyPadroneCommand,\n args: undefined,\n result: undefined,\n issues: undefined,\n stdout,\n stderr,\n error: err,\n };\n }\n },\n\n async repl(inputs: string[]) {\n const stdout: unknown[] = [];\n const stderr: string[] = [];\n\n const runtime = buildRuntime(stdout, stderr, {\n envVars,\n promptAnswers,\n configFiles,\n readLine: createMockReadLine(inputs),\n });\n\n const testProgram = program.runtime(runtime);\n const results: Omit<TestCliResult, 'stdout' | 'stderr'>[] = [];\n\n for await (const r of testProgram.repl({ greeting: false, hint: false })) {\n results.push({\n command: r.command,\n args: r.args,\n result: r.result,\n issues: r.argsResult?.issues as TestCliResult['issues'],\n });\n }\n\n return { results, stdout, stderr };\n },\n };\n\n return builder;\n}\n\nfunction toTestResult(evalResult: PadroneCommandResult, stdout: unknown[], stderr: string[]): TestCliResult {\n return {\n command: evalResult.command,\n args: evalResult.args,\n result: evalResult.result,\n issues: evalResult.argsResult?.issues as TestCliResult['issues'],\n stdout,\n stderr,\n };\n}\n\nfunction buildRuntime(\n stdout: unknown[],\n stderr: string[],\n opts: {\n envVars?: Record<string, string | undefined>;\n promptAnswers?: Record<string, unknown>;\n configFiles?: Record<string, Record<string, unknown>>;\n readLine?: (prompt: string) => Promise<string | null>;\n stdinData?: string;\n },\n): PadroneRuntime {\n const runtime: PadroneRuntime = {\n output: (...args: unknown[]) => stdout.push(...args),\n error: (text: string) => stderr.push(text),\n };\n\n if (opts.envVars) {\n runtime.env = () => opts.envVars!;\n }\n\n if (opts.promptAnswers) {\n runtime.interactive = 'supported';\n runtime.prompt = async (config: InteractivePromptConfig) => opts.promptAnswers![config.name];\n }\n\n if (opts.configFiles) {\n runtime.loadConfigFile = (path: string) => opts.configFiles![path];\n runtime.findFile = (names: string[]) => names.find((n) => n in opts.configFiles!);\n }\n\n if (opts.readLine) {\n runtime.readLine = opts.readLine;\n }\n\n if (opts.stdinData !== undefined) {\n runtime.stdin = {\n isTTY: false,\n async text() {\n return opts.stdinData!;\n },\n async *lines() {\n const lines = opts.stdinData!.split('\\n');\n // Remove trailing empty line from final newline (matches readline behavior)\n if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();\n for (const line of lines) {\n yield line;\n }\n },\n };\n } else {\n // No stdin data: simulate a TTY (no piped input) to avoid reading from process.stdin\n runtime.stdin = {\n isTTY: true,\n async text() {\n return '';\n },\n async *lines() {\n // no lines\n },\n };\n }\n\n return runtime;\n}\n\nfunction createMockReadLine(inputs: string[]): (prompt: string) => Promise<string | null> {\n let index = 0;\n return async (_prompt: string): Promise<string | null> => {\n if (index >= inputs.length) return null;\n return inputs[index++] ?? null;\n };\n}\n"],"mappings":";AAuHA,SAAgB,QAAQ,SAA0C;CAChE,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CAEJ,MAAM,UAA0B;EAC9B,KAAK,MAAc;AACjB,WAAQ;AACR,UAAO;;EAET,IAAI,MAAM;AACR,aAAU;AACV,UAAO;;EAET,OAAO,SAAS;AACd,mBAAgB;AAChB,UAAO;;EAET,OAAO,OAAO;AACZ,iBAAc;AACd,UAAO;;EAET,MAAM,MAAc;AAClB,eAAY;AACZ,UAAO;;EAGT,MAAM,IAAI,UAAmB;GAC3B,MAAM,SAAoB,EAAE;GAC5B,MAAM,SAAmB,EAAE;GAE3B,MAAM,UAAU,aAAa,QAAQ,QAAQ;IAAE;IAAS;IAAe;IAAa;IAAW,CAAC;GAChG,MAAM,cAAc,QAAQ,QAAQ,QAAQ;AAE5C,OAAI;AAEF,WAAO,aADY,MAAM,YAAY,KAAK,YAAY,SAAS,IAAI,EAAE,YAAY,OAAO,CAAC,EACzD,QAAQ,OAAO;YACxC,KAAK;AACZ,WAAO,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CAAC;AAC7D,WAAO;KACL,SAAS,KAAA;KACT,MAAM,KAAA;KACN,QAAQ,KAAA;KACR,QAAQ,KAAA;KACR;KACA;KACA,OAAO;KACR;;;EAIL,MAAM,KAAK,QAAkB;GAC3B,MAAM,SAAoB,EAAE;GAC5B,MAAM,SAAmB,EAAE;GAE3B,MAAM,UAAU,aAAa,QAAQ,QAAQ;IAC3C;IACA;IACA;IACA,UAAU,mBAAmB,OAAO;IACrC,CAAC;GAEF,MAAM,cAAc,QAAQ,QAAQ,QAAQ;GAC5C,MAAM,UAAsD,EAAE;AAE9D,cAAW,MAAM,KAAK,YAAY,KAAK;IAAE,UAAU;IAAO,MAAM;IAAO,CAAC,CACtE,SAAQ,KAAK;IACX,SAAS,EAAE;IACX,MAAM,EAAE;IACR,QAAQ,EAAE;IACV,QAAQ,EAAE,YAAY;IACvB,CAAC;AAGJ,UAAO;IAAE;IAAS;IAAQ;IAAQ;;EAErC;AAED,QAAO;;AAGT,SAAS,aAAa,YAAkC,QAAmB,QAAiC;AAC1G,QAAO;EACL,SAAS,WAAW;EACpB,MAAM,WAAW;EACjB,QAAQ,WAAW;EACnB,QAAQ,WAAW,YAAY;EAC/B;EACA;EACD;;AAGH,SAAS,aACP,QACA,QACA,MAOgB;CAChB,MAAM,UAA0B;EAC9B,SAAS,GAAG,SAAoB,OAAO,KAAK,GAAG,KAAK;EACpD,QAAQ,SAAiB,OAAO,KAAK,KAAK;EAC3C;AAED,KAAI,KAAK,QACP,SAAQ,YAAY,KAAK;AAG3B,KAAI,KAAK,eAAe;AACtB,UAAQ,cAAc;AACtB,UAAQ,SAAS,OAAO,WAAoC,KAAK,cAAe,OAAO;;AAGzF,KAAI,KAAK,aAAa;AACpB,UAAQ,kBAAkB,SAAiB,KAAK,YAAa;AAC7D,UAAQ,YAAY,UAAoB,MAAM,MAAM,MAAM,KAAK,KAAK,YAAa;;AAGnF,KAAI,KAAK,SACP,SAAQ,WAAW,KAAK;AAG1B,KAAI,KAAK,cAAc,KAAA,EACrB,SAAQ,QAAQ;EACd,OAAO;EACP,MAAM,OAAO;AACX,UAAO,KAAK;;EAEd,OAAO,QAAQ;GACb,MAAM,QAAQ,KAAK,UAAW,MAAM,KAAK;AAEzC,OAAI,MAAM,SAAS,KAAK,MAAM,MAAM,SAAS,OAAO,GAAI,OAAM,KAAK;AACnE,QAAK,MAAM,QAAQ,MACjB,OAAM;;EAGX;KAGD,SAAQ,QAAQ;EACd,OAAO;EACP,MAAM,OAAO;AACX,UAAO;;EAET,OAAO,QAAQ;EAGhB;AAGH,QAAO;;AAGT,SAAS,mBAAmB,QAA8D;CACxF,IAAI,QAAQ;AACZ,QAAO,OAAO,YAA4C;AACxD,MAAI,SAAS,OAAO,OAAQ,QAAO;AACnC,SAAO,OAAO,YAAY"}
|
|
1
|
+
{"version":3,"file":"test.mjs","names":[],"sources":["../src/feature/test.ts"],"sourcesContent":["import type { InteractivePromptConfig, PadroneRuntime } from '../core/runtime.ts';\nimport type { AnyPadroneCommand, PadroneCommandResult } from '../types/index.ts';\n\n/**\n * Result from a single command execution in test mode.\n * Extends the standard PadroneCommandResult with captured I/O.\n */\nexport type TestCliResult = {\n /** The matched command. */\n command: AnyPadroneCommand;\n /** Validated arguments (undefined if validation failed). */\n args: unknown;\n /** Action handler return value (undefined if validation failed or no action). */\n result: unknown;\n /** Validation issues, if any. */\n issues: { message: string; path?: PropertyKey[] }[] | undefined;\n /** All values passed to `runtime.output()`. */\n stdout: unknown[];\n /** All strings passed to `runtime.error()`. */\n stderr: string[];\n /** The thrown error, if the command threw (routing error, action error, etc.). */\n error?: unknown;\n};\n\n/**\n * Result from a REPL test session.\n */\nexport type TestReplResult = {\n /** One entry per successfully executed command (validation errors are captured in stderr, not here). */\n results: Omit<TestCliResult, 'stdout' | 'stderr'>[];\n /** All output from the entire REPL session. */\n stdout: unknown[];\n /** All errors from the entire REPL session. */\n stderr: string[];\n};\n\n/**\n * Fluent builder for setting up CLI test scenarios.\n */\nexport type TestCliBuilder = {\n /** Set the CLI input string (e.g. `'deploy --env production'`). */\n args(input: string): TestCliBuilder;\n /** Set environment variables visible to the command. */\n env(vars: Record<string, string | undefined>): TestCliBuilder;\n /** Provide mock answers for interactive prompts. Keys are field names. */\n prompt(answers: Record<string, unknown>): TestCliBuilder;\n /** Provide mock stdin data (simulates piped input). */\n stdin(data: string): TestCliBuilder;\n /**\n * Execute a single command via `eval()` and return the result with captured I/O.\n * @param input - Optional CLI input string. Overrides `.args()` if provided.\n */\n run(input?: string): Promise<TestCliResult>;\n /**\n * Run a REPL session with the given sequence of inputs.\n * Each string in the array is fed as one line of input.\n * The session ends after all inputs are consumed (EOF).\n */\n repl(inputs: string[]): Promise<TestReplResult>;\n};\n\n/**\n * Creates a fluent test builder for a Padrone program.\n * Captures all I/O and provides a clean interface for assertions.\n *\n * Works with any test framework (bun:test, vitest, jest, node:test, etc.).\n *\n * @example\n * ```ts\n * import { testCli } from 'padrone/test'\n *\n * const result = await testCli(myProgram)\n * .args('deploy --env production')\n * .env({ API_KEY: 'xxx' })\n * .run()\n *\n * expect(result.result).toBe('Deployed')\n * expect(result.stdout).toContain('Deploying...')\n * ```\n *\n * @example\n * ```ts\n * // Shorthand: pass input directly to run()\n * const result = await testCli(myProgram).run('deploy --env production')\n * ```\n *\n * @example\n * ```ts\n * // Test interactive prompts\n * const result = await testCli(myProgram)\n * .args('init')\n * .prompt({ name: 'myapp', template: 'react' })\n * .run()\n *\n * expect(result.args).toEqual({ name: 'myapp', template: 'react' })\n * ```\n *\n * @example\n * ```ts\n * // Test REPL sessions\n * const { results } = await testCli(myProgram)\n * .repl(['greet World', 'add --a=2 --b=3'])\n *\n * expect(results[0].result).toBe('Hello, World!')\n * expect(results[1].result).toBe(5)\n * ```\n */\n/**\n * Any program-like object that has `eval`, `runtime`, and `repl` methods.\n * Avoids strict variance issues with `AnyPadroneProgram`.\n */\ntype TestableProgram = {\n eval: (input: string, prefs?: Record<string, unknown>) => any;\n runtime: (runtime: PadroneRuntime) => TestableProgram;\n repl: (options?: { greeting?: false; hint?: false }) => AsyncIterable<any>;\n};\n\nexport function testCli(program: TestableProgram): TestCliBuilder {\n let input: string | undefined;\n let envVars: Record<string, string | undefined> | undefined;\n let promptAnswers: Record<string, unknown> | undefined;\n let stdinData: string | undefined;\n\n const builder: TestCliBuilder = {\n args(args: string) {\n input = args;\n return builder;\n },\n env(vars) {\n envVars = vars;\n return builder;\n },\n prompt(answers) {\n promptAnswers = answers;\n return builder;\n },\n stdin(data: string) {\n stdinData = data;\n return builder;\n },\n\n async run(runInput?: string) {\n const stdout: unknown[] = [];\n const stderr: string[] = [];\n\n const runtime = buildRuntime(stdout, stderr, { envVars, promptAnswers, stdinData });\n const testProgram = program.runtime(runtime);\n\n const evalResult = await testProgram.eval(runInput ?? input ?? '', {});\n if (evalResult.error) {\n stderr.push(evalResult.error instanceof Error ? evalResult.error.message : String(evalResult.error));\n }\n return toTestResult(evalResult, stdout, stderr);\n },\n\n async repl(inputs: string[]) {\n const stdout: unknown[] = [];\n const stderr: string[] = [];\n\n const runtime = buildRuntime(stdout, stderr, {\n envVars,\n promptAnswers,\n readLine: createMockReadLine(inputs),\n });\n\n const testProgram = program.runtime(runtime);\n const results: Omit<TestCliResult, 'stdout' | 'stderr'>[] = [];\n\n for await (const r of testProgram.repl({ greeting: false, hint: false })) {\n results.push({\n command: r.command!,\n args: r.args,\n result: r.result,\n issues: r.argsResult?.issues as TestCliResult['issues'],\n });\n }\n\n return { results, stdout, stderr };\n },\n };\n\n return builder;\n}\n\nfunction toTestResult(evalResult: PadroneCommandResult, stdout: unknown[], stderr: string[]): TestCliResult {\n return {\n command: evalResult.command!,\n args: evalResult.args,\n result: evalResult.result,\n error: evalResult.error,\n issues: evalResult.argsResult?.issues as TestCliResult['issues'],\n stdout,\n stderr,\n };\n}\n\nfunction buildRuntime(\n stdout: unknown[],\n stderr: string[],\n opts: {\n envVars?: Record<string, string | undefined>;\n promptAnswers?: Record<string, unknown>;\n readLine?: (prompt: string) => Promise<string | null>;\n stdinData?: string;\n },\n): PadroneRuntime {\n const runtime: PadroneRuntime = {\n output: (...args: unknown[]) => stdout.push(...args),\n error: (text: string) => stderr.push(text),\n };\n\n if (opts.envVars) {\n runtime.env = () => opts.envVars!;\n }\n\n if (opts.promptAnswers) {\n runtime.interactive = 'supported';\n runtime.prompt = async (config: InteractivePromptConfig) => opts.promptAnswers![config.name];\n }\n\n if (opts.readLine) {\n runtime.readLine = opts.readLine;\n }\n\n if (opts.stdinData !== undefined) {\n runtime.stdin = {\n isTTY: false,\n async text() {\n return opts.stdinData!;\n },\n async *lines() {\n const lines = opts.stdinData!.split('\\n');\n // Remove trailing empty line from final newline (matches readline behavior)\n if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();\n for (const line of lines) {\n yield line;\n }\n },\n };\n } else {\n // No stdin data: simulate a TTY (no piped input) to avoid reading from process.stdin\n runtime.stdin = {\n isTTY: true,\n async text() {\n return '';\n },\n async *lines() {\n // no lines\n },\n };\n }\n\n return runtime;\n}\n\nfunction createMockReadLine(inputs: string[]): (prompt: string) => Promise<string | null> {\n let index = 0;\n return async (_prompt: string): Promise<string | null> => {\n if (index >= inputs.length) return null;\n return inputs[index++] ?? null;\n };\n}\n"],"mappings":";AAqHA,SAAgB,QAAQ,SAA0C;CAChE,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CAEJ,MAAM,UAA0B;EAC9B,KAAK,MAAc;AACjB,WAAQ;AACR,UAAO;;EAET,IAAI,MAAM;AACR,aAAU;AACV,UAAO;;EAET,OAAO,SAAS;AACd,mBAAgB;AAChB,UAAO;;EAET,MAAM,MAAc;AAClB,eAAY;AACZ,UAAO;;EAGT,MAAM,IAAI,UAAmB;GAC3B,MAAM,SAAoB,EAAE;GAC5B,MAAM,SAAmB,EAAE;GAE3B,MAAM,UAAU,aAAa,QAAQ,QAAQ;IAAE;IAAS;IAAe;IAAW,CAAC;GAGnF,MAAM,aAAa,MAFC,QAAQ,QAAQ,QAAQ,CAEP,KAAK,YAAY,SAAS,IAAI,EAAE,CAAC;AACtE,OAAI,WAAW,MACb,QAAO,KAAK,WAAW,iBAAiB,QAAQ,WAAW,MAAM,UAAU,OAAO,WAAW,MAAM,CAAC;AAEtG,UAAO,aAAa,YAAY,QAAQ,OAAO;;EAGjD,MAAM,KAAK,QAAkB;GAC3B,MAAM,SAAoB,EAAE;GAC5B,MAAM,SAAmB,EAAE;GAE3B,MAAM,UAAU,aAAa,QAAQ,QAAQ;IAC3C;IACA;IACA,UAAU,mBAAmB,OAAO;IACrC,CAAC;GAEF,MAAM,cAAc,QAAQ,QAAQ,QAAQ;GAC5C,MAAM,UAAsD,EAAE;AAE9D,cAAW,MAAM,KAAK,YAAY,KAAK;IAAE,UAAU;IAAO,MAAM;IAAO,CAAC,CACtE,SAAQ,KAAK;IACX,SAAS,EAAE;IACX,MAAM,EAAE;IACR,QAAQ,EAAE;IACV,QAAQ,EAAE,YAAY;IACvB,CAAC;AAGJ,UAAO;IAAE;IAAS;IAAQ;IAAQ;;EAErC;AAED,QAAO;;AAGT,SAAS,aAAa,YAAkC,QAAmB,QAAiC;AAC1G,QAAO;EACL,SAAS,WAAW;EACpB,MAAM,WAAW;EACjB,QAAQ,WAAW;EACnB,OAAO,WAAW;EAClB,QAAQ,WAAW,YAAY;EAC/B;EACA;EACD;;AAGH,SAAS,aACP,QACA,QACA,MAMgB;CAChB,MAAM,UAA0B;EAC9B,SAAS,GAAG,SAAoB,OAAO,KAAK,GAAG,KAAK;EACpD,QAAQ,SAAiB,OAAO,KAAK,KAAK;EAC3C;AAED,KAAI,KAAK,QACP,SAAQ,YAAY,KAAK;AAG3B,KAAI,KAAK,eAAe;AACtB,UAAQ,cAAc;AACtB,UAAQ,SAAS,OAAO,WAAoC,KAAK,cAAe,OAAO;;AAGzF,KAAI,KAAK,SACP,SAAQ,WAAW,KAAK;AAG1B,KAAI,KAAK,cAAc,KAAA,EACrB,SAAQ,QAAQ;EACd,OAAO;EACP,MAAM,OAAO;AACX,UAAO,KAAK;;EAEd,OAAO,QAAQ;GACb,MAAM,QAAQ,KAAK,UAAW,MAAM,KAAK;AAEzC,OAAI,MAAM,SAAS,KAAK,MAAM,MAAM,SAAS,OAAO,GAAI,OAAM,KAAK;AACnE,QAAK,MAAM,QAAQ,MACjB,OAAM;;EAGX;KAGD,SAAQ,QAAQ;EACd,OAAO;EACP,MAAM,OAAO;AACX,UAAO;;EAET,OAAO,QAAQ;EAGhB;AAGH,QAAO;;AAGT,SAAS,mBAAmB,QAA8D;CACxF,IAAI,QAAQ;AACZ,QAAO,OAAO,YAA4C;AACxD,MAAI,SAAS,OAAO,OAAQ,QAAO;AACnC,SAAO,OAAO,YAAY"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
//#region src/update-check.ts
|
|
1
|
+
//#region src/feature/update-check.ts
|
|
3
2
|
/**
|
|
4
3
|
* Parses an interval string like '1d', '12h', '30m', '1w' into milliseconds.
|
|
5
4
|
*/
|
|
@@ -43,9 +42,9 @@ function isNewerVersion(current, latest) {
|
|
|
43
42
|
/**
|
|
44
43
|
* Reads the update check cache file.
|
|
45
44
|
*/
|
|
46
|
-
function readCache(cachePath) {
|
|
45
|
+
async function readCache(cachePath) {
|
|
47
46
|
try {
|
|
48
|
-
const { existsSync, readFileSync } =
|
|
47
|
+
const { existsSync, readFileSync } = await import("node:fs");
|
|
49
48
|
if (!existsSync(cachePath)) return void 0;
|
|
50
49
|
const data = JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
51
50
|
if (typeof data.lastCheck === "number" && typeof data.latestVersion === "string") return data;
|
|
@@ -54,10 +53,10 @@ function readCache(cachePath) {
|
|
|
54
53
|
/**
|
|
55
54
|
* Writes the update check cache file.
|
|
56
55
|
*/
|
|
57
|
-
function writeCache(cachePath, data) {
|
|
56
|
+
async function writeCache(cachePath, data) {
|
|
58
57
|
try {
|
|
59
|
-
const { existsSync, mkdirSync, writeFileSync } =
|
|
60
|
-
const { dirname } =
|
|
58
|
+
const { existsSync, mkdirSync, writeFileSync } = await import("node:fs");
|
|
59
|
+
const { dirname } = await import("node:path");
|
|
61
60
|
const dir = dirname(cachePath);
|
|
62
61
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
63
62
|
writeFileSync(cachePath, JSON.stringify(data), "utf-8");
|
|
@@ -66,9 +65,9 @@ function writeCache(cachePath, data) {
|
|
|
66
65
|
/**
|
|
67
66
|
* Resolves the cache path, expanding `~` to the home directory.
|
|
68
67
|
*/
|
|
69
|
-
function resolveCachePath(cachePath) {
|
|
70
|
-
const { homedir } =
|
|
71
|
-
const { resolve } =
|
|
68
|
+
async function resolveCachePath(cachePath) {
|
|
69
|
+
const { homedir } = await import("node:os");
|
|
70
|
+
const { resolve } = await import("node:path");
|
|
72
71
|
if (cachePath.startsWith("~")) return cachePath.replace("~", homedir());
|
|
73
72
|
return resolve(cachePath);
|
|
74
73
|
}
|
|
@@ -99,27 +98,27 @@ function formatUpdateMessage(currentVersion, latestVersion, packageName) {
|
|
|
99
98
|
* This is designed to be non-blocking: the check starts immediately but the
|
|
100
99
|
* result is only consumed after command execution completes.
|
|
101
100
|
*/
|
|
102
|
-
function createUpdateChecker(programName, currentVersion, config, runtime) {
|
|
101
|
+
async function createUpdateChecker(programName, currentVersion, config, runtime) {
|
|
103
102
|
const packageName = config.packageName ?? programName;
|
|
104
103
|
const registry = config.registry ?? "npm";
|
|
105
104
|
const intervalMs = parseInterval(config.interval ?? "1d");
|
|
106
105
|
const disableEnvVar = config.disableEnvVar ?? `${programName.toUpperCase().replace(/-/g, "_")}_NO_UPDATE_CHECK`;
|
|
107
106
|
const defaultCachePath = `~/.config/${programName}-update-check.json`;
|
|
108
|
-
const cachePath = resolveCachePath(config.cache ?? defaultCachePath);
|
|
107
|
+
const cachePath = await resolveCachePath(config.cache ?? defaultCachePath);
|
|
109
108
|
const env = runtime.env();
|
|
110
109
|
if (env.CI || env.CONTINUOUS_INTEGRATION) return noop;
|
|
111
110
|
if (env[disableEnvVar]) return noop;
|
|
112
|
-
if (
|
|
113
|
-
const cached = readCache(cachePath);
|
|
111
|
+
if (runtime.terminal && !runtime.terminal.isTTY) return noop;
|
|
112
|
+
const cached = await readCache(cachePath);
|
|
114
113
|
if (cached && Date.now() - cached.lastCheck < intervalMs) {
|
|
115
114
|
if (isNewerVersion(currentVersion, cached.latestVersion)) return () => {
|
|
116
115
|
runtime.error(formatUpdateMessage(currentVersion, cached.latestVersion, packageName));
|
|
117
116
|
};
|
|
118
117
|
return noop;
|
|
119
118
|
}
|
|
120
|
-
const fetchPromise = fetchLatestVersion(packageName, registry).then((latestVersion) => {
|
|
119
|
+
const fetchPromise = fetchLatestVersion(packageName, registry).then(async (latestVersion) => {
|
|
121
120
|
if (latestVersion) {
|
|
122
|
-
writeCache(cachePath, {
|
|
121
|
+
await writeCache(cachePath, {
|
|
123
122
|
lastCheck: Date.now(),
|
|
124
123
|
latestVersion
|
|
125
124
|
});
|
|
@@ -143,4 +142,4 @@ function noop() {}
|
|
|
143
142
|
//#endregion
|
|
144
143
|
export { createUpdateChecker };
|
|
145
144
|
|
|
146
|
-
//# sourceMappingURL=update-check-
|
|
145
|
+
//# sourceMappingURL=update-check-CZ2VqjnV.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-check-CZ2VqjnV.mjs","names":[],"sources":["../src/feature/update-check.ts"],"sourcesContent":["import type { ResolvedPadroneRuntime } from '../core/runtime.ts';\n\n/**\n * Configuration for the update check feature.\n */\nexport type UpdateCheckConfig = {\n /**\n * The npm package name to check. Defaults to the program name.\n */\n packageName?: string;\n /**\n * Registry to check for updates.\n * - `'npm'` — checks the npm registry (default)\n * - A URL string — custom registry endpoint that returns JSON with a `version` or `dist-tags.latest` field\n */\n registry?: 'npm' | string;\n /**\n * How often to check for updates. Accepts shorthand like `'1d'`, `'12h'`, `'30m'`.\n * Defaults to `'1d'` (once per day).\n */\n interval?: string;\n /**\n * Path to the cache file for storing the last check timestamp and latest version.\n * Defaults to `~/.config/<programName>-update-check.json`.\n */\n cache?: string;\n /**\n * Environment variable name to disable update checks (e.g. `'MYAPP_NO_UPDATE_CHECK'`).\n * When set to a truthy value, update checks are skipped.\n * Defaults to `'<PROGRAM_NAME>_NO_UPDATE_CHECK'` (uppercased, hyphens to underscores).\n */\n disableEnvVar?: string;\n};\n\ntype CacheData = {\n lastCheck: number;\n latestVersion: string;\n};\n\n/**\n * Parses an interval string like '1d', '12h', '30m', '1w' into milliseconds.\n */\nexport function parseInterval(interval: string): number {\n const match = interval.match(/^(\\d+)\\s*(ms|s|m|h|d|w)$/);\n if (!match) return 86_400_000; // default 1d\n\n const value = parseInt(match[1]!, 10);\n const unit = match[2]!;\n\n switch (unit) {\n case 'ms':\n return value;\n case 's':\n return value * 1000;\n case 'm':\n return value * 60_000;\n case 'h':\n return value * 3_600_000;\n case 'd':\n return value * 86_400_000;\n case 'w':\n return value * 604_800_000;\n default:\n return 86_400_000;\n }\n}\n\n/**\n * Compares two semver version strings.\n * Returns true if `latest` is newer than `current`.\n */\nexport function isNewerVersion(current: string, latest: string): boolean {\n const parse = (v: string) => {\n const cleaned = v.replace(/^v/, '');\n const parts = cleaned.split('-');\n const nums = parts[0]!.split('.').map(Number);\n return { major: nums[0] ?? 0, minor: nums[1] ?? 0, patch: nums[2] ?? 0, prerelease: parts[1] };\n };\n\n const c = parse(current);\n const l = parse(latest);\n\n // Don't notify about pre-release versions unless user is already on a pre-release\n if (l.prerelease && !c.prerelease) return false;\n\n if (l.major !== c.major) return l.major > c.major;\n if (l.minor !== c.minor) return l.minor > c.minor;\n if (l.patch !== c.patch) return l.patch > c.patch;\n return false;\n}\n\n/**\n * Reads the update check cache file.\n */\nasync function readCache(cachePath: string): Promise<CacheData | undefined> {\n try {\n const { existsSync, readFileSync } = await import('node:fs');\n if (!existsSync(cachePath)) return undefined;\n const data = JSON.parse(readFileSync(cachePath, 'utf-8'));\n if (typeof data.lastCheck === 'number' && typeof data.latestVersion === 'string') {\n return data as CacheData;\n }\n } catch {\n // Ignore errors\n }\n return undefined;\n}\n\n/**\n * Writes the update check cache file.\n */\nasync function writeCache(cachePath: string, data: CacheData): Promise<void> {\n try {\n const { existsSync, mkdirSync, writeFileSync } = await import('node:fs');\n const { dirname } = await import('node:path');\n const dir = dirname(cachePath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(cachePath, JSON.stringify(data), 'utf-8');\n } catch {\n // Ignore errors — cache is best-effort\n }\n}\n\n/**\n * Resolves the cache path, expanding `~` to the home directory.\n */\nasync function resolveCachePath(cachePath: string): Promise<string> {\n const { homedir } = await import('node:os');\n const { resolve } = await import('node:path');\n if (cachePath.startsWith('~')) {\n return cachePath.replace('~', homedir());\n }\n return resolve(cachePath);\n}\n\n/**\n * Fetches the latest version from the registry.\n */\nasync function fetchLatestVersion(packageName: string, registry: string): Promise<string | undefined> {\n const url = registry === 'npm' ? `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest` : registry;\n\n try {\n const response = await fetch(url);\n if (!response.ok) return undefined;\n const data = (await response.json()) as Record<string, unknown>;\n\n // npm registry returns { version: \"x.y.z\" }\n if (typeof data.version === 'string') return data.version;\n\n // Custom endpoint may return { \"dist-tags\": { latest: \"x.y.z\" } }\n const distTags = data['dist-tags'] as Record<string, string> | undefined;\n if (distTags?.latest) return distTags.latest;\n } catch {\n // Network errors are expected (offline, firewall, etc.)\n }\n return undefined;\n}\n\n/**\n * Formats the update notification message.\n */\nexport function formatUpdateMessage(currentVersion: string, latestVersion: string, packageName: string): string {\n const updateCommand = `npm update -g ${packageName}`;\n return `\\n Update available: ${currentVersion} \\u2192 ${latestVersion}\\n Run \"${updateCommand}\" to update\\n`;\n}\n\n/**\n * Checks for updates in the background. Returns a function that, when called,\n * prints the update notification if a newer version was found.\n *\n * This is designed to be non-blocking: the check starts immediately but the\n * result is only consumed after command execution completes.\n */\nexport async function createUpdateChecker(\n programName: string,\n currentVersion: string,\n config: UpdateCheckConfig,\n runtime: ResolvedPadroneRuntime,\n): Promise<() => void> {\n const packageName = config.packageName ?? programName;\n const registry = config.registry ?? 'npm';\n const intervalMs = parseInterval(config.interval ?? '1d');\n const disableEnvVar = config.disableEnvVar ?? `${programName.toUpperCase().replace(/-/g, '_')}_NO_UPDATE_CHECK`;\n\n const defaultCachePath = `~/.config/${programName}-update-check.json`;\n const cachePath = await resolveCachePath(config.cache ?? defaultCachePath);\n\n // Check if disabled\n const env = runtime.env();\n if (env.CI || env.CONTINUOUS_INTEGRATION) return noop;\n if (env[disableEnvVar]) return noop;\n if (runtime.terminal && !runtime.terminal.isTTY) return noop;\n\n // Check cache — if we checked recently, use cached result\n const cached = await readCache(cachePath);\n if (cached && Date.now() - cached.lastCheck < intervalMs) {\n // Use cached version for display\n if (isNewerVersion(currentVersion, cached.latestVersion)) {\n return () => {\n runtime.error(formatUpdateMessage(currentVersion, cached.latestVersion, packageName));\n };\n }\n return noop;\n }\n\n // Start background fetch\n const fetchPromise = fetchLatestVersion(packageName, registry).then(async (latestVersion) => {\n if (latestVersion) {\n await writeCache(cachePath, { lastCheck: Date.now(), latestVersion });\n if (isNewerVersion(currentVersion, latestVersion)) {\n return latestVersion;\n }\n }\n return undefined;\n });\n\n // Return a function that blocks on the result (briefly — the fetch should be done by now)\n let resolved: string | undefined | null = null; // null = not yet resolved\n fetchPromise.then(\n (v) => {\n resolved = v;\n },\n () => {\n resolved = undefined;\n },\n );\n\n return () => {\n // If the fetch already resolved, use the result synchronously\n if (resolved !== null) {\n if (resolved) {\n runtime.error(formatUpdateMessage(currentVersion, resolved, packageName));\n }\n return;\n }\n\n // Otherwise, we can't block — just skip this time.\n // The cache will be written when the promise resolves, so next invocation will show the message.\n };\n}\n\nfunction noop() {}\n"],"mappings":"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"}
|
package/dist/zod.d.mts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { H as PadroneSchema } from "./index-BaU3X6dY.mjs";
|
|
2
|
+
import * as z from "zod/v4";
|
|
3
|
+
|
|
4
|
+
//#region src/schema/zod.d.ts
|
|
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
|
+
declare function zodAsyncStream<T = string>(itemSchema?: PadroneSchema<unknown, T>): z.ZodCustom<AsyncIterable<T>, AsyncIterable<T>>;
|
|
24
|
+
/**
|
|
25
|
+
* JSON codec for Zod schemas
|
|
26
|
+
* @see https://zod.dev/codecs?id=jsonschema
|
|
27
|
+
* Unlike the example in the docs, this codec also handles the case where the input is already an object
|
|
28
|
+
*/
|
|
29
|
+
declare const jsonCodec: <T extends z.ZodType>(schema: T) => z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodUnknown]>, T>;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { jsonCodec, zodAsyncStream };
|
|
32
|
+
//# sourceMappingURL=zod.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod.d.mts","names":[],"sources":["../src/schema/zod.ts"],"mappings":";;;;;;AAsBA;;;;;;;;;;;;;;;;iBAAgB,cAAA,YAAA,CAA2B,UAAA,GAAa,aAAA,UAAuB,CAAA,IAAE,CAAA,CAAA,SAAA,CAAA,aAAA,CAAA,CAAA,GAAA,aAAA,CAAA,CAAA;;;;;;cASpE,SAAA,aAAuB,CAAA,CAAE,OAAA,EAAS,MAAA,EAAQ,CAAA,KAAC,CAAA,CAAA,QAAA,CAAA,CAAA,CAAA,QAAA,WAAA,CAAA,CAAA,SAAA,EAAA,CAAA,CAAA,UAAA,IAAA,CAAA"}
|
package/dist/zod.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { t as asyncStream } from "./stream-DC4H8YTx.mjs";
|
|
2
|
+
import * as z from "zod/v4";
|
|
3
|
+
//#region src/schema/zod.ts
|
|
4
|
+
/**
|
|
5
|
+
* Creates a Zod schema for an async stream field, ready to use in `.arguments()`.
|
|
6
|
+
* Wraps `z.custom<AsyncIterable<T>>()` with the `asyncStream()` metadata automatically.
|
|
7
|
+
*
|
|
8
|
+
* @param itemSchema - Optional item schema for per-item validation.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { zodAsyncStream } from 'padrone/zod';
|
|
13
|
+
*
|
|
14
|
+
* // String lines
|
|
15
|
+
* z.object({ lines: zodAsyncStream() })
|
|
16
|
+
*
|
|
17
|
+
* // Typed items — each line JSON.parse'd and validated
|
|
18
|
+
* const recordSchema = z.object({ name: z.string(), age: z.number() });
|
|
19
|
+
* z.object({ records: zodAsyncStream(jsonCodec(recordSchema)) })
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
function zodAsyncStream(itemSchema) {
|
|
23
|
+
return z.custom().meta(asyncStream(itemSchema));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* JSON codec for Zod schemas
|
|
27
|
+
* @see https://zod.dev/codecs?id=jsonschema
|
|
28
|
+
* Unlike the example in the docs, this codec also handles the case where the input is already an object
|
|
29
|
+
*/
|
|
30
|
+
const jsonCodec = (schema) => z.codec(z.union([z.string(), z.unknown()]), schema, {
|
|
31
|
+
decode: (jsonString, ctx) => {
|
|
32
|
+
try {
|
|
33
|
+
if (typeof jsonString !== "string") return jsonString;
|
|
34
|
+
return JSON.parse(jsonString);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
ctx.issues.push({
|
|
37
|
+
code: "invalid_format",
|
|
38
|
+
format: "json",
|
|
39
|
+
input: typeof jsonString === "string" ? jsonString : JSON.stringify(jsonString),
|
|
40
|
+
message: err.message
|
|
41
|
+
});
|
|
42
|
+
return z.NEVER;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
encode: (value) => JSON.stringify(value)
|
|
46
|
+
});
|
|
47
|
+
//#endregion
|
|
48
|
+
export { jsonCodec, zodAsyncStream };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=zod.mjs.map
|
package/dist/zod.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod.mjs","names":[],"sources":["../src/schema/zod.ts"],"sourcesContent":["import * as z from 'zod/v4';\nimport type { PadroneSchema } from '../types/index.ts';\nimport { asyncStream } from '../util/stream.ts';\n\n/**\n * Creates a Zod schema for an async stream field, ready to use in `.arguments()`.\n * Wraps `z.custom<AsyncIterable<T>>()` with the `asyncStream()` metadata automatically.\n *\n * @param itemSchema - Optional item schema for per-item validation.\n *\n * @example\n * ```ts\n * import { zodAsyncStream } from 'padrone/zod';\n *\n * // String lines\n * z.object({ lines: zodAsyncStream() })\n *\n * // Typed items — each line JSON.parse'd and validated\n * const recordSchema = z.object({ name: z.string(), age: z.number() });\n * z.object({ records: zodAsyncStream(jsonCodec(recordSchema)) })\n * ```\n */\nexport function zodAsyncStream<T = string>(itemSchema?: PadroneSchema<unknown, T>) {\n return z.custom<AsyncIterable<T>>().meta(asyncStream(itemSchema));\n}\n\n/**\n * JSON codec for Zod schemas\n * @see https://zod.dev/codecs?id=jsonschema\n * Unlike the example in the docs, this codec also handles the case where the input is already an object\n */\nexport const jsonCodec = <T extends z.ZodType>(schema: T) =>\n z.codec(z.union([z.string(), z.unknown()]), schema, {\n decode: (jsonString, ctx) => {\n try {\n // HACK: in some cases the object is already deserialized, we just need to validate it\n if (typeof jsonString !== 'string') return jsonString as z.input<T>;\n return JSON.parse(jsonString) as z.input<T>;\n } catch (err: any) {\n ctx.issues.push({\n code: 'invalid_format',\n format: 'json',\n input: typeof jsonString === 'string' ? jsonString : JSON.stringify(jsonString),\n message: err.message,\n });\n return z.NEVER;\n }\n },\n encode: (value) => JSON.stringify(value),\n });\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,eAA2B,YAAwC;AACjF,QAAO,EAAE,QAA0B,CAAC,KAAK,YAAY,WAAW,CAAC;;;;;;;AAQnE,MAAa,aAAkC,WAC7C,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,EAAE,EAAE,SAAS,CAAC,CAAC,EAAE,QAAQ;CAClD,SAAS,YAAY,QAAQ;AAC3B,MAAI;AAEF,OAAI,OAAO,eAAe,SAAU,QAAO;AAC3C,UAAO,KAAK,MAAM,WAAW;WACtB,KAAU;AACjB,OAAI,OAAO,KAAK;IACd,MAAM;IACN,QAAQ;IACR,OAAO,OAAO,eAAe,WAAW,aAAa,KAAK,UAAU,WAAW;IAC/E,SAAS,IAAI;IACd,CAAC;AACF,UAAO,EAAE;;;CAGb,SAAS,UAAU,KAAK,UAAU,MAAM;CACzC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "padrone",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Create type-safe, interactive CLI apps with Zod schemas",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -81,18 +81,18 @@
|
|
|
81
81
|
},
|
|
82
82
|
"source": "./src/docs/index.ts"
|
|
83
83
|
},
|
|
84
|
-
"./
|
|
85
|
-
"padrone@dev": "./src/
|
|
84
|
+
"./zod": {
|
|
85
|
+
"padrone@dev": "./src/zod.ts",
|
|
86
86
|
"import": {
|
|
87
|
-
"types": "./dist/
|
|
88
|
-
"default": "./dist/
|
|
87
|
+
"types": "./dist/zod.d.mts",
|
|
88
|
+
"default": "./dist/zod.mjs"
|
|
89
89
|
},
|
|
90
|
-
"source": "./src/
|
|
90
|
+
"source": "./src/zod.ts"
|
|
91
91
|
},
|
|
92
92
|
"./package.json": "./package.json"
|
|
93
93
|
},
|
|
94
94
|
"scripts": {
|
|
95
|
-
"build": "tsdown src/index.ts src/test.ts src/
|
|
95
|
+
"build": "tsdown src/index.ts src/test.ts src/zod.ts src/codegen/index.ts src/docs/index.ts --dts --sourcemap",
|
|
96
96
|
"start": "bun --conditions=padrone@dev src/cli/index.ts",
|
|
97
97
|
"dev": "bun --conditions=padrone@dev --watch src/cli/index.ts",
|
|
98
98
|
"test": "bun test --conditions=padrone@dev",
|
|
@@ -105,18 +105,29 @@
|
|
|
105
105
|
"enquirer": "^2.4.1"
|
|
106
106
|
},
|
|
107
107
|
"devDependencies": {
|
|
108
|
-
"
|
|
109
|
-
"
|
|
108
|
+
"@types/react": "^19.2.14",
|
|
109
|
+
"ai": "^6.0.141",
|
|
110
|
+
"ink": "^6.8.0",
|
|
111
|
+
"react": "^19.2.4",
|
|
112
|
+
"tsdown": "^0.21.6",
|
|
110
113
|
"zod": "^4.3.6"
|
|
111
114
|
},
|
|
112
115
|
"peerDependencies": {
|
|
113
116
|
"ai": "5 || 6",
|
|
117
|
+
"ink": "^6.0.0",
|
|
118
|
+
"react": ">=18.0.0",
|
|
114
119
|
"zod": "^3.25.0 || ^4.0.0"
|
|
115
120
|
},
|
|
116
121
|
"peerDependenciesMeta": {
|
|
117
122
|
"ai": {
|
|
118
123
|
"optional": true
|
|
119
124
|
},
|
|
125
|
+
"ink": {
|
|
126
|
+
"optional": true
|
|
127
|
+
},
|
|
128
|
+
"react": {
|
|
129
|
+
"optional": true
|
|
130
|
+
},
|
|
120
131
|
"zod": {
|
|
121
132
|
"optional": true
|
|
122
133
|
}
|
package/src/cli/completions.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { detectShell, getCompletionInstallInstructions,
|
|
3
|
-
import type { PadroneActionContext } from '../types.ts';
|
|
1
|
+
import * as z from 'zod/v4';
|
|
2
|
+
import { detectShell, getCompletionInstallInstructions, setupCompletions } from '../feature/completion.ts';
|
|
3
|
+
import type { PadroneActionContext } from '../types/index.ts';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
appPath
|
|
7
|
-
for
|
|
8
|
-
setup
|
|
9
|
-
}
|
|
5
|
+
export const completionsSchema = z.object({
|
|
6
|
+
appPath: z.string().optional().describe('Path or name of the CLI program (defaults to padrone)'),
|
|
7
|
+
for: z.enum(['bash', 'zsh', 'fish', 'powershell']).optional().describe('Target shell (auto-detected if omitted)'),
|
|
8
|
+
setup: z.boolean().optional().default(false).describe('Write completions to shell config file'),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
type CompletionsArgs = z.infer<typeof completionsSchema>;
|
|
10
12
|
|
|
11
|
-
export function runCompletions(args: CompletionsArgs, _ctx: PadroneActionContext) {
|
|
13
|
+
export async function runCompletions(args: CompletionsArgs, _ctx: PadroneActionContext) {
|
|
14
|
+
const { basename } = await import('node:path');
|
|
12
15
|
const programName = args.appPath ? basename(args.appPath).replace(/\.[cm]?[jt]sx?$/, '') : 'padrone';
|
|
13
|
-
const shell = args.for ?? detectShell();
|
|
16
|
+
const shell = args.for ?? (await detectShell());
|
|
14
17
|
|
|
15
18
|
if (!shell) {
|
|
16
19
|
console.error('Could not detect shell. Use --for to specify one: bash, zsh, fish, powershell');
|
|
@@ -18,7 +21,7 @@ export function runCompletions(args: CompletionsArgs, _ctx: PadroneActionContext
|
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
if (args.setup) {
|
|
21
|
-
const result = setupCompletions(programName, shell);
|
|
24
|
+
const result = await setupCompletions(programName, shell);
|
|
22
25
|
const verb = result.updated ? 'Updated' : 'Added';
|
|
23
26
|
console.log(`${verb} ${programName} completions in ${result.file}`);
|
|
24
27
|
return;
|
package/src/cli/docs.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
2
|
+
import * as z from 'zod/v4';
|
|
3
|
+
import { isPadroneProgram } from '../core/commands.ts';
|
|
4
|
+
import { generateDocs } from '../docs/index.ts';
|
|
5
|
+
import type { PadroneActionContext } from '../types/index.ts';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
entry: string
|
|
8
|
-
output
|
|
9
|
-
format
|
|
10
|
-
includeHidden
|
|
11
|
-
dryRun
|
|
12
|
-
}
|
|
7
|
+
export const docsSchema = z.object({
|
|
8
|
+
entry: z.string().describe('Entry file that exports a Padrone program'),
|
|
9
|
+
output: z.string().optional().default('./docs/cli').describe('Output directory'),
|
|
10
|
+
format: z.enum(['markdown', 'html', 'man', 'json']).optional().default('markdown').describe('Output format'),
|
|
11
|
+
includeHidden: z.boolean().optional().default(false).describe('Include hidden commands and options'),
|
|
12
|
+
dryRun: z.boolean().optional().default(false).describe('Print what would be generated without writing'),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
type DocsArgs = z.infer<typeof docsSchema>;
|
|
13
16
|
|
|
14
17
|
export async function runDocs(args: DocsArgs, _ctx: PadroneActionContext) {
|
|
15
18
|
const entryPath = resolve(args.entry);
|
|
@@ -78,9 +81,3 @@ function findProgram(mod: Record<string, unknown>): any {
|
|
|
78
81
|
|
|
79
82
|
return null;
|
|
80
83
|
}
|
|
81
|
-
|
|
82
|
-
function isPadroneProgram(value: unknown): boolean {
|
|
83
|
-
if (!value || typeof value !== 'object') return false;
|
|
84
|
-
// A PadroneProgram has the internal command symbol (set by createPadrone)
|
|
85
|
-
return commandSymbol in value;
|
|
86
|
-
}
|