clap-ts 0.1.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/LICENSE +21 -0
- package/README.md +752 -0
- package/dist/help.d.ts +31 -0
- package/dist/help.js +414 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +13 -0
- package/dist/parser.d.ts +42 -0
- package/dist/parser.js +646 -0
- package/dist/runner.d.ts +60 -0
- package/dist/runner.js +370 -0
- package/dist/types.d.ts +252 -0
- package/dist/types.js +5 -0
- package/dist/validation.d.ts +12 -0
- package/dist/validation.js +352 -0
- package/package.json +57 -0
package/dist/runner.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command runner - entry point for CLI execution.
|
|
3
|
+
* Handles subcommand resolution, lifecycle hooks, error handling.
|
|
4
|
+
* Supports inferSubcommands, subcommandRequired, allowExternalSubcommands,
|
|
5
|
+
* argsConflictsWithSubcommands, argRequiredElseHelp, and custom styles.
|
|
6
|
+
*/
|
|
7
|
+
import { CliParseError, collectGlobalArgs, getRawArgs, mergeGlobalArgs, parseArgs, } from './parser.js';
|
|
8
|
+
import { validate } from './validation.js';
|
|
9
|
+
import { showError, showHelp, showVersion } from './help.js';
|
|
10
|
+
// ---- defineCommand ----
|
|
11
|
+
/**
|
|
12
|
+
* Define a command with full type inference on arguments.
|
|
13
|
+
* This is the primary API for creating commands.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* const cmd = defineCommand({
|
|
17
|
+
* meta: { name: 'my-tool', version: '1.0.0', description: 'My tool' },
|
|
18
|
+
* args: {
|
|
19
|
+
* verbose: { type: 'boolean', short: 'v', description: 'Verbose output' },
|
|
20
|
+
* port: { type: 'number', short: 'p', default: 3000, description: 'Port' },
|
|
21
|
+
* },
|
|
22
|
+
* run({ args }) {
|
|
23
|
+
* console.log(args.verbose, args.port);
|
|
24
|
+
* },
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function defineCommand(def) {
|
|
29
|
+
return def;
|
|
30
|
+
}
|
|
31
|
+
// ---- defineArgs / defineArg ----
|
|
32
|
+
/**
|
|
33
|
+
* Define a reusable argument group with full type inference.
|
|
34
|
+
* Use this for shared args that are spread into multiple commands.
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* const envArgs = defineArgs({
|
|
38
|
+
* env: { type: 'string', valueParser: ['dev', 'staging', 'prod'] },
|
|
39
|
+
* dev: { type: 'boolean', conflictsWith: ['env', 'staging', 'prod'] },
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export function defineArgs(args) {
|
|
44
|
+
return args;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Define a single argument with full type inference.
|
|
48
|
+
*
|
|
49
|
+
* ```ts
|
|
50
|
+
* const portArg = defineArg({ type: 'number', short: 'p', default: 3003 });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function defineArg(arg) {
|
|
54
|
+
return arg;
|
|
55
|
+
}
|
|
56
|
+
// ---- Subcommand Resolution ----
|
|
57
|
+
/**
|
|
58
|
+
* Find a subcommand by prefix matching (inferSubcommands).
|
|
59
|
+
* Returns the match if exactly one, 'ambiguous' if multiple, undefined if none.
|
|
60
|
+
*/
|
|
61
|
+
function findSubcommandByPrefix(subCommands, token) {
|
|
62
|
+
const matches = [];
|
|
63
|
+
for (const [name, def] of Object.entries(subCommands)) {
|
|
64
|
+
if (name.startsWith(token)) {
|
|
65
|
+
matches.push({ name, def });
|
|
66
|
+
}
|
|
67
|
+
else if (def.meta.aliases?.some((a) => a.startsWith(token))) {
|
|
68
|
+
matches.push({ name, def });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (matches.length === 1) {
|
|
72
|
+
return matches[0];
|
|
73
|
+
}
|
|
74
|
+
if (matches.length > 1) {
|
|
75
|
+
return 'ambiguous';
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Resolve the command chain from the root command and raw args.
|
|
81
|
+
* Supports inferSubcommands for prefix matching.
|
|
82
|
+
*/
|
|
83
|
+
function resolveCommandChain(rootCommand, rawArgs) {
|
|
84
|
+
let current = rootCommand;
|
|
85
|
+
const parentNames = [];
|
|
86
|
+
const remaining = [...rawArgs];
|
|
87
|
+
while (remaining.length > 0 && (current.subCommands || current.meta.allowExternalSubcommands)) {
|
|
88
|
+
const token = remaining[0];
|
|
89
|
+
// Don't interpret flags as subcommands
|
|
90
|
+
if (token.startsWith('-')) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
// Direct match
|
|
94
|
+
if (current.subCommands) {
|
|
95
|
+
const subCmd = current.subCommands[token];
|
|
96
|
+
if (subCmd) {
|
|
97
|
+
parentNames.push(current.meta.name);
|
|
98
|
+
current = subCmd;
|
|
99
|
+
remaining.shift();
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// Alias match
|
|
103
|
+
const aliasMatch = findSubcommandByAlias(current.subCommands, token);
|
|
104
|
+
if (aliasMatch) {
|
|
105
|
+
parentNames.push(current.meta.name);
|
|
106
|
+
current = aliasMatch;
|
|
107
|
+
remaining.shift();
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// inferSubcommands: try prefix matching
|
|
111
|
+
if (current.meta.inferSubcommands) {
|
|
112
|
+
const prefixMatch = findSubcommandByPrefix(current.subCommands, token);
|
|
113
|
+
if (prefixMatch === 'ambiguous') {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if (prefixMatch) {
|
|
117
|
+
parentNames.push(current.meta.name);
|
|
118
|
+
current = prefixMatch.def;
|
|
119
|
+
remaining.shift();
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// allowExternalSubcommands: accept unknown subcommand and stop
|
|
125
|
+
if (current.meta.allowExternalSubcommands) {
|
|
126
|
+
remaining.shift();
|
|
127
|
+
return { command: current, remainingArgs: remaining, parentNames, externalSubcommand: token };
|
|
128
|
+
}
|
|
129
|
+
// Not a subcommand, stop resolution
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
return { command: current, remainingArgs: remaining, parentNames };
|
|
133
|
+
}
|
|
134
|
+
/** Find a subcommand definition by alias. */
|
|
135
|
+
function findSubcommandByAlias(subCommands, token) {
|
|
136
|
+
for (const def of Object.values(subCommands)) {
|
|
137
|
+
if (def.meta.aliases?.includes(token)) {
|
|
138
|
+
return def;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
// ---- runCommand ----
|
|
144
|
+
/**
|
|
145
|
+
* Run a specific command with pre-parsed arguments.
|
|
146
|
+
* Executes the setup -> run -> cleanup lifecycle.
|
|
147
|
+
*/
|
|
148
|
+
export async function runCommand(command, args, rawArgs = [], subCommand) {
|
|
149
|
+
const ctx = {
|
|
150
|
+
rawArgs,
|
|
151
|
+
args,
|
|
152
|
+
cmd: command,
|
|
153
|
+
subCommand,
|
|
154
|
+
data: {},
|
|
155
|
+
};
|
|
156
|
+
let runError;
|
|
157
|
+
// Setup phase
|
|
158
|
+
if (command.setup) {
|
|
159
|
+
await command.setup(ctx);
|
|
160
|
+
}
|
|
161
|
+
// Run phase
|
|
162
|
+
try {
|
|
163
|
+
if (command.run) {
|
|
164
|
+
await command.run(ctx);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
runError = error;
|
|
169
|
+
}
|
|
170
|
+
// Cleanup phase (always runs)
|
|
171
|
+
if (command.cleanup) {
|
|
172
|
+
try {
|
|
173
|
+
await command.cleanup(ctx);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
runError ??= error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (runError) {
|
|
180
|
+
if (runError instanceof Error) {
|
|
181
|
+
throw runError;
|
|
182
|
+
}
|
|
183
|
+
throw new Error(JSON.stringify(runError));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// ---- Typo Suggestion ----
|
|
187
|
+
/** Collect all known subcommand names and aliases from a command. */
|
|
188
|
+
function collectSubcommandNames(subCommands) {
|
|
189
|
+
const names = Object.keys(subCommands);
|
|
190
|
+
for (const def of Object.values(subCommands)) {
|
|
191
|
+
if (def.meta.aliases) {
|
|
192
|
+
names.push(...def.meta.aliases);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return names;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Find the closest match for a string among candidates using simple character diff.
|
|
199
|
+
*/
|
|
200
|
+
function findClosestSubcommand(target, candidates) {
|
|
201
|
+
const a = target.toLowerCase();
|
|
202
|
+
let bestMatch;
|
|
203
|
+
let bestDist = 4;
|
|
204
|
+
for (const name of candidates) {
|
|
205
|
+
const b = name.toLowerCase();
|
|
206
|
+
const dist = simpleCharDistance(a, b, bestDist);
|
|
207
|
+
if (dist < bestDist) {
|
|
208
|
+
bestDist = dist;
|
|
209
|
+
bestMatch = name;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return bestMatch;
|
|
213
|
+
}
|
|
214
|
+
/** Quick character-level distance heuristic. */
|
|
215
|
+
function simpleCharDistance(a, b, maxDist) {
|
|
216
|
+
if (Math.abs(a.length - b.length) >= maxDist) {
|
|
217
|
+
return maxDist;
|
|
218
|
+
}
|
|
219
|
+
let dist = Math.abs(a.length - b.length);
|
|
220
|
+
const minLen = Math.min(a.length, b.length);
|
|
221
|
+
for (let i = 0; i < minLen; i++) {
|
|
222
|
+
if (a[i] !== b[i]) {
|
|
223
|
+
dist++;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return dist;
|
|
227
|
+
}
|
|
228
|
+
// ---- runMain helpers ----
|
|
229
|
+
/** Handle --help request with mode and style support. */
|
|
230
|
+
function handleHelpRequest(command, parentNames, shouldExit, isShortHelp, styles) {
|
|
231
|
+
showHelp(command, parentNames.length > 0 ? parentNames : undefined, isShortHelp, styles);
|
|
232
|
+
if (shouldExit) {
|
|
233
|
+
process.exit(0);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/** Handle --version request. */
|
|
237
|
+
function handleVersionRequest(effectiveCommand, rootCommand, shouldExit) {
|
|
238
|
+
const versionMeta = effectiveCommand.meta.version ? effectiveCommand.meta : rootCommand.meta;
|
|
239
|
+
showVersion(versionMeta);
|
|
240
|
+
if (shouldExit) {
|
|
241
|
+
process.exit(0);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** Handle an unrecognized subcommand with typo suggestion. */
|
|
245
|
+
function handleUnrecognizedSubcommand(unknownName, command, parentNames, shouldExit, styles) {
|
|
246
|
+
const allNames = collectSubcommandNames(command.subCommands);
|
|
247
|
+
let msg = `unrecognized subcommand '${unknownName}'`;
|
|
248
|
+
const bestMatch = findClosestSubcommand(unknownName, allNames);
|
|
249
|
+
if (bestMatch) {
|
|
250
|
+
msg += `\n\n tip: a similar subcommand exists: '${bestMatch}'`;
|
|
251
|
+
}
|
|
252
|
+
showError(msg, command, parentNames.length > 0 ? parentNames : undefined, styles);
|
|
253
|
+
if (shouldExit) {
|
|
254
|
+
process.exit(2);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// ---- runMain ----
|
|
258
|
+
/**
|
|
259
|
+
* Main entry point for CLI applications.
|
|
260
|
+
* Parses args, resolves subcommands, validates, and runs.
|
|
261
|
+
*
|
|
262
|
+
* ```ts
|
|
263
|
+
* const main = defineCommand({ ... });
|
|
264
|
+
* runMain(main);
|
|
265
|
+
* ```
|
|
266
|
+
*/
|
|
267
|
+
export async function runMain(rootCommand, opts) {
|
|
268
|
+
const shouldExit = opts?.exit !== false;
|
|
269
|
+
const showHelpOnEmpty = opts?.showHelpOnEmpty !== false;
|
|
270
|
+
const styles = opts?.styles;
|
|
271
|
+
try {
|
|
272
|
+
const rawArgs = getRawArgs(opts?.argv);
|
|
273
|
+
// Resolve subcommand chain (with inferSubcommands and allowExternalSubcommands)
|
|
274
|
+
const { command, remainingArgs, parentNames, externalSubcommand } = resolveCommandChain(rootCommand, rawArgs);
|
|
275
|
+
// Handle external subcommand: pass to parent command's run handler
|
|
276
|
+
if (externalSubcommand) {
|
|
277
|
+
if (command.run) {
|
|
278
|
+
await runCommand(command, {}, remainingArgs, externalSubcommand);
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
// Merge global args from parent into resolved command
|
|
283
|
+
const globalArgs = collectGlobalArgs(rootCommand);
|
|
284
|
+
const effectiveCommand = {
|
|
285
|
+
...command,
|
|
286
|
+
args: command.args ? mergeGlobalArgs(globalArgs, command.args) : globalArgs,
|
|
287
|
+
};
|
|
288
|
+
// Parse remaining args against the resolved command
|
|
289
|
+
const parseResult = parseArgs(remainingArgs, effectiveCommand);
|
|
290
|
+
// Handle --help
|
|
291
|
+
if (parseResult.helpRequested) {
|
|
292
|
+
handleHelpRequest(effectiveCommand, parentNames, shouldExit, parseResult.helpIsShort, styles);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// Handle --version
|
|
296
|
+
if (parseResult.versionRequested) {
|
|
297
|
+
handleVersionRequest(effectiveCommand, rootCommand, shouldExit);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
// Show help if no args and command has subcommands
|
|
301
|
+
if (showHelpOnEmpty &&
|
|
302
|
+
rawArgs.length === 0 &&
|
|
303
|
+
rootCommand.subCommands &&
|
|
304
|
+
Object.keys(rootCommand.subCommands).length > 0) {
|
|
305
|
+
handleHelpRequest(rootCommand, [], shouldExit, false, styles);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
// argRequiredElseHelp: show help if no args were explicitly provided
|
|
309
|
+
if (command.meta.argRequiredElseHelp && parseResult.explicitlySet.size === 0) {
|
|
310
|
+
handleHelpRequest(command, parentNames, shouldExit, false, styles);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
// If we resolved to a parent command that has subcommands but no run handler,
|
|
314
|
+
// and the user didn't pass a valid subcommand, show help or error
|
|
315
|
+
if (command.subCommands &&
|
|
316
|
+
Object.keys(command.subCommands).length > 0 &&
|
|
317
|
+
!command.run &&
|
|
318
|
+
!parseResult.subCommand) {
|
|
319
|
+
// subcommandRequired: error if no subcommand
|
|
320
|
+
if (command.meta.subcommandRequired) {
|
|
321
|
+
if (parseResult.positionals.length > 0) {
|
|
322
|
+
handleUnrecognizedSubcommand(parseResult.positionals[0], command, parentNames, shouldExit, styles);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
showError("a subcommand is required but one was not provided", command, parentNames.length > 0 ? parentNames : undefined, styles);
|
|
326
|
+
if (shouldExit) {
|
|
327
|
+
process.exit(2);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (parseResult.positionals.length > 0) {
|
|
333
|
+
handleUnrecognizedSubcommand(parseResult.positionals[0], command, parentNames, shouldExit, styles);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
handleHelpRequest(command, parentNames, shouldExit, false, styles);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
// argsConflictsWithSubcommands: error if args + subcommand both present
|
|
340
|
+
if (command.meta.argsConflictsWithSubcommands &&
|
|
341
|
+
parseResult.subCommand &&
|
|
342
|
+
parseResult.explicitlySet.size > 0) {
|
|
343
|
+
showError("arguments cannot be used with subcommands", command, parentNames.length > 0 ? parentNames : undefined, styles);
|
|
344
|
+
if (shouldExit) {
|
|
345
|
+
process.exit(2);
|
|
346
|
+
}
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
// Validate parsed args
|
|
350
|
+
validate(parseResult, effectiveCommand);
|
|
351
|
+
// Run the command
|
|
352
|
+
await runCommand(effectiveCommand, parseResult.args, rawArgs);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
if (error instanceof CliParseError) {
|
|
356
|
+
showError(error.message, rootCommand, undefined, styles);
|
|
357
|
+
if (shouldExit) {
|
|
358
|
+
process.exit(2);
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// Unexpected error
|
|
363
|
+
if (shouldExit) {
|
|
364
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
365
|
+
process.stderr.write(`error: ${message}\n`);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for the clap-ts CLI framework.
|
|
3
|
+
* Matches clap's feature set with TypeScript type safety.
|
|
4
|
+
*/
|
|
5
|
+
/** Argument value type - matches clap's value_parser types. */
|
|
6
|
+
export type ArgType = 'boolean' | 'string' | 'number' | 'enum' | 'positional';
|
|
7
|
+
/** How an argument collects values. */
|
|
8
|
+
export type ArgAction = 'set' | 'append' | 'count';
|
|
9
|
+
/** Min/max constraint for number of values an argument accepts. */
|
|
10
|
+
export interface NumArgs {
|
|
11
|
+
readonly min: number;
|
|
12
|
+
readonly max: number;
|
|
13
|
+
}
|
|
14
|
+
/** Custom value parser function. Receives raw string, returns parsed value or throws. */
|
|
15
|
+
export type ValueParserFn = (value: string) => unknown;
|
|
16
|
+
/** Full argument definition - matches clap::Arg. */
|
|
17
|
+
export interface ArgDef {
|
|
18
|
+
/** Value type for this argument. */
|
|
19
|
+
readonly type: ArgType;
|
|
20
|
+
/** Human-readable description shown in help. */
|
|
21
|
+
readonly description?: string;
|
|
22
|
+
/** Short flag character (e.g., 'v' for -v). */
|
|
23
|
+
readonly short?: string;
|
|
24
|
+
/** Long flag name (e.g., 'verbose' for --verbose). Defaults to the arg key. */
|
|
25
|
+
readonly long?: string;
|
|
26
|
+
/** Additional aliases (hidden from help). Single-char treated as short, multi-char as long. */
|
|
27
|
+
readonly alias?: readonly string[];
|
|
28
|
+
/** Visible aliases shown in help output. Registered as working aliases at parse time. */
|
|
29
|
+
readonly visibleAlias?: readonly string[];
|
|
30
|
+
/** Default value when the argument is not provided. */
|
|
31
|
+
readonly default?: string | number | boolean | readonly string[];
|
|
32
|
+
/** Value to use when the flag is present but no value given (e.g., --port vs --port=8080). */
|
|
33
|
+
readonly defaultMissingValue?: string | number | boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Conditional default: [otherArgName, otherArgValue, defaultValue].
|
|
36
|
+
* If the other arg equals the given value, this default is applied.
|
|
37
|
+
*/
|
|
38
|
+
readonly defaultValueIf?: readonly [string, string, string | number | boolean];
|
|
39
|
+
/** Whether this argument is required. */
|
|
40
|
+
readonly required?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Required unless the named arg(s) are present.
|
|
43
|
+
* Overrides `required: true` when the specified arg(s) are set.
|
|
44
|
+
*/
|
|
45
|
+
readonly requiredUnlessPresent?: string | readonly string[];
|
|
46
|
+
/**
|
|
47
|
+
* Required if another arg equals a specific value: [argName, argValue].
|
|
48
|
+
* Makes this arg required when the condition is met.
|
|
49
|
+
*/
|
|
50
|
+
readonly requiredIfEq?: readonly [string, string];
|
|
51
|
+
/** Cannot be used with ANY other argument. */
|
|
52
|
+
readonly exclusive?: boolean;
|
|
53
|
+
/** Global arg -- inherited by all subcommands. */
|
|
54
|
+
readonly global?: boolean;
|
|
55
|
+
/** Environment variable fallback (checked if arg not provided on CLI). */
|
|
56
|
+
readonly env?: string;
|
|
57
|
+
/** Display name for the value in help (e.g., "PATH", "PORT"). */
|
|
58
|
+
readonly valueName?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Value validation/parsing. Either:
|
|
61
|
+
* - A string array of allowed values (enum-like restriction), or
|
|
62
|
+
* - A function that parses/validates the raw string value (throw to reject).
|
|
63
|
+
*/
|
|
64
|
+
readonly valueParser?: readonly string[] | ValueParserFn;
|
|
65
|
+
/** Character to split values on (e.g., ',' for --tags=a,b,c). */
|
|
66
|
+
readonly valueDelimiter?: string;
|
|
67
|
+
/** Min/max number of values this arg accepts. */
|
|
68
|
+
readonly numArgs?: NumArgs;
|
|
69
|
+
/** Names of args that conflict with this one (mutually exclusive). */
|
|
70
|
+
readonly conflictsWith?: readonly string[];
|
|
71
|
+
/** Names of args that must also be present when this one is used. */
|
|
72
|
+
readonly requires?: readonly string[];
|
|
73
|
+
/** How values are collected: set (replace), append (collect into array), count. */
|
|
74
|
+
readonly action?: ArgAction;
|
|
75
|
+
/** Hide this argument from all help output. */
|
|
76
|
+
readonly hidden?: boolean;
|
|
77
|
+
/** Hide this argument from short help (-h) only. */
|
|
78
|
+
readonly hideShortHelp?: boolean;
|
|
79
|
+
/** Hide this argument from long help (--help) only. */
|
|
80
|
+
readonly hideLongHelp?: boolean;
|
|
81
|
+
/** Hide possible values list from help (when valueParser is string[]). */
|
|
82
|
+
readonly hidePossibleValues?: boolean;
|
|
83
|
+
/** Description for the --no-X variant of boolean flags. */
|
|
84
|
+
readonly negativeDescription?: string;
|
|
85
|
+
/** Accept values that start with a hyphen (e.g., --grep -pattern). */
|
|
86
|
+
readonly allowHyphenValues?: boolean;
|
|
87
|
+
/** Accept negative numbers as values (e.g., --offset -10). */
|
|
88
|
+
readonly allowNegativeNumbers?: boolean;
|
|
89
|
+
/** Mark as trailing var arg -- last positional consumes all remaining args. */
|
|
90
|
+
readonly trailingVarArg?: boolean;
|
|
91
|
+
/** Positional that requires -- before it (like clap's last()). */
|
|
92
|
+
readonly last?: boolean;
|
|
93
|
+
/** Custom section heading in help output (groups args under this heading). */
|
|
94
|
+
readonly helpHeading?: string;
|
|
95
|
+
}
|
|
96
|
+
/** Record of argument name to definition. */
|
|
97
|
+
export type ArgsDef = Record<string, ArgDef>;
|
|
98
|
+
/** Style function that applies formatting to a string. */
|
|
99
|
+
export type StyleFn = (s: string) => string;
|
|
100
|
+
/** Customizable style definitions for help and error output. */
|
|
101
|
+
export interface StylesDef {
|
|
102
|
+
/** Bold text (used for error prefix). */
|
|
103
|
+
readonly bold: StyleFn;
|
|
104
|
+
/** Yellow text. */
|
|
105
|
+
readonly yellow: StyleFn;
|
|
106
|
+
/** Green text. */
|
|
107
|
+
readonly green: StyleFn;
|
|
108
|
+
/** Cyan text. */
|
|
109
|
+
readonly cyan: StyleFn;
|
|
110
|
+
/** Section headings (e.g., "Usage:", "Options:"). */
|
|
111
|
+
readonly heading: StyleFn;
|
|
112
|
+
/** Flag names (e.g., --verbose, -v). */
|
|
113
|
+
readonly flag: StyleFn;
|
|
114
|
+
/** Value placeholders (e.g., <PORT>, <ENV>). */
|
|
115
|
+
readonly value: StyleFn;
|
|
116
|
+
/** Command/subcommand names. */
|
|
117
|
+
readonly command: StyleFn;
|
|
118
|
+
}
|
|
119
|
+
/** Command metadata - matches clap::Command attributes. */
|
|
120
|
+
export interface CommandMeta {
|
|
121
|
+
/** Command name (used in usage line). */
|
|
122
|
+
readonly name: string;
|
|
123
|
+
/** Version string (shown with --version). */
|
|
124
|
+
readonly version?: string;
|
|
125
|
+
/** Short description (one line, shown in parent's subcommand list). */
|
|
126
|
+
readonly description?: string;
|
|
127
|
+
/** Longer "about" text (shown at top of this command's help). */
|
|
128
|
+
readonly about?: string;
|
|
129
|
+
/** Extended help text (shown with --help, not -h). */
|
|
130
|
+
readonly longAbout?: string;
|
|
131
|
+
/** Text prepended before the help output. */
|
|
132
|
+
readonly beforeHelp?: string;
|
|
133
|
+
/** Text appended after the help output. */
|
|
134
|
+
readonly afterHelp?: string;
|
|
135
|
+
/** Hide this command from parent's help subcommand list. */
|
|
136
|
+
readonly hidden?: boolean;
|
|
137
|
+
/** Visible aliases shown next to the command name in help. */
|
|
138
|
+
readonly aliases?: readonly string[];
|
|
139
|
+
/** Require a subcommand to be provided. */
|
|
140
|
+
readonly subcommandRequired?: boolean;
|
|
141
|
+
/** Accept partial subcommand names (e.g., 'ser' matches 'serve'). */
|
|
142
|
+
readonly inferSubcommands?: boolean;
|
|
143
|
+
/** Accept partial long arg names (e.g., '--verb' matches '--verbose'). */
|
|
144
|
+
readonly inferLongArgs?: boolean;
|
|
145
|
+
/** Parent args and subcommands are mutually exclusive. */
|
|
146
|
+
readonly argsConflictsWithSubcommands?: boolean;
|
|
147
|
+
/** Accept subcommands not defined in subCommands. Passed to parent run handler. */
|
|
148
|
+
readonly allowExternalSubcommands?: boolean;
|
|
149
|
+
/** When a subcommand is present, parent's required args are waived. */
|
|
150
|
+
readonly subcommandNegatesReqs?: boolean;
|
|
151
|
+
/** Show help if no arguments are provided (instead of running). */
|
|
152
|
+
readonly argRequiredElseHelp?: boolean;
|
|
153
|
+
/**
|
|
154
|
+
* Custom help template with placeholders:
|
|
155
|
+
* {name}, {version}, {about}, {usage}, {all-args}, {arguments},
|
|
156
|
+
* {options}, {commands}, {before-help}, {after-help}
|
|
157
|
+
*/
|
|
158
|
+
readonly helpTemplate?: string;
|
|
159
|
+
}
|
|
160
|
+
/** Argument group - for organizing related args (like clap's ArgGroup). */
|
|
161
|
+
export interface ArgGroup {
|
|
162
|
+
/** Group name (for display). */
|
|
163
|
+
readonly name: string;
|
|
164
|
+
/** Arg names in this group. */
|
|
165
|
+
readonly args: readonly string[];
|
|
166
|
+
/** Whether the group is required (at least one must be set). */
|
|
167
|
+
readonly required?: boolean;
|
|
168
|
+
/** Whether args in the group are mutually exclusive. */
|
|
169
|
+
readonly multiple?: boolean;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Infer the parsed type from an ArgDef.
|
|
173
|
+
* - boolean -> boolean
|
|
174
|
+
* - number -> number
|
|
175
|
+
* - string/enum/positional -> string
|
|
176
|
+
* - action: 'append' -> string[]
|
|
177
|
+
* - action: 'count' -> number
|
|
178
|
+
*/
|
|
179
|
+
export type InferArgValue<A extends ArgDef> = A['action'] extends 'append' ? string[] : A['action'] extends 'count' ? number : A['type'] extends 'boolean' ? boolean : A['type'] extends 'number' ? number : string;
|
|
180
|
+
/**
|
|
181
|
+
* Infer whether an arg is optional based on required/default.
|
|
182
|
+
* Args with `required: true` are always non-optional.
|
|
183
|
+
* Args with a default are always non-optional.
|
|
184
|
+
* All others may be undefined.
|
|
185
|
+
*/
|
|
186
|
+
export type InferArgOptional<A extends ArgDef, V> = A['required'] extends true ? V : A['default'] extends undefined ? V | undefined : V;
|
|
187
|
+
/** Map an ArgsDef record to parsed argument types. */
|
|
188
|
+
export type ParsedArgs<T extends ArgsDef> = {
|
|
189
|
+
[K in keyof T]: InferArgOptional<T[K], InferArgValue<T[K]>>;
|
|
190
|
+
};
|
|
191
|
+
/** Context passed to command hooks. */
|
|
192
|
+
export interface CommandContext<T extends ArgsDef = ArgsDef> {
|
|
193
|
+
/** Raw argv that was parsed. */
|
|
194
|
+
readonly rawArgs: readonly string[];
|
|
195
|
+
/** Parsed and validated arguments. */
|
|
196
|
+
readonly args: ParsedArgs<T>;
|
|
197
|
+
/** The command definition being executed. */
|
|
198
|
+
readonly cmd: CommandDef<T>;
|
|
199
|
+
/** Name of the resolved subcommand, if any. */
|
|
200
|
+
readonly subCommand?: string;
|
|
201
|
+
/** Arbitrary user data (for passing state between setup/run/cleanup). */
|
|
202
|
+
data: Record<string, unknown>;
|
|
203
|
+
}
|
|
204
|
+
/** Full command definition - matches clap::Command. */
|
|
205
|
+
export interface CommandDef<T extends ArgsDef = ArgsDef> {
|
|
206
|
+
/** Command metadata. */
|
|
207
|
+
readonly meta: CommandMeta;
|
|
208
|
+
/** Argument definitions. */
|
|
209
|
+
readonly args?: T;
|
|
210
|
+
/** Subcommand definitions (name -> command). */
|
|
211
|
+
readonly subCommands?: Record<string, CommandDef<any>>;
|
|
212
|
+
/** Argument groups for validation and help grouping. */
|
|
213
|
+
readonly groups?: readonly ArgGroup[];
|
|
214
|
+
/** Called before run. Return value is ignored; throw to abort. */
|
|
215
|
+
readonly setup?: (ctx: CommandContext<T>) => void | Promise<void>;
|
|
216
|
+
/** Main command handler. */
|
|
217
|
+
readonly run?: (ctx: CommandContext<T>) => void | Promise<void>;
|
|
218
|
+
/** Called after run (even on error). */
|
|
219
|
+
readonly cleanup?: (ctx: CommandContext<T>) => void | Promise<void>;
|
|
220
|
+
}
|
|
221
|
+
/** Options for runMain / runCommand. */
|
|
222
|
+
export interface RunOptions {
|
|
223
|
+
/** Override argv (defaults to Bun.argv / process.argv). */
|
|
224
|
+
readonly argv?: readonly string[];
|
|
225
|
+
/** Exit process on error (default: true). */
|
|
226
|
+
readonly exit?: boolean;
|
|
227
|
+
/** Show help on empty args when command has subcommands (default: true). */
|
|
228
|
+
readonly showHelpOnEmpty?: boolean;
|
|
229
|
+
/** Custom styles for help and error output. */
|
|
230
|
+
readonly styles?: Partial<StylesDef>;
|
|
231
|
+
}
|
|
232
|
+
/** Result of parsing arguments. */
|
|
233
|
+
export interface ParseResult {
|
|
234
|
+
/** Parsed argument values (key -> value). */
|
|
235
|
+
readonly args: Record<string, string | number | boolean | string[]>;
|
|
236
|
+
/** Positional arguments in order. */
|
|
237
|
+
readonly positionals: readonly string[];
|
|
238
|
+
/** Arguments after -- separator. */
|
|
239
|
+
readonly rest: readonly string[];
|
|
240
|
+
/** The subcommand name if one was matched. */
|
|
241
|
+
readonly subCommand?: string;
|
|
242
|
+
/** Whether --help / -h was requested. */
|
|
243
|
+
readonly helpRequested: boolean;
|
|
244
|
+
/** Whether -h (short) was used vs --help (long). */
|
|
245
|
+
readonly helpIsShort: boolean;
|
|
246
|
+
/** Whether --version / -V was requested. */
|
|
247
|
+
readonly versionRequested: boolean;
|
|
248
|
+
/** Unknown flags that were passed. */
|
|
249
|
+
readonly unknown: readonly string[];
|
|
250
|
+
/** Set of arg keys that were explicitly provided (not defaults or env). */
|
|
251
|
+
readonly explicitlySet: ReadonlySet<string>;
|
|
252
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument validation - enforces constraints after parsing.
|
|
3
|
+
* Matches clap's validation: required, exclusive, conflicts, requires,
|
|
4
|
+
* valueParser, numArgs, requiredUnlessPresent, requiredIfEq, groups.
|
|
5
|
+
* Includes typo suggestion via Levenshtein distance.
|
|
6
|
+
*/
|
|
7
|
+
import type { CommandDef, ParseResult } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Validate parsed results against the command definition.
|
|
10
|
+
* Throws CliParseError with clap-style error messages.
|
|
11
|
+
*/
|
|
12
|
+
export declare function validate(parseResult: ParseResult, command: CommandDef): void;
|