padrone 1.1.0 → 1.2.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 +38 -1
- package/LICENSE +1 -1
- package/README.md +60 -30
- package/dist/args-CKNh7Dm9.mjs +175 -0
- package/dist/args-CKNh7Dm9.mjs.map +1 -0
- package/dist/chunk-y_GBKt04.mjs +5 -0
- package/dist/codegen/index.d.mts +305 -0
- package/dist/codegen/index.d.mts.map +1 -0
- package/dist/codegen/index.mjs +1348 -0
- package/dist/codegen/index.mjs.map +1 -0
- package/dist/completion.d.mts +64 -0
- package/dist/completion.d.mts.map +1 -0
- package/dist/completion.mjs +417 -0
- package/dist/completion.mjs.map +1 -0
- package/dist/docs/index.d.mts +34 -0
- package/dist/docs/index.d.mts.map +1 -0
- package/dist/docs/index.mjs +404 -0
- package/dist/docs/index.mjs.map +1 -0
- package/dist/formatter-Dvx7jFXr.d.mts +82 -0
- package/dist/formatter-Dvx7jFXr.d.mts.map +1 -0
- package/dist/help-mUIX0T0V.mjs +1195 -0
- package/dist/help-mUIX0T0V.mjs.map +1 -0
- package/dist/index.d.mts +120 -546
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1180 -1197
- package/dist/index.mjs.map +1 -1
- package/dist/test.d.mts +112 -0
- package/dist/test.d.mts.map +1 -0
- package/dist/test.mjs +138 -0
- package/dist/test.mjs.map +1 -0
- package/dist/types-qrtt0135.d.mts +1037 -0
- package/dist/types-qrtt0135.d.mts.map +1 -0
- package/dist/update-check-EbNDkzyV.mjs +146 -0
- package/dist/update-check-EbNDkzyV.mjs.map +1 -0
- package/package.json +61 -21
- package/src/args.ts +365 -0
- package/src/cli/completions.ts +29 -0
- package/src/cli/docs.ts +86 -0
- package/src/cli/doctor.ts +312 -0
- package/src/cli/index.ts +159 -0
- package/src/cli/init.ts +135 -0
- package/src/cli/link.ts +320 -0
- package/src/cli/wrap.ts +152 -0
- package/src/codegen/README.md +118 -0
- package/src/codegen/code-builder.ts +226 -0
- package/src/codegen/discovery.ts +232 -0
- package/src/codegen/file-emitter.ts +73 -0
- package/src/codegen/generators/barrel-file.ts +16 -0
- package/src/codegen/generators/command-file.ts +184 -0
- package/src/codegen/generators/command-tree.ts +124 -0
- package/src/codegen/index.ts +33 -0
- package/src/codegen/parsers/fish.ts +163 -0
- package/src/codegen/parsers/help.ts +378 -0
- package/src/codegen/parsers/merge.ts +158 -0
- package/src/codegen/parsers/zsh.ts +221 -0
- package/src/codegen/schema-to-code.ts +199 -0
- package/src/codegen/template.ts +69 -0
- package/src/codegen/types.ts +143 -0
- package/src/colorizer.ts +2 -2
- package/src/command-utils.ts +501 -0
- package/src/completion.ts +110 -97
- package/src/create.ts +1036 -305
- package/src/docs/index.ts +607 -0
- package/src/errors.ts +131 -0
- package/src/formatter.ts +149 -63
- package/src/help.ts +151 -55
- package/src/index.ts +12 -15
- package/src/interactive.ts +169 -0
- package/src/parse.ts +31 -16
- package/src/repl-loop.ts +317 -0
- package/src/runtime.ts +304 -0
- package/src/shell-utils.ts +83 -0
- package/src/test.ts +285 -0
- package/src/type-helpers.ts +10 -10
- package/src/type-utils.ts +124 -14
- package/src/types.ts +752 -154
- package/src/update-check.ts +244 -0
- package/src/wrap.ts +44 -40
- package/src/zod.d.ts +2 -2
- package/src/options.ts +0 -180
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type ShellType = 'bash' | 'zsh' | 'fish' | 'powershell';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detects the current shell from environment variables and process info.
|
|
5
|
+
* @returns The detected shell type, or undefined if unknown
|
|
6
|
+
*/
|
|
7
|
+
export function detectShell(): ShellType | undefined {
|
|
8
|
+
if (typeof process === 'undefined') return undefined;
|
|
9
|
+
|
|
10
|
+
// Method 1: Check SHELL environment variable (most common)
|
|
11
|
+
const shellEnv = process.env.SHELL || '';
|
|
12
|
+
if (shellEnv.includes('zsh')) return 'zsh';
|
|
13
|
+
if (shellEnv.includes('bash')) return 'bash';
|
|
14
|
+
if (shellEnv.includes('fish')) return 'fish';
|
|
15
|
+
|
|
16
|
+
// Method 2: Check Windows-specific shells
|
|
17
|
+
if (process.env.PSModulePath || process.env.POWERSHELL_DISTRIBUTION_CHANNEL) {
|
|
18
|
+
return 'powershell';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Method 3: Check parent process on Unix-like systems
|
|
22
|
+
try {
|
|
23
|
+
const ppid = process.ppid;
|
|
24
|
+
if (ppid) {
|
|
25
|
+
const { execSync } = require('node:child_process') as typeof import('node:child_process');
|
|
26
|
+
const processName = execSync(`ps -p ${ppid} -o comm=`, {
|
|
27
|
+
encoding: 'utf-8',
|
|
28
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
29
|
+
}).trim();
|
|
30
|
+
|
|
31
|
+
if (processName.includes('zsh')) return 'zsh';
|
|
32
|
+
if (processName.includes('bash')) return 'bash';
|
|
33
|
+
if (processName.includes('fish')) return 'fish';
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// Ignore errors (e.g., ps not available)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
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');
|
|
45
|
+
const h = home ?? homedir();
|
|
46
|
+
switch (shell) {
|
|
47
|
+
case 'bash':
|
|
48
|
+
return join(h, '.bashrc');
|
|
49
|
+
case 'zsh':
|
|
50
|
+
return join(h, '.zshrc');
|
|
51
|
+
case 'fish':
|
|
52
|
+
return join(h, '.config', 'fish', 'config.fish');
|
|
53
|
+
case 'powershell':
|
|
54
|
+
return process.env.PROFILE || join(h, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1');
|
|
55
|
+
default:
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function escapeRegExp(str: string): string {
|
|
61
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Writes a snippet to a shell config file using begin/end markers for idempotency.
|
|
66
|
+
* If a block with the same begin marker exists, it is replaced. Otherwise the snippet is appended.
|
|
67
|
+
*/
|
|
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');
|
|
71
|
+
const existing = existsSync(rcFile) ? readFileSync(rcFile, 'utf-8') : '';
|
|
72
|
+
|
|
73
|
+
if (existing.includes(beginMarker)) {
|
|
74
|
+
const pattern = new RegExp(`${escapeRegExp(beginMarker)}[\\s\\S]*?${escapeRegExp(endMarker)}`);
|
|
75
|
+
writeFileSync(rcFile, existing.replace(pattern, snippet));
|
|
76
|
+
return { file: rcFile, updated: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
mkdirSync(dirname(rcFile), { recursive: true });
|
|
80
|
+
const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
|
|
81
|
+
writeFileSync(rcFile, `${existing}${separator}\n${snippet}\n`);
|
|
82
|
+
return { file: rcFile, updated: false };
|
|
83
|
+
}
|
package/src/test.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import type { InteractivePromptConfig, PadroneRuntime } from './runtime.ts';
|
|
2
|
+
import type { AnyPadroneCommand, PadroneCommandResult } from './types.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Result from a single command execution in test mode.
|
|
6
|
+
* Extends the standard PadroneCommandResult with captured I/O.
|
|
7
|
+
*/
|
|
8
|
+
export type TestCliResult = {
|
|
9
|
+
/** The matched command. */
|
|
10
|
+
command: AnyPadroneCommand;
|
|
11
|
+
/** Validated arguments (undefined if validation failed). */
|
|
12
|
+
args: unknown;
|
|
13
|
+
/** Action handler return value (undefined if validation failed or no action). */
|
|
14
|
+
result: unknown;
|
|
15
|
+
/** Validation issues, if any. */
|
|
16
|
+
issues: { message: string; path?: PropertyKey[] }[] | undefined;
|
|
17
|
+
/** All values passed to `runtime.output()`. */
|
|
18
|
+
stdout: unknown[];
|
|
19
|
+
/** All strings passed to `runtime.error()`. */
|
|
20
|
+
stderr: string[];
|
|
21
|
+
/** The thrown error, if the command threw (routing error, action error, etc.). */
|
|
22
|
+
error?: unknown;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Result from a REPL test session.
|
|
27
|
+
*/
|
|
28
|
+
export type TestReplResult = {
|
|
29
|
+
/** One entry per successfully executed command (validation errors are captured in stderr, not here). */
|
|
30
|
+
results: Omit<TestCliResult, 'stdout' | 'stderr'>[];
|
|
31
|
+
/** All output from the entire REPL session. */
|
|
32
|
+
stdout: unknown[];
|
|
33
|
+
/** All errors from the entire REPL session. */
|
|
34
|
+
stderr: string[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Fluent builder for setting up CLI test scenarios.
|
|
39
|
+
*/
|
|
40
|
+
export type TestCliBuilder = {
|
|
41
|
+
/** Set the CLI input string (e.g. `'deploy --env production'`). */
|
|
42
|
+
args(input: string): TestCliBuilder;
|
|
43
|
+
/** Set environment variables visible to the command. */
|
|
44
|
+
env(vars: Record<string, string | undefined>): TestCliBuilder;
|
|
45
|
+
/** Provide mock answers for interactive prompts. Keys are field names. */
|
|
46
|
+
prompt(answers: Record<string, unknown>): TestCliBuilder;
|
|
47
|
+
/** Provide mock config files. Keys are file paths, values are parsed config objects. */
|
|
48
|
+
config(files: Record<string, Record<string, unknown>>): TestCliBuilder;
|
|
49
|
+
/** Provide mock stdin data (simulates piped input). */
|
|
50
|
+
stdin(data: string): TestCliBuilder;
|
|
51
|
+
/**
|
|
52
|
+
* Execute a single command via `eval()` and return the result with captured I/O.
|
|
53
|
+
* @param input - Optional CLI input string. Overrides `.args()` if provided.
|
|
54
|
+
*/
|
|
55
|
+
run(input?: string): Promise<TestCliResult>;
|
|
56
|
+
/**
|
|
57
|
+
* Run a REPL session with the given sequence of inputs.
|
|
58
|
+
* Each string in the array is fed as one line of input.
|
|
59
|
+
* The session ends after all inputs are consumed (EOF).
|
|
60
|
+
*/
|
|
61
|
+
repl(inputs: string[]): Promise<TestReplResult>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Creates a fluent test builder for a Padrone program.
|
|
66
|
+
* Captures all I/O and provides a clean interface for assertions.
|
|
67
|
+
*
|
|
68
|
+
* Works with any test framework (bun:test, vitest, jest, node:test, etc.).
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* import { testCli } from 'padrone/test'
|
|
73
|
+
*
|
|
74
|
+
* const result = await testCli(myProgram)
|
|
75
|
+
* .args('deploy --env production')
|
|
76
|
+
* .env({ API_KEY: 'xxx' })
|
|
77
|
+
* .run()
|
|
78
|
+
*
|
|
79
|
+
* expect(result.result).toBe('Deployed')
|
|
80
|
+
* expect(result.stdout).toContain('Deploying...')
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* // Shorthand: pass input directly to run()
|
|
86
|
+
* const result = await testCli(myProgram).run('deploy --env production')
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* // Test interactive prompts
|
|
92
|
+
* const result = await testCli(myProgram)
|
|
93
|
+
* .args('init')
|
|
94
|
+
* .prompt({ name: 'myapp', template: 'react' })
|
|
95
|
+
* .run()
|
|
96
|
+
*
|
|
97
|
+
* expect(result.args).toEqual({ name: 'myapp', template: 'react' })
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* // Test REPL sessions
|
|
103
|
+
* const { results } = await testCli(myProgram)
|
|
104
|
+
* .repl(['greet World', 'add --a=2 --b=3'])
|
|
105
|
+
*
|
|
106
|
+
* expect(results[0].result).toBe('Hello, World!')
|
|
107
|
+
* expect(results[1].result).toBe(5)
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
/**
|
|
111
|
+
* Any program-like object that has `eval`, `runtime`, and `repl` methods.
|
|
112
|
+
* Avoids strict variance issues with `AnyPadroneProgram`.
|
|
113
|
+
*/
|
|
114
|
+
type TestableProgram = {
|
|
115
|
+
eval: (input: string, prefs?: { autoOutput?: boolean }) => any;
|
|
116
|
+
runtime: (runtime: PadroneRuntime) => TestableProgram;
|
|
117
|
+
repl: (options?: { greeting?: false; hint?: false }) => AsyncIterable<any>;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export function testCli(program: TestableProgram): TestCliBuilder {
|
|
121
|
+
let input: string | undefined;
|
|
122
|
+
let envVars: Record<string, string | undefined> | undefined;
|
|
123
|
+
let promptAnswers: Record<string, unknown> | undefined;
|
|
124
|
+
let configFiles: Record<string, Record<string, unknown>> | undefined;
|
|
125
|
+
let stdinData: string | undefined;
|
|
126
|
+
|
|
127
|
+
const builder: TestCliBuilder = {
|
|
128
|
+
args(args: string) {
|
|
129
|
+
input = args;
|
|
130
|
+
return builder;
|
|
131
|
+
},
|
|
132
|
+
env(vars) {
|
|
133
|
+
envVars = vars;
|
|
134
|
+
return builder;
|
|
135
|
+
},
|
|
136
|
+
prompt(answers) {
|
|
137
|
+
promptAnswers = answers;
|
|
138
|
+
return builder;
|
|
139
|
+
},
|
|
140
|
+
config(files) {
|
|
141
|
+
configFiles = files;
|
|
142
|
+
return builder;
|
|
143
|
+
},
|
|
144
|
+
stdin(data: string) {
|
|
145
|
+
stdinData = data;
|
|
146
|
+
return builder;
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
async run(runInput?: string) {
|
|
150
|
+
const stdout: unknown[] = [];
|
|
151
|
+
const stderr: string[] = [];
|
|
152
|
+
|
|
153
|
+
const runtime = buildRuntime(stdout, stderr, { envVars, promptAnswers, configFiles, stdinData });
|
|
154
|
+
const testProgram = program.runtime(runtime);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const evalResult = await testProgram.eval(runInput ?? input ?? '', { autoOutput: false });
|
|
158
|
+
return toTestResult(evalResult, stdout, stderr);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
stderr.push(err instanceof Error ? err.message : String(err));
|
|
161
|
+
return {
|
|
162
|
+
command: undefined as unknown as AnyPadroneCommand,
|
|
163
|
+
args: undefined,
|
|
164
|
+
result: undefined,
|
|
165
|
+
issues: undefined,
|
|
166
|
+
stdout,
|
|
167
|
+
stderr,
|
|
168
|
+
error: err,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
async repl(inputs: string[]) {
|
|
174
|
+
const stdout: unknown[] = [];
|
|
175
|
+
const stderr: string[] = [];
|
|
176
|
+
|
|
177
|
+
const runtime = buildRuntime(stdout, stderr, {
|
|
178
|
+
envVars,
|
|
179
|
+
promptAnswers,
|
|
180
|
+
configFiles,
|
|
181
|
+
readLine: createMockReadLine(inputs),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const testProgram = program.runtime(runtime);
|
|
185
|
+
const results: Omit<TestCliResult, 'stdout' | 'stderr'>[] = [];
|
|
186
|
+
|
|
187
|
+
for await (const r of testProgram.repl({ greeting: false, hint: false })) {
|
|
188
|
+
results.push({
|
|
189
|
+
command: r.command,
|
|
190
|
+
args: r.args,
|
|
191
|
+
result: r.result,
|
|
192
|
+
issues: r.argsResult?.issues as TestCliResult['issues'],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { results, stdout, stderr };
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
return builder;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function toTestResult(evalResult: PadroneCommandResult, stdout: unknown[], stderr: string[]): TestCliResult {
|
|
204
|
+
return {
|
|
205
|
+
command: evalResult.command,
|
|
206
|
+
args: evalResult.args,
|
|
207
|
+
result: evalResult.result,
|
|
208
|
+
issues: evalResult.argsResult?.issues as TestCliResult['issues'],
|
|
209
|
+
stdout,
|
|
210
|
+
stderr,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function buildRuntime(
|
|
215
|
+
stdout: unknown[],
|
|
216
|
+
stderr: string[],
|
|
217
|
+
opts: {
|
|
218
|
+
envVars?: Record<string, string | undefined>;
|
|
219
|
+
promptAnswers?: Record<string, unknown>;
|
|
220
|
+
configFiles?: Record<string, Record<string, unknown>>;
|
|
221
|
+
readLine?: (prompt: string) => Promise<string | null>;
|
|
222
|
+
stdinData?: string;
|
|
223
|
+
},
|
|
224
|
+
): PadroneRuntime {
|
|
225
|
+
const runtime: PadroneRuntime = {
|
|
226
|
+
output: (...args: unknown[]) => stdout.push(...args),
|
|
227
|
+
error: (text: string) => stderr.push(text),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
if (opts.envVars) {
|
|
231
|
+
runtime.env = () => opts.envVars!;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (opts.promptAnswers) {
|
|
235
|
+
runtime.interactive = 'supported';
|
|
236
|
+
runtime.prompt = async (config: InteractivePromptConfig) => opts.promptAnswers![config.name];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (opts.configFiles) {
|
|
240
|
+
runtime.loadConfigFile = (path: string) => opts.configFiles![path];
|
|
241
|
+
runtime.findFile = (names: string[]) => names.find((n) => n in opts.configFiles!);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (opts.readLine) {
|
|
245
|
+
runtime.readLine = opts.readLine;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (opts.stdinData !== undefined) {
|
|
249
|
+
runtime.stdin = {
|
|
250
|
+
isTTY: false,
|
|
251
|
+
async text() {
|
|
252
|
+
return opts.stdinData!;
|
|
253
|
+
},
|
|
254
|
+
async *lines() {
|
|
255
|
+
const lines = opts.stdinData!.split('\n');
|
|
256
|
+
// Remove trailing empty line from final newline (matches readline behavior)
|
|
257
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
|
258
|
+
for (const line of lines) {
|
|
259
|
+
yield line;
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
} else {
|
|
264
|
+
// No stdin data: simulate a TTY (no piped input) to avoid reading from process.stdin
|
|
265
|
+
runtime.stdin = {
|
|
266
|
+
isTTY: true,
|
|
267
|
+
async text() {
|
|
268
|
+
return '';
|
|
269
|
+
},
|
|
270
|
+
async *lines() {
|
|
271
|
+
// no lines
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return runtime;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function createMockReadLine(inputs: string[]): (prompt: string) => Promise<string | null> {
|
|
280
|
+
let index = 0;
|
|
281
|
+
return async (_prompt: string): Promise<string | null> => {
|
|
282
|
+
if (index >= inputs.length) return null;
|
|
283
|
+
return inputs[index++] ?? null;
|
|
284
|
+
};
|
|
285
|
+
}
|
package/src/type-helpers.ts
CHANGED
|
@@ -2,22 +2,22 @@ import type { PickCommandByName, PossibleCommands } from './type-utils.ts';
|
|
|
2
2
|
import type { AnyPadroneCommand, AnyPadroneProgram, PadroneCommand, PadroneSchema } from './types.ts';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Extracts the input type of the
|
|
5
|
+
* Extracts the input type of the arguments schema from a command.
|
|
6
6
|
* @example
|
|
7
7
|
* ```ts
|
|
8
|
-
* type
|
|
8
|
+
* type Args = InferArgsInput<typeof myCommand>;
|
|
9
9
|
* ```
|
|
10
10
|
*/
|
|
11
|
-
export type
|
|
11
|
+
export type InferArgsInput<T extends AnyPadroneCommand> = T['~types']['argsInput'];
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Extracts the output type of the
|
|
14
|
+
* Extracts the output type of the arguments schema from a command.
|
|
15
15
|
* @example
|
|
16
16
|
* ```ts
|
|
17
|
-
* type
|
|
17
|
+
* type Args = InferArgsOutput<typeof myCommand>;
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
|
-
export type
|
|
20
|
+
export type InferArgsOutput<T extends AnyPadroneCommand> = T['~types']['argsOutput'];
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Extracts the input type of the config schema from a command.
|
|
@@ -26,17 +26,17 @@ export type InferOptionsOutput<T extends AnyPadroneCommand> = T['~types']['optio
|
|
|
26
26
|
* type Config = InferConfigInput<typeof myCommand>;
|
|
27
27
|
* ```
|
|
28
28
|
*/
|
|
29
|
-
export type InferConfigInput<T extends AnyPadroneCommand> = T['
|
|
29
|
+
export type InferConfigInput<T extends AnyPadroneCommand> = T['configSchema'] extends PadroneSchema<infer I, any> ? I : never;
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Extracts the output type of the config schema from a command.
|
|
33
|
-
* This is the type after transformation, which should match the
|
|
33
|
+
* This is the type after transformation, which should match the arguments shape.
|
|
34
34
|
* @example
|
|
35
35
|
* ```ts
|
|
36
36
|
* type ConfigOutput = InferConfigOutput<typeof myCommand>;
|
|
37
37
|
* ```
|
|
38
38
|
*/
|
|
39
|
-
export type InferConfigOutput<T extends AnyPadroneCommand> = T['
|
|
39
|
+
export type InferConfigOutput<T extends AnyPadroneCommand> = T['configSchema'] extends PadroneSchema<any, infer O> ? O : never;
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Extracts the input type of the env schema from a command.
|
|
@@ -50,7 +50,7 @@ export type InferEnvInput<T extends AnyPadroneCommand> = T['envSchema'] extends
|
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Extracts the output type of the env schema from a command.
|
|
53
|
-
* This is the type after transformation, which should match the
|
|
53
|
+
* This is the type after transformation, which should match the arguments shape.
|
|
54
54
|
* @example
|
|
55
55
|
* ```ts
|
|
56
56
|
* type EnvOutput = InferEnvOutput<typeof myCommand>;
|
package/src/type-utils.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AnyPadroneCommand } from './types.ts';
|
|
1
|
+
import type { AnyPadroneCommand, PadroneCommand } from './types.ts';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Use this type instead of `any` when you intend to fix it later
|
|
@@ -13,6 +13,54 @@ type IsNever<T> = [T] extends [never] ? true : false;
|
|
|
13
13
|
|
|
14
14
|
export type IsGeneric<T> = IsAny<T> extends true ? true : IsUnknown<T> extends true ? true : IsNever<T> extends true ? true : false;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Detects whether a schema has been branded as async via the `'~async'` property.
|
|
18
|
+
* Standard Schema V1's `validate()` always types its return as `Result | Promise<Result>`
|
|
19
|
+
* regardless of whether the schema is actually async, so we rely on an explicit brand instead.
|
|
20
|
+
*
|
|
21
|
+
* Use `asyncSchema(schema)` to brand a schema, or check for the `{ '~async': true }` property.
|
|
22
|
+
*/
|
|
23
|
+
export type IsAsyncSchema<T> = IsAny<T> extends true ? false : T extends { '~async': true } ? true : false;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Computes the new TAsync flag when a schema is added to a builder.
|
|
27
|
+
* Once TAsync is `true`, it stays `true`. Otherwise, checks if the new schema is branded async.
|
|
28
|
+
*/
|
|
29
|
+
export type OrAsync<TExisting extends boolean, TSchema> = TExisting extends true
|
|
30
|
+
? true
|
|
31
|
+
: IsAsyncSchema<TSchema> extends true
|
|
32
|
+
? true
|
|
33
|
+
: false;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Detects whether argument meta contains interactive or optionalInteractive configuration.
|
|
37
|
+
* When either is `true` or a `string[]`, the command requires async execution for prompting.
|
|
38
|
+
*/
|
|
39
|
+
export type HasInteractive<TMeta> = TMeta extends { interactive: true | string[] }
|
|
40
|
+
? true
|
|
41
|
+
: TMeta extends { optionalInteractive: true | string[] }
|
|
42
|
+
? true
|
|
43
|
+
: false;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Combines schema-level async detection with meta-level interactive detection.
|
|
47
|
+
* Returns `true` if the existing async flag is set, the schema is branded async, or the meta has interactive fields.
|
|
48
|
+
*/
|
|
49
|
+
export type OrAsyncMeta<TExisting extends boolean, TMeta> = TExisting extends true
|
|
50
|
+
? true
|
|
51
|
+
: HasInteractive<TMeta> extends true
|
|
52
|
+
? true
|
|
53
|
+
: false;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Conditionally wraps a type in Promise based on the TAsync flag.
|
|
57
|
+
* - `true` → `Promise<T>`
|
|
58
|
+
* - `false` → `T`
|
|
59
|
+
* - `boolean` (union of true|false) → `Promise<T>` (safe default when async-ness is uncertain)
|
|
60
|
+
* - `any` → `T` (for generic/any typed commands like AnyPadroneCommand)
|
|
61
|
+
*/
|
|
62
|
+
export type MaybePromise<T, TAsync> = IsAny<TAsync> extends true ? T : true extends TAsync ? Promise<T> : T;
|
|
63
|
+
|
|
16
64
|
type SplitString<TName extends string, TSplitBy extends string = ' '> = TName extends `${infer FirstPart}${TSplitBy}${infer RestParts}`
|
|
17
65
|
? [FirstPart, ...SplitString<RestParts, TSplitBy>]
|
|
18
66
|
: [TName];
|
|
@@ -62,6 +110,44 @@ type GetCommandPathsAndAliases<TCommand extends AnyPadroneCommand> = TCommand['~
|
|
|
62
110
|
: Path
|
|
63
111
|
: never;
|
|
64
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Find a direct child command in a tuple by name.
|
|
115
|
+
* Unlike PickCommandByName, this does NOT flatten — it only checks direct children by their `name` field.
|
|
116
|
+
*/
|
|
117
|
+
export type FindDirectChild<TCommands extends AnyPadroneCommand[], TName extends string> = TCommands extends [
|
|
118
|
+
infer First extends AnyPadroneCommand,
|
|
119
|
+
...infer Rest extends AnyPadroneCommand[],
|
|
120
|
+
]
|
|
121
|
+
? First['~types']['name'] extends TName
|
|
122
|
+
? First
|
|
123
|
+
: FindDirectChild<Rest, TName>
|
|
124
|
+
: never;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Replace a command in a tuple by name, or append if not found.
|
|
128
|
+
* Used by `.command()` override semantics: re-registering a name replaces that entry.
|
|
129
|
+
*/
|
|
130
|
+
export type ReplaceOrAppendCommand<TCommands extends [...AnyPadroneCommand[]], TName extends string, TNew extends AnyPadroneCommand> =
|
|
131
|
+
HasDirectChild<TCommands, TName> extends true ? ReplaceInTuple<TCommands, TName, TNew> : [...TCommands, TNew];
|
|
132
|
+
|
|
133
|
+
type HasDirectChild<TCommands extends AnyPadroneCommand[], TName extends string> = TCommands extends [
|
|
134
|
+
infer First extends AnyPadroneCommand,
|
|
135
|
+
...infer Rest extends AnyPadroneCommand[],
|
|
136
|
+
]
|
|
137
|
+
? First['~types']['name'] extends TName
|
|
138
|
+
? true
|
|
139
|
+
: HasDirectChild<Rest, TName>
|
|
140
|
+
: false;
|
|
141
|
+
|
|
142
|
+
type ReplaceInTuple<TCommands extends AnyPadroneCommand[], TName extends string, TNew extends AnyPadroneCommand> = TCommands extends [
|
|
143
|
+
infer First extends AnyPadroneCommand,
|
|
144
|
+
...infer Rest extends AnyPadroneCommand[],
|
|
145
|
+
]
|
|
146
|
+
? First['~types']['name'] extends TName
|
|
147
|
+
? [TNew, ...Rest]
|
|
148
|
+
: [First, ...ReplaceInTuple<Rest, TName, TNew>]
|
|
149
|
+
: [];
|
|
150
|
+
|
|
65
151
|
export type PickCommandByName<
|
|
66
152
|
TCommands extends AnyPadroneCommand[],
|
|
67
153
|
TName extends string | AnyPadroneCommand,
|
|
@@ -130,19 +216,43 @@ type CommandIsUnknownable<TCommand> =
|
|
|
130
216
|
* This is done by recursively splitting the string by the last space, and then checking if the prefix is a valid command name or alias.
|
|
131
217
|
* This is needed to avoid matching the top-level command when there are nested commands.
|
|
132
218
|
*/
|
|
219
|
+
/**
|
|
220
|
+
* Recursively re-paths a command's children under a new parent path.
|
|
221
|
+
* Used by `mount()` to update all nested command paths when a program is mounted as a subcommand.
|
|
222
|
+
*/
|
|
223
|
+
export type RepathCommands<TCommands extends [...AnyPadroneCommand[]], TNewParentPath extends string> = TCommands extends [
|
|
224
|
+
infer First extends AnyPadroneCommand,
|
|
225
|
+
...infer Rest extends AnyPadroneCommand[],
|
|
226
|
+
]
|
|
227
|
+
? [RepathCommand<First, TNewParentPath>, ...RepathCommands<Rest, TNewParentPath>]
|
|
228
|
+
: [];
|
|
229
|
+
|
|
230
|
+
type RepathCommand<TCommand extends AnyPadroneCommand, TNewParentName extends string> = PadroneCommand<
|
|
231
|
+
TCommand['~types']['name'],
|
|
232
|
+
TNewParentName,
|
|
233
|
+
TCommand['~types']['argsSchema'],
|
|
234
|
+
TCommand['~types']['result'],
|
|
235
|
+
RepathCommands<TCommand['~types']['commands'], FullCommandName<TCommand['~types']['name'], TNewParentName>>,
|
|
236
|
+
TCommand['~types']['aliases'],
|
|
237
|
+
TCommand['~types']['configSchema'],
|
|
238
|
+
TCommand['~types']['envSchema'],
|
|
239
|
+
TCommand['~types']['async']
|
|
240
|
+
>;
|
|
241
|
+
|
|
133
242
|
export type PickCommandByPossibleCommands<
|
|
134
243
|
TCommands extends AnyPadroneCommand[],
|
|
135
244
|
TCommand extends PossibleCommands<TCommands, true, true> | SafeString,
|
|
136
|
-
> =
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
?
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
?
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
245
|
+
> =
|
|
246
|
+
CommandIsUnknownable<TCommand> extends true
|
|
247
|
+
? FlattenCommands<TCommands>
|
|
248
|
+
: TCommand extends AnyPadroneCommand
|
|
249
|
+
? TCommand
|
|
250
|
+
: TCommand extends string
|
|
251
|
+
? TCommand extends GetCommandPathsOrAliases<TCommands>
|
|
252
|
+
? PickCommandByName<TCommands, TCommand>
|
|
253
|
+
: SplitLastSpace<TCommand> extends [infer Prefix extends string, infer Rest]
|
|
254
|
+
? IsNever<Rest> extends true
|
|
255
|
+
? PickCommandByName<TCommands, Prefix>
|
|
256
|
+
: PickCommandByPossibleCommands<TCommands, Prefix>
|
|
257
|
+
: never
|
|
258
|
+
: never;
|