@travetto/cli 3.4.5 → 3.4.7
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/README.md +50 -16
- package/__index__.ts +1 -0
- package/package.json +2 -2
- package/src/decorators.ts +5 -4
- package/src/execute.ts +63 -62
- package/src/help.ts +1 -8
- package/src/parse.ts +231 -0
- package/src/schema.ts +14 -127
- package/src/types.ts +13 -9
- package/src/util.ts +29 -5
- package/support/cli.main.ts +3 -8
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ This module also has a tight integration with the [VSCode plugin](https://market
|
|
|
59
59
|
At it's heart, a cli command is the contract defined by what flags, and what arguments the command supports. Within the framework this requires three criteria to be met:
|
|
60
60
|
* The file must be located in the `support/` folder, and have a name that matches `cli.*.ts`
|
|
61
61
|
* The file must be a class that has a main method
|
|
62
|
-
* The class must use the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/decorators.ts#
|
|
62
|
+
* The class must use the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/decorators.ts#L16) decorator
|
|
63
63
|
|
|
64
64
|
**Code: Basic Command**
|
|
65
65
|
```typescript
|
|
@@ -93,7 +93,7 @@ Examples of mappings:
|
|
|
93
93
|
The pattern is that underscores(_) translate to colons (:), and the `cli.` prefix, and `.ts` suffix are dropped.
|
|
94
94
|
|
|
95
95
|
## Binding Flags
|
|
96
|
-
[@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/decorators.ts#
|
|
96
|
+
[@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/decorators.ts#L16) is a wrapper for [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#L14), and so every class that uses the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/decorators.ts#L16) decorator is now a full [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#L14) class. The fields of the class represent the flags that are available to the command.
|
|
97
97
|
|
|
98
98
|
**Code: Basic Command with Flag**
|
|
99
99
|
```typescript
|
|
@@ -130,7 +130,7 @@ $ trv basic:flag --loud
|
|
|
130
130
|
HELLO
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
-
The [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/decorators.ts#
|
|
133
|
+
The [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/decorators.ts#L16) supports the following data types for flags:
|
|
134
134
|
* Boolean values
|
|
135
135
|
* Number values. The [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L172), [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L178), [@Precision](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L166), [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L107) and [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L117) decorators help provide additional validation.
|
|
136
136
|
* String values. [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L107), [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L117), [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L99) and [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L78) provide additional constraints
|
|
@@ -359,8 +359,36 @@ $ MESSAGE=CuStOm trv custom:env-arg 7
|
|
|
359
359
|
CuStOm
|
|
360
360
|
```
|
|
361
361
|
|
|
362
|
+
## Flag File Support
|
|
363
|
+
Sometimes its also convenient, especially with commands that support a variety of flags, to provide easy access to pre-defined sets of flags. Flag files represent a snapshot of command line arguments and flags, as defined in a file. When referenced, these inputs are essentially injected into the command line as if the user had typed them manually.
|
|
364
|
+
|
|
365
|
+
**Code: Example Flag File**
|
|
366
|
+
```bash
|
|
367
|
+
--host localhost
|
|
368
|
+
--port 3306
|
|
369
|
+
--username app
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
As you can see in this file, it provides easy access to predefine the host, port, and user flags.
|
|
373
|
+
|
|
374
|
+
**Code: Using a Flag File**
|
|
375
|
+
```bash
|
|
376
|
+
npx trv call:db +=base --password <custom>
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
The flag files can be included in one of a few ways:
|
|
380
|
+
* `+=<name>` - This translates into $`<mod>/support/<name>.flags`, which is a convenient shorthand.
|
|
381
|
+
* `+=<mod>/path/file.flags` - This is a path-related file that will be resolved from the module's location.
|
|
382
|
+
* `+=/path/file.flags` - This is an absolute path that will be read from the root of the file system.
|
|
383
|
+
Ultimately, after resolution, the content of these files will be injected inline within the location.
|
|
384
|
+
|
|
385
|
+
**Code: Final arguments after Flag File resolution**
|
|
386
|
+
```bash
|
|
387
|
+
npx trv call:db --host localhost --port 3306 --username app --password <custom>
|
|
388
|
+
```
|
|
389
|
+
|
|
362
390
|
## VSCode Integration
|
|
363
|
-
By default, cli commands do not expose themselves to the VSCode extension, as the majority of them are not intended for that sort of operation. [RESTful API](https://github.com/travetto/travetto/tree/main/module/rest#readme "Declarative api for RESTful APIs with support for the dependency injection module.") does expose a cli target `run:rest` that will show up, to help run/debug a rest application. Any command can mark itself as being a run target, and will be eligible for running from within the [VSCode plugin](https://marketplace.visualstudio.com/items?itemName=arcsine.travetto-plugin). This is achieved by setting the `runTarget` field on the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/decorators.ts#
|
|
391
|
+
By default, cli commands do not expose themselves to the VSCode extension, as the majority of them are not intended for that sort of operation. [RESTful API](https://github.com/travetto/travetto/tree/main/module/rest#readme "Declarative api for RESTful APIs with support for the dependency injection module.") does expose a cli target `run:rest` that will show up, to help run/debug a rest application. Any command can mark itself as being a run target, and will be eligible for running from within the [VSCode plugin](https://marketplace.visualstudio.com/items?itemName=arcsine.travetto-plugin). This is achieved by setting the `runTarget` field on the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/decorators.ts#L16) decorator. This means the target will be visible within the editor tooling.
|
|
364
392
|
|
|
365
393
|
**Code: Simple Run Target**
|
|
366
394
|
```typescript
|
|
@@ -382,19 +410,23 @@ export class RunCommand {
|
|
|
382
410
|
|
|
383
411
|
**Code: Anatomy of a Command**
|
|
384
412
|
```typescript
|
|
385
|
-
export interface CliCommandShape {
|
|
413
|
+
export interface CliCommandShape<T extends unknown[] = unknown[]> {
|
|
386
414
|
/**
|
|
387
415
|
* Action target of the command
|
|
388
416
|
*/
|
|
389
|
-
main(...args:
|
|
417
|
+
main(...args: T): OrProm<RunResponse>;
|
|
390
418
|
/**
|
|
391
|
-
*
|
|
419
|
+
* Run before main runs
|
|
392
420
|
*/
|
|
393
|
-
|
|
421
|
+
preMain?(): OrProm<void>;
|
|
394
422
|
/**
|
|
395
423
|
* Extra help
|
|
396
424
|
*/
|
|
397
425
|
help?(): OrProm<string[]>;
|
|
426
|
+
/**
|
|
427
|
+
* Run before help is displayed
|
|
428
|
+
*/
|
|
429
|
+
preHelp?(): OrProm<void>;
|
|
398
430
|
/**
|
|
399
431
|
* Is the command active/eligible for usage
|
|
400
432
|
*/
|
|
@@ -402,15 +434,15 @@ export interface CliCommandShape {
|
|
|
402
434
|
/**
|
|
403
435
|
* Run before binding occurs
|
|
404
436
|
*/
|
|
405
|
-
|
|
437
|
+
preBind?(): OrProm<void>;
|
|
406
438
|
/**
|
|
407
439
|
* Run before validation occurs
|
|
408
440
|
*/
|
|
409
|
-
|
|
441
|
+
preValidate?(): OrProm<void>;
|
|
410
442
|
/**
|
|
411
443
|
* Validation method
|
|
412
444
|
*/
|
|
413
|
-
validate?(...
|
|
445
|
+
validate?(...args: T): OrProm<CliValidationError | CliValidationError[] | undefined>;
|
|
414
446
|
}
|
|
415
447
|
```
|
|
416
448
|
|
|
@@ -420,7 +452,7 @@ If the goal is to run a more complex application, which may include depending on
|
|
|
420
452
|
**Code: Simple Run Target**
|
|
421
453
|
```typescript
|
|
422
454
|
import { DependencyRegistry } from '@travetto/di';
|
|
423
|
-
import { CliCommand, CliUtil } from '@travetto/cli';
|
|
455
|
+
import { CliCommand, CliCommandShape, CliUtil } from '@travetto/cli';
|
|
424
456
|
|
|
425
457
|
import { ServerHandle } from '../src/types';
|
|
426
458
|
|
|
@@ -428,7 +460,7 @@ import { ServerHandle } from '../src/types';
|
|
|
428
460
|
* Run a rest server as an application
|
|
429
461
|
*/
|
|
430
462
|
@CliCommand({ runTarget: true, addModule: true, addEnv: true })
|
|
431
|
-
export class RunRestCommand {
|
|
463
|
+
export class RunRestCommand implements CliCommandShape {
|
|
432
464
|
|
|
433
465
|
/** IPC debug is enabled */
|
|
434
466
|
debugIpc?: boolean;
|
|
@@ -439,8 +471,10 @@ export class RunRestCommand {
|
|
|
439
471
|
/** Port to run on */
|
|
440
472
|
port?: number;
|
|
441
473
|
|
|
442
|
-
|
|
443
|
-
|
|
474
|
+
preMain(): void {
|
|
475
|
+
if (this.port) {
|
|
476
|
+
process.env.REST_PORT = `${this.port}`;
|
|
477
|
+
}
|
|
444
478
|
}
|
|
445
479
|
|
|
446
480
|
async main(): Promise<ServerHandle | void> {
|
|
@@ -479,7 +513,7 @@ A simple example of the validation can be found in the `doc` command:
|
|
|
479
513
|
|
|
480
514
|
**Code: Simple Validation Example**
|
|
481
515
|
```typescript
|
|
482
|
-
async validate(
|
|
516
|
+
async validate(): Promise<CliValidationError | undefined> {
|
|
483
517
|
const docFile = path.resolve(this.input);
|
|
484
518
|
if (!(await fs.stat(docFile).catch(() => false))) {
|
|
485
519
|
return { message: `input: ${this.input} does not exist`, source: 'flag' };
|
package/__index__.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/cli",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.7",
|
|
4
4
|
"description": "CLI infrastructure for Travetto framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"directory": "module/cli"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@travetto/schema": "^3.4.
|
|
32
|
+
"@travetto/schema": "^3.4.4",
|
|
33
33
|
"@travetto/terminal": "^3.4.0"
|
|
34
34
|
},
|
|
35
35
|
"travetto": {
|
package/src/decorators.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Class, ClassInstance,
|
|
1
|
+
import { Class, ClassInstance, defineEnv } from '@travetto/base';
|
|
2
2
|
import { RootIndex } from '@travetto/manifest';
|
|
3
3
|
import { SchemaRegistry } from '@travetto/schema';
|
|
4
4
|
|
|
@@ -6,6 +6,7 @@ import { CliCommandShape, CliCommandShapeFields } from './types';
|
|
|
6
6
|
import { CliCommandRegistry, CliCommandConfigOptions } from './registry';
|
|
7
7
|
import { CliModuleUtil } from './module';
|
|
8
8
|
import { CliUtil } from './util';
|
|
9
|
+
import { CliParseUtil } from './parse';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Decorator to register a CLI command
|
|
@@ -27,7 +28,6 @@ export function CliCommand(cfg: CliCommandConfigOptions = {}) {
|
|
|
27
28
|
preMain: async (cmd) => {
|
|
28
29
|
if (addEnv && 'env' in cmd && typeof cmd.env === 'string') {
|
|
29
30
|
defineEnv({ envName: cmd.env });
|
|
30
|
-
ConsoleManager.setDebug(GlobalEnv.debug, GlobalEnv.devMode);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
});
|
|
@@ -38,13 +38,14 @@ export function CliCommand(cfg: CliCommandConfigOptions = {}) {
|
|
|
38
38
|
SchemaRegistry.registerPendingFieldConfig(target, 'env', String, {
|
|
39
39
|
aliases: ['e'],
|
|
40
40
|
description: 'Application environment',
|
|
41
|
+
default: 'dev',
|
|
41
42
|
required: { active: false }
|
|
42
43
|
});
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
if (addModule) {
|
|
46
47
|
SchemaRegistry.registerPendingFieldConfig(target, 'module', String, {
|
|
47
|
-
aliases: ['m', '
|
|
48
|
+
aliases: ['m', CliParseUtil.toEnvField('TRV_MODULE')],
|
|
48
49
|
description: 'Module to run for',
|
|
49
50
|
required: { active: CliUtil.monoRoot }
|
|
50
51
|
});
|
|
@@ -86,7 +87,7 @@ export function CliFlag(cfg: { name?: string, short?: string, desc?: string, fil
|
|
|
86
87
|
aliases.push(cfg.short.startsWith('-') ? cfg.short : `-${cfg.short}`);
|
|
87
88
|
}
|
|
88
89
|
if (cfg.envVars) {
|
|
89
|
-
aliases.push(...cfg.envVars.map(
|
|
90
|
+
aliases.push(...cfg.envVars.map(CliParseUtil.toEnvField));
|
|
90
91
|
}
|
|
91
92
|
if (typeof prop === 'string') {
|
|
92
93
|
SchemaRegistry.registerPendingFieldFacet(target.constructor, prop, {
|
package/src/execute.ts
CHANGED
|
@@ -1,53 +1,67 @@
|
|
|
1
1
|
import { GlobalTerminal } from '@travetto/terminal';
|
|
2
|
-
import { ConsoleManager,
|
|
2
|
+
import { ConsoleManager, GlobalEnv } from '@travetto/base';
|
|
3
3
|
|
|
4
4
|
import { HelpUtil } from './help';
|
|
5
|
-
import { CliCommandShape } from './types';
|
|
5
|
+
import { CliCommandShape, RunResponse } from './types';
|
|
6
6
|
import { CliCommandRegistry } from './registry';
|
|
7
7
|
import { CliCommandSchemaUtil } from './schema';
|
|
8
8
|
import { CliUnknownCommandError, CliValidationResultError } from './error';
|
|
9
|
+
import { CliParseUtil } from './parse';
|
|
10
|
+
import { CliUtil } from './util';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Execution manager
|
|
12
14
|
*/
|
|
13
15
|
export class ExecutionManager {
|
|
14
16
|
|
|
15
|
-
static async #envInit(cmd: CliCommandShape): Promise<void> {
|
|
16
|
-
if (cmd.envInit) {
|
|
17
|
-
defineEnv(await cmd.envInit());
|
|
18
|
-
ConsoleManager.setDebug(GlobalEnv.debug, GlobalEnv.devMode);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Run help
|
|
24
|
-
*/
|
|
25
|
-
static async help(cmd: CliCommandShape, args: string[]): Promise<void> {
|
|
26
|
-
await cmd.initialize?.();
|
|
27
|
-
await this.#envInit(cmd);
|
|
28
|
-
console.log!(await HelpUtil.renderHelp(cmd));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
17
|
/**
|
|
32
18
|
* Run the given command object with the given arguments
|
|
33
19
|
*/
|
|
34
|
-
static async
|
|
35
|
-
const
|
|
36
|
-
await
|
|
20
|
+
static async #runCommand(cmd: CliCommandShape, args: string[]): Promise<RunResponse> {
|
|
21
|
+
const schema = await CliCommandSchemaUtil.getSchema(cmd);
|
|
22
|
+
const state = await CliParseUtil.parse(schema, args);
|
|
37
23
|
const cfg = CliCommandRegistry.getConfig(cmd);
|
|
38
|
-
await cfg?.preMain?.(cmd);
|
|
39
24
|
|
|
40
|
-
|
|
25
|
+
CliParseUtil.setState(cmd, state);
|
|
26
|
+
|
|
27
|
+
await cmd.preBind?.();
|
|
28
|
+
const known = await CliCommandSchemaUtil.bindInput(cmd, state);
|
|
29
|
+
|
|
30
|
+
await cmd.preValidate?.();
|
|
31
|
+
await CliCommandSchemaUtil.validate(cmd, known);
|
|
41
32
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
33
|
+
await cfg.preMain?.(cmd);
|
|
34
|
+
await cmd.preMain?.();
|
|
35
|
+
ConsoleManager.setDebug(GlobalEnv.debug, GlobalEnv.devMode);
|
|
36
|
+
return cmd.main(...known);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* On error, handle response
|
|
41
|
+
*/
|
|
42
|
+
static async #onError(command: CliCommandShape | undefined, err: unknown): Promise<void> {
|
|
43
|
+
process.exitCode ||= 1; // Trigger error state
|
|
44
|
+
switch (true) {
|
|
45
|
+
case !(err instanceof Error): {
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
case command && err instanceof CliValidationResultError: {
|
|
49
|
+
console.error!(await HelpUtil.renderValidationError(command, err));
|
|
50
|
+
console.error!(await HelpUtil.renderCommandHelp(command));
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case err instanceof CliUnknownCommandError: {
|
|
54
|
+
if (err.help) {
|
|
55
|
+
console.error!(err.help);
|
|
56
|
+
} else {
|
|
57
|
+
console.error!(err.defaultMessage, '\n');
|
|
58
|
+
console.error!(await HelpUtil.renderAllHelp(''));
|
|
59
|
+
}
|
|
60
|
+
break;
|
|
46
61
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
await new Promise<void>(res => result.on('close', res)); // Wait for callback
|
|
62
|
+
default: {
|
|
63
|
+
console.error!(err);
|
|
64
|
+
console.error!();
|
|
51
65
|
}
|
|
52
66
|
}
|
|
53
67
|
}
|
|
@@ -59,38 +73,25 @@ export class ExecutionManager {
|
|
|
59
73
|
static async run(argv: string[]): Promise<void> {
|
|
60
74
|
await GlobalTerminal.init();
|
|
61
75
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
throw err;
|
|
79
|
-
} else if (err instanceof CliValidationResultError) {
|
|
80
|
-
console.error!(await HelpUtil.renderValidationError(command!, err));
|
|
81
|
-
console.error!(await HelpUtil.renderHelp(command));
|
|
82
|
-
} else if (err instanceof CliUnknownCommandError) {
|
|
83
|
-
if (err.help) {
|
|
84
|
-
console.error!(err.help);
|
|
85
|
-
} else {
|
|
86
|
-
console.error!(err.defaultMessage, '\n');
|
|
87
|
-
console.error!(await HelpUtil.renderAllHelp(''));
|
|
88
|
-
}
|
|
89
|
-
} else {
|
|
90
|
-
console.error!(err);
|
|
91
|
-
console.error!();
|
|
92
|
-
}
|
|
76
|
+
let command: CliCommandShape | undefined;
|
|
77
|
+
try {
|
|
78
|
+
const { cmd, args, help } = await CliParseUtil.getArgs(argv);
|
|
79
|
+
|
|
80
|
+
if (!cmd) {
|
|
81
|
+
console.info!(await HelpUtil.renderAllHelp());
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Load a single command
|
|
86
|
+
command = await CliCommandRegistry.getInstance(cmd, true);
|
|
87
|
+
if (help) {
|
|
88
|
+
console.log!(await HelpUtil.renderCommandHelp(command));
|
|
89
|
+
} else {
|
|
90
|
+
const result = await this.#runCommand(command, args);
|
|
91
|
+
await CliUtil.listenForResponse(result);
|
|
93
92
|
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
await this.#onError(command, err);
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
}
|
package/src/help.ts
CHANGED
|
@@ -25,7 +25,7 @@ export class HelpUtil {
|
|
|
25
25
|
static async renderCommandHelp(command: CliCommandShape): Promise<string> {
|
|
26
26
|
const commandName = CliCommandRegistry.getName(command);
|
|
27
27
|
|
|
28
|
-
command.
|
|
28
|
+
await command.preHelp?.();
|
|
29
29
|
|
|
30
30
|
// Ensure finalized
|
|
31
31
|
const { flags, args } = await CliCommandSchemaUtil.getSchema(command);
|
|
@@ -126,13 +126,6 @@ export class HelpUtil {
|
|
|
126
126
|
return lines.map(x => x.trimEnd()).join('\n');
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
/**
|
|
130
|
-
* Render help
|
|
131
|
-
*/
|
|
132
|
-
static async renderHelp(command?: CliCommandShape): Promise<string> {
|
|
133
|
-
return command ? this.renderCommandHelp(command) : this.renderAllHelp();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
129
|
/**
|
|
137
130
|
* Render validation error to a string
|
|
138
131
|
*/
|
package/src/parse.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
|
|
3
|
+
import { RootIndex, path } from '@travetto/manifest';
|
|
4
|
+
import { CliCommandInput, CliCommandSchema, CliCommandShape } from './types';
|
|
5
|
+
|
|
6
|
+
type ParsedFlag = { type: 'flag', input: string, array?: boolean, fieldName: string, value?: unknown };
|
|
7
|
+
type ParsedArg = { type: 'arg', input: string, array?: boolean, index: number };
|
|
8
|
+
type ParsedUnknown = { type: 'unknown', input: string };
|
|
9
|
+
type ParsedInput = ParsedUnknown | ParsedFlag | ParsedArg;
|
|
10
|
+
|
|
11
|
+
export type ParsedState = {
|
|
12
|
+
inputs: string[];
|
|
13
|
+
all: ParsedInput[];
|
|
14
|
+
flags: ParsedFlag[];
|
|
15
|
+
unknown: string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const RAW_SEP = '--';
|
|
19
|
+
const VALID_FLAG = /^-{1,2}[a-z]/i;
|
|
20
|
+
const HELP_FLAG = /^-h|--help$/;
|
|
21
|
+
const LONG_FLAG_WITH_EQ = /^--[a-z][^= ]+=\S+/i;
|
|
22
|
+
const CONFIG_PRE = '+=';
|
|
23
|
+
const ENV_PRE = 'env.';
|
|
24
|
+
const ParsedⲐ = Symbol.for('@travetto/cli:parsed');
|
|
25
|
+
const SPACE = new Set([32, 7, 13, 10]);
|
|
26
|
+
|
|
27
|
+
const isBoolFlag = (x?: CliCommandInput): boolean => x?.type === 'boolean' && !x.array;
|
|
28
|
+
|
|
29
|
+
const getInput = (cfg: { field?: CliCommandInput, rawText?: string, input: string, index?: number, value?: string }): ParsedInput => {
|
|
30
|
+
const { field, input, rawText = input, value, index } = cfg;
|
|
31
|
+
if (!field) {
|
|
32
|
+
return { type: 'unknown', input: rawText };
|
|
33
|
+
} else if (!field.flagNames?.length) {
|
|
34
|
+
return { type: 'arg', input: field ? input : rawText ?? input, array: field.array, index: index! };
|
|
35
|
+
} else {
|
|
36
|
+
return {
|
|
37
|
+
type: 'flag',
|
|
38
|
+
fieldName: field.name,
|
|
39
|
+
array: field.array,
|
|
40
|
+
input: field ? input : rawText ?? input,
|
|
41
|
+
value: value ?? (isBoolFlag(field) ? !input.startsWith('--no-') : undefined)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parsing support for the cli
|
|
48
|
+
*/
|
|
49
|
+
export class CliParseUtil {
|
|
50
|
+
|
|
51
|
+
static toEnvField(k: string): string {
|
|
52
|
+
return `${ENV_PRE}${k}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static readToken(text: string, start = 0): { next: number, value?: string } {
|
|
56
|
+
const collected: number[] = [];
|
|
57
|
+
let i = start;
|
|
58
|
+
let done = false;
|
|
59
|
+
let quote: number | undefined;
|
|
60
|
+
let escaped = false;
|
|
61
|
+
outer: for (; i < text.length; i += 1) {
|
|
62
|
+
const ch = text.charCodeAt(i);
|
|
63
|
+
const space = SPACE.has(ch);
|
|
64
|
+
if (escaped) {
|
|
65
|
+
escaped = false;
|
|
66
|
+
collected.push(ch);
|
|
67
|
+
} else if (done && !space) {
|
|
68
|
+
break outer;
|
|
69
|
+
} else if (!quote && space) {
|
|
70
|
+
done = true;
|
|
71
|
+
} else {
|
|
72
|
+
switch (ch) {
|
|
73
|
+
case 92: /* Backslash */ escaped = true; break;
|
|
74
|
+
case 39: /* Single quote */ case 34: /* Double quote */
|
|
75
|
+
if (quote === ch) { // End quote
|
|
76
|
+
quote = undefined;
|
|
77
|
+
} else if (!quote) {
|
|
78
|
+
quote = ch;
|
|
79
|
+
} else {
|
|
80
|
+
collected.push(ch);
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
default: collected.push(ch);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { next: i, value: collected.length ? String.fromCharCode(...collected) : undefined };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Read configuration file given flag
|
|
92
|
+
*/
|
|
93
|
+
static async readFlagFile(flag: string): Promise<string[]> {
|
|
94
|
+
const key = flag.replace(CONFIG_PRE, '');
|
|
95
|
+
const mod = RootIndex.mainModuleName;
|
|
96
|
+
|
|
97
|
+
// We have a file
|
|
98
|
+
const rel = (key.includes('/') ? key : `@/support/pack.${key}.flags`)
|
|
99
|
+
.replace('@@/', `${RootIndex.manifest.workspacePath}/`)
|
|
100
|
+
.replace('@/', `${mod}/`)
|
|
101
|
+
.replace(/^(@[^\/]+\/[^\/]+)/, (_, imp) => {
|
|
102
|
+
const val = RootIndex.getModule(imp);
|
|
103
|
+
if (!val) {
|
|
104
|
+
throw new Error(`Unknown module file: ${key}, unable to proceed`);
|
|
105
|
+
}
|
|
106
|
+
return val.sourcePath;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const file = path.resolve(rel);
|
|
110
|
+
|
|
111
|
+
if (!await fs.stat(file).catch(() => false)) {
|
|
112
|
+
throw new Error(`Missing flag file: ${key}, unable to proceed`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const data = await fs.readFile(file, 'utf8');
|
|
116
|
+
const args: string[] = [];
|
|
117
|
+
let token: { next: number, value?: string } = { next: 0 };
|
|
118
|
+
while (token.next < data.length) {
|
|
119
|
+
token = this.readToken(data, token.next);
|
|
120
|
+
if (token.value !== undefined) {
|
|
121
|
+
args.push(token.value);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return args;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Parse args to extract command from argv along with other params. Will skip
|
|
129
|
+
* argv[0] and argv[1] if equal to process.argv[0:2]
|
|
130
|
+
*/
|
|
131
|
+
static async getArgs(argv: string[]): Promise<{ cmd?: string, args: string[], help?: boolean }> {
|
|
132
|
+
let offset = 0;
|
|
133
|
+
if (argv[0] === process.argv[0] && argv[1] === process.argv[1]) {
|
|
134
|
+
offset = 2;
|
|
135
|
+
}
|
|
136
|
+
const out = argv.slice(offset);
|
|
137
|
+
const max = out.includes(RAW_SEP) ? out.indexOf(RAW_SEP) : out.length;
|
|
138
|
+
const valid = out.slice(0, max);
|
|
139
|
+
const cmd = valid.length > 0 && !valid[0].startsWith('-') ? valid[0] : undefined;
|
|
140
|
+
const helpIdx = valid.findIndex(x => HELP_FLAG.test(x));
|
|
141
|
+
const args = [];
|
|
142
|
+
for (const item of out.slice(cmd ? 1 : 0)) {
|
|
143
|
+
if (item.startsWith(CONFIG_PRE)) {
|
|
144
|
+
args.push(...await this.readFlagFile(item));
|
|
145
|
+
} else {
|
|
146
|
+
args.push(item);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
args.push(...out.slice(max));
|
|
150
|
+
const res = { cmd, args, help: helpIdx >= 0 };
|
|
151
|
+
return res;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Parse inputs to command
|
|
156
|
+
*/
|
|
157
|
+
static async parse(schema: CliCommandSchema, inputs: string[]): Promise<ParsedState> {
|
|
158
|
+
const flagMap = new Map<string, CliCommandInput>(
|
|
159
|
+
schema.flags.flatMap(f => (f.flagNames ?? []).map(name => [name, f]))
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const out: ParsedInput[] = [];
|
|
163
|
+
|
|
164
|
+
// Load env vars to front
|
|
165
|
+
for (const field of schema.flags) {
|
|
166
|
+
for (const envName of field.envVars ?? []) {
|
|
167
|
+
if (envName in process.env) {
|
|
168
|
+
const value: string = process.env[envName]!;
|
|
169
|
+
if (field.array) {
|
|
170
|
+
out.push(...value.split(/\s*,\s*/g).map(v => getInput({ field, input: `${ENV_PRE}${envName}`, value: v })));
|
|
171
|
+
} else {
|
|
172
|
+
out.push(getInput({ field, input: `${ENV_PRE}${envName}`, value }));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let argIdx = 0;
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < inputs.length; i += 1) {
|
|
181
|
+
const input = inputs[i];
|
|
182
|
+
|
|
183
|
+
if (input === RAW_SEP) { // Raw separator
|
|
184
|
+
out.push(...inputs.slice(i + 1).map(x => getInput({ input: x })));
|
|
185
|
+
break;
|
|
186
|
+
} else if (LONG_FLAG_WITH_EQ.test(input)) {
|
|
187
|
+
const [k, ...v] = input.split('=');
|
|
188
|
+
const field = flagMap.get(k);
|
|
189
|
+
out.push(getInput({ field, rawText: input, input: k, value: v.join('=') }));
|
|
190
|
+
} else if (VALID_FLAG.test(input)) { // Flag
|
|
191
|
+
const field = flagMap.get(input);
|
|
192
|
+
const next = inputs[i + 1];
|
|
193
|
+
if ((next && (VALID_FLAG.test(next) || next === RAW_SEP)) || isBoolFlag(field)) {
|
|
194
|
+
out.push(getInput({ field, input }));
|
|
195
|
+
} else {
|
|
196
|
+
out.push(getInput({ field, input, value: next }));
|
|
197
|
+
i += 1;
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
const field = schema.args[argIdx];
|
|
201
|
+
out.push(getInput({ field, input, index: argIdx }));
|
|
202
|
+
// Move argIdx along if not in a vararg situation
|
|
203
|
+
if (!field?.array) {
|
|
204
|
+
argIdx += 1;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Store for later, if needed
|
|
210
|
+
return {
|
|
211
|
+
inputs,
|
|
212
|
+
all: out,
|
|
213
|
+
unknown: out.filter((x): x is ParsedUnknown => x.type === 'unknown').map(x => x.input),
|
|
214
|
+
flags: out.filter((x): x is ParsedFlag => x.type === 'flag')
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get parse state from the command
|
|
220
|
+
*/
|
|
221
|
+
static getState(cmd: CliCommandShape & { [ParsedⲐ]?: ParsedState }): ParsedState | undefined {
|
|
222
|
+
return cmd[ParsedⲐ];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Set state for a command
|
|
227
|
+
*/
|
|
228
|
+
static setState(cmd: CliCommandShape & { [ParsedⲐ]?: ParsedState }, state: ParsedState): void {
|
|
229
|
+
cmd[ParsedⲐ] = state;
|
|
230
|
+
}
|
|
231
|
+
}
|
package/src/schema.ts
CHANGED
|
@@ -4,36 +4,13 @@ import { BindUtil, FieldConfig, SchemaRegistry, SchemaValidator, ValidationResul
|
|
|
4
4
|
import { CliCommandRegistry } from './registry';
|
|
5
5
|
import { CliCommandInput, CliCommandSchema, CliCommandShape } from './types';
|
|
6
6
|
import { CliValidationResultError } from './error';
|
|
7
|
+
import { ParsedState } from './parse';
|
|
7
8
|
|
|
8
|
-
const VALID_FLAG = /^-{1,2}[a-z]/i;
|
|
9
9
|
const LONG_FLAG = /^--[a-z][^= ]+/i;
|
|
10
|
-
const LONG_FLAG_WITH_EQ = /^--[a-z][^= ]+=\S+/i;
|
|
11
10
|
const SHORT_FLAG = /^-[a-z]/i;
|
|
12
11
|
|
|
13
|
-
type ParsedInput =
|
|
14
|
-
{ type: 'unknown', input: string } |
|
|
15
|
-
{ type: 'arg', input: string, array?: boolean, index: number } |
|
|
16
|
-
{ type: 'flag', input: string, array?: boolean, fieldName: string, value?: unknown };
|
|
17
|
-
|
|
18
12
|
const isBoolFlag = (x?: CliCommandInput): boolean => x?.type === 'boolean' && !x.array;
|
|
19
13
|
|
|
20
|
-
const getInput = (cfg: { field?: CliCommandInput, rawText?: string, input: string, index?: number, value?: string }): ParsedInput => {
|
|
21
|
-
const { field, input, rawText = input, value, index } = cfg;
|
|
22
|
-
if (!field) {
|
|
23
|
-
return { type: 'unknown', input: rawText };
|
|
24
|
-
} else if (!field.flagNames?.length) {
|
|
25
|
-
return { type: 'arg', input: field ? input : rawText ?? input, array: field.array, index: index! };
|
|
26
|
-
} else {
|
|
27
|
-
return {
|
|
28
|
-
type: 'flag',
|
|
29
|
-
fieldName: field.name,
|
|
30
|
-
array: field.array,
|
|
31
|
-
input: field ? input : rawText ?? input,
|
|
32
|
-
value: value ?? (isBoolFlag(field) ? !input.startsWith('--no-') : undefined)
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
|
|
37
14
|
function fieldToInput(x: FieldConfig): CliCommandInput {
|
|
38
15
|
const type = x.type === Date ? 'date' :
|
|
39
16
|
x.type === Boolean ? 'boolean' :
|
|
@@ -136,71 +113,13 @@ export class CliCommandSchemaUtil {
|
|
|
136
113
|
}
|
|
137
114
|
|
|
138
115
|
/**
|
|
139
|
-
*
|
|
140
|
-
*/
|
|
141
|
-
static async parse<T extends CliCommandShape>(cls: Class<T>, inputs: string[]): Promise<ParsedInput[]> {
|
|
142
|
-
const schema = await this.getSchema(cls);
|
|
143
|
-
const flagMap = new Map<string, CliCommandInput>(
|
|
144
|
-
schema.flags.flatMap(f => (f.flagNames ?? []).map(name => [name, f]))
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
const out: ParsedInput[] = [];
|
|
148
|
-
|
|
149
|
-
// Load env vars to front
|
|
150
|
-
for (const field of schema.flags) {
|
|
151
|
-
for (const envName of field.envVars ?? []) {
|
|
152
|
-
if (envName in process.env) {
|
|
153
|
-
const value: string = process.env[envName]!;
|
|
154
|
-
if (field.array) {
|
|
155
|
-
out.push(...value.split(/\s*,\s*/g).map(v => getInput({ field, input: `env.${envName}`, value: v })));
|
|
156
|
-
} else {
|
|
157
|
-
out.push(getInput({ field, input: `env.${envName}`, value }));
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
let argIdx = 0;
|
|
164
|
-
|
|
165
|
-
for (let i = 0; i < inputs.length; i += 1) {
|
|
166
|
-
const input = inputs[i];
|
|
167
|
-
|
|
168
|
-
if (input === '--') { // Raw separator
|
|
169
|
-
out.push(...inputs.slice(i + 1).map(x => getInput({ input: x })));
|
|
170
|
-
break;
|
|
171
|
-
} else if (LONG_FLAG_WITH_EQ.test(input)) {
|
|
172
|
-
const [k, ...v] = input.split('=');
|
|
173
|
-
const field = flagMap.get(k);
|
|
174
|
-
out.push(getInput({ field, rawText: input, input: k, value: v.join('=') }));
|
|
175
|
-
} else if (VALID_FLAG.test(input)) { // Flag
|
|
176
|
-
const field = flagMap.get(input);
|
|
177
|
-
const next = inputs[i + 1];
|
|
178
|
-
if ((next && (VALID_FLAG.test(next) || next === '--')) || isBoolFlag(field)) {
|
|
179
|
-
out.push(getInput({ field, input }));
|
|
180
|
-
} else {
|
|
181
|
-
out.push(getInput({ field, input, value: next }));
|
|
182
|
-
i += 1;
|
|
183
|
-
}
|
|
184
|
-
} else {
|
|
185
|
-
const field = schema.args[argIdx];
|
|
186
|
-
out.push(getInput({ field, input, index: argIdx }));
|
|
187
|
-
// Move argIdx along if not in a vararg situation
|
|
188
|
-
if (!field?.array) {
|
|
189
|
-
argIdx += 1;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return out;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Bind arguments to command
|
|
116
|
+
* Bind parsed inputs to command
|
|
199
117
|
*/
|
|
200
|
-
static async
|
|
118
|
+
static async bindInput<T extends CliCommandShape>(cmd: T, state: ParsedState): Promise<unknown[]> {
|
|
201
119
|
const template: Partial<T> = {};
|
|
120
|
+
const bound: unknown[] = [];
|
|
202
121
|
|
|
203
|
-
for (const arg of
|
|
122
|
+
for (const arg of state.all) {
|
|
204
123
|
switch (arg.type) {
|
|
205
124
|
case 'flag': {
|
|
206
125
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
@@ -213,6 +132,15 @@ export class CliCommandSchemaUtil {
|
|
|
213
132
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
214
133
|
template[key] = value as unknown as T[typeof key];
|
|
215
134
|
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'arg': {
|
|
138
|
+
if (arg.array) {
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
140
|
+
((bound[arg.index] ??= []) as unknown[]).push(arg.input);
|
|
141
|
+
} else {
|
|
142
|
+
bound[arg.index] = arg.input;
|
|
143
|
+
}
|
|
216
144
|
}
|
|
217
145
|
}
|
|
218
146
|
}
|
|
@@ -220,35 +148,9 @@ export class CliCommandSchemaUtil {
|
|
|
220
148
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
221
149
|
const cls = cmd.constructor as Class<CliCommandShape>;
|
|
222
150
|
BindUtil.bindSchemaToObject(cls, cmd, template);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Produce the arguments into the final argument set
|
|
227
|
-
*/
|
|
228
|
-
static async bindArgs(cmd: CliCommandShape, inputs: ParsedInput[]): Promise<unknown[]> {
|
|
229
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
230
|
-
const cls = cmd.constructor as Class<CliCommandShape>;
|
|
231
|
-
const bound: unknown[] = [];
|
|
232
|
-
for (const input of inputs) {
|
|
233
|
-
if (input.type === 'arg') {
|
|
234
|
-
if (input.array) {
|
|
235
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
236
|
-
((bound[input.index] ??= []) as unknown[]).push(input.input);
|
|
237
|
-
} else {
|
|
238
|
-
bound[input.index] = input.input;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
151
|
return BindUtil.coerceMethodParams(cls, 'main', bound);
|
|
243
152
|
}
|
|
244
153
|
|
|
245
|
-
/**
|
|
246
|
-
* Get the unused arguments
|
|
247
|
-
*/
|
|
248
|
-
static getUnusedArgs(args: ParsedInput[]): string[] {
|
|
249
|
-
return args.filter(x => x.type === 'unknown').map(x => x.input);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
154
|
/**
|
|
253
155
|
* Validate command shape with the given arguments
|
|
254
156
|
*/
|
|
@@ -284,19 +186,4 @@ export class CliCommandSchemaUtil {
|
|
|
284
186
|
}
|
|
285
187
|
return cmd;
|
|
286
188
|
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Bind and validate a command with a given set of arguments
|
|
290
|
-
*/
|
|
291
|
-
static async bindAndValidateArgs(cmd: CliCommandShape, args: string[]): Promise<unknown[]> {
|
|
292
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
293
|
-
const cls = cmd.constructor as Class;
|
|
294
|
-
await cmd.initialize?.();
|
|
295
|
-
const parsed = await this.parse(cls, args);
|
|
296
|
-
await this.bindFlags(cmd, parsed);
|
|
297
|
-
const known = await this.bindArgs(cmd, parsed);
|
|
298
|
-
await cmd.finalize?.(this.getUnusedArgs(parsed));
|
|
299
|
-
await this.validate(cmd, known);
|
|
300
|
-
return known;
|
|
301
|
-
}
|
|
302
189
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Closeable
|
|
1
|
+
import { Closeable } from '@travetto/base';
|
|
2
2
|
|
|
3
3
|
type OrProm<T> = T | Promise<T>;
|
|
4
4
|
|
|
5
|
-
type RunResponse = { wait(): Promise<unknown> } | { on(event: 'close', cb: Function): unknown } | Closeable | void | undefined;
|
|
5
|
+
export type RunResponse = { wait(): Promise<unknown> } | { on(event: 'close', cb: Function): unknown } | Closeable | void | undefined;
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Constrained version of Schema's Validation Error
|
|
@@ -21,19 +21,23 @@ export type CliValidationError = {
|
|
|
21
21
|
/**
|
|
22
22
|
* CLI Command Contract
|
|
23
23
|
*/
|
|
24
|
-
export interface CliCommandShape {
|
|
24
|
+
export interface CliCommandShape<T extends unknown[] = unknown[]> {
|
|
25
25
|
/**
|
|
26
26
|
* Action target of the command
|
|
27
27
|
*/
|
|
28
|
-
main(...args:
|
|
28
|
+
main(...args: T): OrProm<RunResponse>;
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* Run before main runs
|
|
31
31
|
*/
|
|
32
|
-
|
|
32
|
+
preMain?(): OrProm<void>;
|
|
33
33
|
/**
|
|
34
34
|
* Extra help
|
|
35
35
|
*/
|
|
36
36
|
help?(): OrProm<string[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Run before help is displayed
|
|
39
|
+
*/
|
|
40
|
+
preHelp?(): OrProm<void>;
|
|
37
41
|
/**
|
|
38
42
|
* Is the command active/eligible for usage
|
|
39
43
|
*/
|
|
@@ -41,15 +45,15 @@ export interface CliCommandShape {
|
|
|
41
45
|
/**
|
|
42
46
|
* Run before binding occurs
|
|
43
47
|
*/
|
|
44
|
-
|
|
48
|
+
preBind?(): OrProm<void>;
|
|
45
49
|
/**
|
|
46
50
|
* Run before validation occurs
|
|
47
51
|
*/
|
|
48
|
-
|
|
52
|
+
preValidate?(): OrProm<void>;
|
|
49
53
|
/**
|
|
50
54
|
* Validation method
|
|
51
55
|
*/
|
|
52
|
-
validate?(...
|
|
56
|
+
validate?(...args: T): OrProm<CliValidationError | CliValidationError[] | undefined>;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
/**
|
package/src/util.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Env, ExecUtil, GlobalEnv } from '@travetto/base';
|
|
2
|
-
import { RootIndex } from '@travetto/manifest';
|
|
1
|
+
import { Env, ExecUtil, GlobalEnv, ShutdownManager } from '@travetto/base';
|
|
2
|
+
import { RootIndex, path } from '@travetto/manifest';
|
|
3
3
|
|
|
4
|
-
import { CliCommandShape, CliCommandShapeFields } from './types';
|
|
4
|
+
import { CliCommandShape, CliCommandShapeFields, RunResponse } from './types';
|
|
5
5
|
import { CliCommandRegistry } from './registry';
|
|
6
6
|
|
|
7
7
|
export class CliUtil {
|
|
@@ -16,8 +16,15 @@ export class CliUtil {
|
|
|
16
16
|
* Get a simplified version of a module name
|
|
17
17
|
* @returns
|
|
18
18
|
*/
|
|
19
|
-
static getSimpleModuleName(
|
|
20
|
-
|
|
19
|
+
static getSimpleModuleName(placeholder: string, module?: string): string {
|
|
20
|
+
const simple = (module ?? RootIndex.mainModuleName).replace(/[\/]/, '_').replace(/@/, '');
|
|
21
|
+
if (!simple) {
|
|
22
|
+
return placeholder;
|
|
23
|
+
} else if (!module && this.monoRoot) {
|
|
24
|
+
return placeholder;
|
|
25
|
+
} else {
|
|
26
|
+
return placeholder.replace('<module>', simple);
|
|
27
|
+
}
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
/**
|
|
@@ -92,4 +99,21 @@ export class CliUtil {
|
|
|
92
99
|
static async writeAndEnsureComplete(data: unknown, channel: 'stdout' | 'stderr' = 'stdout'): Promise<void> {
|
|
93
100
|
return await new Promise(r => process[channel].write(typeof data === 'string' ? data : JSON.stringify(data, null, 2), () => r()));
|
|
94
101
|
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Listen for a run response to finish
|
|
105
|
+
*/
|
|
106
|
+
static async listenForResponse(result: RunResponse): Promise<void> {
|
|
107
|
+
// Listen to result if non-empty
|
|
108
|
+
if (result !== undefined && result !== null) {
|
|
109
|
+
if ('close' in result) {
|
|
110
|
+
ShutdownManager.onShutdown(result, result); // Tie shutdown into app close
|
|
111
|
+
}
|
|
112
|
+
if ('wait' in result) {
|
|
113
|
+
await result.wait(); // Wait for close signal
|
|
114
|
+
} else if ('on' in result) {
|
|
115
|
+
await new Promise<void>(res => result.on('close', res)); // Wait for callback
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
95
119
|
}
|
package/support/cli.main.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
|
|
3
3
|
import { ShutdownManager } from '@travetto/base';
|
|
4
|
-
import { CliCommandShape, CliCommand, CliValidationError } from '@travetto/cli';
|
|
4
|
+
import { CliCommandShape, CliCommand, CliValidationError, CliParseUtil } from '@travetto/cli';
|
|
5
5
|
import { path, RootIndex } from '@travetto/manifest';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -10,8 +10,6 @@ import { path, RootIndex } from '@travetto/manifest';
|
|
|
10
10
|
@CliCommand({ hidden: true })
|
|
11
11
|
export class MainCommand implements CliCommandShape {
|
|
12
12
|
|
|
13
|
-
#unknownArgs?: string[];
|
|
14
|
-
|
|
15
13
|
async #getImport(fileOrImport: string): Promise<string | undefined> {
|
|
16
14
|
// If referenced file exists
|
|
17
15
|
let file = fileOrImport;
|
|
@@ -29,15 +27,12 @@ export class MainCommand implements CliCommandShape {
|
|
|
29
27
|
}
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
finalize(unknownArgs?: string[] | undefined): void | Promise<void> {
|
|
33
|
-
this.#unknownArgs = unknownArgs;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
30
|
async main(fileOrImport: string, args: string[] = []): Promise<void> {
|
|
37
31
|
try {
|
|
38
32
|
const imp = await this.#getImport(fileOrImport);
|
|
39
33
|
const mod = await import(imp!);
|
|
40
|
-
|
|
34
|
+
|
|
35
|
+
await ShutdownManager.exitWithResponse(await mod.main(...args, ...CliParseUtil.getState(this)?.unknown ?? []));
|
|
41
36
|
} catch (err) {
|
|
42
37
|
await ShutdownManager.exitWithResponse(err, true);
|
|
43
38
|
}
|