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
@@ -0,0 +1,19 @@
1
+ import type { StandardJSONSchemaV1, StandardSchemaV1 } from '@standard-schema/spec';
2
+
3
+ /**
4
+ * A schema that supports both validation (StandardSchemaV1) and JSON schema generation (StandardJSONSchemaV1).
5
+ * This is the type required for command arguments in Padrone.
6
+ */
7
+ export type PadroneSchema<Input = unknown, Output = Input> = StandardSchemaV1<Input, Output> & StandardJSONSchemaV1<Input, Output>;
8
+
9
+ /**
10
+ * A schema branded as async. When passed to `.arguments()`, `.configFile()`, or `.env()`,
11
+ * the command is automatically marked as async, causing `parse()` and `cli()` to return Promises.
12
+ *
13
+ * Use the `asyncSchema()` helper to brand an existing schema:
14
+ * ```ts
15
+ * import { asyncSchema } from 'padrone';
16
+ * const schema = asyncSchema(z.object({ name: z.string() }).check(async (v) => { ... }));
17
+ * ```
18
+ */
19
+ export type AsyncPadroneSchema<Input = unknown, Output = Input> = PadroneSchema<Input, Output> & { '~async': true };
@@ -0,0 +1,244 @@
1
+ // ── File resolution ─────────────────────────────────────────────────────
2
+
3
+ /** Returns ordered list of `.env` file names to load (no fs access). */
4
+ export function resolveEnvFiles(modes: string[] = [], local = true, base = true): string[] {
5
+ const files: string[] = [];
6
+ if (base) {
7
+ files.push('.env');
8
+ if (local) files.push('.env.local');
9
+ }
10
+ for (const mode of modes) {
11
+ files.push(`.env.${mode}`);
12
+ if (local) files.push(`.env.${mode}.local`);
13
+ }
14
+ return files;
15
+ }
16
+
17
+ // ── Parser ──────────────────────────────────────────────────────────────
18
+
19
+ /** Parse a `.env` file string into key-value pairs. */
20
+ export function parseEnvFile(content: string): Record<string, string> {
21
+ const result: Record<string, string> = {};
22
+ const lines = content.split('\n');
23
+ let i = 0;
24
+
25
+ while (i < lines.length) {
26
+ const line = lines[i]!.trim();
27
+ i++;
28
+
29
+ // Skip empty lines and comments
30
+ if (!line || line.startsWith('#')) continue;
31
+
32
+ // Strip optional `export ` prefix
33
+ const stripped = line.startsWith('export ') ? line.slice(7) : line;
34
+
35
+ const eqIndex = stripped.indexOf('=');
36
+ if (eqIndex === -1) continue;
37
+
38
+ const key = stripped.slice(0, eqIndex).trim();
39
+ let raw = stripped.slice(eqIndex + 1);
40
+
41
+ // Detect quoted values
42
+ const trimmedRaw = raw.trimStart();
43
+ const quote = trimmedRaw[0];
44
+
45
+ if (quote === '"' || quote === "'" || quote === '`') {
46
+ let value = trimmedRaw.slice(1);
47
+
48
+ // Check for closing quote on the same line
49
+ const closeIndex = findClosingQuote(value, quote);
50
+ if (closeIndex !== -1) {
51
+ value = value.slice(0, closeIndex);
52
+ } else {
53
+ // Multiline: accumulate until closing quote
54
+ while (i < lines.length) {
55
+ const nextLine = lines[i]!;
56
+ i++;
57
+ const ci = findClosingQuote(nextLine, quote);
58
+ if (ci !== -1) {
59
+ value += `\n${nextLine.slice(0, ci)}`;
60
+ break;
61
+ }
62
+ value += `\n${nextLine}`;
63
+ }
64
+ }
65
+
66
+ if (quote === '"') value = unescapeDoubleQuoted(value);
67
+ result[key] = value;
68
+ } else {
69
+ // Unquoted: strip inline comments, trim
70
+ const commentIndex = raw.indexOf(' #');
71
+ if (commentIndex !== -1) raw = raw.slice(0, commentIndex);
72
+ result[key] = raw.trim();
73
+ }
74
+ }
75
+
76
+ return result;
77
+ }
78
+
79
+ function findClosingQuote(s: string, quote: string): number {
80
+ let i = 0;
81
+ while (i < s.length) {
82
+ if (s[i] === '\\' && quote === '"') {
83
+ i += 2; // skip escaped char
84
+ continue;
85
+ }
86
+ if (s[i] === quote) return i;
87
+ i++;
88
+ }
89
+ return -1;
90
+ }
91
+
92
+ function unescapeDoubleQuoted(s: string): string {
93
+ return s.replace(/\\n/g, '\n').replace(/\\r/g, '\r').replace(/\\t/g, '\t').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
94
+ }
95
+
96
+ // ── Variable expansion ──────────────────────────────────────────────────
97
+
98
+ /**
99
+ * Expand `$VAR`, `${VAR}`, `${VAR:-default}`, `${VAR-default}` in a string.
100
+ * Escaped `\$` produces a literal `$`. Undefined variables resolve to `""`.
101
+ */
102
+ export function expandVariables(value: string, env: Record<string, string | undefined>): string {
103
+ let result = '';
104
+ let i = 0;
105
+
106
+ while (i < value.length) {
107
+ if (value[i] === '\\' && value[i + 1] === '$') {
108
+ result += '$';
109
+ i += 2;
110
+ continue;
111
+ }
112
+
113
+ if (value[i] === '$') {
114
+ i++;
115
+ if (i >= value.length) {
116
+ result += '$';
117
+ break;
118
+ }
119
+
120
+ if (value[i] === '{') {
121
+ // ${VAR}, ${VAR:-default}, ${VAR-default}
122
+ i++;
123
+ const closeIdx = value.indexOf('}', i);
124
+ if (closeIdx === -1) {
125
+ result += `\${${value.slice(i)}`;
126
+ break;
127
+ }
128
+
129
+ const expr = value.slice(i, closeIdx);
130
+ i = closeIdx + 1;
131
+
132
+ const colonDashIdx = expr.indexOf(':-');
133
+ const dashIdx = colonDashIdx === -1 ? expr.indexOf('-') : -1;
134
+
135
+ if (colonDashIdx !== -1) {
136
+ // ${VAR:-default} — use default if unset or empty
137
+ const varName = expr.slice(0, colonDashIdx);
138
+ const fallback = expr.slice(colonDashIdx + 2);
139
+ const val = env[varName];
140
+ result += val ? val : expandVariables(fallback, env);
141
+ } else if (dashIdx !== -1) {
142
+ // ${VAR-default} — use default only if unset
143
+ const varName = expr.slice(0, dashIdx);
144
+ const fallback = expr.slice(dashIdx + 1);
145
+ const val = env[varName];
146
+ result += val !== undefined ? val : expandVariables(fallback, env);
147
+ } else {
148
+ result += env[expr] ?? '';
149
+ }
150
+ } else {
151
+ // $VAR — collect word chars
152
+ let varName = '';
153
+ while (i < value.length && /[\w]/.test(value[i]!)) {
154
+ varName += value[i];
155
+ i++;
156
+ }
157
+ if (varName) {
158
+ result += env[varName] ?? '';
159
+ } else {
160
+ result += '$';
161
+ }
162
+ }
163
+ continue;
164
+ }
165
+
166
+ result += value[i];
167
+ i++;
168
+ }
169
+
170
+ return result;
171
+ }
172
+
173
+ // ── File loading ────────────────────────────────────────────────────────
174
+
175
+ export type LoadEnvFilesOptions = {
176
+ dir?: string;
177
+ modes?: string[];
178
+ local?: boolean;
179
+ override?: boolean;
180
+ base?: boolean;
181
+ };
182
+
183
+ /**
184
+ * Load and merge `.env` files, returning the combined key-value map.
185
+ * Variable expansion uses the merged file values + process env as lookup.
186
+ *
187
+ * Returns synchronously when `node:fs`/`node:path` are already cached (typical),
188
+ * or a Promise on the very first call.
189
+ */
190
+ export function loadEnvFiles(
191
+ options: LoadEnvFilesOptions,
192
+ processEnv: Record<string, string | undefined>,
193
+ ): Record<string, string> | Promise<Record<string, string>> {
194
+ if (typeof process === 'undefined') return {};
195
+
196
+ try {
197
+ if (_fs && _path) return loadEnvFilesSync(_fs, _path, options, processEnv);
198
+ return initNodeModules()
199
+ .then(() => loadEnvFilesSync(_fs!, _path!, options, processEnv))
200
+ .catch(() => ({}) as Record<string, string>);
201
+ } catch {
202
+ return {};
203
+ }
204
+ }
205
+
206
+ // ── Internals ───────────────────────────────────────────────────────────
207
+
208
+ let _fs: typeof import('node:fs') | undefined;
209
+ let _path: typeof import('node:path') | undefined;
210
+
211
+ async function initNodeModules(): Promise<void> {
212
+ if (_fs && _path) return;
213
+ _fs = await import('node:fs');
214
+ _path = await import('node:path');
215
+ }
216
+
217
+ if (typeof process !== 'undefined') initNodeModules();
218
+
219
+ function loadEnvFilesSync(
220
+ fs: typeof import('node:fs'),
221
+ path: typeof import('node:path'),
222
+ options: LoadEnvFilesOptions,
223
+ processEnv: Record<string, string | undefined>,
224
+ ): Record<string, string> {
225
+ const dir = options.dir ?? process.cwd();
226
+ const fileNames = resolveEnvFiles(options.modes, options.local ?? true, options.base ?? true);
227
+ const merged: Record<string, string> = {};
228
+
229
+ for (const name of fileNames) {
230
+ const filePath = path.resolve(dir, name);
231
+ if (!fs.existsSync(filePath)) continue;
232
+ const content = fs.readFileSync(filePath, 'utf-8');
233
+ const parsed = parseEnvFile(content);
234
+ Object.assign(merged, parsed);
235
+ }
236
+
237
+ // Expand variables: file values + processEnv as lookup (processEnv wins in lookup unless override)
238
+ const lookup = options.override ? { ...processEnv, ...merged } : { ...merged, ...processEnv };
239
+ for (const key of Object.keys(merged)) {
240
+ merged[key] = expandVariables(merged[key]!, lookup);
241
+ }
242
+
243
+ return merged;
244
+ }
@@ -1,10 +1,19 @@
1
+ /**
2
+ * Convert a camelCase string to kebab-case.
3
+ * Returns null if the string has no uppercase letters (no conversion needed).
4
+ */
5
+ export function camelToKebab(str: string): string | null {
6
+ if (!/[A-Z]/.test(str)) return null;
7
+ return str.replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`);
8
+ }
9
+
1
10
  export type ShellType = 'bash' | 'zsh' | 'fish' | 'powershell';
2
11
 
3
12
  /**
4
13
  * Detects the current shell from environment variables and process info.
5
14
  * @returns The detected shell type, or undefined if unknown
6
15
  */
7
- export function detectShell(): ShellType | undefined {
16
+ export async function detectShell(): Promise<ShellType | undefined> {
8
17
  if (typeof process === 'undefined') return undefined;
9
18
 
10
19
  // Method 1: Check SHELL environment variable (most common)
@@ -22,7 +31,7 @@ export function detectShell(): ShellType | undefined {
22
31
  try {
23
32
  const ppid = process.ppid;
24
33
  if (ppid) {
25
- const { execSync } = require('node:child_process') as typeof import('node:child_process');
34
+ const { execSync } = (await import('node:child_process')) as typeof import('node:child_process');
26
35
  const processName = execSync(`ps -p ${ppid} -o comm=`, {
27
36
  encoding: 'utf-8',
28
37
  stdio: ['pipe', 'pipe', 'ignore'],
@@ -39,9 +48,9 @@ export function detectShell(): ShellType | undefined {
39
48
  return undefined;
40
49
  }
41
50
 
42
- export function getRcFile(shell: ShellType, home?: string): string | null {
43
- const { homedir } = require('node:os') as typeof import('node:os');
44
- const { join } = require('node:path') as typeof import('node:path');
51
+ export async function getRcFile(shell: ShellType, home?: string): Promise<string | null> {
52
+ const { homedir } = await import('node:os');
53
+ const { join } = await import('node:path');
45
54
  const h = home ?? homedir();
46
55
  switch (shell) {
47
56
  case 'bash':
@@ -51,7 +60,10 @@ export function getRcFile(shell: ShellType, home?: string): string | null {
51
60
  case 'fish':
52
61
  return join(h, '.config', 'fish', 'config.fish');
53
62
  case 'powershell':
54
- return process.env.PROFILE || join(h, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1');
63
+ return (
64
+ (typeof process !== 'undefined' ? process.env.PROFILE : undefined) ||
65
+ join(h, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1')
66
+ );
55
67
  default:
56
68
  return null;
57
69
  }
@@ -65,9 +77,14 @@ export function escapeRegExp(str: string): string {
65
77
  * Writes a snippet to a shell config file using begin/end markers for idempotency.
66
78
  * If a block with the same begin marker exists, it is replaced. Otherwise the snippet is appended.
67
79
  */
68
- export function writeToRcFile(rcFile: string, snippet: string, beginMarker: string, endMarker: string): { file: string; updated: boolean } {
69
- const { existsSync, mkdirSync, readFileSync, writeFileSync } = require('node:fs') as typeof import('node:fs');
70
- const { dirname } = require('node:path') as typeof import('node:path');
80
+ export async function writeToRcFile(
81
+ rcFile: string,
82
+ snippet: string,
83
+ beginMarker: string,
84
+ endMarker: string,
85
+ ): Promise<{ file: string; updated: boolean }> {
86
+ const { existsSync, mkdirSync, readFileSync, writeFileSync } = await import('node:fs');
87
+ const { dirname } = await import('node:path');
71
88
  const existing = existsSync(rcFile) ? readFileSync(rcFile, 'utf-8') : '';
72
89
 
73
90
  if (existing.includes(beginMarker)) {
@@ -0,0 +1,101 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { PadroneSchema } from '../types/index.ts';
3
+
4
+ export interface AsyncStreamMeta {
5
+ [x: string]: unknown;
6
+ readonly asyncStream: number;
7
+ readonly itemSchema?: StandardSchemaV1;
8
+ }
9
+
10
+ let asyncStreamIdCounter = 1;
11
+ export const asyncStreamRegistry = new Map<number, AsyncStreamMeta>();
12
+
13
+ /**
14
+ * Returns metadata to mark a schema field as an async stream via `.meta()`.
15
+ *
16
+ * When used with `stdin`, padrone pipes stdin data as an `AsyncIterable` instead of
17
+ * buffering it. Each line is validated against the item schema (if provided) as it arrives.
18
+ *
19
+ * @param itemSchema - Optional item schema for per-item validation.
20
+ * Non-string schemas cause each stdin line to be `JSON.parse`'d before validation.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { asyncStream } from 'padrone';
25
+ *
26
+ * // String lines
27
+ * z.object({ lines: z.custom<AsyncIterable<string>>().meta(asyncStream()) })
28
+ *
29
+ * // Typed items — each line JSON.parse'd and validated
30
+ * z.object({ records: z.custom<AsyncIterable<{ name: string }>>().meta(asyncStream(recordSchema)) })
31
+ * ```
32
+ */
33
+ export function asyncStream<T = string>(itemSchema?: PadroneSchema<T>): AsyncStreamMeta {
34
+ const id = asyncStreamIdCounter++;
35
+ const meta: AsyncStreamMeta = itemSchema ? { asyncStream: id, itemSchema } : { asyncStream: id };
36
+ asyncStreamRegistry.set(id, meta);
37
+ return meta;
38
+ }
39
+
40
+ /** Stdin interface matching PadroneRuntime.stdin */
41
+ interface StdinSource {
42
+ isTTY?: boolean;
43
+ text(): Promise<string>;
44
+ lines(): AsyncIterable<string>;
45
+ }
46
+
47
+ /**
48
+ * Creates an `AsyncIterable` from a stdin source, optionally validating each item.
49
+ * When no stdin is available (TTY / undefined), yields nothing.
50
+ *
51
+ * - No item schema: yields raw string lines
52
+ * - With item schema: `JSON.parse`s each line, validates, then yields
53
+ */
54
+ export function createStdinStream(stdin: StdinSource | undefined, itemSchema?: StandardSchemaV1): AsyncIterable<unknown> {
55
+ if (!stdin) return emptyAsyncIterable;
56
+
57
+ if (!itemSchema) return stdin.lines();
58
+
59
+ return {
60
+ async *[Symbol.asyncIterator]() {
61
+ for await (const line of stdin.lines()) {
62
+ const result = itemSchema['~standard'].validate(line);
63
+ const resolved = result instanceof Promise ? await result : result;
64
+ if ('issues' in resolved && resolved.issues) {
65
+ throw new Error(`Stream item validation failed: ${resolved.issues.map((i) => i.message).join(', ')}`);
66
+ }
67
+ yield (resolved as { value: unknown }).value;
68
+ }
69
+ },
70
+ };
71
+ }
72
+
73
+ const emptyAsyncIterable: AsyncIterable<never> = {
74
+ async *[Symbol.asyncIterator]() {},
75
+ };
76
+
77
+ const textEncoder = /* @__PURE__ */ new TextEncoder();
78
+ const textDecoder = /* @__PURE__ */ new TextDecoder();
79
+
80
+ /** Concatenate multiple `Uint8Array` chunks into a single array. */
81
+ export function concatBytes(chunks: Uint8Array[]): Uint8Array {
82
+ if (chunks.length === 1) return chunks[0]!;
83
+ let totalLength = 0;
84
+ for (const chunk of chunks) totalLength += chunk.byteLength;
85
+ const result = new Uint8Array(totalLength);
86
+ let offset = 0;
87
+ for (const chunk of chunks) {
88
+ result.set(chunk, offset);
89
+ offset += chunk.byteLength;
90
+ }
91
+ return result;
92
+ }
93
+
94
+ /** Read an async iterable of chunks into a UTF-8 string. */
95
+ export async function readStreamAsText(stream: AsyncIterable<Uint8Array | string>): Promise<string> {
96
+ const chunks: Uint8Array[] = [];
97
+ for await (const chunk of stream) {
98
+ chunks.push(typeof chunk === 'string' ? textEncoder.encode(chunk) : chunk);
99
+ }
100
+ return textDecoder.decode(concatBytes(chunks));
101
+ }
@@ -1,5 +1,11 @@
1
+ import type {
2
+ AnyPadroneCommand,
3
+ AnyPadroneProgram,
4
+ PadroneCommand,
5
+ PadroneContextInterceptor,
6
+ PadroneInterceptorFn,
7
+ } from '../types/index.ts';
1
8
  import type { PickCommandByName, PossibleCommands } from './type-utils.ts';
2
- import type { AnyPadroneCommand, AnyPadroneProgram, PadroneCommand, PadroneSchema } from './types.ts';
3
9
 
4
10
  /**
5
11
  * Extracts the input type of the arguments schema from a command.
@@ -20,43 +26,44 @@ export type InferArgsInput<T extends AnyPadroneCommand> = T['~types']['argsInput
20
26
  export type InferArgsOutput<T extends AnyPadroneCommand> = T['~types']['argsOutput'];
21
27
 
22
28
  /**
23
- * Extracts the input type of the config schema from a command.
29
+ * Extracts the user-defined context type from a command (excludes interceptor-provided context).
24
30
  * @example
25
31
  * ```ts
26
- * type Config = InferConfigInput<typeof myCommand>;
32
+ * type Ctx = InferContext<typeof myCommand>;
27
33
  * ```
28
34
  */
29
- export type InferConfigInput<T extends AnyPadroneCommand> = T['configSchema'] extends PadroneSchema<infer I, any> ? I : never;
35
+ export type InferContext<T extends AnyPadroneCommand> = T['~types']['context'];
30
36
 
31
37
  /**
32
- * Extracts the output type of the config schema from a command.
33
- * This is the type after transformation, which should match the arguments shape.
38
+ * Extracts the interceptor-provided context type from a command.
34
39
  * @example
35
40
  * ```ts
36
- * type ConfigOutput = InferConfigOutput<typeof myCommand>;
41
+ * type Provided = InferContextProvided<typeof myCommand>;
37
42
  * ```
38
43
  */
39
- export type InferConfigOutput<T extends AnyPadroneCommand> = T['configSchema'] extends PadroneSchema<any, infer O> ? O : never;
44
+ export type InferContextProvided<T extends AnyPadroneCommand> = T['~types']['contextProvided'];
40
45
 
41
46
  /**
42
- * Extracts the input type of the env schema from a command.
43
- * This represents the raw environment variables shape (e.g., { PORT: string }).
47
+ * Extracts the context type that a context-providing interceptor injects.
44
48
  * @example
45
49
  * ```ts
46
- * type EnvInput = InferEnvInput<typeof myCommand>;
50
+ * type AuthCtx = InferInterceptorContext<typeof withAuth>;
47
51
  * ```
48
52
  */
49
- export type InferEnvInput<T extends AnyPadroneCommand> = T['envSchema'] extends PadroneSchema<infer I, any> ? I : never;
53
+ export type InferInterceptorContext<T extends PadroneContextInterceptor<any>> = T['~context'];
50
54
 
51
55
  /**
52
- * Extracts the output type of the env schema from a command.
53
- * This is the type after transformation, which should match the arguments shape.
56
+ * Extracts the required context type from an interceptor with `.requires()`.
54
57
  * @example
55
58
  * ```ts
56
- * type EnvOutput = InferEnvOutput<typeof myCommand>;
59
+ * type Requires = InferInterceptorRequires<typeof withAuth>;
57
60
  * ```
58
61
  */
59
- export type InferEnvOutput<T extends AnyPadroneCommand> = T['envSchema'] extends PadroneSchema<any, infer O> ? O : never;
62
+ export type InferInterceptorRequires<T extends PadroneInterceptorFn & { '~contextRequires': any }> = T['~contextRequires'] extends (
63
+ ctx: infer R,
64
+ ) => void
65
+ ? R
66
+ : unknown;
60
67
 
61
68
  /**
62
69
  * Gets a command type by its path from a program or command tree.