@guanghechen/commander 2.0.1 → 3.0.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 -0
- package/lib/cjs/index.cjs +264 -90
- package/lib/esm/index.mjs +244 -90
- package/lib/types/index.d.ts +30 -17
- package/package.json +1 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file. See
|
|
4
|
+
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
## 3.0.0 (2026-02-08)
|
|
7
|
+
|
|
8
|
+
### BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
- Change subcommand registration API to `subcommand(name, cmd)`
|
|
11
|
+
|
|
12
|
+
## 2.1.0 (2026-02-08)
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
- Add `--write` option to `CompletionCommand` for direct file output
|
|
17
|
+
- Add `help` subcommand support for commands with subcommands
|
|
18
|
+
- Detect `--help`/`--version` before parsing to avoid required argument errors
|
|
19
|
+
- Add `#normalizeArgv()` preprocessing to simplify `--no-*` option handling
|
|
20
|
+
- Add `implements ICommand` for explicit interface implementation
|
|
21
|
+
|
|
22
|
+
## 2.0.1 (2025-02-07)
|
|
23
|
+
|
|
24
|
+
### Documentation
|
|
25
|
+
|
|
26
|
+
- Update README.md
|
|
27
|
+
|
|
28
|
+
### Miscellaneous
|
|
29
|
+
|
|
30
|
+
- Add LICENSE file
|
|
31
|
+
- Clean up build configs and standardize package exports
|
|
32
|
+
- Migrate from lerna to changesets
|
|
33
|
+
|
|
34
|
+
## 2.0.0 (2025-01-15)
|
|
35
|
+
|
|
36
|
+
### Features
|
|
37
|
+
|
|
38
|
+
- Initial stable release: A minimal, type-safe command-line interface builder with fluent API
|
package/lib/cjs/index.cjs
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var fs = require('node:fs');
|
|
4
|
+
var path = require('node:path');
|
|
5
|
+
|
|
6
|
+
function _interopNamespaceDefault(e) {
|
|
7
|
+
var n = Object.create(null);
|
|
8
|
+
if (e) {
|
|
9
|
+
Object.keys(e).forEach(function (k) {
|
|
10
|
+
if (k !== 'default') {
|
|
11
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
12
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
get: function () { return e[k]; }
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
n.default = e;
|
|
20
|
+
return Object.freeze(n);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
|
|
24
|
+
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
|
|
25
|
+
|
|
3
26
|
class CommanderError extends Error {
|
|
4
27
|
kind;
|
|
5
28
|
commandPath;
|
|
@@ -44,32 +67,25 @@ class Command {
|
|
|
44
67
|
#name;
|
|
45
68
|
#description;
|
|
46
69
|
#version;
|
|
47
|
-
#
|
|
70
|
+
#helpSubcommandEnabled;
|
|
48
71
|
#options = [];
|
|
49
72
|
#arguments = [];
|
|
50
73
|
#subcommands = [];
|
|
51
74
|
#action;
|
|
52
|
-
#parent;
|
|
53
75
|
constructor(config) {
|
|
54
|
-
this.#name = config.name;
|
|
76
|
+
this.#name = config.name ?? '';
|
|
55
77
|
this.#description = config.description;
|
|
56
78
|
this.#version = config.version;
|
|
57
|
-
this.#
|
|
79
|
+
this.#helpSubcommandEnabled = config.help ?? false;
|
|
58
80
|
}
|
|
59
81
|
get name() {
|
|
60
82
|
return this.#name;
|
|
61
83
|
}
|
|
62
|
-
get aliases() {
|
|
63
|
-
return this.#aliases;
|
|
64
|
-
}
|
|
65
84
|
get description() {
|
|
66
85
|
return this.#description;
|
|
67
86
|
}
|
|
68
87
|
get version() {
|
|
69
|
-
return this.#version
|
|
70
|
-
}
|
|
71
|
-
get parent() {
|
|
72
|
-
return this.#parent;
|
|
88
|
+
return this.#version;
|
|
73
89
|
}
|
|
74
90
|
get options() {
|
|
75
91
|
return [...this.#options];
|
|
@@ -92,33 +108,43 @@ class Command {
|
|
|
92
108
|
this.#action = fn;
|
|
93
109
|
return this;
|
|
94
110
|
}
|
|
95
|
-
subcommand(cmd) {
|
|
96
|
-
|
|
97
|
-
|
|
111
|
+
subcommand(name, cmd) {
|
|
112
|
+
if (this.#helpSubcommandEnabled && name === 'help') {
|
|
113
|
+
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name when help subcommand is enabled', this.#getCommandPath());
|
|
114
|
+
}
|
|
115
|
+
const existing = this.#subcommands.find(e => e.command === cmd);
|
|
116
|
+
if (existing) {
|
|
117
|
+
existing.aliases.push(name);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
cmd.#name = name;
|
|
121
|
+
this.#subcommands.push({ name, aliases: [], command: cmd });
|
|
122
|
+
}
|
|
98
123
|
return this;
|
|
99
124
|
}
|
|
100
125
|
async run(params) {
|
|
101
126
|
const { argv, envs, reporter } = params;
|
|
102
127
|
try {
|
|
103
|
-
const
|
|
104
|
-
const {
|
|
105
|
-
const ctx = {
|
|
106
|
-
cmd: command,
|
|
107
|
-
envs,
|
|
108
|
-
reporter: reporter ?? new DefaultReporter(),
|
|
109
|
-
argv,
|
|
110
|
-
};
|
|
128
|
+
const processedArgv = this.#processHelpSubcommand(argv);
|
|
129
|
+
const { command, remaining } = this.#route(processedArgv);
|
|
111
130
|
const allOptions = command.#getMergedOptions();
|
|
112
131
|
const hasUserHelp = allOptions.some(o => o.long === 'help' && !command.#isBuiltinOption(o));
|
|
113
132
|
const hasUserVersion = allOptions.some(o => o.long === 'version' && !command.#isBuiltinOption(o));
|
|
114
|
-
if (!hasUserHelp &&
|
|
133
|
+
if (!hasUserHelp && command.#hasHelpFlag(remaining, allOptions)) {
|
|
115
134
|
console.log(command.formatHelp());
|
|
116
135
|
return;
|
|
117
136
|
}
|
|
118
|
-
if (!hasUserVersion &&
|
|
137
|
+
if (!hasUserVersion && command.#hasVersionFlag(remaining, allOptions)) {
|
|
119
138
|
console.log(command.version ?? 'unknown');
|
|
120
139
|
return;
|
|
121
140
|
}
|
|
141
|
+
const { opts, args } = command.parse(remaining);
|
|
142
|
+
const ctx = {
|
|
143
|
+
cmd: command,
|
|
144
|
+
envs,
|
|
145
|
+
reporter: reporter ?? new DefaultReporter(),
|
|
146
|
+
argv,
|
|
147
|
+
};
|
|
122
148
|
for (const opt of allOptions) {
|
|
123
149
|
if (opt.apply && opts[opt.long] !== undefined) {
|
|
124
150
|
opt.apply(opts[opt.long], ctx);
|
|
@@ -177,16 +203,8 @@ class Command {
|
|
|
177
203
|
opts[opt.long] = result.value;
|
|
178
204
|
remaining = result.remaining;
|
|
179
205
|
}
|
|
180
|
-
const optionByLong =
|
|
181
|
-
|
|
182
|
-
for (const opt of allOptions) {
|
|
183
|
-
if (!opt.resolver) {
|
|
184
|
-
optionByLong.set(opt.long, opt);
|
|
185
|
-
if (opt.short) {
|
|
186
|
-
optionByShort.set(opt.short, opt);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
206
|
+
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(allOptions, true);
|
|
207
|
+
remaining = this.#normalizeArgv(remaining, booleanOptions);
|
|
190
208
|
let i = 0;
|
|
191
209
|
while (i < remaining.length) {
|
|
192
210
|
const token = remaining[i];
|
|
@@ -285,15 +303,19 @@ class Command {
|
|
|
285
303
|
}
|
|
286
304
|
lines.push('');
|
|
287
305
|
}
|
|
306
|
+
const showHelpSubcommand = this.#helpSubcommandEnabled && this.#subcommands.length > 0;
|
|
288
307
|
if (this.#subcommands.length > 0) {
|
|
289
308
|
lines.push('Commands:');
|
|
290
309
|
const cmdLines = [];
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
310
|
+
if (showHelpSubcommand) {
|
|
311
|
+
cmdLines.push({ name: 'help', desc: 'Show help for a command' });
|
|
312
|
+
}
|
|
313
|
+
for (const entry of this.#subcommands) {
|
|
314
|
+
let name = entry.name;
|
|
315
|
+
if (entry.aliases.length > 0) {
|
|
316
|
+
name += `, ${entry.aliases.join(', ')}`;
|
|
295
317
|
}
|
|
296
|
-
cmdLines.push({ name, desc:
|
|
318
|
+
cmdLines.push({ name, desc: entry.command.#description });
|
|
297
319
|
}
|
|
298
320
|
const maxNameLen = Math.max(...cmdLines.map(l => l.name.length));
|
|
299
321
|
for (const { name, desc } of cmdLines) {
|
|
@@ -320,11 +342,33 @@ class Command {
|
|
|
320
342
|
return {
|
|
321
343
|
name: this.#name,
|
|
322
344
|
description: this.#description,
|
|
323
|
-
aliases:
|
|
345
|
+
aliases: [],
|
|
324
346
|
options,
|
|
325
|
-
subcommands: this.#subcommands.map(
|
|
347
|
+
subcommands: this.#subcommands.map(entry => {
|
|
348
|
+
const subMeta = entry.command.getCompletionMeta();
|
|
349
|
+
return {
|
|
350
|
+
...subMeta,
|
|
351
|
+
name: entry.name,
|
|
352
|
+
aliases: entry.aliases,
|
|
353
|
+
};
|
|
354
|
+
}),
|
|
326
355
|
};
|
|
327
356
|
}
|
|
357
|
+
#processHelpSubcommand(argv) {
|
|
358
|
+
if (!this.#helpSubcommandEnabled || this.#subcommands.length === 0)
|
|
359
|
+
return argv;
|
|
360
|
+
if (argv.length < 1 || argv[0] !== 'help')
|
|
361
|
+
return argv;
|
|
362
|
+
if (argv.length === 1) {
|
|
363
|
+
return ['--help'];
|
|
364
|
+
}
|
|
365
|
+
const subName = argv[1];
|
|
366
|
+
const entry = this.#subcommands.find(e => e.name === subName || e.aliases.includes(subName));
|
|
367
|
+
if (entry) {
|
|
368
|
+
return [subName, '--help', ...argv.slice(2)];
|
|
369
|
+
}
|
|
370
|
+
return argv;
|
|
371
|
+
}
|
|
328
372
|
#route(argv) {
|
|
329
373
|
let current = this;
|
|
330
374
|
let idx = 0;
|
|
@@ -332,10 +376,10 @@ class Command {
|
|
|
332
376
|
const token = argv[idx];
|
|
333
377
|
if (token.startsWith('-'))
|
|
334
378
|
break;
|
|
335
|
-
const
|
|
336
|
-
if (!
|
|
379
|
+
const entry = current.#subcommands.find(e => e.name === token || e.aliases.includes(token));
|
|
380
|
+
if (!entry)
|
|
337
381
|
break;
|
|
338
|
-
current =
|
|
382
|
+
current = entry.command;
|
|
339
383
|
idx += 1;
|
|
340
384
|
}
|
|
341
385
|
return { command: current, remaining: argv.slice(idx) };
|
|
@@ -352,17 +396,6 @@ class Command {
|
|
|
352
396
|
else {
|
|
353
397
|
optName = token.slice(2);
|
|
354
398
|
}
|
|
355
|
-
if (optName.startsWith('no-')) {
|
|
356
|
-
const actualName = optName.slice(3);
|
|
357
|
-
const opt = optionByLong.get(actualName);
|
|
358
|
-
if (opt && opt.type === 'boolean') {
|
|
359
|
-
if (inlineValue !== undefined) {
|
|
360
|
-
throw new CommanderError('InvalidBooleanValue', `"--no-${actualName}" does not accept a value`, this.#getCommandPath());
|
|
361
|
-
}
|
|
362
|
-
opts[actualName] = false;
|
|
363
|
-
return idx + 1;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
399
|
const opt = optionByLong.get(optName);
|
|
367
400
|
if (!opt) {
|
|
368
401
|
throw new CommanderError('UnknownOption', `unknown option "--${optName}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
@@ -429,7 +462,7 @@ class Command {
|
|
|
429
462
|
}
|
|
430
463
|
#applyValue(opt, rawValue, opts) {
|
|
431
464
|
const type = opt.type ?? 'string';
|
|
432
|
-
let parsedValue;
|
|
465
|
+
let parsedValue = rawValue;
|
|
433
466
|
if (opt.coerce) {
|
|
434
467
|
parsedValue = opt.coerce(rawValue);
|
|
435
468
|
}
|
|
@@ -448,8 +481,6 @@ class Command {
|
|
|
448
481
|
parsedValue = num;
|
|
449
482
|
break;
|
|
450
483
|
}
|
|
451
|
-
default:
|
|
452
|
-
parsedValue = rawValue;
|
|
453
484
|
}
|
|
454
485
|
}
|
|
455
486
|
if (type === 'string[]' || type === 'number[]') {
|
|
@@ -462,33 +493,17 @@ class Command {
|
|
|
462
493
|
}
|
|
463
494
|
}
|
|
464
495
|
#getMergedOptions() {
|
|
465
|
-
const ancestors = [];
|
|
466
|
-
for (let node = this; node; node = node.#parent) {
|
|
467
|
-
ancestors.unshift(node);
|
|
468
|
-
}
|
|
469
496
|
const optionMap = new Map();
|
|
470
|
-
const hasUserHelp =
|
|
471
|
-
const hasUserVersion =
|
|
497
|
+
const hasUserHelp = this.#options.some(o => o.long === 'help');
|
|
498
|
+
const hasUserVersion = this.#options.some(o => o.long === 'version');
|
|
472
499
|
if (!hasUserHelp) {
|
|
473
500
|
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
474
501
|
}
|
|
475
502
|
if (!hasUserVersion) {
|
|
476
503
|
optionMap.set('version', BUILTIN_VERSION_OPTION);
|
|
477
504
|
}
|
|
478
|
-
for (const
|
|
479
|
-
|
|
480
|
-
optionMap.set(opt.long, opt);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
const shortToLong = new Map();
|
|
484
|
-
for (const [long, opt] of optionMap) {
|
|
485
|
-
if (opt.short) {
|
|
486
|
-
const existing = shortToLong.get(opt.short);
|
|
487
|
-
if (existing && existing !== long) {
|
|
488
|
-
throw new CommanderError('OptionConflict', `short option "-${opt.short}" is used by both "--${existing}" and "--${long}"`, this.#getCommandPath());
|
|
489
|
-
}
|
|
490
|
-
shortToLong.set(opt.short, long);
|
|
491
|
-
}
|
|
505
|
+
for (const opt of this.#options) {
|
|
506
|
+
optionMap.set(opt.long, opt);
|
|
492
507
|
}
|
|
493
508
|
return Array.from(optionMap.values());
|
|
494
509
|
}
|
|
@@ -533,20 +548,109 @@ class Command {
|
|
|
533
548
|
#isBuiltinOption(opt) {
|
|
534
549
|
return opt === BUILTIN_HELP_OPTION || opt === BUILTIN_VERSION_OPTION;
|
|
535
550
|
}
|
|
536
|
-
#
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
551
|
+
#buildOptionMaps(allOptions, excludeResolver = false) {
|
|
552
|
+
const optionByLong = new Map();
|
|
553
|
+
const optionByShort = new Map();
|
|
554
|
+
const booleanOptions = new Set();
|
|
555
|
+
for (const opt of allOptions) {
|
|
556
|
+
if (excludeResolver && opt.resolver)
|
|
557
|
+
continue;
|
|
558
|
+
optionByLong.set(opt.long, opt);
|
|
559
|
+
if (opt.short) {
|
|
560
|
+
optionByShort.set(opt.short, opt);
|
|
561
|
+
}
|
|
562
|
+
if (opt.type === 'boolean') {
|
|
563
|
+
booleanOptions.add(opt.long);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return { optionByLong, optionByShort, booleanOptions };
|
|
567
|
+
}
|
|
568
|
+
#hasHelpFlag(argv, allOptions) {
|
|
569
|
+
return this.#hasBuiltinFlag(argv, 'help', 'h', allOptions);
|
|
570
|
+
}
|
|
571
|
+
#hasVersionFlag(argv, allOptions) {
|
|
572
|
+
return this.#hasBuiltinFlag(argv, 'version', 'V', allOptions);
|
|
573
|
+
}
|
|
574
|
+
#hasBuiltinFlag(argv, flagLong, flagShort, allOptions) {
|
|
575
|
+
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(allOptions);
|
|
576
|
+
const normalizedArgv = this.#normalizeArgv(argv, booleanOptions);
|
|
577
|
+
for (let i = 0; i < normalizedArgv.length; i++) {
|
|
578
|
+
const arg = normalizedArgv[i];
|
|
579
|
+
if (arg === '--') {
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
if (arg === `--${flagLong}` || (flagShort && arg === `-${flagShort}`)) {
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
if (this.#optionConsumesNextValue(arg, optionByLong, optionByShort)) {
|
|
586
|
+
i += 1;
|
|
587
|
+
}
|
|
540
588
|
}
|
|
541
|
-
return
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
#optionConsumesNextValue(arg, optionByLong, optionByShort) {
|
|
592
|
+
if (arg.startsWith('--')) {
|
|
593
|
+
const eqIdx = arg.indexOf('=');
|
|
594
|
+
if (eqIdx !== -1) {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
const optName = arg.slice(2);
|
|
598
|
+
const opt = optionByLong.get(optName);
|
|
599
|
+
if (!opt) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
const type = opt.type ?? 'string';
|
|
603
|
+
return type !== 'boolean';
|
|
604
|
+
}
|
|
605
|
+
if (arg.startsWith('-') && arg.length === 2) {
|
|
606
|
+
const opt = optionByShort.get(arg[1]);
|
|
607
|
+
if (!opt) {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
const type = opt.type ?? 'string';
|
|
611
|
+
return type !== 'boolean';
|
|
612
|
+
}
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
#normalizeArgv(argv, booleanOptions) {
|
|
616
|
+
const result = [];
|
|
617
|
+
let seenDoubleDash = false;
|
|
618
|
+
for (const arg of argv) {
|
|
619
|
+
if (arg === '--') {
|
|
620
|
+
seenDoubleDash = true;
|
|
621
|
+
result.push(arg);
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (!seenDoubleDash && arg.startsWith('--no-')) {
|
|
625
|
+
const eqIdx = arg.indexOf('=');
|
|
626
|
+
if (eqIdx !== -1) {
|
|
627
|
+
const optName = arg.slice(5, eqIdx);
|
|
628
|
+
if (booleanOptions.has(optName)) {
|
|
629
|
+
throw new CommanderError('InvalidBooleanValue', `"--no-${optName}" does not accept a value`, this.#getCommandPath());
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
const optName = arg.slice(5);
|
|
634
|
+
if (booleanOptions.has(optName)) {
|
|
635
|
+
result.push(`--${optName}=false`);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
result.push(arg);
|
|
641
|
+
}
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
644
|
+
#getCommandPath() {
|
|
645
|
+
return this.#name;
|
|
542
646
|
}
|
|
543
647
|
}
|
|
544
648
|
|
|
545
649
|
class CompletionCommand extends Command {
|
|
546
650
|
constructor(root, config) {
|
|
547
|
-
const
|
|
651
|
+
const paths = config.paths;
|
|
652
|
+
const programName = config.programName ?? root.name;
|
|
548
653
|
super({
|
|
549
|
-
name,
|
|
550
654
|
description: 'Generate shell completion script',
|
|
551
655
|
});
|
|
552
656
|
this.option({
|
|
@@ -563,10 +667,16 @@ class CompletionCommand extends Command {
|
|
|
563
667
|
long: 'pwsh',
|
|
564
668
|
type: 'boolean',
|
|
565
669
|
description: 'Generate PowerShell completion script',
|
|
670
|
+
})
|
|
671
|
+
.option({
|
|
672
|
+
long: 'write',
|
|
673
|
+
short: 'w',
|
|
674
|
+
type: 'string',
|
|
675
|
+
description: 'Write to file (default path if no value given)',
|
|
676
|
+
resolver: argv => resolveOptionalStringOption(argv, 'write', 'w'),
|
|
566
677
|
})
|
|
567
678
|
.action(({ opts }) => {
|
|
568
679
|
const meta = root.getCompletionMeta();
|
|
569
|
-
const programName = root.name;
|
|
570
680
|
const selectedShells = [
|
|
571
681
|
opts['bash'] && 'bash',
|
|
572
682
|
opts['fish'] && 'fish',
|
|
@@ -575,25 +685,89 @@ class CompletionCommand extends Command {
|
|
|
575
685
|
if (selectedShells.length === 0) {
|
|
576
686
|
console.error('Please specify a shell: --bash, --fish, or --pwsh');
|
|
577
687
|
process.exit(1);
|
|
688
|
+
return;
|
|
578
689
|
}
|
|
579
690
|
if (selectedShells.length > 1) {
|
|
580
691
|
console.error('Please specify only one shell option');
|
|
581
692
|
process.exit(1);
|
|
693
|
+
return;
|
|
582
694
|
}
|
|
583
|
-
|
|
695
|
+
const shell = selectedShells[0];
|
|
696
|
+
let script;
|
|
697
|
+
switch (shell) {
|
|
584
698
|
case 'bash':
|
|
585
|
-
|
|
699
|
+
script = new BashCompletion(meta, programName).generate();
|
|
586
700
|
break;
|
|
587
701
|
case 'fish':
|
|
588
|
-
|
|
702
|
+
script = new FishCompletion(meta, programName).generate();
|
|
589
703
|
break;
|
|
590
704
|
case 'pwsh':
|
|
591
|
-
|
|
705
|
+
script = new PwshCompletion(meta, programName).generate();
|
|
592
706
|
break;
|
|
593
707
|
}
|
|
708
|
+
const writeOpt = opts['write'];
|
|
709
|
+
if (writeOpt !== undefined) {
|
|
710
|
+
const filePath = typeof writeOpt === 'string' && writeOpt !== '' ? writeOpt : paths[shell];
|
|
711
|
+
const expandedPath = expandHome(filePath);
|
|
712
|
+
const dir = path__namespace.dirname(expandedPath);
|
|
713
|
+
if (!fs__namespace.existsSync(dir)) {
|
|
714
|
+
fs__namespace.mkdirSync(dir, { recursive: true });
|
|
715
|
+
}
|
|
716
|
+
fs__namespace.writeFileSync(expandedPath, script, 'utf-8');
|
|
717
|
+
console.log(`Completion script written to: ${expandedPath}`);
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
console.log(script);
|
|
721
|
+
}
|
|
594
722
|
});
|
|
595
723
|
}
|
|
596
724
|
}
|
|
725
|
+
function expandHome(filepath) {
|
|
726
|
+
if (filepath.startsWith('~/') || filepath === '~') {
|
|
727
|
+
const home = process.env['HOME'] || process.env['USERPROFILE'] || '';
|
|
728
|
+
return filepath.replace(/^~/, home);
|
|
729
|
+
}
|
|
730
|
+
return filepath;
|
|
731
|
+
}
|
|
732
|
+
function resolveOptionalStringOption(argv, longName, shortName) {
|
|
733
|
+
const remaining = [];
|
|
734
|
+
let value;
|
|
735
|
+
for (let i = 0; i < argv.length; i++) {
|
|
736
|
+
const arg = argv[i];
|
|
737
|
+
if (arg.startsWith(`--${longName}=`)) {
|
|
738
|
+
value = arg.slice(`--${longName}=`.length);
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (arg === `--${longName}`) {
|
|
742
|
+
const next = argv[i + 1];
|
|
743
|
+
if (next !== undefined && !next.startsWith('-')) {
|
|
744
|
+
value = next;
|
|
745
|
+
i += 1;
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
value = '';
|
|
749
|
+
}
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (arg.startsWith(`-${shortName}=`)) {
|
|
753
|
+
value = arg.slice(`-${shortName}=`.length);
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
if (arg === `-${shortName}`) {
|
|
757
|
+
const next = argv[i + 1];
|
|
758
|
+
if (next !== undefined && !next.startsWith('-')) {
|
|
759
|
+
value = next;
|
|
760
|
+
i += 1;
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
value = '';
|
|
764
|
+
}
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
remaining.push(arg);
|
|
768
|
+
}
|
|
769
|
+
return { value, remaining };
|
|
770
|
+
}
|
|
597
771
|
class BashCompletion {
|
|
598
772
|
#meta;
|
|
599
773
|
#programName;
|
package/lib/esm/index.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
|
|
1
4
|
class CommanderError extends Error {
|
|
2
5
|
kind;
|
|
3
6
|
commandPath;
|
|
@@ -42,32 +45,25 @@ class Command {
|
|
|
42
45
|
#name;
|
|
43
46
|
#description;
|
|
44
47
|
#version;
|
|
45
|
-
#
|
|
48
|
+
#helpSubcommandEnabled;
|
|
46
49
|
#options = [];
|
|
47
50
|
#arguments = [];
|
|
48
51
|
#subcommands = [];
|
|
49
52
|
#action;
|
|
50
|
-
#parent;
|
|
51
53
|
constructor(config) {
|
|
52
|
-
this.#name = config.name;
|
|
54
|
+
this.#name = config.name ?? '';
|
|
53
55
|
this.#description = config.description;
|
|
54
56
|
this.#version = config.version;
|
|
55
|
-
this.#
|
|
57
|
+
this.#helpSubcommandEnabled = config.help ?? false;
|
|
56
58
|
}
|
|
57
59
|
get name() {
|
|
58
60
|
return this.#name;
|
|
59
61
|
}
|
|
60
|
-
get aliases() {
|
|
61
|
-
return this.#aliases;
|
|
62
|
-
}
|
|
63
62
|
get description() {
|
|
64
63
|
return this.#description;
|
|
65
64
|
}
|
|
66
65
|
get version() {
|
|
67
|
-
return this.#version
|
|
68
|
-
}
|
|
69
|
-
get parent() {
|
|
70
|
-
return this.#parent;
|
|
66
|
+
return this.#version;
|
|
71
67
|
}
|
|
72
68
|
get options() {
|
|
73
69
|
return [...this.#options];
|
|
@@ -90,33 +86,43 @@ class Command {
|
|
|
90
86
|
this.#action = fn;
|
|
91
87
|
return this;
|
|
92
88
|
}
|
|
93
|
-
subcommand(cmd) {
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
subcommand(name, cmd) {
|
|
90
|
+
if (this.#helpSubcommandEnabled && name === 'help') {
|
|
91
|
+
throw new CommanderError('ConfigurationError', '"help" is a reserved subcommand name when help subcommand is enabled', this.#getCommandPath());
|
|
92
|
+
}
|
|
93
|
+
const existing = this.#subcommands.find(e => e.command === cmd);
|
|
94
|
+
if (existing) {
|
|
95
|
+
existing.aliases.push(name);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
cmd.#name = name;
|
|
99
|
+
this.#subcommands.push({ name, aliases: [], command: cmd });
|
|
100
|
+
}
|
|
96
101
|
return this;
|
|
97
102
|
}
|
|
98
103
|
async run(params) {
|
|
99
104
|
const { argv, envs, reporter } = params;
|
|
100
105
|
try {
|
|
101
|
-
const
|
|
102
|
-
const {
|
|
103
|
-
const ctx = {
|
|
104
|
-
cmd: command,
|
|
105
|
-
envs,
|
|
106
|
-
reporter: reporter ?? new DefaultReporter(),
|
|
107
|
-
argv,
|
|
108
|
-
};
|
|
106
|
+
const processedArgv = this.#processHelpSubcommand(argv);
|
|
107
|
+
const { command, remaining } = this.#route(processedArgv);
|
|
109
108
|
const allOptions = command.#getMergedOptions();
|
|
110
109
|
const hasUserHelp = allOptions.some(o => o.long === 'help' && !command.#isBuiltinOption(o));
|
|
111
110
|
const hasUserVersion = allOptions.some(o => o.long === 'version' && !command.#isBuiltinOption(o));
|
|
112
|
-
if (!hasUserHelp &&
|
|
111
|
+
if (!hasUserHelp && command.#hasHelpFlag(remaining, allOptions)) {
|
|
113
112
|
console.log(command.formatHelp());
|
|
114
113
|
return;
|
|
115
114
|
}
|
|
116
|
-
if (!hasUserVersion &&
|
|
115
|
+
if (!hasUserVersion && command.#hasVersionFlag(remaining, allOptions)) {
|
|
117
116
|
console.log(command.version ?? 'unknown');
|
|
118
117
|
return;
|
|
119
118
|
}
|
|
119
|
+
const { opts, args } = command.parse(remaining);
|
|
120
|
+
const ctx = {
|
|
121
|
+
cmd: command,
|
|
122
|
+
envs,
|
|
123
|
+
reporter: reporter ?? new DefaultReporter(),
|
|
124
|
+
argv,
|
|
125
|
+
};
|
|
120
126
|
for (const opt of allOptions) {
|
|
121
127
|
if (opt.apply && opts[opt.long] !== undefined) {
|
|
122
128
|
opt.apply(opts[opt.long], ctx);
|
|
@@ -175,16 +181,8 @@ class Command {
|
|
|
175
181
|
opts[opt.long] = result.value;
|
|
176
182
|
remaining = result.remaining;
|
|
177
183
|
}
|
|
178
|
-
const optionByLong =
|
|
179
|
-
|
|
180
|
-
for (const opt of allOptions) {
|
|
181
|
-
if (!opt.resolver) {
|
|
182
|
-
optionByLong.set(opt.long, opt);
|
|
183
|
-
if (opt.short) {
|
|
184
|
-
optionByShort.set(opt.short, opt);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
184
|
+
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(allOptions, true);
|
|
185
|
+
remaining = this.#normalizeArgv(remaining, booleanOptions);
|
|
188
186
|
let i = 0;
|
|
189
187
|
while (i < remaining.length) {
|
|
190
188
|
const token = remaining[i];
|
|
@@ -283,15 +281,19 @@ class Command {
|
|
|
283
281
|
}
|
|
284
282
|
lines.push('');
|
|
285
283
|
}
|
|
284
|
+
const showHelpSubcommand = this.#helpSubcommandEnabled && this.#subcommands.length > 0;
|
|
286
285
|
if (this.#subcommands.length > 0) {
|
|
287
286
|
lines.push('Commands:');
|
|
288
287
|
const cmdLines = [];
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
288
|
+
if (showHelpSubcommand) {
|
|
289
|
+
cmdLines.push({ name: 'help', desc: 'Show help for a command' });
|
|
290
|
+
}
|
|
291
|
+
for (const entry of this.#subcommands) {
|
|
292
|
+
let name = entry.name;
|
|
293
|
+
if (entry.aliases.length > 0) {
|
|
294
|
+
name += `, ${entry.aliases.join(', ')}`;
|
|
293
295
|
}
|
|
294
|
-
cmdLines.push({ name, desc:
|
|
296
|
+
cmdLines.push({ name, desc: entry.command.#description });
|
|
295
297
|
}
|
|
296
298
|
const maxNameLen = Math.max(...cmdLines.map(l => l.name.length));
|
|
297
299
|
for (const { name, desc } of cmdLines) {
|
|
@@ -318,11 +320,33 @@ class Command {
|
|
|
318
320
|
return {
|
|
319
321
|
name: this.#name,
|
|
320
322
|
description: this.#description,
|
|
321
|
-
aliases:
|
|
323
|
+
aliases: [],
|
|
322
324
|
options,
|
|
323
|
-
subcommands: this.#subcommands.map(
|
|
325
|
+
subcommands: this.#subcommands.map(entry => {
|
|
326
|
+
const subMeta = entry.command.getCompletionMeta();
|
|
327
|
+
return {
|
|
328
|
+
...subMeta,
|
|
329
|
+
name: entry.name,
|
|
330
|
+
aliases: entry.aliases,
|
|
331
|
+
};
|
|
332
|
+
}),
|
|
324
333
|
};
|
|
325
334
|
}
|
|
335
|
+
#processHelpSubcommand(argv) {
|
|
336
|
+
if (!this.#helpSubcommandEnabled || this.#subcommands.length === 0)
|
|
337
|
+
return argv;
|
|
338
|
+
if (argv.length < 1 || argv[0] !== 'help')
|
|
339
|
+
return argv;
|
|
340
|
+
if (argv.length === 1) {
|
|
341
|
+
return ['--help'];
|
|
342
|
+
}
|
|
343
|
+
const subName = argv[1];
|
|
344
|
+
const entry = this.#subcommands.find(e => e.name === subName || e.aliases.includes(subName));
|
|
345
|
+
if (entry) {
|
|
346
|
+
return [subName, '--help', ...argv.slice(2)];
|
|
347
|
+
}
|
|
348
|
+
return argv;
|
|
349
|
+
}
|
|
326
350
|
#route(argv) {
|
|
327
351
|
let current = this;
|
|
328
352
|
let idx = 0;
|
|
@@ -330,10 +354,10 @@ class Command {
|
|
|
330
354
|
const token = argv[idx];
|
|
331
355
|
if (token.startsWith('-'))
|
|
332
356
|
break;
|
|
333
|
-
const
|
|
334
|
-
if (!
|
|
357
|
+
const entry = current.#subcommands.find(e => e.name === token || e.aliases.includes(token));
|
|
358
|
+
if (!entry)
|
|
335
359
|
break;
|
|
336
|
-
current =
|
|
360
|
+
current = entry.command;
|
|
337
361
|
idx += 1;
|
|
338
362
|
}
|
|
339
363
|
return { command: current, remaining: argv.slice(idx) };
|
|
@@ -350,17 +374,6 @@ class Command {
|
|
|
350
374
|
else {
|
|
351
375
|
optName = token.slice(2);
|
|
352
376
|
}
|
|
353
|
-
if (optName.startsWith('no-')) {
|
|
354
|
-
const actualName = optName.slice(3);
|
|
355
|
-
const opt = optionByLong.get(actualName);
|
|
356
|
-
if (opt && opt.type === 'boolean') {
|
|
357
|
-
if (inlineValue !== undefined) {
|
|
358
|
-
throw new CommanderError('InvalidBooleanValue', `"--no-${actualName}" does not accept a value`, this.#getCommandPath());
|
|
359
|
-
}
|
|
360
|
-
opts[actualName] = false;
|
|
361
|
-
return idx + 1;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
377
|
const opt = optionByLong.get(optName);
|
|
365
378
|
if (!opt) {
|
|
366
379
|
throw new CommanderError('UnknownOption', `unknown option "--${optName}" for command "${this.#getCommandPath()}"`, this.#getCommandPath());
|
|
@@ -427,7 +440,7 @@ class Command {
|
|
|
427
440
|
}
|
|
428
441
|
#applyValue(opt, rawValue, opts) {
|
|
429
442
|
const type = opt.type ?? 'string';
|
|
430
|
-
let parsedValue;
|
|
443
|
+
let parsedValue = rawValue;
|
|
431
444
|
if (opt.coerce) {
|
|
432
445
|
parsedValue = opt.coerce(rawValue);
|
|
433
446
|
}
|
|
@@ -446,8 +459,6 @@ class Command {
|
|
|
446
459
|
parsedValue = num;
|
|
447
460
|
break;
|
|
448
461
|
}
|
|
449
|
-
default:
|
|
450
|
-
parsedValue = rawValue;
|
|
451
462
|
}
|
|
452
463
|
}
|
|
453
464
|
if (type === 'string[]' || type === 'number[]') {
|
|
@@ -460,33 +471,17 @@ class Command {
|
|
|
460
471
|
}
|
|
461
472
|
}
|
|
462
473
|
#getMergedOptions() {
|
|
463
|
-
const ancestors = [];
|
|
464
|
-
for (let node = this; node; node = node.#parent) {
|
|
465
|
-
ancestors.unshift(node);
|
|
466
|
-
}
|
|
467
474
|
const optionMap = new Map();
|
|
468
|
-
const hasUserHelp =
|
|
469
|
-
const hasUserVersion =
|
|
475
|
+
const hasUserHelp = this.#options.some(o => o.long === 'help');
|
|
476
|
+
const hasUserVersion = this.#options.some(o => o.long === 'version');
|
|
470
477
|
if (!hasUserHelp) {
|
|
471
478
|
optionMap.set('help', BUILTIN_HELP_OPTION);
|
|
472
479
|
}
|
|
473
480
|
if (!hasUserVersion) {
|
|
474
481
|
optionMap.set('version', BUILTIN_VERSION_OPTION);
|
|
475
482
|
}
|
|
476
|
-
for (const
|
|
477
|
-
|
|
478
|
-
optionMap.set(opt.long, opt);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
const shortToLong = new Map();
|
|
482
|
-
for (const [long, opt] of optionMap) {
|
|
483
|
-
if (opt.short) {
|
|
484
|
-
const existing = shortToLong.get(opt.short);
|
|
485
|
-
if (existing && existing !== long) {
|
|
486
|
-
throw new CommanderError('OptionConflict', `short option "-${opt.short}" is used by both "--${existing}" and "--${long}"`, this.#getCommandPath());
|
|
487
|
-
}
|
|
488
|
-
shortToLong.set(opt.short, long);
|
|
489
|
-
}
|
|
483
|
+
for (const opt of this.#options) {
|
|
484
|
+
optionMap.set(opt.long, opt);
|
|
490
485
|
}
|
|
491
486
|
return Array.from(optionMap.values());
|
|
492
487
|
}
|
|
@@ -531,20 +526,109 @@ class Command {
|
|
|
531
526
|
#isBuiltinOption(opt) {
|
|
532
527
|
return opt === BUILTIN_HELP_OPTION || opt === BUILTIN_VERSION_OPTION;
|
|
533
528
|
}
|
|
534
|
-
#
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
529
|
+
#buildOptionMaps(allOptions, excludeResolver = false) {
|
|
530
|
+
const optionByLong = new Map();
|
|
531
|
+
const optionByShort = new Map();
|
|
532
|
+
const booleanOptions = new Set();
|
|
533
|
+
for (const opt of allOptions) {
|
|
534
|
+
if (excludeResolver && opt.resolver)
|
|
535
|
+
continue;
|
|
536
|
+
optionByLong.set(opt.long, opt);
|
|
537
|
+
if (opt.short) {
|
|
538
|
+
optionByShort.set(opt.short, opt);
|
|
539
|
+
}
|
|
540
|
+
if (opt.type === 'boolean') {
|
|
541
|
+
booleanOptions.add(opt.long);
|
|
542
|
+
}
|
|
538
543
|
}
|
|
539
|
-
return
|
|
544
|
+
return { optionByLong, optionByShort, booleanOptions };
|
|
545
|
+
}
|
|
546
|
+
#hasHelpFlag(argv, allOptions) {
|
|
547
|
+
return this.#hasBuiltinFlag(argv, 'help', 'h', allOptions);
|
|
548
|
+
}
|
|
549
|
+
#hasVersionFlag(argv, allOptions) {
|
|
550
|
+
return this.#hasBuiltinFlag(argv, 'version', 'V', allOptions);
|
|
551
|
+
}
|
|
552
|
+
#hasBuiltinFlag(argv, flagLong, flagShort, allOptions) {
|
|
553
|
+
const { optionByLong, optionByShort, booleanOptions } = this.#buildOptionMaps(allOptions);
|
|
554
|
+
const normalizedArgv = this.#normalizeArgv(argv, booleanOptions);
|
|
555
|
+
for (let i = 0; i < normalizedArgv.length; i++) {
|
|
556
|
+
const arg = normalizedArgv[i];
|
|
557
|
+
if (arg === '--') {
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
if (arg === `--${flagLong}` || (flagShort && arg === `-${flagShort}`)) {
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
if (this.#optionConsumesNextValue(arg, optionByLong, optionByShort)) {
|
|
564
|
+
i += 1;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
#optionConsumesNextValue(arg, optionByLong, optionByShort) {
|
|
570
|
+
if (arg.startsWith('--')) {
|
|
571
|
+
const eqIdx = arg.indexOf('=');
|
|
572
|
+
if (eqIdx !== -1) {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
const optName = arg.slice(2);
|
|
576
|
+
const opt = optionByLong.get(optName);
|
|
577
|
+
if (!opt) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
const type = opt.type ?? 'string';
|
|
581
|
+
return type !== 'boolean';
|
|
582
|
+
}
|
|
583
|
+
if (arg.startsWith('-') && arg.length === 2) {
|
|
584
|
+
const opt = optionByShort.get(arg[1]);
|
|
585
|
+
if (!opt) {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
const type = opt.type ?? 'string';
|
|
589
|
+
return type !== 'boolean';
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
#normalizeArgv(argv, booleanOptions) {
|
|
594
|
+
const result = [];
|
|
595
|
+
let seenDoubleDash = false;
|
|
596
|
+
for (const arg of argv) {
|
|
597
|
+
if (arg === '--') {
|
|
598
|
+
seenDoubleDash = true;
|
|
599
|
+
result.push(arg);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (!seenDoubleDash && arg.startsWith('--no-')) {
|
|
603
|
+
const eqIdx = arg.indexOf('=');
|
|
604
|
+
if (eqIdx !== -1) {
|
|
605
|
+
const optName = arg.slice(5, eqIdx);
|
|
606
|
+
if (booleanOptions.has(optName)) {
|
|
607
|
+
throw new CommanderError('InvalidBooleanValue', `"--no-${optName}" does not accept a value`, this.#getCommandPath());
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
const optName = arg.slice(5);
|
|
612
|
+
if (booleanOptions.has(optName)) {
|
|
613
|
+
result.push(`--${optName}=false`);
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
result.push(arg);
|
|
619
|
+
}
|
|
620
|
+
return result;
|
|
621
|
+
}
|
|
622
|
+
#getCommandPath() {
|
|
623
|
+
return this.#name;
|
|
540
624
|
}
|
|
541
625
|
}
|
|
542
626
|
|
|
543
627
|
class CompletionCommand extends Command {
|
|
544
628
|
constructor(root, config) {
|
|
545
|
-
const
|
|
629
|
+
const paths = config.paths;
|
|
630
|
+
const programName = config.programName ?? root.name;
|
|
546
631
|
super({
|
|
547
|
-
name,
|
|
548
632
|
description: 'Generate shell completion script',
|
|
549
633
|
});
|
|
550
634
|
this.option({
|
|
@@ -561,10 +645,16 @@ class CompletionCommand extends Command {
|
|
|
561
645
|
long: 'pwsh',
|
|
562
646
|
type: 'boolean',
|
|
563
647
|
description: 'Generate PowerShell completion script',
|
|
648
|
+
})
|
|
649
|
+
.option({
|
|
650
|
+
long: 'write',
|
|
651
|
+
short: 'w',
|
|
652
|
+
type: 'string',
|
|
653
|
+
description: 'Write to file (default path if no value given)',
|
|
654
|
+
resolver: argv => resolveOptionalStringOption(argv, 'write', 'w'),
|
|
564
655
|
})
|
|
565
656
|
.action(({ opts }) => {
|
|
566
657
|
const meta = root.getCompletionMeta();
|
|
567
|
-
const programName = root.name;
|
|
568
658
|
const selectedShells = [
|
|
569
659
|
opts['bash'] && 'bash',
|
|
570
660
|
opts['fish'] && 'fish',
|
|
@@ -573,25 +663,89 @@ class CompletionCommand extends Command {
|
|
|
573
663
|
if (selectedShells.length === 0) {
|
|
574
664
|
console.error('Please specify a shell: --bash, --fish, or --pwsh');
|
|
575
665
|
process.exit(1);
|
|
666
|
+
return;
|
|
576
667
|
}
|
|
577
668
|
if (selectedShells.length > 1) {
|
|
578
669
|
console.error('Please specify only one shell option');
|
|
579
670
|
process.exit(1);
|
|
671
|
+
return;
|
|
580
672
|
}
|
|
581
|
-
|
|
673
|
+
const shell = selectedShells[0];
|
|
674
|
+
let script;
|
|
675
|
+
switch (shell) {
|
|
582
676
|
case 'bash':
|
|
583
|
-
|
|
677
|
+
script = new BashCompletion(meta, programName).generate();
|
|
584
678
|
break;
|
|
585
679
|
case 'fish':
|
|
586
|
-
|
|
680
|
+
script = new FishCompletion(meta, programName).generate();
|
|
587
681
|
break;
|
|
588
682
|
case 'pwsh':
|
|
589
|
-
|
|
683
|
+
script = new PwshCompletion(meta, programName).generate();
|
|
590
684
|
break;
|
|
591
685
|
}
|
|
686
|
+
const writeOpt = opts['write'];
|
|
687
|
+
if (writeOpt !== undefined) {
|
|
688
|
+
const filePath = typeof writeOpt === 'string' && writeOpt !== '' ? writeOpt : paths[shell];
|
|
689
|
+
const expandedPath = expandHome(filePath);
|
|
690
|
+
const dir = path.dirname(expandedPath);
|
|
691
|
+
if (!fs.existsSync(dir)) {
|
|
692
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
693
|
+
}
|
|
694
|
+
fs.writeFileSync(expandedPath, script, 'utf-8');
|
|
695
|
+
console.log(`Completion script written to: ${expandedPath}`);
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
console.log(script);
|
|
699
|
+
}
|
|
592
700
|
});
|
|
593
701
|
}
|
|
594
702
|
}
|
|
703
|
+
function expandHome(filepath) {
|
|
704
|
+
if (filepath.startsWith('~/') || filepath === '~') {
|
|
705
|
+
const home = process.env['HOME'] || process.env['USERPROFILE'] || '';
|
|
706
|
+
return filepath.replace(/^~/, home);
|
|
707
|
+
}
|
|
708
|
+
return filepath;
|
|
709
|
+
}
|
|
710
|
+
function resolveOptionalStringOption(argv, longName, shortName) {
|
|
711
|
+
const remaining = [];
|
|
712
|
+
let value;
|
|
713
|
+
for (let i = 0; i < argv.length; i++) {
|
|
714
|
+
const arg = argv[i];
|
|
715
|
+
if (arg.startsWith(`--${longName}=`)) {
|
|
716
|
+
value = arg.slice(`--${longName}=`.length);
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
if (arg === `--${longName}`) {
|
|
720
|
+
const next = argv[i + 1];
|
|
721
|
+
if (next !== undefined && !next.startsWith('-')) {
|
|
722
|
+
value = next;
|
|
723
|
+
i += 1;
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
value = '';
|
|
727
|
+
}
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (arg.startsWith(`-${shortName}=`)) {
|
|
731
|
+
value = arg.slice(`-${shortName}=`.length);
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
if (arg === `-${shortName}`) {
|
|
735
|
+
const next = argv[i + 1];
|
|
736
|
+
if (next !== undefined && !next.startsWith('-')) {
|
|
737
|
+
value = next;
|
|
738
|
+
i += 1;
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
value = '';
|
|
742
|
+
}
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
remaining.push(arg);
|
|
746
|
+
}
|
|
747
|
+
return { value, remaining };
|
|
748
|
+
}
|
|
595
749
|
class BashCompletion {
|
|
596
750
|
#meta;
|
|
597
751
|
#programName;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -57,22 +57,20 @@ interface IArgument {
|
|
|
57
57
|
}
|
|
58
58
|
/** Command configuration */
|
|
59
59
|
interface ICommandConfig {
|
|
60
|
-
/** Command name (
|
|
61
|
-
name
|
|
62
|
-
/** Command aliases */
|
|
63
|
-
aliases?: string[];
|
|
60
|
+
/** Command name (only effective for root command) */
|
|
61
|
+
name?: string;
|
|
64
62
|
/** Command description */
|
|
65
63
|
description: string;
|
|
66
64
|
/** Version (only effective for root command) */
|
|
67
65
|
version?: string;
|
|
66
|
+
/** Enable built-in "help" subcommand (only effective when command has subcommands) */
|
|
67
|
+
help?: boolean;
|
|
68
68
|
}
|
|
69
69
|
/** Forward declaration for Command class */
|
|
70
70
|
interface ICommand {
|
|
71
71
|
readonly name: string;
|
|
72
|
-
readonly aliases: string[];
|
|
73
72
|
readonly description: string;
|
|
74
73
|
readonly version: string | undefined;
|
|
75
|
-
readonly parent: ICommand | undefined;
|
|
76
74
|
readonly options: IOption[];
|
|
77
75
|
readonly arguments: IArgument[];
|
|
78
76
|
}
|
|
@@ -142,10 +140,21 @@ interface ICompletionMeta {
|
|
|
142
140
|
options: ICompletionOptionMeta[];
|
|
143
141
|
subcommands: ICompletionMeta[];
|
|
144
142
|
}
|
|
143
|
+
/** Shell completion paths configuration */
|
|
144
|
+
interface ICompletionPaths {
|
|
145
|
+
/** Bash completion file path (e.g., ~/.local/share/bash-completion/completions/{name}) */
|
|
146
|
+
bash: string;
|
|
147
|
+
/** Fish completion file path (e.g., ~/.config/fish/completions/{name}.fish) */
|
|
148
|
+
fish: string;
|
|
149
|
+
/** PowerShell completion file path (only ~ expansion supported, not $PROFILE) */
|
|
150
|
+
pwsh: string;
|
|
151
|
+
}
|
|
145
152
|
/** CompletionCommand configuration */
|
|
146
153
|
interface ICompletionCommandConfig {
|
|
147
|
-
/**
|
|
148
|
-
|
|
154
|
+
/** Program name for completion scripts (defaults to root.name) */
|
|
155
|
+
programName?: string;
|
|
156
|
+
/** Default completion file paths for each shell (required for --write support) */
|
|
157
|
+
paths: ICompletionPaths;
|
|
149
158
|
}
|
|
150
159
|
|
|
151
160
|
/**
|
|
@@ -154,20 +163,18 @@ interface ICompletionCommandConfig {
|
|
|
154
163
|
* @module @guanghechen/commander
|
|
155
164
|
*/
|
|
156
165
|
|
|
157
|
-
declare class Command {
|
|
166
|
+
declare class Command implements ICommand {
|
|
158
167
|
#private;
|
|
159
168
|
constructor(config: ICommandConfig);
|
|
160
169
|
get name(): string;
|
|
161
|
-
get aliases(): string[];
|
|
162
170
|
get description(): string;
|
|
163
171
|
get version(): string | undefined;
|
|
164
|
-
get parent(): Command | undefined;
|
|
165
172
|
get options(): IOption[];
|
|
166
173
|
get arguments(): IArgument[];
|
|
167
174
|
option(opt: IOption): this;
|
|
168
175
|
argument(arg: IArgument): this;
|
|
169
176
|
action(fn: IAction): this;
|
|
170
|
-
subcommand(cmd: Command): this;
|
|
177
|
+
subcommand(name: string, cmd: Command): this;
|
|
171
178
|
run(params: IRunParams): Promise<void>;
|
|
172
179
|
parse(argv: string[]): IParseResult;
|
|
173
180
|
formatHelp(): string;
|
|
@@ -186,16 +193,22 @@ declare class Command {
|
|
|
186
193
|
* @example
|
|
187
194
|
* ```typescript
|
|
188
195
|
* const root = new Command({ name: 'mycli', description: 'My CLI' })
|
|
189
|
-
* root.subcommand(new CompletionCommand(root
|
|
196
|
+
* root.subcommand('completion', new CompletionCommand(root, {
|
|
197
|
+
* paths: {
|
|
198
|
+
* bash: `~/.local/share/bash-completion/completions/mycli`,
|
|
199
|
+
* fish: `~/.config/fish/completions/mycli.fish`,
|
|
200
|
+
* pwsh: `~/.config/powershell/Microsoft.PowerShell_profile.ps1`,
|
|
201
|
+
* }
|
|
202
|
+
* }))
|
|
190
203
|
*
|
|
191
204
|
* // Usage:
|
|
192
205
|
* // mycli completion --bash > ~/.local/share/bash-completion/completions/mycli
|
|
193
|
-
* // mycli completion --fish
|
|
194
|
-
* // mycli completion --
|
|
206
|
+
* // mycli completion --fish --write (writes to default path)
|
|
207
|
+
* // mycli completion --fish --write /custom/path.fish
|
|
195
208
|
* ```
|
|
196
209
|
*/
|
|
197
210
|
declare class CompletionCommand extends Command {
|
|
198
|
-
constructor(root: Command, config
|
|
211
|
+
constructor(root: Command, config: ICompletionCommandConfig);
|
|
199
212
|
}
|
|
200
213
|
declare class BashCompletion {
|
|
201
214
|
#private;
|
|
@@ -214,4 +227,4 @@ declare class PwshCompletion {
|
|
|
214
227
|
}
|
|
215
228
|
|
|
216
229
|
export { BashCompletion, Command, CommanderError, CompletionCommand, FishCompletion, PwshCompletion };
|
|
217
|
-
export type { IAction, IActionParams, IArgument, IArgumentKind, ICommand, ICommandConfig, ICommandContext, ICommanderErrorKind, ICompletionCommandConfig, ICompletionMeta, ICompletionOptionMeta, IOption, IOptionType, IParseResult, IReporter, IRunParams, IShellType };
|
|
230
|
+
export type { IAction, IActionParams, IArgument, IArgumentKind, ICommand, ICommandConfig, ICommandContext, ICommanderErrorKind, ICompletionCommandConfig, ICompletionMeta, ICompletionOptionMeta, ICompletionPaths, IOption, IOptionType, IParseResult, IReporter, IRunParams, IShellType };
|