@travetto/cli 8.0.0-alpha.1 → 8.0.0-alpha.11
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 +22 -21
- package/__index__.ts +0 -1
- package/bin/trv.js +2 -1
- package/package.json +3 -3
- package/src/execute.ts +40 -50
- package/src/help.ts +68 -27
- package/src/parse.ts +4 -5
- package/src/registry/decorator.ts +25 -30
- package/src/registry/registry-adapter.ts +10 -15
- package/src/registry/registry-index.ts +10 -5
- package/src/schema-export.ts +4 -3
- package/src/schema.ts +17 -18
- package/src/service.ts +18 -1
- package/src/types.ts +5 -3
- package/src/util.ts +16 -15
- package/support/cli.service.ts +34 -15
- package/support/entry.trv.ts +1 -1
- package/src/error.ts +0 -59
- package/support/bin/util.ts +0 -16
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/registry/decorator.ts#
|
|
62
|
+
* The class must use the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L20) decorator
|
|
63
63
|
|
|
64
64
|
**Code: Basic Command**
|
|
65
65
|
```typescript
|
|
@@ -75,12 +75,12 @@ export class BasicCommand {
|
|
|
75
75
|
|
|
76
76
|
**Terminal: Basic Command Help**
|
|
77
77
|
```bash
|
|
78
|
-
$ trv basic
|
|
78
|
+
$ trv basic --help
|
|
79
79
|
|
|
80
80
|
Usage: basic [options]
|
|
81
81
|
|
|
82
82
|
Options:
|
|
83
|
-
|
|
83
|
+
--help display help for command
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
## Command Naming
|
|
@@ -94,7 +94,7 @@ Examples of mappings:
|
|
|
94
94
|
The pattern is that underscores(_) translate to colons (:), and the `cli.` prefix, and `.ts` suffix are dropped.
|
|
95
95
|
|
|
96
96
|
## Binding Flags
|
|
97
|
-
[@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#
|
|
97
|
+
[@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L20) is a wrapper for [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#L19), and so every class that uses the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L20) decorator is now a full [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#L19) class. The fields of the class represent the flags that are available to the command.
|
|
98
98
|
|
|
99
99
|
**Code: Basic Command with Flag**
|
|
100
100
|
```typescript
|
|
@@ -113,13 +113,13 @@ export class BasicCommand {
|
|
|
113
113
|
|
|
114
114
|
**Terminal: Basic Command with Flag Help**
|
|
115
115
|
```bash
|
|
116
|
-
$ trv basic:flag
|
|
116
|
+
$ trv basic:flag --help
|
|
117
117
|
|
|
118
118
|
Usage: basic:flag [options]
|
|
119
119
|
|
|
120
120
|
Options:
|
|
121
121
|
-l, --loud
|
|
122
|
-
|
|
122
|
+
--help display help for command
|
|
123
123
|
```
|
|
124
124
|
|
|
125
125
|
As you can see the command now has the support of a basic boolean flag to determine if the response should be loud or not. The default value here is undefined/false, and so is an opt-in experience.
|
|
@@ -131,7 +131,7 @@ $ trv basic:flag --loud
|
|
|
131
131
|
HELLO
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
-
The [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#
|
|
134
|
+
The [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L20) supports the following data types for flags:
|
|
135
135
|
* Boolean values
|
|
136
136
|
* Number values. The [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L172), [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L179), [@Precision](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L165), [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L99) and [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L110) decorators help provide additional validation.
|
|
137
137
|
* String values. [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L99), [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L110), [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L90) and [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L64) provide additional constraints
|
|
@@ -158,12 +158,12 @@ export class BasicCommand {
|
|
|
158
158
|
|
|
159
159
|
**Terminal: Basic Command**
|
|
160
160
|
```bash
|
|
161
|
-
$ trv basic:arg
|
|
161
|
+
$ trv basic:arg --help
|
|
162
162
|
|
|
163
163
|
Usage: basic:arg [options] [volume:number]
|
|
164
164
|
|
|
165
165
|
Options:
|
|
166
|
-
|
|
166
|
+
--help display help for command
|
|
167
167
|
```
|
|
168
168
|
|
|
169
169
|
**Terminal: Basic Command with Invalid Loud Arg**
|
|
@@ -176,7 +176,7 @@ Execution failed:
|
|
|
176
176
|
Usage: basic:arg [options] [volume:number]
|
|
177
177
|
|
|
178
178
|
Options:
|
|
179
|
-
|
|
179
|
+
--help display help for command
|
|
180
180
|
```
|
|
181
181
|
|
|
182
182
|
**Terminal: Basic Command with Loud Arg > 7**
|
|
@@ -213,13 +213,13 @@ export class BasicCommand {
|
|
|
213
213
|
|
|
214
214
|
**Terminal: Basic Command**
|
|
215
215
|
```bash
|
|
216
|
-
$ trv basic:arg-list
|
|
216
|
+
$ trv basic:arg-list --help
|
|
217
217
|
|
|
218
218
|
Usage: basic:arg-list [options] <volumes...:number>
|
|
219
219
|
|
|
220
220
|
Options:
|
|
221
221
|
-r, --reverse
|
|
222
|
-
|
|
222
|
+
--help display help for command
|
|
223
223
|
```
|
|
224
224
|
|
|
225
225
|
**Terminal: Basic Arg List**
|
|
@@ -240,7 +240,7 @@ Usage: basic:arg-list [options] <volumes...:number>
|
|
|
240
240
|
|
|
241
241
|
Options:
|
|
242
242
|
-r, --reverse
|
|
243
|
-
|
|
243
|
+
--help display help for command
|
|
244
244
|
```
|
|
245
245
|
|
|
246
246
|
**Terminal: Basic Arg List with Reverse**
|
|
@@ -279,13 +279,13 @@ export class CustomCommand {
|
|
|
279
279
|
|
|
280
280
|
**Terminal: Custom Command Help**
|
|
281
281
|
```bash
|
|
282
|
-
$ trv custom:arg
|
|
282
|
+
$ trv custom:arg --help
|
|
283
283
|
|
|
284
284
|
Usage: custom:arg [options] [volume:number]
|
|
285
285
|
|
|
286
286
|
Options:
|
|
287
287
|
-m, --message <string> The message to send back to the user (default: "hello")
|
|
288
|
-
|
|
288
|
+
--help display help for command
|
|
289
289
|
```
|
|
290
290
|
|
|
291
291
|
**Terminal: Custom Command Help with overridden Text**
|
|
@@ -330,13 +330,13 @@ export class CustomCommand {
|
|
|
330
330
|
|
|
331
331
|
**Terminal: Custom Command Help**
|
|
332
332
|
```bash
|
|
333
|
-
$ trv custom:env-arg
|
|
333
|
+
$ trv custom:env-arg --help
|
|
334
334
|
|
|
335
335
|
Usage: custom:env-arg [options] [volume:number]
|
|
336
336
|
|
|
337
337
|
Options:
|
|
338
338
|
-t, --text <string> The message to send back to the user (default: "hello")
|
|
339
|
-
|
|
339
|
+
--help display help for command
|
|
340
340
|
```
|
|
341
341
|
|
|
342
342
|
**Terminal: Custom Command Help with default Text**
|
|
@@ -390,7 +390,7 @@ npx trv call:db --host localhost --port 3306 --username app --password <custom>
|
|
|
390
390
|
```
|
|
391
391
|
|
|
392
392
|
## VSCode Integration
|
|
393
|
-
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. [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications") does expose a cli target `web:http` that will show up, to help run/debug a web 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/registry/decorator.ts#
|
|
393
|
+
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. [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications") does expose a cli target `web:http` that will show up, to help run/debug a web 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/registry/decorator.ts#L20) decorator. This means the target will be visible within the editor tooling.
|
|
394
394
|
|
|
395
395
|
**Code: Simple Run Target**
|
|
396
396
|
```typescript
|
|
@@ -460,7 +460,7 @@ export class WebHttpCommand implements CliCommandShape {
|
|
|
460
460
|
profile: string[];
|
|
461
461
|
|
|
462
462
|
@CliRestartOnChangeFlag()
|
|
463
|
-
restartOnChange: boolean =
|
|
463
|
+
restartOnChange: boolean = Runtime.localDevelopment;
|
|
464
464
|
|
|
465
465
|
@CliDebugIpcFlag()
|
|
466
466
|
debugIpc?: boolean;
|
|
@@ -542,7 +542,7 @@ A simple example of the validation can be found in the `doc` command:
|
|
|
542
542
|
```typescript
|
|
543
543
|
@Validator(async (cmd) => {
|
|
544
544
|
const docFile = path.resolve(cmd.input);
|
|
545
|
-
if (!(await fs.stat(docFile
|
|
545
|
+
if (!(await fs.stat(docFile, { throwIfNoEntry: false }))) {
|
|
546
546
|
return { message: `input: ${cmd.input} does not exist`, path: 'input', source: 'flag', kind: 'invalid' };
|
|
547
547
|
}
|
|
548
548
|
})
|
|
@@ -558,7 +558,8 @@ $ trv service --help
|
|
|
558
558
|
Usage: service [options] <action:restart|start|status|stop> [services...:string]
|
|
559
559
|
|
|
560
560
|
Options:
|
|
561
|
-
-
|
|
561
|
+
-q, --quiet (default: false)
|
|
562
|
+
--help display help for command
|
|
562
563
|
|
|
563
564
|
Available Services
|
|
564
565
|
--------------------
|
package/__index__.ts
CHANGED
package/bin/trv.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// @ts-check
|
|
3
|
+
import '@travetto/runtime/support/patch.js';
|
|
3
4
|
import '@travetto/compiler/bin/hook.js';
|
|
4
5
|
const { invoke } = await import('@travetto/compiler/support/invoke.ts');
|
|
5
|
-
await invoke('exec',
|
|
6
|
+
await invoke('exec', '@travetto/cli/support/entry.trv.ts', ...process.argv.slice(2));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/cli",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI infrastructure for Travetto framework",
|
|
6
6
|
"keywords": [
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"directory": "module/cli"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@travetto/schema": "^8.0.0-alpha.
|
|
33
|
-
"@travetto/terminal": "^8.0.0-alpha.
|
|
32
|
+
"@travetto/schema": "^8.0.0-alpha.6",
|
|
33
|
+
"@travetto/terminal": "^8.0.0-alpha.6"
|
|
34
34
|
},
|
|
35
35
|
"travetto": {
|
|
36
36
|
"displayName": "Command Line Interface",
|
package/src/execute.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { ConsoleManager, getClass, Runtime, ShutdownManager, Util } from '@trave
|
|
|
3
3
|
import { HelpUtil } from './help.ts';
|
|
4
4
|
import { CliCommandRegistryIndex } from './registry/registry-index.ts';
|
|
5
5
|
import { CliCommandSchemaUtil } from './schema.ts';
|
|
6
|
-
import { CliUnknownCommandError, CliValidationResultError } from './error.ts';
|
|
7
6
|
import { CliParseUtil } from './parse.ts';
|
|
8
7
|
import type { CliCommandShape } from './types.ts';
|
|
9
8
|
|
|
@@ -12,51 +11,52 @@ import type { CliCommandShape } from './types.ts';
|
|
|
12
11
|
*/
|
|
13
12
|
export class ExecutionManager {
|
|
14
13
|
|
|
15
|
-
/**
|
|
16
|
-
static async
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} else if (error instanceof CliUnknownCommandError) {
|
|
22
|
-
if (error.help) {
|
|
23
|
-
console.error!(error.help);
|
|
24
|
-
} else {
|
|
25
|
-
console.error!(error.defaultMessage, '\n');
|
|
26
|
-
console.error!(await HelpUtil.renderAllHelp(''));
|
|
27
|
-
}
|
|
28
|
-
} else {
|
|
29
|
-
console.error!(error);
|
|
14
|
+
/** Command Execution */
|
|
15
|
+
static async execute(instance: CliCommandShape, args: unknown[]): Promise<void> {
|
|
16
|
+
const config = CliCommandRegistryIndex.get(getClass(instance));
|
|
17
|
+
|
|
18
|
+
for (const item of config.preMain) {
|
|
19
|
+
await item.handler(instance);
|
|
30
20
|
}
|
|
31
|
-
|
|
21
|
+
|
|
22
|
+
// Wait 50ms to allow stdout to flush on shutdown
|
|
23
|
+
ShutdownManager.signal.addEventListener('abort', () => Util.blockingTimeout(50));
|
|
24
|
+
ConsoleManager.debug(Runtime.debug);
|
|
25
|
+
await instance.main(...args);
|
|
32
26
|
}
|
|
33
27
|
|
|
34
|
-
/**
|
|
35
|
-
static async
|
|
36
|
-
|
|
37
|
-
const fullArgs = await CliParseUtil.expandArgs(schema, args);
|
|
28
|
+
/** Extract configuration and show help as needed */
|
|
29
|
+
static async getExecutionCommand(argv: string[]): Promise<(() => Promise<void>) | undefined> {
|
|
30
|
+
let command: CliCommandShape | undefined;
|
|
38
31
|
|
|
39
|
-
const
|
|
40
|
-
|
|
32
|
+
const { cmd, args, help } = CliParseUtil.getArgs(argv);
|
|
33
|
+
if (!cmd) {
|
|
34
|
+
console.info!(await HelpUtil.renderAllHelp());
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
try {
|
|
39
|
+
const [{ instance, schema }] = await CliCommandRegistryIndex.load([cmd]);
|
|
40
|
+
command = instance;
|
|
41
|
+
const fullArgs = await CliParseUtil.expandArgs(schema, args);
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const { command, boundArgs } = await this.#bindCommand(cmd, args);
|
|
43
|
+
const state = await CliParseUtil.parse(schema, fullArgs);
|
|
44
|
+
CliParseUtil.setState(instance, state);
|
|
49
45
|
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
const boundArgs = CliCommandSchemaUtil.bindInput(instance, state);
|
|
47
|
+
await instance.finalize?.(help);
|
|
52
48
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
if (help) {
|
|
50
|
+
console.log!(await HelpUtil.renderCommandHelp(instance));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
await CliCommandSchemaUtil.validate(command, boundArgs);
|
|
55
|
+
|
|
56
|
+
return this.execute.bind(this, instance, boundArgs);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
await HelpUtil.renderError(error, cmd, command);
|
|
59
|
+
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
@@ -65,20 +65,10 @@ export class ExecutionManager {
|
|
|
65
65
|
*/
|
|
66
66
|
static async run(argv: string[]): Promise<void> {
|
|
67
67
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const { cmd, args, help } = CliParseUtil.getArgs(argv);
|
|
72
|
-
if (!cmd) {
|
|
73
|
-
console.info!(await HelpUtil.renderAllHelp());
|
|
74
|
-
} else if (help) {
|
|
75
|
-
const { command } = await this.#bindCommand(cmd, args);
|
|
76
|
-
console.log!(await HelpUtil.renderCommandHelp(command));
|
|
77
|
-
} else {
|
|
78
|
-
await this.#runCommand(cmd, args);
|
|
79
|
-
}
|
|
68
|
+
const execute = await this.getExecutionCommand(argv);
|
|
69
|
+
await execute?.();
|
|
80
70
|
} catch (error) {
|
|
81
|
-
|
|
71
|
+
console.error!(error);
|
|
82
72
|
} finally {
|
|
83
73
|
await ShutdownManager.shutdown();
|
|
84
74
|
}
|
package/src/help.ts
CHANGED
|
@@ -1,27 +1,51 @@
|
|
|
1
1
|
import util from 'node:util';
|
|
2
2
|
|
|
3
|
-
import { castKey, getClass, JSONUtil } from '@travetto/runtime';
|
|
4
|
-
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
3
|
+
import { castKey, getClass, JSONUtil, Runtime } from '@travetto/runtime';
|
|
4
|
+
import { SchemaRegistryIndex, ValidationResultError } from '@travetto/schema';
|
|
5
5
|
|
|
6
6
|
import { cliTpl } from './color.ts';
|
|
7
|
-
import type
|
|
8
|
-
import { CliCommandRegistryIndex } from './registry/registry-index.ts';
|
|
9
|
-
import type { CliValidationResultError } from './error.ts';
|
|
7
|
+
import { HELP_FLAG, type CliCommandShape } from './types.ts';
|
|
8
|
+
import { CliCommandRegistryIndex, UNKNOWN_COMMAND } from './registry/registry-index.ts';
|
|
10
9
|
import { CliSchemaExportUtil } from './schema-export.ts';
|
|
11
10
|
|
|
12
|
-
const validationSourceMap: Record<string, string> = {
|
|
13
|
-
arg: 'Argument',
|
|
14
|
-
flag: 'Flag'
|
|
15
|
-
};
|
|
11
|
+
const validationSourceMap: Record<string, string> = { arg: 'Argument', flag: 'Flag' };
|
|
16
12
|
|
|
17
13
|
const ifDefined = <T>(value: T | null | '' | undefined): T | undefined =>
|
|
18
14
|
(value === null || value === '' || value === undefined) ? undefined : value;
|
|
19
15
|
|
|
16
|
+
const MODULE_TO_COMMAND = {
|
|
17
|
+
'@travetto/doc': ['doc'],
|
|
18
|
+
'@travetto/email-compiler': ['email:compile', 'email:test', 'email:editor'],
|
|
19
|
+
'@travetto/eslint': ['eslint', 'eslint:register', 'lint', 'lint:register'],
|
|
20
|
+
'@travetto/model': ['model:install', 'model:export'],
|
|
21
|
+
'@travetto/openapi': ['openapi:spec', 'openapi:client'],
|
|
22
|
+
'@travetto/pack': ['pack', 'pack:zip', 'pack:docker'],
|
|
23
|
+
'@travetto/repo': ['repo:publish', 'repo:version', 'repo:exec', 'repo:list'],
|
|
24
|
+
'@travetto/test': ['test', 'test:watch', 'test:direct'],
|
|
25
|
+
'@travetto/web-http': ['web:http'],
|
|
26
|
+
'@travetto/web-rpc': ['web:rpc-client'],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const COMMAND_TO_MODULE = Object.fromEntries(Object.entries(MODULE_TO_COMMAND).flatMap(([k, v]) => v.map(sv => [sv, k])));
|
|
30
|
+
|
|
20
31
|
/**
|
|
21
32
|
* Utilities for showing help
|
|
22
33
|
*/
|
|
23
34
|
export class HelpUtil {
|
|
24
35
|
|
|
36
|
+
/** Render the unknown command message */
|
|
37
|
+
static renderUnknownCommandMessage(command: string): string {
|
|
38
|
+
const module = COMMAND_TO_MODULE[command];
|
|
39
|
+
if (module) {
|
|
40
|
+
return cliTpl`
|
|
41
|
+
${{ title: 'Missing Package' }}\n${'-'.repeat(20)}\nTo use ${{ input: command }} please run:\n
|
|
42
|
+
${{ identifier: Runtime.getInstallCommand(module) }}
|
|
43
|
+
`;
|
|
44
|
+
} else {
|
|
45
|
+
return cliTpl`${{ subtitle: 'Unknown command' }}: ${{ input: command }}`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
25
49
|
/**
|
|
26
50
|
* Render command-specific help
|
|
27
51
|
* @param command
|
|
@@ -31,27 +55,24 @@ export class HelpUtil {
|
|
|
31
55
|
const { name: commandName } = CliCommandRegistryIndex.get(getClass(command));
|
|
32
56
|
const args = schema.methods.main?.parameters ?? [];
|
|
33
57
|
|
|
34
|
-
|
|
58
|
+
const usage = [cliTpl`${{ title: 'Usage:' }} ${{ param: commandName }} ${{ input: '[options]' }}`,];
|
|
59
|
+
const params: string[] = [];
|
|
60
|
+
const descriptions: string[] = [];
|
|
35
61
|
|
|
36
62
|
// Ensure finalized
|
|
37
|
-
|
|
38
|
-
const usage: string[] = [cliTpl`${{ title: 'Usage:' }} ${{ param: commandName }} ${{ input: '[options]' }}`,];
|
|
39
63
|
for (const field of args) {
|
|
40
64
|
const type = field.type === String && field.enum && field.enum?.values.length <= 7 ? field.enum?.values?.join('|') : field.type.name.toLowerCase();
|
|
41
65
|
const arg = `${field.name}${field.array ? '...' : ''}:${type}`;
|
|
42
66
|
usage.push(cliTpl`${{ input: field.required?.active !== false ? `<${arg}>` : `[${arg}]` }}`);
|
|
43
67
|
}
|
|
44
68
|
|
|
45
|
-
const params: string[] = [];
|
|
46
|
-
const descriptions: string[] = [];
|
|
47
|
-
|
|
48
69
|
for (const field of Object.values(schema.fields)) {
|
|
49
70
|
const key = castKey<CliCommandShape>(field.name);
|
|
50
71
|
const defaultValue = ifDefined(command[key]) ?? ifDefined(field.default);
|
|
51
72
|
const aliases = (field.aliases ?? [])
|
|
52
73
|
.filter(flag => flag.startsWith('-'))
|
|
53
74
|
.filter(flag =>
|
|
54
|
-
(field.type !== Boolean) || (
|
|
75
|
+
(field.type !== Boolean) || (defaultValue !== true ? !flag.startsWith('--no-') : flag.startsWith('--'))
|
|
55
76
|
);
|
|
56
77
|
let type: string | undefined;
|
|
57
78
|
|
|
@@ -61,29 +82,33 @@ export class HelpUtil {
|
|
|
61
82
|
({ type } = CliSchemaExportUtil.baseInputType(field));
|
|
62
83
|
}
|
|
63
84
|
|
|
64
|
-
const
|
|
85
|
+
const parameter = [
|
|
65
86
|
cliTpl`${{ param: aliases.join(', ') }}`,
|
|
66
87
|
...(type ? [cliTpl`${{ type: `<${type}>` }}`] : []),
|
|
67
88
|
];
|
|
68
89
|
|
|
69
|
-
params.push(
|
|
70
|
-
const
|
|
90
|
+
params.push(parameter.join(' '));
|
|
91
|
+
const parts = [cliTpl`${{ title: field.description }}`];
|
|
71
92
|
|
|
72
|
-
if (
|
|
73
|
-
|
|
93
|
+
if (defaultValue !== undefined) {
|
|
94
|
+
parts.push(cliTpl`(default: ${{ input: JSONUtil.toUTF8(defaultValue) }})`);
|
|
74
95
|
}
|
|
75
|
-
descriptions.push(
|
|
96
|
+
descriptions.push(parts.join(' '));
|
|
76
97
|
}
|
|
77
98
|
|
|
99
|
+
params.push(cliTpl`${{ param: HELP_FLAG }}`);
|
|
100
|
+
descriptions.push('display help for command');
|
|
101
|
+
|
|
102
|
+
|
|
78
103
|
const paramWidths = params.map(item => util.stripVTControlCharacters(item).length);
|
|
79
104
|
const descWidths = descriptions.map(item => util.stripVTControlCharacters(item).length);
|
|
80
105
|
|
|
81
106
|
const paramWidth = Math.max(...paramWidths);
|
|
82
107
|
const descWidth = Math.max(...descWidths);
|
|
83
108
|
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
86
|
-
|
|
109
|
+
const extendedHelpText = await (command.help?.() ?? []);
|
|
110
|
+
if (extendedHelpText.length && extendedHelpText.at(-1) !== '') {
|
|
111
|
+
extendedHelpText.push('');
|
|
87
112
|
}
|
|
88
113
|
|
|
89
114
|
return [
|
|
@@ -94,7 +119,7 @@ export class HelpUtil {
|
|
|
94
119
|
` ${params[i]}${' '.repeat((paramWidth - paramWidths[i]))} ${descriptions[i].padEnd(descWidth)}${' '.repeat((descWidth - descWidths[i]))}`
|
|
95
120
|
),
|
|
96
121
|
'',
|
|
97
|
-
...
|
|
122
|
+
...extendedHelpText
|
|
98
123
|
].map(line => line.trimEnd()).join('\n');
|
|
99
124
|
}
|
|
100
125
|
|
|
@@ -132,7 +157,7 @@ export class HelpUtil {
|
|
|
132
157
|
/**
|
|
133
158
|
* Render validation error to a string
|
|
134
159
|
*/
|
|
135
|
-
static renderValidationError(validationError:
|
|
160
|
+
static renderValidationError(validationError: ValidationResultError): string {
|
|
136
161
|
return [
|
|
137
162
|
cliTpl`${{ failure: 'Execution failed' }}:`,
|
|
138
163
|
...validationError.details.errors.map(error => {
|
|
@@ -144,4 +169,20 @@ export class HelpUtil {
|
|
|
144
169
|
'',
|
|
145
170
|
].join('\n');
|
|
146
171
|
}
|
|
172
|
+
|
|
173
|
+
/** Error handler */
|
|
174
|
+
static async renderError(error: unknown, cmd: string, command?: CliCommandShape): Promise<void> {
|
|
175
|
+
process.exitCode ??= 1;
|
|
176
|
+
if (error instanceof ValidationResultError) {
|
|
177
|
+
console.error!(this.renderValidationError(error));
|
|
178
|
+
} else if (error instanceof Error) {
|
|
179
|
+
console.error!(cliTpl`${{ failure: error.stack }}\n`);
|
|
180
|
+
}
|
|
181
|
+
if (command) {
|
|
182
|
+
console.error!(await this.renderCommandHelp(command));
|
|
183
|
+
} else if (error === UNKNOWN_COMMAND) {
|
|
184
|
+
console.error!(this.renderUnknownCommandMessage(cmd));
|
|
185
|
+
}
|
|
186
|
+
console.error!();
|
|
187
|
+
}
|
|
147
188
|
}
|
package/src/parse.ts
CHANGED
|
@@ -4,13 +4,12 @@ import path from 'node:path';
|
|
|
4
4
|
import { Runtime } from '@travetto/runtime';
|
|
5
5
|
import type { SchemaClassConfig, SchemaFieldConfig, SchemaInputConfig } from '@travetto/schema';
|
|
6
6
|
|
|
7
|
-
import type
|
|
7
|
+
import { HELP_FLAG, type ParsedState } from './types.ts';
|
|
8
8
|
|
|
9
9
|
type ParsedInput = ParsedState['all'][number];
|
|
10
10
|
|
|
11
11
|
const RAW_SEPARATOR = '--';
|
|
12
12
|
const VALID_FLAG = /^-{1,2}[a-z]/i;
|
|
13
|
-
const HELP_FLAG = /^(-h|--help)$/;
|
|
14
13
|
const LONG_FLAG_WITH_EQ = /^--[a-z][^= ]+=\S+/i;
|
|
15
14
|
const CONFIG_PREFIX = '+=';
|
|
16
15
|
const SPACE = new Set([32, 7, 13, 10]);
|
|
@@ -95,7 +94,7 @@ export class CliParseUtil {
|
|
|
95
94
|
|
|
96
95
|
const file = path.resolve(relativePath);
|
|
97
96
|
|
|
98
|
-
if (!await fs.stat(file
|
|
97
|
+
if (!await fs.stat(file, { throwIfNoEntry: false })) {
|
|
99
98
|
throw new Error(`Missing flag file: ${key}, unable to proceed`);
|
|
100
99
|
}
|
|
101
100
|
|
|
@@ -124,9 +123,9 @@ export class CliParseUtil {
|
|
|
124
123
|
const max = out.includes(RAW_SEPARATOR) ? out.indexOf(RAW_SEPARATOR) : out.length;
|
|
125
124
|
const valid = out.slice(0, max);
|
|
126
125
|
const cmd = valid.length > 0 && !valid[0].startsWith('-') ? valid[0] : undefined;
|
|
127
|
-
const
|
|
126
|
+
const help = valid.includes(HELP_FLAG);
|
|
128
127
|
const args = out.slice(cmd ? 1 : 0);
|
|
129
|
-
const result = { cmd, args, help
|
|
128
|
+
const result = { cmd, args, help };
|
|
130
129
|
return result;
|
|
131
130
|
}
|
|
132
131
|
|
|
@@ -10,13 +10,6 @@ import { CliUtil } from '../util.ts';
|
|
|
10
10
|
type CliCommandConfigOptions = { runTarget?: boolean };
|
|
11
11
|
type CliFlagOptions = { full?: string, short?: string, envVars?: string[] };
|
|
12
12
|
|
|
13
|
-
function runBeforeMain<T>(cls: Class, handler: (item: T) => (unknown | Promise<unknown>), runTarget?: boolean): void {
|
|
14
|
-
CliCommandRegistryIndex.getForRegister(cls).register({
|
|
15
|
-
runTarget,
|
|
16
|
-
preMain: [async (cmd): Promise<void> => { await handler(castTo(cmd)); }]
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
13
|
/**
|
|
21
14
|
* Decorator to register a CLI command
|
|
22
15
|
*
|
|
@@ -72,9 +65,7 @@ export function CliProfilesFlag(config: CliFlagOptions = {}) {
|
|
|
72
65
|
description: 'Application profiles'
|
|
73
66
|
});
|
|
74
67
|
|
|
75
|
-
|
|
76
|
-
Env.TRV_PROFILES.set([...cmd[property] ?? [], ...(Env.TRV_PROFILES.list ?? [])])
|
|
77
|
-
);
|
|
68
|
+
CliCommandRegistryIndex.registerPreMain<typeof instance>(cls, 1, cmd => Env.TRV_PROFILES.add(...cmd[property] ?? []));
|
|
78
69
|
};
|
|
79
70
|
};
|
|
80
71
|
|
|
@@ -103,12 +94,8 @@ export function CliModuleFlag(config: CliFlagOptions & { scope?: 'current' | 'co
|
|
|
103
94
|
const runModule = (config.scope === 'command' ? commandModule : providedModule) || Runtime.main.name;
|
|
104
95
|
|
|
105
96
|
// If we need to run as a specific module
|
|
106
|
-
if (runModule !== Runtime.main.name) {
|
|
107
|
-
|
|
108
|
-
RuntimeIndex.reinitForModule(runModule);
|
|
109
|
-
} catch {
|
|
110
|
-
return { source: 'flag', message: `${runModule} is an unknown module`, kind: 'custom', path: property };
|
|
111
|
-
}
|
|
97
|
+
if (runModule !== Runtime.main.name && RuntimeIndex.getModule(runModule) === undefined) {
|
|
98
|
+
return { source: 'flag', message: `${runModule} is an unknown module`, kind: 'custom', path: property };
|
|
112
99
|
}
|
|
113
100
|
|
|
114
101
|
if (!(await CliModuleUtil.moduleHasDependency(runModule, commandModule))) {
|
|
@@ -116,6 +103,15 @@ export function CliModuleFlag(config: CliFlagOptions & { scope?: 'current' | 'co
|
|
|
116
103
|
}
|
|
117
104
|
}],
|
|
118
105
|
});
|
|
106
|
+
|
|
107
|
+
CliCommandRegistryIndex.registerPreMain<typeof instance>(cls, 5, cmd => {
|
|
108
|
+
const typed: (typeof cmd) & { [property]?: string } = castTo(cmd);
|
|
109
|
+
const providedModule = typed[property];
|
|
110
|
+
const runModule = (config.scope === 'command' ? commandModule : providedModule) || Runtime.main.name;
|
|
111
|
+
if (runModule !== Runtime.main.name) {
|
|
112
|
+
RuntimeIndex.reinitForModule(runModule);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
119
115
|
};
|
|
120
116
|
}
|
|
121
117
|
|
|
@@ -127,14 +123,15 @@ export function CliModuleFlag(config: CliFlagOptions & { scope?: 'current' | 'co
|
|
|
127
123
|
export function CliRestartOnChangeFlag(config: CliFlagOptions = {}) {
|
|
128
124
|
return function <K extends string, T extends Partial<Record<K, boolean>>>(instance: T, property: K): void {
|
|
129
125
|
const cls = getClass(instance);
|
|
126
|
+
if (Runtime.production) { return; }
|
|
127
|
+
|
|
130
128
|
SchemaRegistryIndex.getForRegister(cls).registerField(property, {
|
|
131
129
|
...CliParseUtil.buildAliases(config),
|
|
132
|
-
description: 'Should the invocation automatically restart on source changes'
|
|
133
|
-
default: Runtime.localDevelopment,
|
|
134
|
-
required: { active: false },
|
|
130
|
+
description: 'Should the invocation automatically restart on source changes'
|
|
135
131
|
});
|
|
136
132
|
|
|
137
|
-
|
|
133
|
+
CliCommandRegistryIndex.getForRegister(cls).register({ runTarget: true });
|
|
134
|
+
CliCommandRegistryIndex.registerPreMain<typeof instance>(cls, 20, cmd => CliUtil.runWithRestartOnChange(cmd[property]));
|
|
138
135
|
};
|
|
139
136
|
}
|
|
140
137
|
|
|
@@ -145,20 +142,18 @@ export function CliRestartOnChangeFlag(config: CliFlagOptions = {}) {
|
|
|
145
142
|
*/
|
|
146
143
|
export function CliDebugIpcFlag(config: CliFlagOptions = {}) {
|
|
147
144
|
return function <K extends string, T extends Partial<Record<K, boolean>>>(instance: T, property: K): void {
|
|
145
|
+
if (Runtime.production) { return; }
|
|
146
|
+
|
|
148
147
|
const cls = getClass(instance);
|
|
149
148
|
SchemaRegistryIndex.getForRegister(cls).registerField(property, {
|
|
150
149
|
...CliParseUtil.buildAliases(config, Env.TRV_DEBUG_IPC.key),
|
|
151
|
-
description: 'Should the invocation automatically restart on source changes'
|
|
152
|
-
default: Runtime.localDevelopment,
|
|
153
|
-
required: { active: false },
|
|
150
|
+
description: 'Should the invocation automatically restart on source changes'
|
|
154
151
|
});
|
|
155
152
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
true
|
|
162
|
-
);
|
|
153
|
+
CliCommandRegistryIndex.getForRegister(cls).register({ runTarget: true });
|
|
154
|
+
CliCommandRegistryIndex.registerPreMain<typeof instance>(cls, 10, cmd => {
|
|
155
|
+
const cliConfig = CliCommandRegistryIndex.get(cls);
|
|
156
|
+
return cmd[property] && CliUtil.runWithDebugIpc(cliConfig.name);
|
|
157
|
+
});
|
|
163
158
|
};
|
|
164
159
|
}
|
|
@@ -29,22 +29,10 @@ export class CliCommandRegistryAdapter implements RegistryAdapter<CliCommandConf
|
|
|
29
29
|
this.#cls = cls;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
// TODO: handle when aliases overlap/conflict
|
|
33
|
+
finalize(parent?: CliCommandConfig): void {
|
|
33
34
|
// Add help command
|
|
34
35
|
const schema = SchemaRegistryIndex.getConfig(this.#cls);
|
|
35
|
-
|
|
36
|
-
// Add help to every command
|
|
37
|
-
(schema.fields ??= {}).help = {
|
|
38
|
-
type: Boolean,
|
|
39
|
-
name: 'help',
|
|
40
|
-
class: this.#cls,
|
|
41
|
-
description: 'display help for command',
|
|
42
|
-
required: { active: false },
|
|
43
|
-
default: false,
|
|
44
|
-
access: 'readonly',
|
|
45
|
-
aliases: ['-h', '--help']
|
|
46
|
-
};
|
|
47
|
-
|
|
48
36
|
const used = new Set(Object.values(schema.fields)
|
|
49
37
|
.flatMap(field => field.aliases ?? [])
|
|
50
38
|
.filter(alias => !alias.startsWith(ENV_PREFIX))
|
|
@@ -75,6 +63,13 @@ export class CliCommandRegistryAdapter implements RegistryAdapter<CliCommandConf
|
|
|
75
63
|
aliases.push(`--no-${long}`);
|
|
76
64
|
}
|
|
77
65
|
}
|
|
66
|
+
|
|
67
|
+
if (parent) {
|
|
68
|
+
this.#config.preMain = [...this.#config.preMain, ...parent?.preMain ?? []];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Sort
|
|
72
|
+
this.#config.preMain = this.#config.preMain.toSorted((left, right) => left.priority - right.priority);
|
|
78
73
|
}
|
|
79
74
|
|
|
80
75
|
get(): CliCommandConfig {
|
|
@@ -86,7 +81,7 @@ export class CliCommandRegistryAdapter implements RegistryAdapter<CliCommandConf
|
|
|
86
81
|
*/
|
|
87
82
|
register(...configs: Partial<CliCommandConfig>[]): CliCommandConfig {
|
|
88
83
|
const metadata = describeFunction(this.#cls);
|
|
89
|
-
this.#config ??= { cls: this.#cls, name: getName(metadata.import), preMain: [], runTarget:
|
|
84
|
+
this.#config ??= { cls: this.#cls, name: getName(metadata.import), preMain: [], runTarget: false };
|
|
90
85
|
return combineClasses(this.#config, ...configs);
|
|
91
86
|
}
|
|
92
87
|
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { type Class, getClass, getParentClass, isClass, Runtime, RuntimeIndex } from '@travetto/runtime';
|
|
1
|
+
import { type Any, type Class, getClass, getParentClass, isClass, Runtime, RuntimeIndex } from '@travetto/runtime';
|
|
2
2
|
import { type RegistryAdapter, type RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
|
|
3
3
|
import { type SchemaClassConfig, SchemaRegistryIndex } from '@travetto/schema';
|
|
4
4
|
|
|
5
|
-
import type { CliCommandConfig, CliCommandShape } from '../types.ts';
|
|
6
|
-
import { CliUnknownCommandError } from '../error.ts';
|
|
5
|
+
import type { CliCommandConfig, CliCommandShape, PreMainHandler } from '../types.ts';
|
|
7
6
|
import { CliCommandRegistryAdapter } from './registry-adapter.ts';
|
|
8
7
|
|
|
9
8
|
const CLI_FILE_REGEX = /\/cli[.](?<name>.{0,100}?)([.]tsx?)?$/;
|
|
@@ -11,6 +10,8 @@ const getName = (field: string): string => (field.match(CLI_FILE_REGEX)?.groups?
|
|
|
11
10
|
|
|
12
11
|
type CliCommandLoadResult = { command: string, config: CliCommandConfig, instance: CliCommandShape, schema: SchemaClassConfig };
|
|
13
12
|
|
|
13
|
+
export const UNKNOWN_COMMAND = Symbol();
|
|
14
|
+
|
|
14
15
|
export class CliCommandRegistryIndex implements RegistryIndex {
|
|
15
16
|
|
|
16
17
|
static #instance = Registry.registerIndex(this);
|
|
@@ -27,6 +28,10 @@ export class CliCommandRegistryIndex implements RegistryIndex {
|
|
|
27
28
|
return this.#instance.load(names);
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
static registerPreMain<T = Any>(cls: Class, priority: number, handler: PreMainHandler<T>['handler']): void {
|
|
32
|
+
CliCommandRegistryIndex.getForRegister(cls).register({ preMain: [{ handler, priority }] });
|
|
33
|
+
}
|
|
34
|
+
|
|
30
35
|
#fileMapping: Map<string, string>;
|
|
31
36
|
#instanceMapping: Map<string, CliCommandShape> = new Map();
|
|
32
37
|
|
|
@@ -57,7 +62,7 @@ export class CliCommandRegistryIndex implements RegistryIndex {
|
|
|
57
62
|
*/
|
|
58
63
|
async #getInstance(name: string): Promise<CliCommandShape> {
|
|
59
64
|
if (!this.hasCommand(name)) {
|
|
60
|
-
throw
|
|
65
|
+
throw UNKNOWN_COMMAND;
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
if (this.#instanceMapping.has(name)) {
|
|
@@ -95,7 +100,7 @@ export class CliCommandRegistryIndex implements RegistryIndex {
|
|
|
95
100
|
this.#instanceMapping.set(name, result);
|
|
96
101
|
return result;
|
|
97
102
|
}
|
|
98
|
-
throw
|
|
103
|
+
throw UNKNOWN_COMMAND;
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
hasCommand(name: string): boolean {
|
package/src/schema-export.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Class, describeFunction } from '@travetto/runtime';
|
|
1
|
+
import { castTo, type Class, describeFunction } from '@travetto/runtime';
|
|
2
2
|
import { type SchemaInputConfig, SchemaRegistryIndex } from '@travetto/schema';
|
|
3
3
|
|
|
4
4
|
import { CliCommandRegistryIndex } from '../src/registry/registry-index.ts';
|
|
@@ -9,7 +9,7 @@ import { CliCommandRegistryIndex } from '../src/registry/registry-index.ts';
|
|
|
9
9
|
export type CliCommandInput<K extends string = string> = {
|
|
10
10
|
name: string;
|
|
11
11
|
description?: string;
|
|
12
|
-
type: 'string' | 'file' | 'number' | 'boolean' | 'date' | 'regex' | 'module';
|
|
12
|
+
type: 'string' | 'file' | 'number' | 'bigint' | 'boolean' | 'date' | 'regex' | 'module';
|
|
13
13
|
fileExtensions?: string[];
|
|
14
14
|
choices?: unknown[];
|
|
15
15
|
required?: boolean;
|
|
@@ -37,11 +37,12 @@ export class CliSchemaExportUtil {
|
|
|
37
37
|
* Get the base type for a CLI command input
|
|
38
38
|
*/
|
|
39
39
|
static baseInputType(config: SchemaInputConfig): Pick<CliCommandInput, 'type' | 'fileExtensions'> {
|
|
40
|
-
switch (config.type) {
|
|
40
|
+
switch (castTo<Function>(config.type)) {
|
|
41
41
|
case Date: return { type: 'date' };
|
|
42
42
|
case Boolean: return { type: 'boolean' };
|
|
43
43
|
case Number: return { type: 'number' };
|
|
44
44
|
case RegExp: return { type: 'regex' };
|
|
45
|
+
case BigInt: return { type: 'bigint' };
|
|
45
46
|
case String: {
|
|
46
47
|
switch (true) {
|
|
47
48
|
case config.specifiers?.includes('module'): return { type: 'module' };
|
package/src/schema.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { castKey, castTo, getClass } from '@travetto/runtime';
|
|
|
2
2
|
import { BindUtil, SchemaRegistryIndex, SchemaValidator, ValidationResultError, type ValidationError } from '@travetto/schema';
|
|
3
3
|
|
|
4
4
|
import type { ParsedState, CliCommandShape } from './types.ts';
|
|
5
|
-
import { CliValidationResultError } from './error.ts';
|
|
6
5
|
|
|
7
6
|
const getSource = (source: string | undefined, defaultSource: ValidationError['source']): ValidationError['source'] => {
|
|
8
7
|
switch (source) {
|
|
@@ -15,7 +14,7 @@ const getSource = (source: string | undefined, defaultSource: ValidationError['s
|
|
|
15
14
|
};
|
|
16
15
|
|
|
17
16
|
const transformErrors = (source: 'arg' | 'flag', error: unknown): ValidationError[] => {
|
|
18
|
-
if (error instanceof
|
|
17
|
+
if (error instanceof ValidationResultError) {
|
|
19
18
|
return error.details.errors.map(value => ({ source: getSource(value.source, source), ...value }));
|
|
20
19
|
} else {
|
|
21
20
|
throw error;
|
|
@@ -32,16 +31,16 @@ export class CliCommandSchemaUtil {
|
|
|
32
31
|
/**
|
|
33
32
|
* Bind parsed inputs to command
|
|
34
33
|
*/
|
|
35
|
-
static bindInput<T extends CliCommandShape>(
|
|
34
|
+
static bindInput<T extends CliCommandShape>(command: T, state: ParsedState): unknown[] {
|
|
36
35
|
const template: Partial<T> = {};
|
|
37
36
|
const bound: unknown[] = [];
|
|
38
37
|
|
|
39
|
-
for (const
|
|
40
|
-
switch (
|
|
38
|
+
for (const item of state.all) {
|
|
39
|
+
switch (item.type) {
|
|
41
40
|
case 'flag': {
|
|
42
|
-
const key = castKey<T>(
|
|
43
|
-
const value =
|
|
44
|
-
if (
|
|
41
|
+
const key = castKey<T>(item.fieldName);
|
|
42
|
+
const value = item.value!;
|
|
43
|
+
if (item.array) {
|
|
45
44
|
castTo<unknown[]>(template[key] ??= castTo([])).push(value);
|
|
46
45
|
} else {
|
|
47
46
|
template[key] = castTo(value);
|
|
@@ -49,36 +48,36 @@ export class CliCommandSchemaUtil {
|
|
|
49
48
|
break;
|
|
50
49
|
}
|
|
51
50
|
case 'arg': {
|
|
52
|
-
if (
|
|
53
|
-
castTo<unknown[]>(bound[
|
|
51
|
+
if (item.array) {
|
|
52
|
+
castTo<unknown[]>(bound[item.index] ??= []).push(item.input);
|
|
54
53
|
} else {
|
|
55
|
-
bound[
|
|
54
|
+
bound[item.index] = item.input;
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
57
|
}
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
const cls = getClass(
|
|
62
|
-
BindUtil.bindSchemaToObject(cls,
|
|
60
|
+
const cls = getClass(command);
|
|
61
|
+
BindUtil.bindSchemaToObject(cls, command, template);
|
|
63
62
|
return BindUtil.coerceMethodParams(cls, 'main', bound);
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
/**
|
|
67
66
|
* Validate command shape with the given arguments
|
|
68
67
|
*/
|
|
69
|
-
static async validate(
|
|
70
|
-
const cls = getClass(
|
|
68
|
+
static async validate(command: CliCommandShape, args: unknown[]): Promise<typeof command> {
|
|
69
|
+
const cls = getClass(command);
|
|
71
70
|
const paramNames = SchemaRegistryIndex.get(cls).getMethod('main').parameters.map(config => config.name!);
|
|
72
71
|
|
|
73
72
|
const results = await Promise.all([
|
|
74
|
-
SchemaValidator.validate(cls,
|
|
73
|
+
SchemaValidator.validate(cls, command).then(() => [], transformFlagErrors),
|
|
75
74
|
SchemaValidator.validateMethod(cls, 'main', args, paramNames).then(() => [], transformArgErrors),
|
|
76
75
|
]);
|
|
77
76
|
|
|
78
77
|
const errors = results.flat();
|
|
79
78
|
if (errors.length) {
|
|
80
|
-
throw new
|
|
79
|
+
throw new ValidationResultError(errors);
|
|
81
80
|
}
|
|
82
|
-
return
|
|
81
|
+
return command;
|
|
83
82
|
}
|
|
84
83
|
}
|
package/src/service.ts
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import rl from 'node:readline/promises';
|
|
4
4
|
import net from 'node:net';
|
|
5
5
|
|
|
6
|
-
import { ExecUtil, TimeUtil, Util } from '@travetto/runtime';
|
|
6
|
+
import { ExecUtil, Runtime, RuntimeIndex, TimeUtil, Util } from '@travetto/runtime';
|
|
7
7
|
|
|
8
8
|
const ports = (value: number | `${number}:${number}`): [number, number] =>
|
|
9
9
|
typeof value === 'number' ?
|
|
@@ -36,6 +36,23 @@ export type ServiceAction = 'start' | 'stop' | 'status' | 'restart';
|
|
|
36
36
|
*/
|
|
37
37
|
export class ServiceRunner {
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Find all services
|
|
41
|
+
*/
|
|
42
|
+
static async findServices(services: string[]): Promise<ServiceDescriptor[]> {
|
|
43
|
+
return (await Promise.all(
|
|
44
|
+
RuntimeIndex.find({
|
|
45
|
+
module: module => module.roles.includes('std'),
|
|
46
|
+
folder: folder => folder === 'support',
|
|
47
|
+
file: file => /support\/service[.]/.test(file.sourceFile)
|
|
48
|
+
})
|
|
49
|
+
.map(file => Runtime.importFrom<{ service: ServiceDescriptor }>(file.import).then(value => value.service))
|
|
50
|
+
))
|
|
51
|
+
.filter(file => !!file)
|
|
52
|
+
.filter(file => services?.length ? services.includes(file.name) : true)
|
|
53
|
+
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
54
|
+
}
|
|
55
|
+
|
|
39
56
|
#descriptor: ServiceDescriptor;
|
|
40
57
|
constructor(descriptor: ServiceDescriptor) { this.#descriptor = descriptor; }
|
|
41
58
|
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { Class } from '@travetto/runtime';
|
|
1
|
+
import type { Any, Class } from '@travetto/runtime';
|
|
2
|
+
|
|
3
|
+
export const HELP_FLAG = '--help';
|
|
2
4
|
|
|
3
5
|
type OrProm<T> = T | Promise<T>;
|
|
4
6
|
type ParsedFlag = { type: 'flag', input: string, array?: boolean, fieldName: string, value?: unknown };
|
|
@@ -32,7 +34,7 @@ export interface CliCommandShape {
|
|
|
32
34
|
help?(): OrProm<string[]>;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
type PreMainHandler = (cmd:
|
|
37
|
+
export type PreMainHandler<T extends Any = Any> = { priority: number, handler: (cmd: T) => Any };
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
40
|
* CLI Command schema shape
|
|
@@ -41,5 +43,5 @@ export interface CliCommandConfig {
|
|
|
41
43
|
cls: Class<CliCommandShape>;
|
|
42
44
|
name: string;
|
|
43
45
|
runTarget?: boolean;
|
|
44
|
-
preMain
|
|
46
|
+
preMain: PreMainHandler[];
|
|
45
47
|
}
|
package/src/util.ts
CHANGED
|
@@ -2,11 +2,12 @@ import { spawn, type ChildProcess } from 'node:child_process';
|
|
|
2
2
|
|
|
3
3
|
import { RuntimeError, JSONUtil, Env, ExecUtil, Runtime, ShutdownManager, Util, WatchUtil } from '@travetto/runtime';
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const IPC_INVALID_ENV = new Set([
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
)
|
|
5
|
+
const IPC_VALID_ENV = new Set(['NODE_OPTIONS', 'PATH', Env.DEBUG.key, Env.NODE_ENV.key]);
|
|
6
|
+
const IPC_INVALID_ENV = new Set([
|
|
7
|
+
Env.TRV_CLI_IPC, Env.TRV_DEBUG_IPC, Env.TRV_DEBUG_BREAK, Env.TRV_MANIFEST, Env.TRV_MODULE, Env.TRV_RESTART_TARGET
|
|
8
|
+
].map(item => item.key));
|
|
9
|
+
const validEnv = ([key]: [key: string, value: unknown]): boolean =>
|
|
10
|
+
IPC_VALID_ENV.has(key) || (key.startsWith('TRV_') && !IPC_INVALID_ENV.has(key));
|
|
10
11
|
|
|
11
12
|
export class CliUtil {
|
|
12
13
|
/**
|
|
@@ -14,8 +15,7 @@ export class CliUtil {
|
|
|
14
15
|
*/
|
|
15
16
|
static getSimpleModuleName(placeholder: string, module?: string): string {
|
|
16
17
|
const simple = (module ?? Runtime.main.name).replace(/[\/]/, '_').replace(/@/, '');
|
|
17
|
-
|
|
18
|
-
return placeholder.replace('<module>', targetModule);
|
|
18
|
+
return simple ? placeholder.replace('<module>', simple) : placeholder;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
@@ -33,10 +33,11 @@ export class CliUtil {
|
|
|
33
33
|
ShutdownManager.disableInterrupt();
|
|
34
34
|
|
|
35
35
|
let child: ChildProcess | undefined;
|
|
36
|
-
|
|
36
|
+
await WatchUtil.watchCompilerEvents('file', () => ShutdownManager.shutdownChild(child!, { reason: 'restart', mode: 'exit' }));
|
|
37
|
+
|
|
37
38
|
process
|
|
38
39
|
.on('SIGINT', () => ShutdownManager.shutdownChild(child!, { mode: 'exit' }))
|
|
39
|
-
.on('message',
|
|
40
|
+
.on('message', message => child?.send?.(message!));
|
|
40
41
|
|
|
41
42
|
const env = { ...process.env, ...Env.TRV_RESTART_TARGET.export(true) };
|
|
42
43
|
|
|
@@ -77,32 +78,32 @@ export class CliUtil {
|
|
|
77
78
|
return; // Server not running, run normal
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
const env: Record<string, string> = {};
|
|
81
81
|
const request = {
|
|
82
82
|
type: '@travetto/cli:run',
|
|
83
83
|
data: {
|
|
84
84
|
name,
|
|
85
|
-
env,
|
|
85
|
+
env: Object.fromEntries(Object.entries(process.env).filter(validEnv)),
|
|
86
86
|
cwd: process.cwd(),
|
|
87
87
|
args: process.argv.slice(3),
|
|
88
88
|
}
|
|
89
89
|
};
|
|
90
|
-
console.log('Triggering IPC request', request);
|
|
91
90
|
|
|
92
|
-
|
|
91
|
+
console.log('Triggering IPC request', request);
|
|
93
92
|
const sent = await doFetch({ method: 'POST', body: JSONUtil.toUTF8(request) });
|
|
94
93
|
|
|
95
94
|
if (!sent.ok) {
|
|
96
95
|
throw new RuntimeError(`IPC Request failed: ${sent.status} ${await sent.text()}`);
|
|
97
96
|
}
|
|
97
|
+
|
|
98
|
+
await ShutdownManager.shutdown({ mode: 'exit' });
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
/**
|
|
101
102
|
* Write data to channel and ensure its flushed before continuing
|
|
102
103
|
*/
|
|
103
104
|
static async writeAndEnsureComplete(data: unknown, channel: 'stdout' | 'stderr' = 'stdout'): Promise<void> {
|
|
104
|
-
|
|
105
|
-
JSONUtil.toUTF8Pretty(data),
|
|
105
|
+
await new Promise<unknown>(resolve => process[channel].write(typeof data === 'string' ? data :
|
|
106
|
+
JSONUtil.toUTF8Pretty(data), resolve));
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
/**
|
package/support/cli.service.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
2
|
+
|
|
1
3
|
import { type CliCommandShape, CliCommand, cliTpl } from '@travetto/cli';
|
|
2
4
|
import { Terminal } from '@travetto/terminal';
|
|
3
5
|
import { AsyncQueue, Util } from '@travetto/runtime';
|
|
4
6
|
import { MethodValidator, type ValidationError } from '@travetto/schema';
|
|
5
7
|
|
|
6
8
|
import { ServiceRunner, type ServiceAction } from '../src/service.ts';
|
|
7
|
-
import { getServices } from './bin/util.ts';
|
|
8
9
|
|
|
9
|
-
async function validateService(
|
|
10
|
-
const all = await
|
|
10
|
+
async function validateService(_: ServiceAction, services: string[]): Promise<ValidationError | undefined> {
|
|
11
|
+
const all = await ServiceRunner.findServices(services);
|
|
11
12
|
|
|
12
13
|
if (!all.length) {
|
|
13
14
|
return { message: 'No services found', source: 'arg', kind: 'invalid', path: 'services' };
|
|
@@ -20,8 +21,10 @@ async function validateService(action: ServiceAction, services: string[]): Promi
|
|
|
20
21
|
@CliCommand()
|
|
21
22
|
export class CliServiceCommand implements CliCommandShape {
|
|
22
23
|
|
|
24
|
+
quiet = false;
|
|
25
|
+
|
|
23
26
|
async help(): Promise<string[]> {
|
|
24
|
-
const all = await
|
|
27
|
+
const all = await ServiceRunner.findServices([]);
|
|
25
28
|
return [
|
|
26
29
|
cliTpl`${{ title: 'Available Services' }}`,
|
|
27
30
|
'-'.repeat(20),
|
|
@@ -31,32 +34,48 @@ export class CliServiceCommand implements CliCommandShape {
|
|
|
31
34
|
|
|
32
35
|
@MethodValidator(validateService)
|
|
33
36
|
async main(action: ServiceAction, services: string[] = []): Promise<void> {
|
|
34
|
-
const all = await
|
|
37
|
+
const all = await ServiceRunner.findServices(services);
|
|
35
38
|
const maxName = Math.max(...all.map(service => service.name.length), 'Service'.length) + 3;
|
|
36
39
|
const maxVersion = Math.max(...all.map(service => `${service.version}`.length), 'Version'.length) + 3;
|
|
37
40
|
const maxStatus = 20;
|
|
38
41
|
const queue = new AsyncQueue<{ idx: number, text: string, done?: boolean }>();
|
|
39
42
|
|
|
43
|
+
const failureMessages: string[] = [];
|
|
44
|
+
|
|
40
45
|
const jobs = all.map(async (descriptor, i) => {
|
|
41
46
|
const identifier = descriptor.name.padEnd(maxName);
|
|
42
47
|
const type = `${descriptor.version}`.padStart(maxVersion - 3).padEnd(maxVersion);
|
|
43
|
-
let
|
|
48
|
+
let message: string;
|
|
44
49
|
for await (const [valueType, value] of new ServiceRunner(descriptor).action(action)) {
|
|
45
50
|
const details = { [valueType === 'message' ? 'subtitle' : valueType]: value };
|
|
46
|
-
queue.add({ idx: i, text:
|
|
51
|
+
queue.add({ idx: i, text: message = cliTpl`${{ identifier }} ${{ type }} ${details}` });
|
|
52
|
+
if (valueType === 'failure') {
|
|
53
|
+
failureMessages.push(message);
|
|
54
|
+
}
|
|
47
55
|
}
|
|
48
|
-
queue.add({ idx: i, done: true, text:
|
|
56
|
+
queue.add({ idx: i, done: true, text: message! });
|
|
49
57
|
});
|
|
50
58
|
|
|
51
59
|
Promise.all(jobs).then(() => Util.queueMacroTask()).then(() => queue.close());
|
|
52
60
|
|
|
53
|
-
const term = new Terminal();
|
|
54
|
-
await term.writer.writeLines([
|
|
55
|
-
'',
|
|
56
|
-
cliTpl`${{ title: 'Service'.padEnd(maxName) }} ${{ title: 'Version'.padEnd(maxVersion) }} ${{ title: 'Status' }}`,
|
|
57
|
-
''.padEnd(maxName + maxVersion + maxStatus + 3, '-'),
|
|
58
|
-
]).commit();
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
if (this.quiet) {
|
|
63
|
+
for await (const _ of queue) { }
|
|
64
|
+
if (failureMessages.length) {
|
|
65
|
+
console.error('Failure');
|
|
66
|
+
failureMessages.map(stripVTControlCharacters).map(item => console.error(item));
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
const term = new Terminal();
|
|
70
|
+
await term.writer.writeLines([
|
|
71
|
+
'',
|
|
72
|
+
cliTpl`${{ title: 'Service'.padEnd(maxName) }} ${{ title: 'Version'.padEnd(maxVersion) }} ${{ title: 'Status' }}`,
|
|
73
|
+
''.padEnd(maxName + maxVersion + maxStatus + 3, '-'),
|
|
74
|
+
]).commit();
|
|
75
|
+
|
|
76
|
+
await term.streamList(queue);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
process.exitCode = failureMessages.length ? 1 : 0;
|
|
61
80
|
}
|
|
62
81
|
}
|
package/support/entry.trv.ts
CHANGED
package/src/error.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { RuntimeError, Runtime } from '@travetto/runtime';
|
|
2
|
-
import type { ValidationError } from '@travetto/schema';
|
|
3
|
-
|
|
4
|
-
import { cliTpl } from './color.ts';
|
|
5
|
-
import type { CliCommandShape } from './types.ts';
|
|
6
|
-
|
|
7
|
-
const COMMAND_PACKAGE = [
|
|
8
|
-
[/^test(:watch)?$/, 'test', false],
|
|
9
|
-
[/^lint(:register)?$/, 'eslint', false],
|
|
10
|
-
[/^model:(install|export)$/, 'model', true],
|
|
11
|
-
[/^openapi:(spec|client)$/, 'openapi', true],
|
|
12
|
-
[/^email:(compile|editor)$/, 'email-compiler', false],
|
|
13
|
-
[/^pack(:zip|:docker)?$/, 'pack', false],
|
|
14
|
-
[/^web:http$/, 'web-http', true],
|
|
15
|
-
[/^web:rpc-client$/, 'web-rpc', true],
|
|
16
|
-
] as const;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Provides a contract for unknown commands
|
|
20
|
-
*/
|
|
21
|
-
export class CliUnknownCommandError extends Error {
|
|
22
|
-
|
|
23
|
-
#getMissingCommandHelp(cmd: string): string | undefined {
|
|
24
|
-
const matchedConfig = COMMAND_PACKAGE.find(([regex]) => regex.test(cmd));
|
|
25
|
-
if (matchedConfig) {
|
|
26
|
-
const [, pkg, production] = matchedConfig;
|
|
27
|
-
const install = Runtime.getInstallCommand(`@travetto/${pkg}`, production);
|
|
28
|
-
return cliTpl`
|
|
29
|
-
${{ title: 'Missing Package' }}\n${'-'.repeat(20)}\nTo use ${{ input: cmd }} please run:\n
|
|
30
|
-
${{ identifier: install }}
|
|
31
|
-
`;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
help?: string;
|
|
36
|
-
cmd: string;
|
|
37
|
-
|
|
38
|
-
constructor(cmd: string) {
|
|
39
|
-
super(`Unknown command: ${cmd}`);
|
|
40
|
-
this.cmd = cmd;
|
|
41
|
-
this.help = this.#getMissingCommandHelp(cmd);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
get defaultMessage(): string {
|
|
45
|
-
return cliTpl`${{ subtitle: 'Unknown command' }}: ${{ input: this.cmd }}`;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Provides a basic error wrapper for cli validation issues
|
|
51
|
-
*/
|
|
52
|
-
export class CliValidationResultError extends RuntimeError<{ errors: ValidationError[] }> {
|
|
53
|
-
command: CliCommandShape;
|
|
54
|
-
|
|
55
|
-
constructor(command: CliCommandShape, errors: ValidationError[]) {
|
|
56
|
-
super('', { details: { errors } });
|
|
57
|
-
this.command = command;
|
|
58
|
-
}
|
|
59
|
-
}
|
package/support/bin/util.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { RuntimeIndex, Runtime } from '@travetto/runtime';
|
|
2
|
-
import type { ServiceDescriptor } from '../../__index__.ts';
|
|
3
|
-
|
|
4
|
-
export async function getServices(services: string[]): Promise<ServiceDescriptor[]> {
|
|
5
|
-
return (await Promise.all(
|
|
6
|
-
RuntimeIndex.find({
|
|
7
|
-
module: module => module.roles.includes('std'),
|
|
8
|
-
folder: folder => folder === 'support',
|
|
9
|
-
file: file => /support\/service[.]/.test(file.sourceFile)
|
|
10
|
-
})
|
|
11
|
-
.map(file => Runtime.importFrom<{ service: ServiceDescriptor }>(file.import).then(value => value.service))
|
|
12
|
-
))
|
|
13
|
-
.filter(file => !!file)
|
|
14
|
-
.filter(file => services?.length ? services.includes(file.name) : true)
|
|
15
|
-
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
16
|
-
}
|