cli-forge 0.10.1 → 0.11.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/.eslintrc.json +35 -0
- package/LICENSE.md +5 -0
- package/cli.js +9 -0
- package/{bin → dist/bin}/cli.d.ts +1 -1
- package/{bin → dist/bin}/cli.js +2 -2
- package/dist/bin/cli.js.map +1 -0
- package/{bin → dist/bin}/commands/generate-documentation.d.ts +1 -1
- package/{bin → dist/bin}/commands/generate-documentation.js +58 -11
- package/dist/bin/commands/generate-documentation.js.map +1 -0
- package/{bin → dist/bin}/commands/init.d.ts +1 -1
- package/{bin → dist/bin}/commands/init.js +11 -6
- package/dist/bin/commands/init.js.map +1 -0
- package/dist/bin/utils/fs.js.map +1 -0
- package/{src → dist}/index.d.ts +1 -1
- package/dist/index.js.map +1 -0
- package/dist/lib/cli-option-groups.js.map +1 -0
- package/dist/lib/composable-builder.js.map +1 -0
- package/dist/lib/configuration-providers.js.map +1 -0
- package/{src → dist}/lib/documentation.d.ts +3 -3
- package/dist/lib/documentation.js.map +1 -0
- package/dist/lib/format-help.js.map +1 -0
- package/{src → dist}/lib/interactive-shell.d.ts +1 -1
- package/{src → dist}/lib/interactive-shell.js +6 -3
- package/dist/lib/interactive-shell.js.map +1 -0
- package/{src → dist}/lib/internal-cli.d.ts +3 -3
- package/{src → dist}/lib/internal-cli.js +8 -1
- package/dist/lib/internal-cli.js.map +1 -0
- package/{src → dist}/lib/public-api.d.ts +41 -6
- package/dist/lib/public-api.js.map +1 -0
- package/dist/lib/test-harness.js.map +1 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/middleware/zod.d.ts +4 -0
- package/dist/middleware/zod.js +18 -0
- package/dist/middleware/zod.js.map +1 -0
- package/dist/middleware.d.ts +1 -0
- package/dist/middleware.js +5 -0
- package/dist/middleware.js.map +1 -0
- package/package.json +28 -10
- package/project.json +7 -0
- package/src/bin/cli.ts +17 -0
- package/src/bin/commands/generate-documentation.ts +403 -0
- package/src/bin/commands/init.ts +320 -0
- package/src/bin/utils/fs.ts +11 -0
- package/src/index.ts +7 -0
- package/src/lib/cli-option-groups.ts +69 -0
- package/src/lib/composable-builder.ts +9 -0
- package/src/lib/configuration-providers.ts +36 -0
- package/src/lib/documentation.spec.ts +156 -0
- package/src/lib/documentation.ts +107 -0
- package/src/lib/format-help.ts +149 -0
- package/src/lib/interactive-shell.ts +115 -0
- package/src/lib/internal-cli.spec.ts +345 -0
- package/src/lib/internal-cli.ts +529 -0
- package/src/lib/public-api.ts +449 -0
- package/src/lib/test-harness.spec.ts +29 -0
- package/src/lib/test-harness.ts +69 -0
- package/src/lib/utils.spec.ts +25 -0
- package/src/lib/utils.ts +144 -0
- package/src/middleware/zod.ts +21 -0
- package/src/middleware.ts +1 -0
- package/tsconfig.json +23 -0
- package/tsconfig.lib.json +20 -0
- package/tsconfig.lib.json.tsbuildinfo +1 -0
- package/tsconfig.spec.json +26 -0
- package/vitest.config.mts +18 -0
- package/bin/cli.js.map +0 -1
- package/bin/commands/generate-documentation.js.map +0 -1
- package/bin/commands/init.js.map +0 -1
- package/bin/utils/fs.js.map +0 -1
- package/src/index.js.map +0 -1
- package/src/lib/cli-option-groups.js.map +0 -1
- package/src/lib/composable-builder.js.map +0 -1
- package/src/lib/configuration-providers.js.map +0 -1
- package/src/lib/documentation.js.map +0 -1
- package/src/lib/format-help.js.map +0 -1
- package/src/lib/interactive-shell.js.map +0 -1
- package/src/lib/internal-cli.js.map +0 -1
- package/src/lib/public-api.js.map +0 -1
- package/src/lib/test-harness.js.map +0 -1
- package/src/lib/utils.js.map +0 -1
- /package/{bin → dist/bin}/utils/fs.d.ts +0 -0
- /package/{bin → dist/bin}/utils/fs.js +0 -0
- /package/{src → dist}/index.js +0 -0
- /package/{src → dist}/lib/cli-option-groups.d.ts +0 -0
- /package/{src → dist}/lib/cli-option-groups.js +0 -0
- /package/{src → dist}/lib/composable-builder.d.ts +0 -0
- /package/{src → dist}/lib/composable-builder.js +0 -0
- /package/{src → dist}/lib/configuration-providers.d.ts +0 -0
- /package/{src → dist}/lib/configuration-providers.js +0 -0
- /package/{src → dist}/lib/documentation.js +0 -0
- /package/{src → dist}/lib/format-help.d.ts +0 -0
- /package/{src → dist}/lib/format-help.js +0 -0
- /package/{src → dist}/lib/public-api.js +0 -0
- /package/{src → dist}/lib/test-harness.d.ts +0 -0
- /package/{src → dist}/lib/test-harness.js +0 -0
- /package/{src → dist}/lib/utils.d.ts +0 -0
- /package/{src → dist}/lib/utils.js +0 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArgvParser,
|
|
3
|
+
EnvOptionConfig,
|
|
4
|
+
OptionConfig,
|
|
5
|
+
ParsedArgs,
|
|
6
|
+
ValidationFailedError,
|
|
7
|
+
fromCamelOrDashedCaseToConstCase,
|
|
8
|
+
hideBin,
|
|
9
|
+
type ConfigurationFiles,
|
|
10
|
+
} from '@cli-forge/parser';
|
|
11
|
+
import { getCallingFile, getParentPackageJson } from './utils';
|
|
12
|
+
import { INTERACTIVE_SHELL, InteractiveShell } from './interactive-shell';
|
|
13
|
+
import { CLI, CLICommandOptions, Command, ErrorHandler } from './public-api';
|
|
14
|
+
import { readOptionGroupsForCLI } from './cli-option-groups';
|
|
15
|
+
import { formatHelp } from './format-help';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The base class for a CLI application. This class is used to define the structure of the CLI.
|
|
19
|
+
*
|
|
20
|
+
* {@link cli} is provided as a small helper function to create a new CLI instance.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { cli } from 'cli-forge';
|
|
25
|
+
*
|
|
26
|
+
* cli('basic-cli').command('hello', {
|
|
27
|
+
* builder: (args) =>
|
|
28
|
+
* args.option('name', {
|
|
29
|
+
* type: 'string',
|
|
30
|
+
* }),
|
|
31
|
+
* handler: (args) => {
|
|
32
|
+
* console.log(`Hello, ${args.name}!`);
|
|
33
|
+
* }).forge();
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export class InternalCLI<TArgs extends ParsedArgs = ParsedArgs>
|
|
37
|
+
implements CLI<TArgs>
|
|
38
|
+
{
|
|
39
|
+
/**
|
|
40
|
+
* For internal use only. Stick to properties available on {@link CLI}.
|
|
41
|
+
*/
|
|
42
|
+
registeredCommands: Record<string, InternalCLI<any>> = {};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* For internal use only. Stick to properties available on {@link CLI}.
|
|
46
|
+
*/
|
|
47
|
+
commandChain: string[] = [];
|
|
48
|
+
|
|
49
|
+
private requiresCommand: 'IMPLICIT' | 'EXPLICIT' | false = 'IMPLICIT';
|
|
50
|
+
|
|
51
|
+
private _configuration?: CLICommandOptions<any, any>;
|
|
52
|
+
|
|
53
|
+
private _versionOverride?: string;
|
|
54
|
+
|
|
55
|
+
private registeredErrorHandlers: Array<ErrorHandler> = [
|
|
56
|
+
(e: unknown, actions) => {
|
|
57
|
+
if (e instanceof ValidationFailedError) {
|
|
58
|
+
this.printHelp();
|
|
59
|
+
console.log();
|
|
60
|
+
console.log(e.message);
|
|
61
|
+
console.log(e.errors.map((e) => ` - ${e.message}`).join('\n'));
|
|
62
|
+
actions.exit(1);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
private registeredMiddleware: Array<(args: TArgs) => void> = [];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A list of option groups that have been registered with the CLI. Grouped Options are displayed together in the help text.
|
|
71
|
+
*
|
|
72
|
+
* For internal use only. Stick to properties available on {@link CLI}.
|
|
73
|
+
*/
|
|
74
|
+
registeredOptionGroups: Array<{
|
|
75
|
+
label: string;
|
|
76
|
+
sortOrder: number;
|
|
77
|
+
keys: Array<keyof TArgs>;
|
|
78
|
+
}> = [];
|
|
79
|
+
|
|
80
|
+
getGroupedOptions() {
|
|
81
|
+
return readOptionGroupsForCLI(this);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get configuration() {
|
|
85
|
+
return this._configuration;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private set configuration(value: CLICommandOptions<any, any> | undefined) {
|
|
89
|
+
this._configuration = value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The parser used to parse the arguments for the current command.
|
|
94
|
+
*
|
|
95
|
+
* Meant for internal use only. Stick to properties available on {@link CLI}.
|
|
96
|
+
*
|
|
97
|
+
* If you need this kind of info, please open an issue on the GitHub repo with
|
|
98
|
+
* your use case.
|
|
99
|
+
*/
|
|
100
|
+
parser = new ArgvParser<TArgs>({
|
|
101
|
+
unmatchedParser: (arg) => {
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
103
|
+
let currentCommand: InternalCLI<any> = this;
|
|
104
|
+
for (const command of this.commandChain) {
|
|
105
|
+
currentCommand = currentCommand.registeredCommands[command];
|
|
106
|
+
}
|
|
107
|
+
const command = currentCommand.registeredCommands[arg];
|
|
108
|
+
if (command && command.configuration) {
|
|
109
|
+
command.parser = this.parser;
|
|
110
|
+
command.configuration.builder?.(command);
|
|
111
|
+
this.commandChain.push(arg);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
.option('help', {
|
|
118
|
+
type: 'boolean',
|
|
119
|
+
alias: ['h'],
|
|
120
|
+
description: 'Show help for the current command',
|
|
121
|
+
})
|
|
122
|
+
.option('version', {
|
|
123
|
+
type: 'boolean',
|
|
124
|
+
description: 'Show the version number for the CLI',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param name What should the name of the cli command be?
|
|
129
|
+
* @param configuration Configuration for the current CLI command.
|
|
130
|
+
*/
|
|
131
|
+
constructor(
|
|
132
|
+
public name: string,
|
|
133
|
+
rootCommandConfiguration?: CLICommandOptions<TArgs>
|
|
134
|
+
) {
|
|
135
|
+
if (rootCommandConfiguration) {
|
|
136
|
+
this.withRootCommandConfiguration(rootCommandConfiguration);
|
|
137
|
+
} else {
|
|
138
|
+
this.requiresCommand = 'IMPLICIT';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
withRootCommandConfiguration<TRootCommandArgs extends TArgs>(
|
|
143
|
+
configuration: CLICommandOptions<TArgs, TRootCommandArgs>
|
|
144
|
+
): InternalCLI<TArgs> {
|
|
145
|
+
this.configuration = configuration;
|
|
146
|
+
this.requiresCommand = false;
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
command<TCommandArgs extends TArgs>(
|
|
151
|
+
keyOrCommand: string | Command<TArgs, TCommandArgs>,
|
|
152
|
+
options?: CLICommandOptions<TArgs, TCommandArgs>
|
|
153
|
+
): CLI<TArgs> {
|
|
154
|
+
if (typeof keyOrCommand === 'string') {
|
|
155
|
+
const key = keyOrCommand;
|
|
156
|
+
if (!options) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
'options must be provided when calling `command` with a string'
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (key === '$0' || options.alias?.includes('$0')) {
|
|
162
|
+
this.withRootCommandConfiguration({
|
|
163
|
+
...this._configuration,
|
|
164
|
+
builder: options.builder as any,
|
|
165
|
+
handler: options.handler as any,
|
|
166
|
+
description: options.description,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const cmd = new InternalCLI<TArgs>(key).withRootCommandConfiguration(
|
|
170
|
+
options
|
|
171
|
+
);
|
|
172
|
+
this.registeredCommands[key] = cmd;
|
|
173
|
+
if (options.alias) {
|
|
174
|
+
for (const alias of options.alias) {
|
|
175
|
+
this.registeredCommands[alias] = cmd;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} else if (keyOrCommand instanceof InternalCLI) {
|
|
179
|
+
const cmd = keyOrCommand;
|
|
180
|
+
this.registeredCommands[cmd.name] = cmd;
|
|
181
|
+
if (cmd.configuration?.alias) {
|
|
182
|
+
for (const alias of cmd.configuration.alias) {
|
|
183
|
+
this.registeredCommands[alias] = cmd;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
const { name, ...configuration } = keyOrCommand as {
|
|
188
|
+
name: string;
|
|
189
|
+
} & CLICommandOptions<TArgs, TCommandArgs>;
|
|
190
|
+
this.command<TCommandArgs>(name, configuration);
|
|
191
|
+
}
|
|
192
|
+
return this;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
commands(...a0: Command[] | Command[][]): CLI<TArgs> {
|
|
196
|
+
const commands = a0.flat();
|
|
197
|
+
for (const val of commands) {
|
|
198
|
+
if (val instanceof InternalCLI) {
|
|
199
|
+
this.registeredCommands[val.name] = val;
|
|
200
|
+
// Include any options that were defined via cli(...).option() instead of via builder
|
|
201
|
+
this.parser.augment(val.parser);
|
|
202
|
+
} else {
|
|
203
|
+
const { name, ...configuration } = val as {
|
|
204
|
+
name: string;
|
|
205
|
+
} & CLICommandOptions<any, any>;
|
|
206
|
+
this.command(name, configuration);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return this;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
option<
|
|
213
|
+
TOption extends string,
|
|
214
|
+
const TOptionConfig extends OptionConfig<any, any, any, any>
|
|
215
|
+
>(name: TOption, config: TOptionConfig) {
|
|
216
|
+
this.parser.option(name, config);
|
|
217
|
+
// Interface modifies the return type to reflect new params, cast is necessay.... I think 🤔
|
|
218
|
+
return this as any;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
positional<
|
|
222
|
+
TOption extends string,
|
|
223
|
+
const TOptionConfig extends OptionConfig<any, any, any, any>
|
|
224
|
+
>(name: TOption, config: TOptionConfig) {
|
|
225
|
+
this.parser.positional(name, config);
|
|
226
|
+
// Interface modifies the return type to reflect new params, cast is necessay.... I think 🤔
|
|
227
|
+
return this as any;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
conflicts(...args: [string, string, ...string[]]): CLI<TArgs> {
|
|
231
|
+
this.parser.conflicts(...args);
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
implies(option: string, ...impliedOptions: string[]): CLI<TArgs> {
|
|
236
|
+
this.parser.implies(option, ...impliedOptions);
|
|
237
|
+
return this;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
env(
|
|
241
|
+
a0: string | EnvOptionConfig | undefined = fromCamelOrDashedCaseToConstCase(
|
|
242
|
+
this.name
|
|
243
|
+
)
|
|
244
|
+
) {
|
|
245
|
+
if (typeof a0 === 'string') {
|
|
246
|
+
this.parser.env(a0);
|
|
247
|
+
} else {
|
|
248
|
+
a0.prefix ??= fromCamelOrDashedCaseToConstCase(this.name);
|
|
249
|
+
this.parser.env(a0);
|
|
250
|
+
}
|
|
251
|
+
return this;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
demandCommand() {
|
|
255
|
+
this.requiresCommand = 'EXPLICIT';
|
|
256
|
+
return this;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
usage(usageText: string) {
|
|
260
|
+
this.configuration ??= {};
|
|
261
|
+
this.configuration.usage = usageText;
|
|
262
|
+
return this;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
examples(...examples: string[]) {
|
|
266
|
+
this.configuration ??= {};
|
|
267
|
+
this.configuration.examples ??= [];
|
|
268
|
+
this.configuration.examples.push(...examples);
|
|
269
|
+
return this;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
version(version?: string) {
|
|
273
|
+
this._versionOverride = version;
|
|
274
|
+
return this;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Gets help text for the current command as a string.
|
|
279
|
+
* @returns Help text for the current command.
|
|
280
|
+
*/
|
|
281
|
+
formatHelp() {
|
|
282
|
+
return formatHelp(this);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Prints help text for the current command to the console.
|
|
287
|
+
*/
|
|
288
|
+
printHelp() {
|
|
289
|
+
console.log(this.formatHelp());
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
middleware<TArgs2>(
|
|
293
|
+
callback: (args: TArgs) => TArgs2 | Promise<TArgs2>
|
|
294
|
+
): CLI<TArgs2 extends void ? TArgs : TArgs & TArgs2> {
|
|
295
|
+
this.registeredMiddleware.push(callback);
|
|
296
|
+
// If middleware returns void, TArgs doesn't change...
|
|
297
|
+
// If it returns something, we need to merge it into TArgs...
|
|
298
|
+
// that's not here though, its where we apply the middleware results.
|
|
299
|
+
return this as any;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Runs the current command.
|
|
304
|
+
* @param cmd The command to run.
|
|
305
|
+
* @param args The arguments to pass to the command.
|
|
306
|
+
*/
|
|
307
|
+
async runCommand<T extends ParsedArgs>(args: T, originalArgV: string[]) {
|
|
308
|
+
const middlewares: Array<(args: any) => void> = [
|
|
309
|
+
...this.registeredMiddleware,
|
|
310
|
+
];
|
|
311
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
312
|
+
let cmd: InternalCLI<any> = this;
|
|
313
|
+
for (const command of this.commandChain) {
|
|
314
|
+
cmd = cmd.registeredCommands[command];
|
|
315
|
+
middlewares.push(...cmd.registeredMiddleware);
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
if (cmd.requiresCommand) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
`${[this.name, ...this.commandChain].join(' ')} requires a command`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
if (cmd.configuration?.handler) {
|
|
324
|
+
for (const middleware of middlewares) {
|
|
325
|
+
const middlewareResult = await middleware(args);
|
|
326
|
+
if (
|
|
327
|
+
middlewareResult !== void 0 &&
|
|
328
|
+
typeof middlewareResult === 'object'
|
|
329
|
+
) {
|
|
330
|
+
args = middlewareResult as T;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
await cmd.configuration.handler(args, {
|
|
334
|
+
command: cmd,
|
|
335
|
+
});
|
|
336
|
+
} else {
|
|
337
|
+
// We can treat a command as a subshell if it has subcommands
|
|
338
|
+
if (Object.keys(cmd.registeredCommands).length > 0) {
|
|
339
|
+
if (!process.stdout.isTTY) {
|
|
340
|
+
// If we're not in a TTY, we can't run an interactive shell...
|
|
341
|
+
// Maybe we should warn here?
|
|
342
|
+
} else if (!INTERACTIVE_SHELL) {
|
|
343
|
+
const tui = new InteractiveShell(this, {
|
|
344
|
+
prependArgs: originalArgV,
|
|
345
|
+
});
|
|
346
|
+
await new Promise<void>((res) => {
|
|
347
|
+
['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach((s) =>
|
|
348
|
+
process.on(s, () => {
|
|
349
|
+
tui.close();
|
|
350
|
+
res();
|
|
351
|
+
})
|
|
352
|
+
);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// No subcommands so subshell doesn't make sense
|
|
357
|
+
// No handler, so nothing to run
|
|
358
|
+
else {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`${[this.name, ...this.commandChain].join(' ')} is not implemented.`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
} catch (e) {
|
|
365
|
+
process.exitCode = 1;
|
|
366
|
+
console.error(e);
|
|
367
|
+
this.printHelp();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
enableInteractiveShell() {
|
|
372
|
+
if (this.requiresCommand === 'EXPLICIT') {
|
|
373
|
+
throw new Error(
|
|
374
|
+
'Interactive shell is not supported for commands that require a command.'
|
|
375
|
+
);
|
|
376
|
+
} else if (process.stdout.isTTY) {
|
|
377
|
+
this.requiresCommand = false;
|
|
378
|
+
}
|
|
379
|
+
return this;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private versionHandler() {
|
|
383
|
+
if (this._versionOverride) {
|
|
384
|
+
console.log(this._versionOverride);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
let mainFile = require?.main?.filename;
|
|
388
|
+
mainFile ??= getCallingFile();
|
|
389
|
+
if (!mainFile) {
|
|
390
|
+
console.log('unknown');
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const packageJson = getParentPackageJson(mainFile);
|
|
394
|
+
console.log(packageJson.version ?? 'unknown');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
private async withErrorHandlers<T>(cb: () => T): Promise<Awaited<T>> {
|
|
398
|
+
try {
|
|
399
|
+
return await cb();
|
|
400
|
+
} catch (e) {
|
|
401
|
+
for (const handler of this.registeredErrorHandlers) {
|
|
402
|
+
try {
|
|
403
|
+
handler(e, {
|
|
404
|
+
exit: (c) => {
|
|
405
|
+
process.exit(c);
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
// Error was handled, no need to continue
|
|
409
|
+
break;
|
|
410
|
+
} catch {
|
|
411
|
+
// Error was not handled, continue to the next handler
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
throw e;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
errorHandler(handler: ErrorHandler) {
|
|
419
|
+
this.registeredErrorHandlers.unshift(handler);
|
|
420
|
+
return this;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
group(
|
|
424
|
+
labelOrConfigObject:
|
|
425
|
+
| string
|
|
426
|
+
| { label: string; keys: (keyof TArgs)[]; sortOrder: number },
|
|
427
|
+
keys?: (keyof TArgs)[]
|
|
428
|
+
): CLI<TArgs> {
|
|
429
|
+
const config =
|
|
430
|
+
typeof labelOrConfigObject === 'object'
|
|
431
|
+
? labelOrConfigObject
|
|
432
|
+
: {
|
|
433
|
+
label: labelOrConfigObject,
|
|
434
|
+
keys: keys as (keyof TArgs)[],
|
|
435
|
+
sortOrder: Object.keys(this.registeredOptionGroups).length,
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
if (!config.keys) {
|
|
439
|
+
throw new Error('keys must be provided when calling `group`.');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
this.registeredOptionGroups.push(config);
|
|
443
|
+
return this;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
config(
|
|
447
|
+
provider: ConfigurationFiles.ConfigurationProvider<TArgs>
|
|
448
|
+
): CLI<TArgs> {
|
|
449
|
+
this.parser.config(
|
|
450
|
+
provider as ConfigurationFiles.ConfigurationProvider<any>
|
|
451
|
+
);
|
|
452
|
+
return this;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Parses argv and executes the CLI
|
|
457
|
+
* @param args argv. Defaults to process.argv.slice(2)
|
|
458
|
+
* @returns Promise that resolves when the handler completes.
|
|
459
|
+
*/
|
|
460
|
+
forge = (args: string[] = hideBin(process.argv)) =>
|
|
461
|
+
this.withErrorHandlers(async () => {
|
|
462
|
+
// Parsing the args does two things:
|
|
463
|
+
// - builds argv to pass to handler
|
|
464
|
+
// - fills the command chain + registers commands
|
|
465
|
+
let argv: TArgs & { help?: boolean; version?: boolean };
|
|
466
|
+
let validationFailedError: ValidationFailedError<TArgs> | undefined;
|
|
467
|
+
try {
|
|
468
|
+
argv = this.parser.parse(args);
|
|
469
|
+
} catch (e) {
|
|
470
|
+
if (e instanceof ValidationFailedError) {
|
|
471
|
+
argv = e.partialArgV as TArgs;
|
|
472
|
+
validationFailedError = e;
|
|
473
|
+
} else {
|
|
474
|
+
throw e;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
478
|
+
let currentCommand: InternalCLI<any> = this;
|
|
479
|
+
for (const command of this.commandChain) {
|
|
480
|
+
currentCommand = currentCommand.registeredCommands[command];
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (argv.version) {
|
|
484
|
+
this.versionHandler();
|
|
485
|
+
return argv;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (argv.help) {
|
|
489
|
+
this.printHelp();
|
|
490
|
+
return argv;
|
|
491
|
+
} else if (validationFailedError) {
|
|
492
|
+
throw validationFailedError;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const finalArgV =
|
|
496
|
+
this.commandChain.length === 0 && this.configuration?.builder
|
|
497
|
+
? (
|
|
498
|
+
this.configuration.builder?.(this as any) as InternalCLI<TArgs>
|
|
499
|
+
).parser.parse(args)
|
|
500
|
+
: argv;
|
|
501
|
+
|
|
502
|
+
await this.runCommand(finalArgV, args);
|
|
503
|
+
return finalArgV as TArgs;
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
getParser() {
|
|
507
|
+
return this.parser.asReadonly();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
getSubcommands() {
|
|
511
|
+
return this.registeredCommands as Readonly<Record<string, InternalCLI>>;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
clone() {
|
|
515
|
+
const clone = new InternalCLI<TArgs>(this.name);
|
|
516
|
+
clone.parser = this.parser.clone(clone.parser.options) as any;
|
|
517
|
+
if (this.configuration) {
|
|
518
|
+
clone.withRootCommandConfiguration(this.configuration);
|
|
519
|
+
}
|
|
520
|
+
clone.registeredCommands = {};
|
|
521
|
+
for (const command in this.registeredCommands ?? {}) {
|
|
522
|
+
clone.command(this.registeredCommands[command].clone());
|
|
523
|
+
// this.registeredCommands[command].clone();
|
|
524
|
+
}
|
|
525
|
+
clone.commandChain = [...this.commandChain];
|
|
526
|
+
clone.requiresCommand = this.requiresCommand;
|
|
527
|
+
return clone;
|
|
528
|
+
}
|
|
529
|
+
}
|