padrone 1.5.0 → 1.7.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 +44 -0
  2. package/README.md +15 -11
  3. package/dist/{args-D5PNDyNu.mjs → args-Cnq0nwSM.mjs} +91 -41
  4. package/dist/args-Cnq0nwSM.mjs.map +1 -0
  5. package/dist/codegen/index.mjs +4 -4
  6. package/dist/codegen/index.mjs.map +1 -1
  7. package/dist/commands-B_gufyR9.mjs +514 -0
  8. package/dist/commands-B_gufyR9.mjs.map +1 -0
  9. package/dist/{completion.mjs → completion-BEuflbDO.mjs} +12 -82
  10. package/dist/completion-BEuflbDO.mjs.map +1 -0
  11. package/dist/docs/index.d.mts +4 -4
  12. package/dist/docs/index.d.mts.map +1 -1
  13. package/dist/docs/index.mjs +10 -12
  14. package/dist/docs/index.mjs.map +1 -1
  15. package/dist/{errors-BiVrBgi6.mjs → errors-DA4KzK1M.mjs} +26 -3
  16. package/dist/errors-DA4KzK1M.mjs.map +1 -0
  17. package/dist/{formatter-DtHzbP22.d.mts → formatter-DrvhDMrq.d.mts} +3 -3
  18. package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
  19. package/dist/{help-bbmu9-qd.mjs → help-BtxLgrF_.mjs} +190 -43
  20. package/dist/help-BtxLgrF_.mjs.map +1 -0
  21. package/dist/{types-Ch8Mk6Qb.d.mts → index-D6-7dz0l.d.mts} +634 -745
  22. package/dist/index-D6-7dz0l.d.mts.map +1 -0
  23. package/dist/index.d.mts +869 -36
  24. package/dist/index.d.mts.map +1 -1
  25. package/dist/index.mjs +3884 -1699
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/{mcp-mLWIdUIu.mjs → mcp-6-Jw4Bpq.mjs} +13 -15
  28. package/dist/mcp-6-Jw4Bpq.mjs.map +1 -0
  29. package/dist/{serve-B0u43DK7.mjs → serve-YVTPzBCl.mjs} +12 -14
  30. package/dist/serve-YVTPzBCl.mjs.map +1 -0
  31. package/dist/{stream-BcC146Ud.mjs → stream-DC4H8YTx.mjs} +24 -3
  32. package/dist/stream-DC4H8YTx.mjs.map +1 -0
  33. package/dist/test.d.mts +5 -8
  34. package/dist/test.d.mts.map +1 -1
  35. package/dist/test.mjs +2 -13
  36. package/dist/test.mjs.map +1 -1
  37. package/dist/{update-check-CFX1FV3v.mjs → update-check-CZ2VqjnV.mjs} +16 -17
  38. package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
  39. package/dist/zod.d.mts +2 -2
  40. package/dist/zod.d.mts.map +1 -1
  41. package/dist/zod.mjs +2 -2
  42. package/dist/zod.mjs.map +1 -1
  43. package/package.json +15 -12
  44. package/src/cli/completions.ts +14 -11
  45. package/src/cli/docs.ts +13 -10
  46. package/src/cli/doctor.ts +22 -18
  47. package/src/cli/index.ts +28 -82
  48. package/src/cli/init.ts +10 -7
  49. package/src/cli/link.ts +20 -16
  50. package/src/cli/wrap.ts +14 -11
  51. package/src/codegen/schema-to-code.ts +2 -2
  52. package/src/{args.ts → core/args.ts} +32 -225
  53. package/src/core/commands.ts +373 -0
  54. package/src/core/create.ts +301 -0
  55. package/src/core/default-runtime.ts +239 -0
  56. package/src/{errors.ts → core/errors.ts} +22 -0
  57. package/src/core/exec.ts +259 -0
  58. package/src/core/interceptors.ts +302 -0
  59. package/src/{parse.ts → core/parse.ts} +36 -89
  60. package/src/core/program-methods.ts +301 -0
  61. package/src/core/results.ts +229 -0
  62. package/src/core/runtime.ts +246 -0
  63. package/src/core/validate.ts +247 -0
  64. package/src/docs/index.ts +12 -13
  65. package/src/extension/auto-output.ts +146 -0
  66. package/src/extension/color.ts +38 -0
  67. package/src/extension/completion.ts +49 -0
  68. package/src/extension/config.ts +262 -0
  69. package/src/extension/env.ts +101 -0
  70. package/src/extension/help.ts +192 -0
  71. package/src/extension/index.ts +44 -0
  72. package/src/extension/ink.ts +93 -0
  73. package/src/extension/interactive.ts +106 -0
  74. package/src/extension/logger.ts +262 -0
  75. package/src/extension/man.ts +51 -0
  76. package/src/extension/mcp.ts +52 -0
  77. package/src/extension/progress-renderer.ts +338 -0
  78. package/src/extension/progress.ts +299 -0
  79. package/src/extension/repl.ts +94 -0
  80. package/src/extension/serve.ts +48 -0
  81. package/src/extension/signal.ts +87 -0
  82. package/src/extension/stdin.ts +62 -0
  83. package/src/extension/suggestions.ts +114 -0
  84. package/src/extension/timing.ts +81 -0
  85. package/src/extension/tracing.ts +175 -0
  86. package/src/extension/update-check.ts +77 -0
  87. package/src/extension/utils.ts +51 -0
  88. package/src/extension/version.ts +63 -0
  89. package/src/{completion.ts → feature/completion.ts} +12 -12
  90. package/src/{interactive.ts → feature/interactive.ts} +4 -4
  91. package/src/{mcp.ts → feature/mcp.ts} +12 -15
  92. package/src/{repl-loop.ts → feature/repl-loop.ts} +10 -13
  93. package/src/{serve.ts → feature/serve.ts} +11 -15
  94. package/src/feature/test.ts +262 -0
  95. package/src/{update-check.ts → feature/update-check.ts} +16 -16
  96. package/src/{wrap.ts → feature/wrap.ts} +10 -8
  97. package/src/index.ts +115 -30
  98. package/src/{formatter.ts → output/formatter.ts} +124 -176
  99. package/src/{help.ts → output/help.ts} +22 -8
  100. package/src/output/output-indicator.ts +87 -0
  101. package/src/output/primitives.ts +335 -0
  102. package/src/output/styling.ts +221 -0
  103. package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
  104. package/src/schema/zod.ts +50 -0
  105. package/src/test.ts +2 -276
  106. package/src/types/args-meta.ts +151 -0
  107. package/src/types/builder.ts +718 -0
  108. package/src/types/command.ts +157 -0
  109. package/src/types/index.ts +60 -0
  110. package/src/types/interceptor.ts +296 -0
  111. package/src/types/preferences.ts +83 -0
  112. package/src/types/result.ts +71 -0
  113. package/src/types/schema.ts +19 -0
  114. package/src/util/dotenv.ts +244 -0
  115. package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
  116. package/src/{stream.ts → util/stream.ts} +27 -1
  117. package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
  118. package/src/{type-utils.ts → util/type-utils.ts} +71 -33
  119. package/src/util/utils.ts +51 -0
  120. package/src/zod.ts +1 -50
  121. package/dist/args-D5PNDyNu.mjs.map +0 -1
  122. package/dist/chunk-CjcI7cDX.mjs +0 -15
  123. package/dist/command-utils-B1D-HqCd.mjs +0 -1117
  124. package/dist/command-utils-B1D-HqCd.mjs.map +0 -1
  125. package/dist/completion.d.mts +0 -64
  126. package/dist/completion.d.mts.map +0 -1
  127. package/dist/completion.mjs.map +0 -1
  128. package/dist/errors-BiVrBgi6.mjs.map +0 -1
  129. package/dist/formatter-DtHzbP22.d.mts.map +0 -1
  130. package/dist/help-bbmu9-qd.mjs.map +0 -1
  131. package/dist/mcp-mLWIdUIu.mjs.map +0 -1
  132. package/dist/serve-B0u43DK7.mjs.map +0 -1
  133. package/dist/stream-BcC146Ud.mjs.map +0 -1
  134. package/dist/types-Ch8Mk6Qb.d.mts.map +0 -1
  135. package/dist/update-check-CFX1FV3v.mjs.map +0 -1
  136. package/src/command-utils.ts +0 -882
  137. package/src/create.ts +0 -1829
  138. package/src/runtime.ts +0 -497
  139. package/src/types.ts +0 -1291
  140. package/src/utils.ts +0 -140
  141. /package/src/{colorizer.ts → output/colorizer.ts} +0 -0
@@ -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)) {
@@ -1,5 +1,5 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
- import type { PadroneSchema } from './types.ts';
2
+ import type { PadroneSchema } from '../types/index.ts';
3
3
 
4
4
  export interface AsyncStreamMeta {
5
5
  [x: string]: unknown;
@@ -73,3 +73,29 @@ export function createStdinStream(stdin: StdinSource | undefined, itemSchema?: S
73
73
  const emptyAsyncIterable: AsyncIterable<never> = {
74
74
  async *[Symbol.asyncIterator]() {},
75
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.
@@ -1,4 +1,6 @@
1
- import type { AnyPadroneCommand, PadroneCommand } from './types.ts';
1
+ import type { PadroneBuilder, PadroneProgram } from '../types/builder.ts';
2
+ import type { AnyPadroneCommand, PadroneCommand } from '../types/index.ts';
3
+ import type { PadroneSchema } from '../types/schema.ts';
2
4
 
3
5
  /**
4
6
  * Use this type instead of `any` when you intend to fix it later
@@ -137,40 +139,76 @@ type GetCommandPathsAndAliases<TCommand extends AnyPadroneCommand> = TCommand['~
137
139
  /**
138
140
  * Find a direct child command in a tuple by name.
139
141
  * Unlike PickCommandByName, this does NOT flatten — it only checks direct children by their `name` field.
142
+ * Uses indexed access (O(1) depth) instead of recursive tuple walking.
140
143
  */
141
- export type FindDirectChild<TCommands extends AnyPadroneCommand[], TName extends string> = TCommands extends [
142
- infer First extends AnyPadroneCommand,
143
- ...infer Rest extends AnyPadroneCommand[],
144
- ]
145
- ? First['~types']['name'] extends TName
146
- ? First
147
- : FindDirectChild<Rest, TName>
148
- : never;
144
+ export type FindDirectChild<TCommands extends AnyPadroneCommand[], TName extends string> = Extract<
145
+ TCommands[number],
146
+ { '~types': { name: TName } }
147
+ >;
149
148
 
150
149
  /**
151
150
  * Replace a command in a tuple by name, or append if not found.
152
151
  * Used by `.command()` override semantics: re-registering a name replaces that entry.
152
+ * Uses mapped type (O(1) depth) instead of recursive tuple walking.
153
153
  */
154
154
  export type ReplaceOrAppendCommand<TCommands extends [...AnyPadroneCommand[]], TName extends string, TNew extends AnyPadroneCommand> =
155
155
  HasDirectChild<TCommands, TName> extends true ? ReplaceInTuple<TCommands, TName, TNew> : [...TCommands, TNew];
156
156
 
157
- type HasDirectChild<TCommands extends AnyPadroneCommand[], TName extends string> = TCommands extends [
158
- infer First extends AnyPadroneCommand,
159
- ...infer Rest extends AnyPadroneCommand[],
160
- ]
161
- ? First['~types']['name'] extends TName
162
- ? true
163
- : HasDirectChild<Rest, TName>
157
+ type HasDirectChild<TCommands extends AnyPadroneCommand[], TName extends string> = TName extends TCommands[number]['~types']['name']
158
+ ? true
164
159
  : false;
165
160
 
166
- type ReplaceInTuple<TCommands extends AnyPadroneCommand[], TName extends string, TNew extends AnyPadroneCommand> = TCommands extends [
167
- infer First extends AnyPadroneCommand,
168
- ...infer Rest extends AnyPadroneCommand[],
169
- ]
170
- ? First['~types']['name'] extends TName
171
- ? [TNew, ...Rest]
172
- : [First, ...ReplaceInTuple<Rest, TName, TNew>]
173
- : [];
161
+ type ReplaceInTuple<TCommands extends AnyPadroneCommand[], TName extends string, TNew extends AnyPadroneCommand> = {
162
+ [K in keyof TCommands]: TCommands[K] extends AnyPadroneCommand
163
+ ? TCommands[K]['~types']['name'] extends TName
164
+ ? TNew
165
+ : TCommands[K]
166
+ : TCommands[K];
167
+ };
168
+
169
+ /**
170
+ * Utility type for extensions that add a command to a builder/program.
171
+ * Replaces the boilerplate `With*<T>` pattern used across all extension files.
172
+ */
173
+ export type WithCommand<T, TName extends string, TCmd extends AnyPadroneCommand> = T extends {
174
+ '~types': {
175
+ programName: infer PN extends string;
176
+ name: infer N extends string;
177
+ parentName: infer PaN extends string;
178
+ argsSchema: infer A extends PadroneSchema;
179
+ result: infer R;
180
+ commands: infer C extends [...AnyPadroneCommand[]];
181
+ async: infer AS extends boolean;
182
+ context: infer CTX;
183
+ contextProvided: infer CTXP;
184
+ };
185
+ }
186
+ ? T extends { run: any }
187
+ ? PadroneProgram<PN, N, PaN, A, R, ReplaceOrAppendCommand<C, TName, TCmd>, any, AS, CTX, CTXP>
188
+ : PadroneBuilder<PN, N, PaN, A, R, ReplaceOrAppendCommand<C, TName, TCmd>, any, AS, CTX, CTXP>
189
+ : T;
190
+
191
+ /**
192
+ * Utility type for extensions that register a context-providing interceptor.
193
+ * Extends `TContextProvided` with `TProvides` while preserving all other builder/program type params.
194
+ */
195
+ export type WithInterceptor<T, TProvides> = T extends {
196
+ '~types': {
197
+ programName: infer PN extends string;
198
+ name: infer N extends string;
199
+ parentName: infer PaN extends string;
200
+ argsSchema: infer A extends PadroneSchema;
201
+ result: infer R;
202
+ commands: infer C extends [...AnyPadroneCommand[]];
203
+ async: infer AS extends boolean;
204
+ context: infer CTX;
205
+ contextProvided: infer CTXP;
206
+ };
207
+ }
208
+ ? T extends { run: any }
209
+ ? PadroneProgram<PN, N, PaN, A, R, C, any, AS, CTX, CTXP & TProvides>
210
+ : PadroneBuilder<PN, N, PaN, A, R, C, any, AS, CTX, CTXP & TProvides>
211
+ : T;
174
212
 
175
213
  export type PickCommandByName<
176
214
  TCommands extends AnyPadroneCommand[],
@@ -187,13 +225,13 @@ export type PickCommandByName<
187
225
 
188
226
  export type FlattenCommands<TCommands extends AnyPadroneCommand[]> = TCommands extends []
189
227
  ? never
190
- : TCommands extends [infer FirstCommand, ...infer RestCommands]
191
- ?
192
- | (RestCommands extends AnyPadroneCommand[] ? FlattenCommands<RestCommands> : never)
193
- | (FirstCommand extends AnyPadroneCommand ? FlattenCommands<FirstCommand['~types']['commands']> | FirstCommand : never)
194
- : IsAny<TCommands[number]> extends true
228
+ : number extends TCommands['length']
229
+ ? IsAny<TCommands[number]> extends true
195
230
  ? never
196
- : TCommands[number];
231
+ : TCommands[number]
232
+ : TCommands[number] extends infer Cmd extends AnyPadroneCommand
233
+ ? Cmd | FlattenCommands<Cmd['~types']['commands']>
234
+ : never;
197
235
 
198
236
  /**
199
237
  * Get all command paths including alias paths for all commands.
@@ -258,9 +296,9 @@ type RepathCommand<TCommand extends AnyPadroneCommand, TNewParentName extends st
258
296
  TCommand['~types']['result'],
259
297
  RepathCommands<TCommand['~types']['commands'], FullCommandName<TCommand['~types']['name'], TNewParentName>>,
260
298
  TCommand['~types']['aliases'],
261
- TCommand['~types']['configSchema'],
262
- TCommand['~types']['envSchema'],
263
- TCommand['~types']['async']
299
+ TCommand['~types']['async'],
300
+ TCommand['~types']['context'],
301
+ TCommand['~types']['contextProvided']
264
302
  >;
265
303
 
266
304
  export type PickCommandByPossibleCommands<
@@ -0,0 +1,51 @@
1
+ import type { AnyPadroneCommand } from '../types/index.ts';
2
+
3
+ export function getRootCommand(cmd: AnyPadroneCommand): AnyPadroneCommand {
4
+ let current = cmd;
5
+ while (current.parent) current = current.parent;
6
+ return current;
7
+ }
8
+
9
+ async function readVersionFromPackageJson(): Promise<string> {
10
+ try {
11
+ const fs = await import('node:fs');
12
+ const path = await import('node:path');
13
+ let dir = process.cwd();
14
+
15
+ // Walk up the directory tree looking for package.json
16
+ for (let i = 0; i < 10; i++) {
17
+ const pkgPath = path.join(dir, 'package.json');
18
+ if (fs.existsSync(pkgPath)) {
19
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
20
+ if (pkg.version) return pkg.version;
21
+ }
22
+ const parentDir = path.dirname(dir);
23
+ if (parentDir === dir) break; // Reached root
24
+ dir = parentDir;
25
+ }
26
+ } catch {
27
+ // Ignore errors (e.g., fs not available in browser)
28
+ }
29
+ return '0.0.0';
30
+ }
31
+
32
+ /**
33
+ * Attempts to get the version from various sources:
34
+ * 1. Explicit version set on the command
35
+ * 2. npm_package_version environment variable (set by npm/yarn/pnpm when running scripts)
36
+ * 3. package.json in current or parent directories
37
+ *
38
+ * Returns synchronously when an explicit version or env var is available.
39
+ * Falls back to async package.json discovery otherwise.
40
+ */
41
+ export function getVersion(explicitVersion?: string): string | Promise<string> {
42
+ if (explicitVersion) return explicitVersion;
43
+
44
+ if (typeof process !== 'undefined' && process.env?.npm_package_version) {
45
+ return process.env.npm_package_version;
46
+ }
47
+
48
+ if (typeof process !== 'undefined') return readVersionFromPackageJson();
49
+
50
+ return '0.0.0';
51
+ }