clap-ts 0.2.0 → 0.3.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.js CHANGED
@@ -7,25 +7,58 @@
7
7
  * and custom styles.
8
8
  */
9
9
  import { styleText } from 'node:util';
10
+ import { hasSubCommands, possibleValues, subCommandsOf } from './parser.js';
10
11
  // ---- Color Support ----
11
- /** Get terminal width, defaulting to 80 if not available. */
12
- function getTerminalWidth() {
12
+ /** Get terminal width, honouring termWidth and maxTermWidth, defaulting to 80. */
13
+ function getTerminalWidth(meta) {
14
+ if (meta.termWidth !== undefined && meta.termWidth > 0) {
15
+ return meta.termWidth;
16
+ }
17
+ let width = 80;
13
18
  if (typeof process.stdout?.columns === 'number' && process.stdout.columns > 0) {
14
- return process.stdout.columns;
19
+ width = process.stdout.columns;
20
+ }
21
+ if (meta.maxTermWidth !== undefined && meta.maxTermWidth > 0) {
22
+ width = Math.min(width, meta.maxTermWidth);
15
23
  }
16
- return 80;
24
+ return width;
17
25
  }
18
26
  /** Create style functions, merging optional user overrides. */
19
- function createStyles(overrides) {
27
+ const NO_STYLE = {
28
+ bold: (s) => s,
29
+ yellow: (s) => s,
30
+ green: (s) => s,
31
+ cyan: (s) => s,
32
+ heading: (s) => s,
33
+ flag: (s) => s,
34
+ value: (s) => s,
35
+ command: (s) => s,
36
+ };
37
+ /** Whether colour is on for this command: 'never' off, 'always' forced, else auto. */
38
+ function colourEnabled(meta) {
39
+ if (meta.color === 'never' || meta.disableColoredHelp === true) {
40
+ return 'off';
41
+ }
42
+ return meta.color === 'always' ? 'force' : 'auto';
43
+ }
44
+ function createStyles(overrides, mode = 'auto') {
45
+ if (mode === 'off') {
46
+ return overrides ? { ...NO_STYLE, ...overrides } : NO_STYLE;
47
+ }
48
+ // styleText only colourises when it judges the stream capable; `always` goes
49
+ // around that check with the codes it would have used.
50
+ const paint = mode === 'force'
51
+ ? (codes, text) => styleText(codes, text, { validateStream: false })
52
+ : (codes, text) => styleText(codes, text);
20
53
  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),
54
+ bold: (s) => paint('bold', s),
55
+ yellow: (s) => paint('yellow', s),
56
+ green: (s) => paint('green', s),
57
+ cyan: (s) => paint('cyan', s),
58
+ heading: (s) => paint(['bold', 'yellow'], s),
59
+ flag: (s) => paint('green', s),
60
+ value: (s) => paint('cyan', s),
61
+ command: (s) => paint('bold', s),
29
62
  };
30
63
  if (!overrides) {
31
64
  return defaults;
@@ -35,7 +68,8 @@ function createStyles(overrides) {
35
68
  // ---- Help Text Helpers ----
36
69
  /** Wrap text to fit within a given width, preserving leading indent. */
37
70
  function wrapText(text, maxWidth, indent) {
38
- if (text.length + indent <= maxWidth) {
71
+ const available = Math.max(MIN_DESC_WIDTH, maxWidth - indent);
72
+ if (text.length <= available) {
39
73
  return text;
40
74
  }
41
75
  const words = text.split(/\s+/);
@@ -46,7 +80,7 @@ function wrapText(text, maxWidth, indent) {
46
80
  if (currentLine.length === 0) {
47
81
  currentLine = word;
48
82
  }
49
- else if (currentLine.length + 1 + word.length + indent <= maxWidth) {
83
+ else if (currentLine.length + 1 + word.length <= available) {
50
84
  currentLine += ` ${word}`;
51
85
  }
52
86
  else {
@@ -93,15 +127,14 @@ function formatArgFlag(key, def, styles) {
93
127
  }
94
128
  }
95
129
  // 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}>`);
130
+ if (def.type !== 'boolean' || def.valueName || def.valueNames) {
131
+ const optional = def.numArgs !== undefined && def.numArgs.min === 0;
132
+ const open = optional ? '[' : '<';
133
+ const close = optional ? ']' : '>';
134
+ const names = def.valueNames ?? [def.valueName ?? def.type.toUpperCase()];
135
+ for (const name of names) {
136
+ parts.push(` ${styles.value(`${open}${name}${close}`)}`);
137
+ rawParts.push(` ${open}${name}${close}`);
105
138
  }
106
139
  }
107
140
  return {
@@ -112,27 +145,57 @@ function formatArgFlag(key, def, styles) {
112
145
  /** Build the description suffix: [default: x] [env: VAR] [possible values: a, b] */
113
146
  function formatArgSuffix(def) {
114
147
  const suffixes = [];
115
- if (def.default !== undefined && def.type !== 'boolean') {
148
+ if (def.default !== undefined && def.type !== 'boolean' && !def.hideDefaultValue) {
116
149
  const defaultStr = Array.isArray(def.default) ? def.default.join(', ') : String(def.default);
117
150
  suffixes.push(`[default: ${defaultStr}]`);
118
151
  }
119
- if (def.env) {
120
- suffixes.push(`[env: ${def.env}]`);
152
+ if (def.env && !def.hideEnv) {
153
+ const current = def.hideEnvValues || def.secret ? undefined : process.env[def.env];
154
+ suffixes.push(current ? `[env: ${def.env}=${current}]` : `[env: ${def.env}]`);
121
155
  }
122
- if (def.valueParser && Array.isArray(def.valueParser) && def.valueParser.length > 0 && !def.hidePossibleValues) {
123
- suffixes.push(`[possible values: ${def.valueParser.join(', ')}]`);
156
+ if (!def.hidePossibleValues) {
157
+ const visible = possibleValues(def).filter((v) => !v.hidden);
158
+ if (visible.length > 0) {
159
+ suffixes.push(`[possible values: ${visible.map((v) => v.name).join(', ')}]`);
160
+ }
124
161
  }
125
162
  if (def.required) {
126
163
  suffixes.push('[required]');
127
164
  }
165
+ if (def.deprecated !== undefined && def.deprecated !== false) {
166
+ suffixes.push(typeof def.deprecated === 'string' ? `[deprecated: ${def.deprecated}]` : '[deprecated]');
167
+ }
128
168
  return suffixes.length > 0 ? ` ${suffixes.join(' ')}` : '';
129
169
  }
170
+ /** Description for an arg, preferring the long form in long help. */
171
+ function argDescription(def, isShortHelp) {
172
+ const base = (!isShortHelp && def.longDescription) || def.description || '';
173
+ return base + formatArgSuffix(def);
174
+ }
175
+ /** Sort help entries by displayOrder, keeping declaration order as the tiebreak. */
176
+ function byDisplayOrder(entries, nextDisplayOrder) {
177
+ const hasExplicit = entries.some(([, def]) => def.displayOrder !== undefined);
178
+ if (!hasExplicit && nextDisplayOrder === undefined) {
179
+ return entries;
180
+ }
181
+ // Args without an explicit order fall in after nextDisplayOrder, keeping
182
+ // declaration order among themselves.
183
+ const implicitBase = nextDisplayOrder ?? Number.MAX_SAFE_INTEGER;
184
+ return entries
185
+ .map((entry, index) => ({
186
+ entry,
187
+ order: entry[1].displayOrder ?? implicitBase + index,
188
+ index,
189
+ }))
190
+ .sort((a, b) => a.order - b.order || a.index - b.index)
191
+ .map((wrapped) => wrapped.entry);
192
+ }
130
193
  /** Append usage parts for options, positionals, and subcommands. */
131
194
  function appendUsageParts(usageParts, command) {
132
195
  const argsDef = command.args ?? {};
133
196
  const hasOptions = Object.values(argsDef).some((d) => d.type !== 'positional' && !d.hidden);
134
197
  const positionals = Object.entries(argsDef).filter(([_, d]) => d.type === 'positional');
135
- const hasSubcommands = command.subCommands && Object.keys(command.subCommands).length > 0;
198
+ const hasSubcommands = hasSubCommands(command);
136
199
  if (hasOptions) {
137
200
  usageParts.push('[OPTIONS]');
138
201
  }
@@ -146,7 +209,7 @@ function appendUsageParts(usageParts, command) {
146
209
  }
147
210
  }
148
211
  if (hasSubcommands) {
149
- usageParts.push('[COMMAND]');
212
+ usageParts.push(`[${command.meta.subcommandValueName ?? 'COMMAND'}]`);
150
213
  }
151
214
  }
152
215
  /** Check if an arg should be hidden based on help mode. */
@@ -162,18 +225,60 @@ function isArgHiddenForMode(def, isShortHelp) {
162
225
  }
163
226
  return false;
164
227
  }
228
+ /** Below this many columns beside a flag, help drops to next-line layout. */
229
+ const MIN_DESC_WIDTH = 20;
165
230
  /** Render aligned entries (flag + description with padding). */
166
231
  function renderAlignedEntries(entries, termWidth, lines) {
167
232
  if (entries.length === 0) {
168
233
  return;
169
234
  }
170
- const maxLen = Math.max(...entries.map((e) => e.rawLen));
235
+ const inline = entries.filter((e) => !e.nextLine);
236
+ const maxLen = inline.length > 0 ? Math.max(...inline.map((e) => e.rawLen)) : 0;
171
237
  const descIndent = maxLen + 4;
238
+ // A narrow terminal leaves too little room beside the flag to wrap into, so
239
+ // the whole section drops to next-line help rather than overflowing.
240
+ const forceNextLine = termWidth - (descIndent + 2) < MIN_DESC_WIDTH;
172
241
  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}`);
242
+ if (entry.nextLine || forceNextLine) {
243
+ lines.push(entry.label);
244
+ if (entry.desc) {
245
+ lines.push(` ${wrapText(entry.desc, termWidth, 6)}`);
246
+ }
247
+ }
248
+ else if (entry.desc) {
249
+ const padding = ' '.repeat(Math.max(2, descIndent - entry.rawLen));
250
+ lines.push(`${entry.label}${padding}${wrapText(entry.desc, termWidth, descIndent)}`);
251
+ }
252
+ else {
253
+ lines.push(entry.label);
254
+ }
255
+ if (entry.detail) {
256
+ for (const line of entry.detail) {
257
+ lines.push(line);
258
+ }
259
+ }
260
+ }
261
+ }
262
+ /**
263
+ * Per-value help block, as clap renders under an option in long help:
264
+ *
265
+ * Possible values:
266
+ * - fast: skip the slow checks
267
+ */
268
+ function possibleValueDetail(def, styles, isShortHelp) {
269
+ if (isShortHelp || def.hidePossibleValues) {
270
+ return undefined;
176
271
  }
272
+ const visible = possibleValues(def).filter((v) => !v.hidden);
273
+ if (!visible.some((v) => v.help)) {
274
+ return undefined;
275
+ }
276
+ const detail = ['', ` ${styles.heading('Possible values:')}`];
277
+ for (const value of visible) {
278
+ detail.push(` - ${styles.value(value.name)}${value.help ? `: ${value.help}` : ''}`);
279
+ }
280
+ detail.push('');
281
+ return detail;
177
282
  }
178
283
  // ---- Template Rendering ----
179
284
  /**
@@ -189,19 +294,20 @@ function renderHelpTemplate(template, command, styles, termWidth, fullName, isSh
189
294
  appendUsageParts(usageParts, command);
190
295
  const usageStr = usageParts.join(' ');
191
296
  const argsLines = [];
192
- renderPositionalSection(argsLines, argsDef, styles, termWidth, isShortHelp);
297
+ renderPositionalSection(argsLines, argsDef, styles, termWidth, isShortHelp, meta.nextDisplayOrder);
193
298
  const argumentsStr = argsLines.join('\n');
194
299
  const optLines = [];
195
300
  renderOptionsSection(optLines, argsDef, meta, styles, termWidth, isShortHelp);
196
301
  const optionsStr = optLines.join('\n');
197
302
  const cmdLines = [];
198
- if (command.subCommands && Object.keys(command.subCommands).length > 0) {
303
+ if (hasSubCommands(command)) {
199
304
  renderSubcommandSection(cmdLines, command, styles, termWidth, fullName);
200
305
  }
201
306
  const commandsStr = cmdLines.join('\n');
202
307
  return template
203
308
  .replaceAll('{name}', meta.name)
204
309
  .replaceAll('{version}', meta.version ?? '')
310
+ .replaceAll('{author}', meta.author ?? '')
205
311
  .replaceAll('{about}', meta.about ?? meta.description ?? '')
206
312
  .replaceAll('{usage}', usageStr)
207
313
  .replaceAll('{all-args}', [argumentsStr, optionsStr].filter(Boolean).join('\n'))
@@ -213,7 +319,7 @@ function renderHelpTemplate(template, command, styles, termWidth, fullName, isSh
213
319
  }
214
320
  // ---- Section Renderers (extracted for reuse) ----
215
321
  /** Render positional arguments section. */
216
- function renderPositionalSection(lines, argsDef, styles, termWidth, isShortHelp) {
322
+ function renderPositionalSection(lines, argsDef, styles, termWidth, isShortHelp, nextDisplayOrder) {
217
323
  const positionals = Object.entries(argsDef).filter(([_, d]) => d.type === 'positional');
218
324
  const visiblePositionals = positionals.filter(([_, d]) => !isArgHiddenForMode(d, isShortHelp));
219
325
  if (visiblePositionals.length === 0) {
@@ -221,12 +327,17 @@ function renderPositionalSection(lines, argsDef, styles, termWidth, isShortHelp)
221
327
  }
222
328
  lines.push(styles.heading('Arguments:'));
223
329
  const entries = [];
224
- for (const [key, def] of visiblePositionals) {
330
+ for (const [key, def] of byDisplayOrder(visiblePositionals, nextDisplayOrder)) {
225
331
  const name = def.valueName ?? key.toUpperCase();
226
332
  const label = ` ${styles.value(`<${name}>`)}`;
227
333
  const rawLen = name.length + 4;
228
- const desc = (def.description ?? '') + formatArgSuffix(def);
229
- entries.push({ label, rawLen, desc });
334
+ entries.push({
335
+ label,
336
+ rawLen,
337
+ desc: argDescription(def, isShortHelp),
338
+ nextLine: def.nextLineHelp,
339
+ detail: possibleValueDetail(def, styles, isShortHelp),
340
+ });
230
341
  }
231
342
  renderAlignedEntries(entries, termWidth, lines);
232
343
  }
@@ -238,7 +349,7 @@ function renderOptionsSection(lines, argsDef, meta, styles, termWidth, isShortHe
238
349
  }
239
350
  // Group options by helpHeading
240
351
  const groups = new Map();
241
- const defaultHeading = 'Options';
352
+ const defaultHeading = meta.nextHelpHeading ?? 'Options';
242
353
  for (const entry of options) {
243
354
  const heading = entry[1].helpHeading ?? defaultHeading;
244
355
  let group = groups.get(heading);
@@ -252,10 +363,15 @@ function renderOptionsSection(lines, argsDef, meta, styles, termWidth, isShortHe
252
363
  lines.push('');
253
364
  lines.push(styles.heading(`${heading}:`));
254
365
  const entries = [];
255
- for (const [key, def] of groupOptions) {
366
+ for (const [key, def] of byDisplayOrder(groupOptions, meta.nextDisplayOrder)) {
256
367
  const { flag, rawLen } = formatArgFlag(key, def, styles);
257
- const desc = (def.description ?? '') + formatArgSuffix(def);
258
- entries.push({ label: ` ${flag}`, rawLen: rawLen + 2, desc });
368
+ entries.push({
369
+ label: ` ${flag}`,
370
+ rawLen: rawLen + 2,
371
+ desc: argDescription(def, isShortHelp),
372
+ nextLine: def.nextLineHelp,
373
+ detail: possibleValueDetail(def, styles, isShortHelp),
374
+ });
259
375
  // Boolean negation: --no-flag
260
376
  if (def.type === 'boolean' && def.negativeDescription) {
261
377
  const longName = def.long ?? key;
@@ -270,11 +386,17 @@ function renderOptionsSection(lines, argsDef, meta, styles, termWidth, isShortHe
270
386
  }
271
387
  // Add built-in --help and --version to the default "Options" group
272
388
  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) {
389
+ if (!meta.disableHelpFlag) {
390
+ const helpFlag = ` ${styles.flag('-h')}, ${styles.flag('--help')}`;
391
+ entries.push({ label: helpFlag, rawLen: ' -h, --help'.length, desc: 'Print help' });
392
+ }
393
+ if (meta.version && !meta.disableVersionFlag) {
276
394
  const versionFlag = ` ${styles.flag('-V')}, ${styles.flag('--version')}`;
277
- entries.push({ label: versionFlag, rawLen: 17, desc: 'Print version' });
395
+ entries.push({
396
+ label: versionFlag,
397
+ rawLen: ' -V, --version'.length,
398
+ desc: 'Print version',
399
+ });
278
400
  }
279
401
  }
280
402
  renderAlignedEntries(entries, termWidth, lines);
@@ -288,21 +410,27 @@ function renderOptionsSection(lines, argsDef, meta, styles, termWidth, isShortHe
288
410
  */
289
411
  export function renderHelp(command, parentNames, isShortHelp = false, styleOverrides) {
290
412
  const { meta } = command;
291
- const styles = createStyles(styleOverrides);
292
- const termWidth = getTerminalWidth();
293
- const fullName = parentNames ? [...parentNames, meta.name].join(' ') : meta.name;
413
+ if (meta.overrideHelp !== undefined) {
414
+ return meta.overrideHelp.endsWith('\n') ? meta.overrideHelp : `${meta.overrideHelp}\n`;
415
+ }
416
+ const styles = createStyles(styleOverrides, colourEnabled(meta));
417
+ const termWidth = getTerminalWidth(meta);
418
+ const usageName = meta.binName ?? meta.name;
419
+ const fullName = parentNames ? [...parentNames, usageName].join(' ') : usageName;
294
420
  // Custom template override
295
421
  if (meta.helpTemplate) {
296
422
  return renderHelpTemplate(meta.helpTemplate, command, styles, termWidth, fullName, isShortHelp);
297
423
  }
298
424
  const lines = [];
299
425
  // Before help text
300
- if (meta.beforeHelp) {
301
- lines.push(meta.beforeHelp);
426
+ const beforeHelp = (!isShortHelp && meta.beforeLongHelp) || meta.beforeHelp;
427
+ if (beforeHelp) {
428
+ lines.push(beforeHelp);
302
429
  lines.push('');
303
430
  }
304
431
  // Header: "Description (name vX.Y.Z)"
305
- const nameVersion = meta.version ? `${meta.name} v${meta.version}` : meta.name;
432
+ const headerName = meta.displayName ?? meta.name;
433
+ const nameVersion = meta.version ? `${headerName} v${meta.version}` : headerName;
306
434
  const headerDesc = meta.about ?? meta.description ?? '';
307
435
  if (headerDesc) {
308
436
  lines.push(`${headerDesc} (${nameVersion})`);
@@ -317,13 +445,18 @@ export function renderHelp(command, parentNames, isShortHelp = false, styleOverr
317
445
  }
318
446
  lines.push('');
319
447
  // Usage line
320
- const usageParts = [styles.heading('Usage:'), styles.command(fullName)];
321
- appendUsageParts(usageParts, command);
322
- lines.push(usageParts.join(' '));
448
+ if (meta.overrideUsage !== undefined) {
449
+ lines.push(`${styles.heading('Usage:')} ${meta.overrideUsage}`);
450
+ }
451
+ else {
452
+ const usageParts = [styles.heading('Usage:'), styles.command(fullName)];
453
+ appendUsageParts(usageParts, command);
454
+ lines.push(usageParts.join(' '));
455
+ }
323
456
  // Positional arguments
324
457
  const argsDef = command.args ?? {};
325
458
  const posLines = [];
326
- renderPositionalSection(posLines, argsDef, styles, termWidth, isShortHelp);
459
+ renderPositionalSection(posLines, argsDef, styles, termWidth, isShortHelp, meta.nextDisplayOrder);
327
460
  if (posLines.length > 0) {
328
461
  lines.push('');
329
462
  lines.push(...posLines);
@@ -333,25 +466,55 @@ export function renderHelp(command, parentNames, isShortHelp = false, styleOverr
333
466
  renderOptionsSection(optLines, argsDef, meta, styles, termWidth, isShortHelp);
334
467
  lines.push(...optLines);
335
468
  // Subcommands
336
- const hasSubcommands = command.subCommands && Object.keys(command.subCommands).length > 0;
469
+ const hasSubcommands = hasSubCommands(command);
337
470
  if (hasSubcommands) {
338
471
  renderSubcommandSection(lines, command, styles, termWidth, fullName);
339
472
  }
340
473
  // After help
341
- if (meta.afterHelp) {
474
+ const afterHelp = (!isShortHelp && meta.afterLongHelp) || meta.afterHelp;
475
+ if (afterHelp) {
342
476
  lines.push('');
343
- lines.push(meta.afterHelp);
477
+ lines.push(afterHelp);
344
478
  }
345
479
  lines.push('');
346
480
  return lines.join('\n');
347
481
  }
348
482
  /** Render the subcommands section of help output. */
483
+ /**
484
+ * One indented line per visible arg of a subcommand, as clap's flatten_help
485
+ * shows so `git stash --help` can summarise `push` and `pop` in place.
486
+ */
487
+ function flattenedSubcommandArgs(sub, styles) {
488
+ const argsDef = sub.args ?? {};
489
+ const visible = Object.entries(argsDef).filter(([, def]) => !def.hidden);
490
+ if (visible.length === 0) {
491
+ return undefined;
492
+ }
493
+ const named = visible.map(([key, def]) => ({
494
+ raw: def.type === 'positional'
495
+ ? `<${def.valueName ?? key.toUpperCase()}>`
496
+ : `--${def.long ?? key}`,
497
+ def,
498
+ }));
499
+ const width = Math.max(...named.map((n) => n.raw.length));
500
+ return named.map(({ raw, def }) => {
501
+ const painted = def.type === 'positional' ? styles.value(raw) : styles.flag(raw);
502
+ const padding = ' '.repeat(width - raw.length + 2);
503
+ return ` ${painted}${def.description ? `${padding}${def.description}` : ''}`;
504
+ });
505
+ }
349
506
  function renderSubcommandSection(lines, command, styles, termWidth, fullName) {
507
+ const flatten = command.meta.flattenHelp === true;
350
508
  lines.push('');
351
- lines.push(styles.heading('Commands:'));
509
+ lines.push(styles.heading(`${command.meta.subcommandHelpHeading ?? 'Commands'}:`));
352
510
  const subEntries = [];
353
511
  const rendered = new Set();
354
- for (const [name, def] of Object.entries(command.subCommands)) {
512
+ const subs = Object.entries(subCommandsOf(command));
513
+ if (subs.some(([, def]) => def.meta.displayOrder !== undefined)) {
514
+ subs.sort((a, b) => (a[1].meta.displayOrder ?? Number.MAX_SAFE_INTEGER) -
515
+ (b[1].meta.displayOrder ?? Number.MAX_SAFE_INTEGER));
516
+ }
517
+ for (const [name, def] of subs) {
355
518
  if (def.meta.hidden) {
356
519
  continue;
357
520
  }
@@ -359,11 +522,24 @@ function renderSubcommandSection(lines, command, styles, termWidth, fullName) {
359
522
  continue;
360
523
  }
361
524
  rendered.add(name);
525
+ // Visible aliases and any flag form share the parenthesised suffix.
526
+ const extras = [...(def.meta.aliases ?? [])];
527
+ if (def.meta.shortFlag !== undefined) {
528
+ extras.push(`-${def.meta.shortFlag}`);
529
+ }
530
+ if (def.meta.longFlag !== undefined) {
531
+ extras.push(`--${def.meta.longFlag}`);
532
+ }
533
+ for (const alias of def.meta.visibleShortFlagAliases ?? []) {
534
+ extras.push(`-${alias}`);
535
+ }
536
+ for (const alias of def.meta.visibleLongFlagAliases ?? []) {
537
+ extras.push(`--${alias}`);
538
+ }
362
539
  let label;
363
540
  let rawLen;
364
- const { aliases } = def.meta;
365
- if (aliases && aliases.length > 0) {
366
- const aliasStr = aliases.join(', ');
541
+ if (extras.length > 0) {
542
+ const aliasStr = extras.join(', ');
367
543
  label = ` ${styles.command(name)} (${aliasStr})`;
368
544
  rawLen = name.length + aliasStr.length + 5;
369
545
  }
@@ -371,8 +547,25 @@ function renderSubcommandSection(lines, command, styles, termWidth, fullName) {
371
547
  label = ` ${styles.command(name)}`;
372
548
  rawLen = name.length + 2;
373
549
  }
374
- const desc = def.meta.description ?? '';
375
- subEntries.push({ label, rawLen, desc });
550
+ const deprecated = def.meta.deprecated;
551
+ const note = deprecated === undefined || deprecated === false
552
+ ? ''
553
+ : typeof deprecated === 'string'
554
+ ? ` [deprecated: ${deprecated}]`
555
+ : ' [deprecated]';
556
+ subEntries.push({
557
+ label,
558
+ rawLen,
559
+ desc: (def.meta.description ?? '') + note,
560
+ detail: flatten ? flattenedSubcommandArgs(def, styles) : undefined,
561
+ });
562
+ }
563
+ if (!command.meta.disableHelpSubcommand) {
564
+ subEntries.push({
565
+ label: ` ${styles.command('help')}`,
566
+ rawLen: 6,
567
+ desc: 'Print this message or the help of the given subcommand(s)',
568
+ });
376
569
  }
377
570
  renderAlignedEntries(subEntries, termWidth, lines);
378
571
  lines.push('');
@@ -383,8 +576,12 @@ function renderSubcommandSection(lines, command, styles, termWidth, fullName) {
383
576
  */
384
577
  export function renderUsage(command, parentNames, styleOverrides) {
385
578
  const { meta } = command;
386
- const styles = createStyles(styleOverrides);
387
- const fullName = parentNames ? [...parentNames, meta.name].join(' ') : meta.name;
579
+ const styles = createStyles(styleOverrides, colourEnabled(meta));
580
+ if (meta.overrideUsage !== undefined) {
581
+ return `${styles.heading('Usage:')} ${meta.overrideUsage}`;
582
+ }
583
+ const usageName = meta.binName ?? meta.name;
584
+ const fullName = parentNames ? [...parentNames, usageName].join(' ') : usageName;
388
585
  const usageParts = [styles.heading('Usage:'), styles.command(fullName)];
389
586
  appendUsageParts(usageParts, command);
390
587
  return usageParts.join(' ');
@@ -392,23 +589,21 @@ export function renderUsage(command, parentNames, styleOverrides) {
392
589
  /**
393
590
  * Print help to stdout.
394
591
  */
395
- export function showHelp(command, parentNames, isShortHelp = false, styleOverrides) {
396
- const text = renderHelp(command, parentNames, isShortHelp, styleOverrides);
397
- process.stdout.write(text);
592
+ export function showHelp(command, parentNames, isShortHelp = false, styleOverrides, out = process.stdout) {
593
+ out.write(renderHelp(command, parentNames, isShortHelp, styleOverrides));
398
594
  }
399
595
  /**
400
596
  * Print version to stdout.
401
597
  */
402
- export function showVersion(meta) {
403
- const version = meta.version ?? '0.0.0';
404
- process.stdout.write(`${meta.name} ${version}\n`);
598
+ export function showVersion(meta, isShort = false, out = process.stdout) {
599
+ const version = (!isShort && meta.longVersion) || meta.version || '0.0.0';
600
+ out.write(`${meta.displayName ?? meta.name} ${version}\n`);
405
601
  }
406
602
  /**
407
603
  * Print an error message with usage hint.
408
604
  */
409
- export function showError(message, command, parentNames, styleOverrides) {
410
- const styles = createStyles(styleOverrides);
605
+ export function showError(message, command, parentNames, styleOverrides, out = process.stderr) {
606
+ const styles = createStyles(styleOverrides, colourEnabled(command.meta));
411
607
  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);
608
+ out.write(`${styles.bold('error:')} ${message}\n\n${usage}\n\nFor more information, try '${styles.flag('--help')}'.\n`);
414
609
  }
package/dist/index.d.ts CHANGED
@@ -3,9 +3,8 @@
3
3
  *
4
4
  * Re-exports all public API.
5
5
  */
6
- export type { ArgType, ArgAction, NumArgs, ValueParserFn, ValueHint, Shell, 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';
6
+ export type { ArgType, ArgAction, NumArgs, ValueParserFn, PossibleValue, ValueHint, ColorChoice, ValueSource, Shell, OutputSink, MissingArg, ArgDef, ArgsDef, StyleFn, StylesDef, CommandMeta, ArgGroup, ParsedArgs, CommandContext, CommandDef, RunOptions, ParseResult, InferArgValue, InferArgOptional, } from './types.js';
7
+ export { parseArgs, getRawArgs, collectGlobalArgs, mergeGlobalArgs, subCommandsOf, hasSubCommands, possibleValues, matchesPossibleValue, CliParseError, } from './parser.js';
8
8
  export { validate } from './validation.js';
9
9
  export { renderHelp, renderUsage, showHelp, showVersion, showError } from './help.js';
10
10
  export { defineCommand, defineArgs, defineArg, runCommand, runMain } from './runner.js';
11
- export { generateCompletions, withCompletions } from './completions.js';
package/dist/index.js CHANGED
@@ -4,12 +4,12 @@
4
4
  * Re-exports all public API.
5
5
  */
6
6
  // Parser
7
- export { parseArgs, getRawArgs, collectGlobalArgs, mergeGlobalArgs, CliParseError, } from './parser.js';
7
+ export { parseArgs, getRawArgs, collectGlobalArgs, mergeGlobalArgs, subCommandsOf, hasSubCommands, possibleValues, matchesPossibleValue, CliParseError, } from './parser.js';
8
8
  // Validation
9
9
  export { validate } from './validation.js';
10
10
  // Help renderer
11
11
  export { renderHelp, renderUsage, showHelp, showVersion, showError } from './help.js';
12
12
  // Runner (main API)
13
13
  export { defineCommand, defineArgs, defineArg, runCommand, runMain } from './runner.js';
14
- // Shell completions
15
- export { generateCompletions, withCompletions } from './completions.js';
14
+ // Generators live behind subpaths so a running CLI never loads them:
15
+ // clap-ts/completions, clap-ts/man, clap-ts/markdown
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Put generated completions and man pages where the system will find them.
3
+ *
4
+ * ```ts
5
+ * import { withInstallers } from 'clap-ts/install';
6
+ *
7
+ * runMain(withInstallers(main));
8
+ * // my-tool completions install zsh
9
+ * // my-tool man install
10
+ * ```
11
+ *
12
+ * Everything respects `XDG_DATA_HOME`, and nothing is written until the target
13
+ * directory is created, so a dry run can report the path without touching disk.
14
+ */
15
+ import type { CommandDef, Shell } from './types.js';
16
+ /** Where a shell looks for user completions, and what the file must be called. */
17
+ export interface CompletionTarget {
18
+ readonly dir: string;
19
+ readonly file: string;
20
+ /** A line the user must add themselves, when sourcing is not automatic. */
21
+ readonly manualStep?: string;
22
+ }
23
+ /**
24
+ * The per-user completion path for a shell.
25
+ *
26
+ * bash, zsh and fish load these directories on their own. powershell, elvish
27
+ * and nushell have no drop-in directory, so those report a line to add to the
28
+ * profile instead.
29
+ */
30
+ export declare function completionTarget(shell: Shell, binaryName: string): CompletionTarget;
31
+ /** What an install did, or would have done. */
32
+ export interface InstallResult {
33
+ readonly paths: readonly string[];
34
+ readonly manualStep?: string;
35
+ readonly dryRun: boolean;
36
+ }
37
+ export interface InstallOptions {
38
+ /** Report the paths without writing anything. */
39
+ readonly dryRun?: boolean;
40
+ /** Write here instead of the per-user location. */
41
+ readonly dir?: string;
42
+ /** Binary name; defaults to the command's binName or name. */
43
+ readonly binaryName?: string;
44
+ }
45
+ /** Write the completion script for one shell to its per-user location. */
46
+ export declare function installCompletions(command: CommandDef, shell: Shell, opts?: InstallOptions): InstallResult;
47
+ /** Write a man page per command into the per-user man directory. */
48
+ export declare function installManPages(command: CommandDef, opts?: InstallOptions): InstallResult;
49
+ /**
50
+ * Add `completions` and `man` subcommands that both print and install.
51
+ *
52
+ * `tool completions bash` writes the script to stdout as before;
53
+ * `tool completions install bash` puts it where the shell will find it.
54
+ */
55
+ export declare function withInstallers(rootCommand: CommandDef<any>): CommandDef<any>;