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/dist/help.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Help renderer - generates clap-style help output.
3
+ * Respects NO_COLOR, TERM=dumb, CI for color output.
4
+ * Wraps text to terminal width.
5
+ * Supports helpHeading grouping, helpTemplate, beforeHelp,
6
+ * hideShortHelp/hideLongHelp, visibleAlias, hidePossibleValues,
7
+ * and custom styles.
8
+ */
9
+ import type { CommandDef, CommandMeta, StylesDef } from './types.js';
10
+ /**
11
+ * Render the full help text for a command.
12
+ * Matches clap's help format. Supports helpTemplate override,
13
+ * helpHeading grouping, beforeHelp, and help mode filtering.
14
+ */
15
+ export declare function renderHelp(command: CommandDef, parentNames?: string[], isShortHelp?: boolean, styleOverrides?: Partial<StylesDef>): string;
16
+ /**
17
+ * Render a short usage message (shown on errors).
18
+ */
19
+ export declare function renderUsage(command: CommandDef, parentNames?: string[], styleOverrides?: Partial<StylesDef>): string;
20
+ /**
21
+ * Print help to stdout.
22
+ */
23
+ export declare function showHelp(command: CommandDef, parentNames?: string[], isShortHelp?: boolean, styleOverrides?: Partial<StylesDef>): void;
24
+ /**
25
+ * Print version to stdout.
26
+ */
27
+ export declare function showVersion(meta: CommandMeta): void;
28
+ /**
29
+ * Print an error message with usage hint.
30
+ */
31
+ export declare function showError(message: string, command: CommandDef, parentNames?: string[], styleOverrides?: Partial<StylesDef>): void;
package/dist/help.js ADDED
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Help renderer - generates clap-style help output.
3
+ * Respects NO_COLOR, TERM=dumb, CI for color output.
4
+ * Wraps text to terminal width.
5
+ * Supports helpHeading grouping, helpTemplate, beforeHelp,
6
+ * hideShortHelp/hideLongHelp, visibleAlias, hidePossibleValues,
7
+ * and custom styles.
8
+ */
9
+ import { styleText } from 'node:util';
10
+ // ---- Color Support ----
11
+ /** Get terminal width, defaulting to 80 if not available. */
12
+ function getTerminalWidth() {
13
+ if (typeof process.stdout?.columns === 'number' && process.stdout.columns > 0) {
14
+ return process.stdout.columns;
15
+ }
16
+ return 80;
17
+ }
18
+ /** Create style functions, merging optional user overrides. */
19
+ function createStyles(overrides) {
20
+ const defaults = {
21
+ bold: (s) => styleText('bold', s),
22
+ yellow: (s) => styleText('yellow', s),
23
+ green: (s) => styleText('green', s),
24
+ cyan: (s) => styleText('cyan', s),
25
+ heading: (s) => styleText(['bold', 'yellow'], s),
26
+ flag: (s) => styleText('green', s),
27
+ value: (s) => styleText('cyan', s),
28
+ command: (s) => styleText('bold', s),
29
+ };
30
+ if (!overrides) {
31
+ return defaults;
32
+ }
33
+ return { ...defaults, ...overrides };
34
+ }
35
+ // ---- Help Text Helpers ----
36
+ /** Wrap text to fit within a given width, preserving leading indent. */
37
+ function wrapText(text, maxWidth, indent) {
38
+ if (text.length + indent <= maxWidth) {
39
+ return text;
40
+ }
41
+ const words = text.split(/\s+/);
42
+ const lines = [];
43
+ let currentLine = '';
44
+ const padding = ' '.repeat(indent);
45
+ for (const word of words) {
46
+ if (currentLine.length === 0) {
47
+ currentLine = word;
48
+ }
49
+ else if (currentLine.length + 1 + word.length + indent <= maxWidth) {
50
+ currentLine += ` ${word}`;
51
+ }
52
+ else {
53
+ lines.push(currentLine);
54
+ currentLine = word;
55
+ }
56
+ }
57
+ if (currentLine.length > 0) {
58
+ lines.push(currentLine);
59
+ }
60
+ return lines.join(`\n${padding}`);
61
+ }
62
+ /** Format a flag string for display: "-s, --long, --visible-alias <VALUE>" */
63
+ function formatArgFlag(key, def, styles) {
64
+ const parts = [];
65
+ const rawParts = [];
66
+ // Short flag
67
+ if (def.short) {
68
+ parts.push(styles.flag(`-${def.short}`));
69
+ rawParts.push(`-${def.short}`);
70
+ }
71
+ // Long flag
72
+ const longName = def.long ?? key;
73
+ if (def.short) {
74
+ parts.push(`, ${styles.flag(`--${longName}`)}`);
75
+ rawParts.push(`, --${longName}`);
76
+ }
77
+ else {
78
+ // Pad to align with flags that have short
79
+ parts.push(` ${styles.flag(`--${longName}`)}`);
80
+ rawParts.push(` --${longName}`);
81
+ }
82
+ // Visible aliases (shown in help, unlike hidden aliases)
83
+ if (def.visibleAlias) {
84
+ for (const alias of def.visibleAlias) {
85
+ if (alias.length === 1) {
86
+ parts.push(`, ${styles.flag(`-${alias}`)}`);
87
+ rawParts.push(`, -${alias}`);
88
+ }
89
+ else {
90
+ parts.push(`, ${styles.flag(`--${alias}`)}`);
91
+ rawParts.push(`, --${alias}`);
92
+ }
93
+ }
94
+ }
95
+ // Value placeholder
96
+ if (def.type !== 'boolean' || def.valueName) {
97
+ const valueName = def.valueName ?? def.type.toUpperCase();
98
+ if (def.numArgs && def.numArgs.min === 0) {
99
+ parts.push(` ${styles.value(`[${valueName}]`)}`);
100
+ rawParts.push(` [${valueName}]`);
101
+ }
102
+ else {
103
+ parts.push(` ${styles.value(`<${valueName}>`)}`);
104
+ rawParts.push(` <${valueName}>`);
105
+ }
106
+ }
107
+ return {
108
+ flag: parts.join(''),
109
+ rawLen: rawParts.join('').length,
110
+ };
111
+ }
112
+ /** Build the description suffix: [default: x] [env: VAR] [possible values: a, b] */
113
+ function formatArgSuffix(def) {
114
+ const suffixes = [];
115
+ if (def.default !== undefined && def.type !== 'boolean') {
116
+ const defaultStr = Array.isArray(def.default) ? def.default.join(', ') : String(def.default);
117
+ suffixes.push(`[default: ${defaultStr}]`);
118
+ }
119
+ if (def.env) {
120
+ suffixes.push(`[env: ${def.env}]`);
121
+ }
122
+ if (def.valueParser && Array.isArray(def.valueParser) && def.valueParser.length > 0 && !def.hidePossibleValues) {
123
+ suffixes.push(`[possible values: ${def.valueParser.join(', ')}]`);
124
+ }
125
+ if (def.required) {
126
+ suffixes.push('[required]');
127
+ }
128
+ return suffixes.length > 0 ? ` ${suffixes.join(' ')}` : '';
129
+ }
130
+ /** Append usage parts for options, positionals, and subcommands. */
131
+ function appendUsageParts(usageParts, command) {
132
+ const argsDef = command.args ?? {};
133
+ const hasOptions = Object.values(argsDef).some((d) => d.type !== 'positional' && !d.hidden);
134
+ const positionals = Object.entries(argsDef).filter(([_, d]) => d.type === 'positional');
135
+ const hasSubcommands = command.subCommands && Object.keys(command.subCommands).length > 0;
136
+ if (hasOptions) {
137
+ usageParts.push('[OPTIONS]');
138
+ }
139
+ for (const [key, def] of positionals) {
140
+ const name = def.valueName ?? key.toUpperCase();
141
+ if (def.required) {
142
+ usageParts.push(`<${name}>`);
143
+ }
144
+ else {
145
+ usageParts.push(`[${name}]`);
146
+ }
147
+ }
148
+ if (hasSubcommands) {
149
+ usageParts.push('[COMMAND]');
150
+ }
151
+ }
152
+ /** Check if an arg should be hidden based on help mode. */
153
+ function isArgHiddenForMode(def, isShortHelp) {
154
+ if (def.hidden) {
155
+ return true;
156
+ }
157
+ if (isShortHelp && def.hideShortHelp) {
158
+ return true;
159
+ }
160
+ if (!isShortHelp && def.hideLongHelp) {
161
+ return true;
162
+ }
163
+ return false;
164
+ }
165
+ /** Render aligned entries (flag + description with padding). */
166
+ function renderAlignedEntries(entries, termWidth, lines) {
167
+ if (entries.length === 0) {
168
+ return;
169
+ }
170
+ const maxLen = Math.max(...entries.map((e) => e.rawLen));
171
+ const descIndent = maxLen + 4;
172
+ for (const entry of entries) {
173
+ const padding = ' '.repeat(Math.max(2, descIndent - entry.rawLen));
174
+ const wrappedDesc = wrapText(entry.desc, termWidth, descIndent + 2);
175
+ lines.push(`${entry.label}${padding}${wrappedDesc}`);
176
+ }
177
+ }
178
+ // ---- Template Rendering ----
179
+ /**
180
+ * Render help using a custom template with placeholders.
181
+ * Placeholders: {name}, {version}, {about}, {usage}, {all-args},
182
+ * {arguments}, {options}, {commands}, {before-help}, {after-help}
183
+ */
184
+ function renderHelpTemplate(template, command, styles, termWidth, fullName, isShortHelp) {
185
+ const { meta } = command;
186
+ const argsDef = command.args ?? {};
187
+ // Build each section as a string
188
+ const usageParts = [styles.heading('Usage:'), styles.command(fullName)];
189
+ appendUsageParts(usageParts, command);
190
+ const usageStr = usageParts.join(' ');
191
+ const argsLines = [];
192
+ renderPositionalSection(argsLines, argsDef, styles, termWidth, isShortHelp);
193
+ const argumentsStr = argsLines.join('\n');
194
+ const optLines = [];
195
+ renderOptionsSection(optLines, argsDef, meta, styles, termWidth, isShortHelp);
196
+ const optionsStr = optLines.join('\n');
197
+ const cmdLines = [];
198
+ if (command.subCommands && Object.keys(command.subCommands).length > 0) {
199
+ renderSubcommandSection(cmdLines, command, styles, termWidth, fullName);
200
+ }
201
+ const commandsStr = cmdLines.join('\n');
202
+ return template
203
+ .replaceAll('{name}', meta.name)
204
+ .replaceAll('{version}', meta.version ?? '')
205
+ .replaceAll('{about}', meta.about ?? meta.description ?? '')
206
+ .replaceAll('{usage}', usageStr)
207
+ .replaceAll('{all-args}', [argumentsStr, optionsStr].filter(Boolean).join('\n'))
208
+ .replaceAll('{arguments}', argumentsStr)
209
+ .replaceAll('{options}', optionsStr)
210
+ .replaceAll('{commands}', commandsStr)
211
+ .replaceAll('{before-help}', meta.beforeHelp ?? '')
212
+ .replaceAll('{after-help}', meta.afterHelp ?? '');
213
+ }
214
+ // ---- Section Renderers (extracted for reuse) ----
215
+ /** Render positional arguments section. */
216
+ function renderPositionalSection(lines, argsDef, styles, termWidth, isShortHelp) {
217
+ const positionals = Object.entries(argsDef).filter(([_, d]) => d.type === 'positional');
218
+ const visiblePositionals = positionals.filter(([_, d]) => !isArgHiddenForMode(d, isShortHelp));
219
+ if (visiblePositionals.length === 0) {
220
+ return;
221
+ }
222
+ lines.push(styles.heading('Arguments:'));
223
+ const entries = [];
224
+ for (const [key, def] of visiblePositionals) {
225
+ const name = def.valueName ?? key.toUpperCase();
226
+ const label = ` ${styles.value(`<${name}>`)}`;
227
+ const rawLen = name.length + 4;
228
+ const desc = (def.description ?? '') + formatArgSuffix(def);
229
+ entries.push({ label, rawLen, desc });
230
+ }
231
+ renderAlignedEntries(entries, termWidth, lines);
232
+ }
233
+ /** Render options section, grouped by helpHeading. */
234
+ function renderOptionsSection(lines, argsDef, meta, styles, termWidth, isShortHelp) {
235
+ const options = Object.entries(argsDef).filter(([_, d]) => d.type !== 'positional' && !isArgHiddenForMode(d, isShortHelp));
236
+ if (options.length === 0) {
237
+ return;
238
+ }
239
+ // Group options by helpHeading
240
+ const groups = new Map();
241
+ const defaultHeading = 'Options';
242
+ for (const entry of options) {
243
+ const heading = entry[1].helpHeading ?? defaultHeading;
244
+ let group = groups.get(heading);
245
+ if (!group) {
246
+ group = [];
247
+ groups.set(heading, group);
248
+ }
249
+ group.push(entry);
250
+ }
251
+ for (const [heading, groupOptions] of groups) {
252
+ lines.push('');
253
+ lines.push(styles.heading(`${heading}:`));
254
+ const entries = [];
255
+ for (const [key, def] of groupOptions) {
256
+ const { flag, rawLen } = formatArgFlag(key, def, styles);
257
+ const desc = (def.description ?? '') + formatArgSuffix(def);
258
+ entries.push({ label: ` ${flag}`, rawLen: rawLen + 2, desc });
259
+ // Boolean negation: --no-flag
260
+ if (def.type === 'boolean' && def.negativeDescription) {
261
+ const longName = def.long ?? key;
262
+ const negFlag = ` ${styles.flag(`--no-${longName}`)}`;
263
+ const negRawLen = longName.length + 10;
264
+ entries.push({
265
+ label: ` ${negFlag}`,
266
+ rawLen: negRawLen + 2,
267
+ desc: def.negativeDescription,
268
+ });
269
+ }
270
+ }
271
+ // Add built-in --help and --version to the default "Options" group
272
+ if (heading === defaultHeading) {
273
+ const helpFlag = ` ${styles.flag('-h')}, ${styles.flag('--help')}`;
274
+ entries.push({ label: helpFlag, rawLen: 14, desc: 'Print help' });
275
+ if (meta.version) {
276
+ const versionFlag = ` ${styles.flag('-V')}, ${styles.flag('--version')}`;
277
+ entries.push({ label: versionFlag, rawLen: 17, desc: 'Print version' });
278
+ }
279
+ }
280
+ renderAlignedEntries(entries, termWidth, lines);
281
+ }
282
+ }
283
+ // ---- Main Renderer ----
284
+ /**
285
+ * Render the full help text for a command.
286
+ * Matches clap's help format. Supports helpTemplate override,
287
+ * helpHeading grouping, beforeHelp, and help mode filtering.
288
+ */
289
+ export function renderHelp(command, parentNames, isShortHelp = false, styleOverrides) {
290
+ const { meta } = command;
291
+ const styles = createStyles(styleOverrides);
292
+ const termWidth = getTerminalWidth();
293
+ const fullName = parentNames ? [...parentNames, meta.name].join(' ') : meta.name;
294
+ // Custom template override
295
+ if (meta.helpTemplate) {
296
+ return renderHelpTemplate(meta.helpTemplate, command, styles, termWidth, fullName, isShortHelp);
297
+ }
298
+ const lines = [];
299
+ // Before help text
300
+ if (meta.beforeHelp) {
301
+ lines.push(meta.beforeHelp);
302
+ lines.push('');
303
+ }
304
+ // Header: "Description (name vX.Y.Z)"
305
+ const nameVersion = meta.version ? `${meta.name} v${meta.version}` : meta.name;
306
+ const headerDesc = meta.about ?? meta.description ?? '';
307
+ if (headerDesc) {
308
+ lines.push(`${headerDesc} (${nameVersion})`);
309
+ }
310
+ else {
311
+ lines.push(nameVersion);
312
+ }
313
+ // Long about (if any, only in long help mode)
314
+ if (meta.longAbout && !isShortHelp) {
315
+ lines.push('');
316
+ lines.push(meta.longAbout);
317
+ }
318
+ lines.push('');
319
+ // Usage line
320
+ const usageParts = [styles.heading('Usage:'), styles.command(fullName)];
321
+ appendUsageParts(usageParts, command);
322
+ lines.push(usageParts.join(' '));
323
+ // Positional arguments
324
+ const argsDef = command.args ?? {};
325
+ const posLines = [];
326
+ renderPositionalSection(posLines, argsDef, styles, termWidth, isShortHelp);
327
+ if (posLines.length > 0) {
328
+ lines.push('');
329
+ lines.push(...posLines);
330
+ }
331
+ // Options (grouped by helpHeading)
332
+ const optLines = [];
333
+ renderOptionsSection(optLines, argsDef, meta, styles, termWidth, isShortHelp);
334
+ lines.push(...optLines);
335
+ // Subcommands
336
+ const hasSubcommands = command.subCommands && Object.keys(command.subCommands).length > 0;
337
+ if (hasSubcommands) {
338
+ renderSubcommandSection(lines, command, styles, termWidth, fullName);
339
+ }
340
+ // After help
341
+ if (meta.afterHelp) {
342
+ lines.push('');
343
+ lines.push(meta.afterHelp);
344
+ }
345
+ lines.push('');
346
+ return lines.join('\n');
347
+ }
348
+ /** Render the subcommands section of help output. */
349
+ function renderSubcommandSection(lines, command, styles, termWidth, fullName) {
350
+ lines.push('');
351
+ lines.push(styles.heading('Commands:'));
352
+ const subEntries = [];
353
+ const rendered = new Set();
354
+ for (const [name, def] of Object.entries(command.subCommands)) {
355
+ if (def.meta.hidden) {
356
+ continue;
357
+ }
358
+ if (rendered.has(name)) {
359
+ continue;
360
+ }
361
+ rendered.add(name);
362
+ let label;
363
+ let rawLen;
364
+ const { aliases } = def.meta;
365
+ if (aliases && aliases.length > 0) {
366
+ const aliasStr = aliases.join(', ');
367
+ label = ` ${styles.command(name)} (${aliasStr})`;
368
+ rawLen = name.length + aliasStr.length + 5;
369
+ }
370
+ else {
371
+ label = ` ${styles.command(name)}`;
372
+ rawLen = name.length + 2;
373
+ }
374
+ const desc = def.meta.description ?? '';
375
+ subEntries.push({ label, rawLen, desc });
376
+ }
377
+ renderAlignedEntries(subEntries, termWidth, lines);
378
+ lines.push('');
379
+ lines.push(`Use ${styles.command(`${fullName} <command> --help`)} for more information about a command.`);
380
+ }
381
+ /**
382
+ * Render a short usage message (shown on errors).
383
+ */
384
+ export function renderUsage(command, parentNames, styleOverrides) {
385
+ const { meta } = command;
386
+ const styles = createStyles(styleOverrides);
387
+ const fullName = parentNames ? [...parentNames, meta.name].join(' ') : meta.name;
388
+ const usageParts = [styles.heading('Usage:'), styles.command(fullName)];
389
+ appendUsageParts(usageParts, command);
390
+ return usageParts.join(' ');
391
+ }
392
+ /**
393
+ * Print help to stdout.
394
+ */
395
+ export function showHelp(command, parentNames, isShortHelp = false, styleOverrides) {
396
+ const text = renderHelp(command, parentNames, isShortHelp, styleOverrides);
397
+ process.stdout.write(text);
398
+ }
399
+ /**
400
+ * Print version to stdout.
401
+ */
402
+ export function showVersion(meta) {
403
+ const version = meta.version ?? '0.0.0';
404
+ process.stdout.write(`${meta.name} ${version}\n`);
405
+ }
406
+ /**
407
+ * Print an error message with usage hint.
408
+ */
409
+ export function showError(message, command, parentNames, styleOverrides) {
410
+ const styles = createStyles(styleOverrides);
411
+ const usage = renderUsage(command, parentNames, styleOverrides);
412
+ const output = `${styles.bold('error:')} ${message}\n\n${usage}\n\nFor more information, try '${styles.flag('--help')}'.\n`;
413
+ process.stderr.write(output);
414
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * clap-ts - A type-safe CLI argument parser for TypeScript, inspired by Rust's clap.
3
+ *
4
+ * Re-exports all public API.
5
+ */
6
+ export type { ArgType, ArgAction, NumArgs, ValueParserFn, ArgDef, ArgsDef, StyleFn, StylesDef, CommandMeta, ArgGroup, ParsedArgs, CommandContext, CommandDef, RunOptions, ParseResult, InferArgValue, InferArgOptional, } from './types.js';
7
+ export { parseArgs, getRawArgs, collectGlobalArgs, mergeGlobalArgs, CliParseError, } from './parser.js';
8
+ export { validate } from './validation.js';
9
+ export { renderHelp, renderUsage, showHelp, showVersion, showError } from './help.js';
10
+ export { defineCommand, defineArgs, defineArg, runCommand, runMain } from './runner.js';
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * clap-ts - A type-safe CLI argument parser for TypeScript, inspired by Rust's clap.
3
+ *
4
+ * Re-exports all public API.
5
+ */
6
+ // Parser
7
+ export { parseArgs, getRawArgs, collectGlobalArgs, mergeGlobalArgs, CliParseError, } from './parser.js';
8
+ // Validation
9
+ export { validate } from './validation.js';
10
+ // Help renderer
11
+ export { renderHelp, renderUsage, showHelp, showVersion, showError } from './help.js';
12
+ // Runner (main API)
13
+ export { defineCommand, defineArgs, defineArg, runCommand, runMain } from './runner.js';
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Argument parser - delegates core tokenizing to node:util parseArgs,
3
+ * then layers on: env fallback, type coercion, count/append actions,
4
+ * numArgs with defaultMissingValue, global args, kebab-to-camel mapping,
5
+ * subcommand detection, and default values.
6
+ *
7
+ * node:util parseArgs handles:
8
+ * --flag, --flag=value, --flag value, -f, -fvalue, -abc (combined booleans),
9
+ * -- separator, positionals
10
+ *
11
+ * We handle on top:
12
+ * conflictsWith / requires (in validation.ts),
13
+ * env variable fallback, action: 'append' (via multiple:true), action: 'count',
14
+ * numArgs with defaultMissingValue, global args merge, valueParser enum validation
15
+ * (in validation.ts), number type coercion, kebab-to-camel mapping,
16
+ * required arg validation (in validation.ts), typo suggestions (in validation.ts),
17
+ * valueDelimiter splitting, function valueParser, trailingVarArg, last,
18
+ * allowHyphenValues, allowNegativeNumbers, inferLongArgs, defaultValueIf.
19
+ */
20
+ import type { ArgsDef, ParseResult, CommandDef } from './types.js';
21
+ /** Get the raw argv slice (after the binary/script path). */
22
+ export declare function getRawArgs(argv?: readonly string[]): string[];
23
+ export declare class CliParseError extends Error {
24
+ constructor(message: string);
25
+ }
26
+ /**
27
+ * Parse raw argument tokens against a command definition.
28
+ *
29
+ * Uses node:util parseArgs for core tokenizing, then layers on all clap-ts features.
30
+ */
31
+ export declare function parseArgs(rawArgs: readonly string[], command: CommandDef): ParseResult;
32
+ /**
33
+ * Collect global args from a command.
34
+ * Returns a merged ArgsDef of all global args.
35
+ */
36
+ export declare function collectGlobalArgs(command: CommandDef): ArgsDef;
37
+ /**
38
+ * Merge global args into a subcommand's args.
39
+ * Global args from the parent are added to the child unless the child
40
+ * already defines an arg with the same name.
41
+ */
42
+ export declare function mergeGlobalArgs(parentGlobals: ArgsDef, childArgs: ArgsDef): ArgsDef;