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
|
@@ -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 } =
|
|
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 } =
|
|
44
|
-
const { join } =
|
|
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
|
|
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(
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
29
|
+
* Extracts the user-defined context type from a command (excludes interceptor-provided context).
|
|
24
30
|
* @example
|
|
25
31
|
* ```ts
|
|
26
|
-
* type
|
|
32
|
+
* type Ctx = InferContext<typeof myCommand>;
|
|
27
33
|
* ```
|
|
28
34
|
*/
|
|
29
|
-
export type
|
|
35
|
+
export type InferContext<T extends AnyPadroneCommand> = T['~types']['context'];
|
|
30
36
|
|
|
31
37
|
/**
|
|
32
|
-
* Extracts the
|
|
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
|
|
41
|
+
* type Provided = InferContextProvided<typeof myCommand>;
|
|
37
42
|
* ```
|
|
38
43
|
*/
|
|
39
|
-
export type
|
|
44
|
+
export type InferContextProvided<T extends AnyPadroneCommand> = T['~types']['contextProvided'];
|
|
40
45
|
|
|
41
46
|
/**
|
|
42
|
-
* Extracts the
|
|
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
|
|
50
|
+
* type AuthCtx = InferInterceptorContext<typeof withAuth>;
|
|
47
51
|
* ```
|
|
48
52
|
*/
|
|
49
|
-
export type
|
|
53
|
+
export type InferInterceptorContext<T extends PadroneContextInterceptor<any>> = T['~context'];
|
|
50
54
|
|
|
51
55
|
/**
|
|
52
|
-
* Extracts the
|
|
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
|
|
59
|
+
* type Requires = InferInterceptorRequires<typeof withAuth>;
|
|
57
60
|
* ```
|
|
58
61
|
*/
|
|
59
|
-
export type
|
|
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.
|