@travetto/cli 8.0.0-alpha.2 → 8.0.0-alpha.22
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 +85 -63
- package/bin/trv.js +2 -1
- package/package.json +3 -3
- package/src/execute.ts +37 -18
- package/src/help.ts +118 -63
- package/src/parse.ts +4 -5
- package/src/registry/decorator.ts +26 -31
- package/src/registry/registry-adapter.ts +10 -15
- package/src/registry/registry-index.ts +6 -2
- package/src/schema-export.ts +1 -1
- package/src/types.ts +5 -3
- package/src/util.ts +16 -15
- package/support/cli.cli_schema.ts +4 -1
- package/support/cli.main.ts +4 -1
- package/support/cli.service.ts +8 -5
- package/support/entry.trv.ts +1 -1
package/README.md
CHANGED
|
@@ -22,34 +22,38 @@ $ trv --help
|
|
|
22
22
|
Usage: [options] [command]
|
|
23
23
|
|
|
24
24
|
Commands:
|
|
25
|
-
doc
|
|
26
|
-
doc:angular
|
|
27
|
-
doc:mapping
|
|
28
|
-
email:compile
|
|
29
|
-
email:editor
|
|
30
|
-
email:test
|
|
31
|
-
eslint
|
|
32
|
-
eslint:register
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
25
|
+
doc Generate documentation outputs from a module `DOC.tsx` entry file.
|
|
26
|
+
doc:angular Generate documentation into the angular webapp under related/travetto.github.io
|
|
27
|
+
doc:mapping Generate module mapping for
|
|
28
|
+
email:compile Compile all email templates into generated runtime artifacts.
|
|
29
|
+
email:editor Start the email template editor service for interactive preview and testing.
|
|
30
|
+
email:test Render and send a template file to a target recipient for quick validation.
|
|
31
|
+
eslint Run ESLint for the workspace or changed files.
|
|
32
|
+
eslint:register Generate the workspace ESLint configuration entry file.
|
|
33
|
+
llm:support:execute Execute llm-support operations with dry-run by default.
|
|
34
|
+
llm:support:mcp Minimal MCP stdio server for llm-support tools.
|
|
35
|
+
llm:support:plan Build plan-first execution details for llm-support operations.
|
|
36
|
+
llm:support:recommend Recommend llm-support bundles, workflows, and operations.
|
|
37
|
+
llm:support:status Show llm-support execution coverage status.
|
|
38
|
+
model:export Export model definitions for a selected provider and model set.
|
|
39
|
+
model:install Install or update model definitions for a selected provider.
|
|
40
|
+
openapi:client Generate API clients from an OpenAPI specification using the generator image.
|
|
41
|
+
openapi:spec Generate the OpenAPI specification for the selected module.
|
|
42
|
+
pack Build a standard module package artifact.
|
|
43
|
+
pack:docker Build container-ready artifacts and optionally publish Docker images.
|
|
44
|
+
pack:lambda Build an AWS Lambda-ready zip package using the pack pipeline.
|
|
45
|
+
pack:zip Build a deployable zip artifact using the standard pack pipeline.
|
|
46
|
+
repo:exec Execute a shell command across workspace modules.
|
|
47
|
+
repo:list List workspace modules and their relationships.
|
|
48
|
+
repo:publish Publish unpublished workspace modules to the package registry.
|
|
49
|
+
repo:version Bump workspace module versions and optionally commit/tag release metadata.
|
|
50
|
+
repo:version-sync Synchronize package versions and dependency ranges across the monorepo.
|
|
51
|
+
run:double Doubles a number
|
|
52
|
+
service Manage development services (start/stop/restart/status) across the workspace.
|
|
53
|
+
test Execute the test framework for targeted files, suites, or methods.
|
|
54
|
+
test:watch Start the test watcher for continuous test execution.
|
|
55
|
+
web:http Start the configured web HTTP server for a module.
|
|
56
|
+
web:rpc-client Generate web-rpc client artifacts from a specified provider or leveraging local config.
|
|
53
57
|
```
|
|
54
58
|
|
|
55
59
|
This listing is from the [Travetto](https://travetto.dev) monorepo, and represents the majority of tools that can be invoked from the command line.
|
|
@@ -59,7 +63,7 @@ This module also has a tight integration with the [VSCode plugin](https://market
|
|
|
59
63
|
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
64
|
* The file must be located in the `support/` folder, and have a name that matches `cli.*.ts`
|
|
61
65
|
* 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#
|
|
66
|
+
* The class must use the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L20) decorator
|
|
63
67
|
|
|
64
68
|
**Code: Basic Command**
|
|
65
69
|
```typescript
|
|
@@ -75,12 +79,12 @@ export class BasicCommand {
|
|
|
75
79
|
|
|
76
80
|
**Terminal: Basic Command Help**
|
|
77
81
|
```bash
|
|
78
|
-
$ trv basic
|
|
82
|
+
$ trv basic --help
|
|
79
83
|
|
|
80
84
|
Usage: basic [options]
|
|
81
85
|
|
|
82
86
|
Options:
|
|
83
|
-
|
|
87
|
+
--help display help for command
|
|
84
88
|
```
|
|
85
89
|
|
|
86
90
|
## Command Naming
|
|
@@ -94,7 +98,7 @@ Examples of mappings:
|
|
|
94
98
|
The pattern is that underscores(_) translate to colons (:), and the `cli.` prefix, and `.ts` suffix are dropped.
|
|
95
99
|
|
|
96
100
|
## Binding Flags
|
|
97
|
-
[@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#
|
|
101
|
+
[@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
102
|
|
|
99
103
|
**Code: Basic Command with Flag**
|
|
100
104
|
```typescript
|
|
@@ -113,13 +117,13 @@ export class BasicCommand {
|
|
|
113
117
|
|
|
114
118
|
**Terminal: Basic Command with Flag Help**
|
|
115
119
|
```bash
|
|
116
|
-
$ trv basic:flag
|
|
120
|
+
$ trv basic:flag --help
|
|
117
121
|
|
|
118
122
|
Usage: basic:flag [options]
|
|
119
123
|
|
|
120
124
|
Options:
|
|
121
125
|
-l, --loud
|
|
122
|
-
|
|
126
|
+
--help display help for command
|
|
123
127
|
```
|
|
124
128
|
|
|
125
129
|
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 +135,7 @@ $ trv basic:flag --loud
|
|
|
131
135
|
HELLO
|
|
132
136
|
```
|
|
133
137
|
|
|
134
|
-
The [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#
|
|
138
|
+
The [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L20) supports the following data types for flags:
|
|
135
139
|
* Boolean values
|
|
136
140
|
* 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
141
|
* 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 +162,12 @@ export class BasicCommand {
|
|
|
158
162
|
|
|
159
163
|
**Terminal: Basic Command**
|
|
160
164
|
```bash
|
|
161
|
-
$ trv basic:arg
|
|
165
|
+
$ trv basic:arg --help
|
|
162
166
|
|
|
163
167
|
Usage: basic:arg [options] [volume:number]
|
|
164
168
|
|
|
165
169
|
Options:
|
|
166
|
-
|
|
170
|
+
--help display help for command
|
|
167
171
|
```
|
|
168
172
|
|
|
169
173
|
**Terminal: Basic Command with Invalid Loud Arg**
|
|
@@ -176,7 +180,7 @@ Execution failed:
|
|
|
176
180
|
Usage: basic:arg [options] [volume:number]
|
|
177
181
|
|
|
178
182
|
Options:
|
|
179
|
-
|
|
183
|
+
--help display help for command
|
|
180
184
|
```
|
|
181
185
|
|
|
182
186
|
**Terminal: Basic Command with Loud Arg > 7**
|
|
@@ -213,13 +217,13 @@ export class BasicCommand {
|
|
|
213
217
|
|
|
214
218
|
**Terminal: Basic Command**
|
|
215
219
|
```bash
|
|
216
|
-
$ trv basic:arg-list
|
|
220
|
+
$ trv basic:arg-list --help
|
|
217
221
|
|
|
218
222
|
Usage: basic:arg-list [options] <volumes...:number>
|
|
219
223
|
|
|
220
224
|
Options:
|
|
221
225
|
-r, --reverse
|
|
222
|
-
|
|
226
|
+
--help display help for command
|
|
223
227
|
```
|
|
224
228
|
|
|
225
229
|
**Terminal: Basic Arg List**
|
|
@@ -240,7 +244,7 @@ Usage: basic:arg-list [options] <volumes...:number>
|
|
|
240
244
|
|
|
241
245
|
Options:
|
|
242
246
|
-r, --reverse
|
|
243
|
-
|
|
247
|
+
--help display help for command
|
|
244
248
|
```
|
|
245
249
|
|
|
246
250
|
**Terminal: Basic Arg List with Reverse**
|
|
@@ -259,7 +263,7 @@ import { CliCommand } from '@travetto/cli';
|
|
|
259
263
|
import { Max, Min } from '@travetto/schema';
|
|
260
264
|
|
|
261
265
|
/**
|
|
262
|
-
*
|
|
266
|
+
* Example command with a custom argument
|
|
263
267
|
*/
|
|
264
268
|
@CliCommand()
|
|
265
269
|
export class CustomCommand {
|
|
@@ -279,13 +283,15 @@ export class CustomCommand {
|
|
|
279
283
|
|
|
280
284
|
**Terminal: Custom Command Help**
|
|
281
285
|
```bash
|
|
282
|
-
$ trv custom:arg
|
|
286
|
+
$ trv custom:arg --help
|
|
283
287
|
|
|
284
288
|
Usage: custom:arg [options] [volume:number]
|
|
285
289
|
|
|
290
|
+
Example command with a custom argument
|
|
291
|
+
|
|
286
292
|
Options:
|
|
287
293
|
-m, --message <string> The message to send back to the user (default: "hello")
|
|
288
|
-
|
|
294
|
+
--help display help for command
|
|
289
295
|
```
|
|
290
296
|
|
|
291
297
|
**Terminal: Custom Command Help with overridden Text**
|
|
@@ -311,7 +317,7 @@ import { CliCommand } from '@travetto/cli';
|
|
|
311
317
|
import { Max, Min } from '@travetto/schema';
|
|
312
318
|
|
|
313
319
|
/**
|
|
314
|
-
*
|
|
320
|
+
* Example of a command with a custom environment variable argument
|
|
315
321
|
*/
|
|
316
322
|
@CliCommand()
|
|
317
323
|
export class CustomCommand {
|
|
@@ -330,13 +336,15 @@ export class CustomCommand {
|
|
|
330
336
|
|
|
331
337
|
**Terminal: Custom Command Help**
|
|
332
338
|
```bash
|
|
333
|
-
$ trv custom:env-arg
|
|
339
|
+
$ trv custom:env-arg --help
|
|
334
340
|
|
|
335
341
|
Usage: custom:env-arg [options] [volume:number]
|
|
336
342
|
|
|
343
|
+
Example of a command with a custom environment variable argument
|
|
344
|
+
|
|
337
345
|
Options:
|
|
338
346
|
-t, --text <string> The message to send back to the user (default: "hello")
|
|
339
|
-
|
|
347
|
+
--help display help for command
|
|
340
348
|
```
|
|
341
349
|
|
|
342
350
|
**Terminal: Custom Command Help with default Text**
|
|
@@ -390,7 +398,7 @@ npx trv call:db --host localhost --port 3306 --username app --password <custom>
|
|
|
390
398
|
```
|
|
391
399
|
|
|
392
400
|
## 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#
|
|
401
|
+
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
402
|
|
|
395
403
|
**Code: Simple Run Target**
|
|
396
404
|
```typescript
|
|
@@ -442,7 +450,12 @@ import { Registry } from '@travetto/registry';
|
|
|
442
450
|
import type { WebHttpServer } from '../src/types.ts';
|
|
443
451
|
|
|
444
452
|
/**
|
|
445
|
-
*
|
|
453
|
+
* Start the configured web HTTP server for a module.
|
|
454
|
+
*
|
|
455
|
+
* Initializes registry and server bindings, supports restart-aware development
|
|
456
|
+
* flags, and can attempt to clear conflicting port owners in local workflows.
|
|
457
|
+
*
|
|
458
|
+
* @example trv web:http -m <MODULE> -p 3000
|
|
446
459
|
*/
|
|
447
460
|
@CliCommand()
|
|
448
461
|
export class WebHttpCommand implements CliCommandShape {
|
|
@@ -460,7 +473,7 @@ export class WebHttpCommand implements CliCommandShape {
|
|
|
460
473
|
profile: string[];
|
|
461
474
|
|
|
462
475
|
@CliRestartOnChangeFlag()
|
|
463
|
-
restartOnChange: boolean =
|
|
476
|
+
restartOnChange: boolean = Runtime.localDevelopment;
|
|
464
477
|
|
|
465
478
|
@CliDebugIpcFlag()
|
|
466
479
|
debugIpc?: boolean;
|
|
@@ -542,7 +555,7 @@ A simple example of the validation can be found in the `doc` command:
|
|
|
542
555
|
```typescript
|
|
543
556
|
@Validator(async (cmd) => {
|
|
544
557
|
const docFile = path.resolve(cmd.input);
|
|
545
|
-
if (!(await fs.stat(docFile
|
|
558
|
+
if (!(await fs.stat(docFile, { throwIfNoEntry: false }))) {
|
|
546
559
|
return { message: `input: ${cmd.input} does not exist`, path: 'input', source: 'flag', kind: 'invalid' };
|
|
547
560
|
}
|
|
548
561
|
})
|
|
@@ -551,26 +564,31 @@ A simple example of the validation can be found in the `doc` command:
|
|
|
551
564
|
## CLI - service
|
|
552
565
|
The module provides the ability to start/stop/restart services as [docker](https://www.docker.com/community-edition) containers. This is meant to be used for development purposes, to minimize the effort of getting an application up and running. Services can be targeted individually or handled as a group.
|
|
553
566
|
|
|
554
|
-
**Terminal:
|
|
567
|
+
**Terminal: Help for service**
|
|
555
568
|
```bash
|
|
556
569
|
$ trv service --help
|
|
557
570
|
|
|
558
571
|
Usage: service [options] <action:restart|start|status|stop> [services...:string]
|
|
559
572
|
|
|
573
|
+
Manage development services (start/stop/restart/status) across the workspace.
|
|
574
|
+
|
|
575
|
+
Services are discovered from registered descriptors and executed with streamed
|
|
576
|
+
terminal feedback, including optional quiet mode.
|
|
577
|
+
|
|
560
578
|
Options:
|
|
561
579
|
-q, --quiet (default: false)
|
|
562
|
-
|
|
580
|
+
--help display help for command
|
|
563
581
|
|
|
564
582
|
Available Services
|
|
565
583
|
--------------------
|
|
566
584
|
* dynamodb@3.3.0
|
|
567
|
-
* elasticsearch@9.2.
|
|
585
|
+
* elasticsearch@9.2.8
|
|
568
586
|
* firestore@latest
|
|
569
|
-
* mongodb@8.
|
|
587
|
+
* mongodb@8.3
|
|
570
588
|
* mysql@9.6
|
|
571
|
-
* postgresql@18.
|
|
589
|
+
* postgresql@18.3
|
|
572
590
|
* redis@8.4
|
|
573
|
-
* s3@4.
|
|
591
|
+
* s3@4.12.4
|
|
574
592
|
```
|
|
575
593
|
|
|
576
594
|
A sample of all services available to the entire framework:
|
|
@@ -582,13 +600,13 @@ $ trv service status
|
|
|
582
600
|
Service Version Status
|
|
583
601
|
-------------------------------------------------
|
|
584
602
|
dynamodb 3.3.0 Running 93af422e793a
|
|
585
|
-
elasticsearch 9.2.
|
|
603
|
+
elasticsearch 9.2.8 Running ed76ee063d13
|
|
586
604
|
firestore latest Running feec2e5e95b4
|
|
587
|
-
mongodb 8.
|
|
605
|
+
mongodb 8.3 Running 5513eba6734e
|
|
588
606
|
mysql 9.6 Running 307bc66d442a
|
|
589
|
-
postgresql 18.
|
|
607
|
+
postgresql 18.3 Running e78291e71040
|
|
590
608
|
redis 8.4 Running 77ba279b4e30
|
|
591
|
-
s3 4.
|
|
609
|
+
s3 4.12.4 Running fdacfc55b9e3
|
|
592
610
|
```
|
|
593
611
|
|
|
594
612
|
### Defining new Services
|
|
@@ -598,12 +616,16 @@ The services are defined as plain typescript files within the framework and can
|
|
|
598
616
|
```typescript
|
|
599
617
|
import type { ServiceDescriptor } from '@travetto/cli';
|
|
600
618
|
|
|
601
|
-
const version = process.env.MONGO_VERSION || '8.
|
|
619
|
+
const version = process.env.MONGO_VERSION || '8.3';
|
|
602
620
|
|
|
603
621
|
export const service: ServiceDescriptor = {
|
|
604
622
|
name: 'mongodb',
|
|
605
623
|
version,
|
|
606
624
|
port: 27017,
|
|
607
|
-
image: `mongo:${version}
|
|
625
|
+
image: `mongo:${version}`,
|
|
626
|
+
env: {
|
|
627
|
+
// Temp until mongo image fixes orbstack issue
|
|
628
|
+
GLIBC_TUNABLES: 'glibc.pthread.rseq=1'
|
|
629
|
+
}
|
|
608
630
|
};
|
|
609
631
|
```
|
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.22",
|
|
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.17",
|
|
33
|
+
"@travetto/terminal": "^8.0.0-alpha.16"
|
|
34
34
|
},
|
|
35
35
|
"travetto": {
|
|
36
36
|
"displayName": "Command Line Interface",
|
package/src/execute.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConsoleManager, Runtime, ShutdownManager, Util } from '@travetto/runtime';
|
|
1
|
+
import { ConsoleManager, getClass, Runtime, ShutdownManager, Util } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import { HelpUtil } from './help.ts';
|
|
4
4
|
import { CliCommandRegistryIndex } from './registry/registry-index.ts';
|
|
@@ -11,20 +11,32 @@ import type { CliCommandShape } from './types.ts';
|
|
|
11
11
|
*/
|
|
12
12
|
export class ExecutionManager {
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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);
|
|
20
|
+
}
|
|
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);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Extract configuration and show help as needed */
|
|
29
|
+
static async getExecutionCommand(argv: string[]): Promise<(() => Promise<void>) | undefined> {
|
|
19
30
|
let command: CliCommandShape | undefined;
|
|
20
31
|
|
|
21
32
|
const { cmd, args, help } = CliParseUtil.getArgs(argv);
|
|
22
33
|
if (!cmd) {
|
|
23
|
-
|
|
34
|
+
console.info!(await HelpUtil.renderAllHelp());
|
|
35
|
+
return;
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
try {
|
|
27
|
-
const [{ instance, schema
|
|
39
|
+
const [{ instance, schema }] = await CliCommandRegistryIndex.load([cmd]);
|
|
28
40
|
command = instance;
|
|
29
41
|
const fullArgs = await CliParseUtil.expandArgs(schema, args);
|
|
30
42
|
|
|
@@ -35,22 +47,29 @@ export class ExecutionManager {
|
|
|
35
47
|
await instance.finalize?.(help);
|
|
36
48
|
|
|
37
49
|
if (help) {
|
|
38
|
-
|
|
50
|
+
console.log!(await HelpUtil.renderCommandHelp(instance));
|
|
51
|
+
return;
|
|
39
52
|
}
|
|
40
53
|
|
|
41
54
|
await CliCommandSchemaUtil.validate(command, boundArgs);
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
ShutdownManager.signal.addEventListener('abort', () => Util.blockingTimeout(50));
|
|
45
|
-
|
|
46
|
-
for (const preMain of config.preMain ?? []) {
|
|
47
|
-
await preMain(instance);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
ConsoleManager.debug(Runtime.debug);
|
|
51
|
-
await instance.main(...boundArgs);
|
|
56
|
+
return this.execute.bind(this, instance, boundArgs);
|
|
52
57
|
} catch (error) {
|
|
53
58
|
await HelpUtil.renderError(error, cmd, command);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Execute the command line
|
|
64
|
+
* @param args
|
|
65
|
+
*/
|
|
66
|
+
static async run(argv: string[]): Promise<void> {
|
|
67
|
+
try {
|
|
68
|
+
const execute = await this.getExecutionCommand(argv);
|
|
69
|
+
await execute?.();
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error!(error);
|
|
72
|
+
process.exitCode ??= 1;
|
|
54
73
|
} finally {
|
|
55
74
|
await ShutdownManager.shutdown();
|
|
56
75
|
}
|
package/src/help.ts
CHANGED
|
@@ -1,72 +1,73 @@
|
|
|
1
1
|
import util from 'node:util';
|
|
2
2
|
|
|
3
|
-
import { castKey, getClass, JSONUtil, Runtime } from '@travetto/runtime';
|
|
3
|
+
import { castKey, CodecUtil, getClass, JSONUtil, Runtime } from '@travetto/runtime';
|
|
4
4
|
import { SchemaRegistryIndex, ValidationResultError } from '@travetto/schema';
|
|
5
5
|
|
|
6
6
|
import { cliTpl } from './color.ts';
|
|
7
|
-
import type
|
|
7
|
+
import { HELP_FLAG, type CliCommandShape } from './types.ts';
|
|
8
8
|
import { CliCommandRegistryIndex, UNKNOWN_COMMAND } from './registry/registry-index.ts';
|
|
9
9
|
import { CliSchemaExportUtil } from './schema-export.ts';
|
|
10
10
|
|
|
11
|
-
const validationSourceMap: Record<string, string> = {
|
|
12
|
-
arg: 'Argument',
|
|
13
|
-
flag: 'Flag'
|
|
14
|
-
};
|
|
11
|
+
const validationSourceMap: Record<string, string> = { arg: 'Argument', flag: 'Flag' };
|
|
15
12
|
|
|
16
13
|
const ifDefined = <T>(value: T | null | '' | undefined): T | undefined =>
|
|
17
14
|
(value === null || value === '' || value === undefined) ? undefined : value;
|
|
18
15
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
]);
|
|
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])));
|
|
33
30
|
|
|
34
31
|
/**
|
|
35
32
|
* Utilities for showing help
|
|
36
33
|
*/
|
|
37
34
|
export class HelpUtil {
|
|
38
35
|
|
|
39
|
-
/**
|
|
40
|
-
static
|
|
41
|
-
const install = INSTALL_COMMANDS.get(cmd);
|
|
42
|
-
if (install) {
|
|
43
|
-
return cliTpl`
|
|
44
|
-
${{ title: 'Missing Package' }}\n${'-'.repeat(20)}\nTo use ${{ input: cmd }} please run:\n
|
|
45
|
-
${{ identifier: install }}
|
|
46
|
-
`;
|
|
47
|
-
} else {
|
|
48
|
-
return cliTpl`${{ subtitle: 'Unknown command' }}: ${{ input: cmd }}`;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Render command-specific help
|
|
54
|
-
* @param command
|
|
55
|
-
*/
|
|
56
|
-
static async renderCommandHelp(command: CliCommandShape): Promise<string> {
|
|
36
|
+
/** Get usage help for a command */
|
|
37
|
+
static getUsageMessage(command: CliCommandShape): string[] {
|
|
57
38
|
const schema = SchemaRegistryIndex.getConfig(getClass(command));
|
|
58
39
|
const { name: commandName } = CliCommandRegistryIndex.get(getClass(command));
|
|
59
|
-
const args = schema.methods.main?.parameters ?? [];
|
|
60
40
|
|
|
61
|
-
|
|
41
|
+
const usage: string[] = [];
|
|
42
|
+
|
|
43
|
+
usage.push(
|
|
44
|
+
cliTpl`${{ title: 'Usage:' }} ${{ param: commandName }} ${{ input: '[options]' }}`
|
|
45
|
+
);
|
|
62
46
|
|
|
63
|
-
|
|
64
|
-
for (const field of
|
|
47
|
+
// Ensure finalized
|
|
48
|
+
for (const field of schema.methods.main?.parameters ?? []) {
|
|
65
49
|
const type = field.type === String && field.enum && field.enum?.values.length <= 7 ? field.enum?.values?.join('|') : field.type.name.toLowerCase();
|
|
66
50
|
const arg = `${field.name}${field.array ? '...' : ''}:${type}`;
|
|
67
51
|
usage.push(cliTpl`${{ input: field.required?.active !== false ? `<${arg}>` : `[${arg}]` }}`);
|
|
68
52
|
}
|
|
53
|
+
usage.push('');
|
|
54
|
+
return usage;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Get description help for a command */
|
|
58
|
+
static getDescriptionMessage(command: CliCommandShape): string[] {
|
|
59
|
+
const schema = SchemaRegistryIndex.getConfig(getClass(command));
|
|
60
|
+
const description: string[] = [];
|
|
61
|
+
|
|
62
|
+
if (schema.description) {
|
|
63
|
+
description.push(cliTpl`${{ title: 'Description:' }}`, ...schema.description.split('\n').map(line => ` ${line}`), '');
|
|
64
|
+
}
|
|
65
|
+
return description;
|
|
66
|
+
}
|
|
69
67
|
|
|
68
|
+
/** Get options help for a command */
|
|
69
|
+
static getOptionsMessage(command: CliCommandShape): string[] {
|
|
70
|
+
const schema = SchemaRegistryIndex.getConfig(getClass(command));
|
|
70
71
|
const params: string[] = [];
|
|
71
72
|
const descriptions: string[] = [];
|
|
72
73
|
|
|
@@ -76,7 +77,7 @@ ${{ identifier: install }}
|
|
|
76
77
|
const aliases = (field.aliases ?? [])
|
|
77
78
|
.filter(flag => flag.startsWith('-'))
|
|
78
79
|
.filter(flag =>
|
|
79
|
-
(field.type !== Boolean) || (
|
|
80
|
+
(field.type !== Boolean) || (defaultValue !== true ? !flag.startsWith('--no-') : flag.startsWith('--'))
|
|
80
81
|
);
|
|
81
82
|
let type: string | undefined;
|
|
82
83
|
|
|
@@ -86,40 +87,92 @@ ${{ identifier: install }}
|
|
|
86
87
|
({ type } = CliSchemaExportUtil.baseInputType(field));
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
const
|
|
90
|
+
const parameter = [
|
|
90
91
|
cliTpl`${{ param: aliases.join(', ') }}`,
|
|
91
92
|
...(type ? [cliTpl`${{ type: `<${type}>` }}`] : []),
|
|
92
93
|
];
|
|
93
94
|
|
|
94
|
-
params.push(
|
|
95
|
-
const
|
|
95
|
+
params.push(parameter.join(' '));
|
|
96
|
+
const parts = [cliTpl`${{ title: field.description }}`];
|
|
96
97
|
|
|
97
|
-
if (
|
|
98
|
-
|
|
98
|
+
if (defaultValue !== undefined) {
|
|
99
|
+
parts.push(cliTpl`(default: ${{ input: JSONUtil.toUTF8(defaultValue) }})`);
|
|
99
100
|
}
|
|
100
|
-
descriptions.push(
|
|
101
|
+
descriptions.push(parts.join(' '));
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
params.push(cliTpl`${{ param: HELP_FLAG }}`);
|
|
105
|
+
descriptions.push('display help for command');
|
|
106
|
+
|
|
107
|
+
|
|
103
108
|
const paramWidths = params.map(item => util.stripVTControlCharacters(item).length);
|
|
104
109
|
const descWidths = descriptions.map(item => util.stripVTControlCharacters(item).length);
|
|
105
110
|
|
|
106
111
|
const paramWidth = Math.max(...paramWidths);
|
|
107
112
|
const descWidth = Math.max(...descWidths);
|
|
108
113
|
|
|
109
|
-
const
|
|
110
|
-
if (helpText.length && helpText.at(-1) !== '') {
|
|
111
|
-
helpText.push('');
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return [
|
|
115
|
-
usage.join(' '),
|
|
116
|
-
'',
|
|
114
|
+
const options: string[] = [
|
|
117
115
|
cliTpl`${{ title: 'Options:' }}`,
|
|
118
116
|
...params.map((_, i) =>
|
|
119
117
|
` ${params[i]}${' '.repeat((paramWidth - paramWidths[i]))} ${descriptions[i].padEnd(descWidth)}${' '.repeat((descWidth - descWidths[i]))}`
|
|
120
118
|
),
|
|
121
|
-
''
|
|
122
|
-
|
|
119
|
+
''
|
|
120
|
+
];
|
|
121
|
+
return options;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Get extended help for a command */
|
|
125
|
+
static async getExtendedHelpMessage(command: CliCommandShape): Promise<string[]> {
|
|
126
|
+
const extendedHelpText = await (command.help?.() ?? []);
|
|
127
|
+
if (extendedHelpText.length && extendedHelpText.at(-1) !== '') {
|
|
128
|
+
extendedHelpText.push('');
|
|
129
|
+
}
|
|
130
|
+
return extendedHelpText;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Get examples for a command */
|
|
134
|
+
static getExamplesMessage(command: CliCommandShape): string[] {
|
|
135
|
+
const schema = SchemaRegistryIndex.getConfig(getClass(command));
|
|
136
|
+
const examples: string[] = [];
|
|
137
|
+
if (schema.examples) {
|
|
138
|
+
examples.push(cliTpl`${{ title: 'Examples:' }}`);
|
|
139
|
+
for (const example of schema.examples) {
|
|
140
|
+
for (const line of example.split('\n')) {
|
|
141
|
+
examples.push(
|
|
142
|
+
line.trim().startsWith('>') ?
|
|
143
|
+
cliTpl` ${{ input: line.substring(line.indexOf('> ') + 2).trim() }}` :
|
|
144
|
+
cliTpl` ${{ subtitle: line.trim() }}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
examples.push('');
|
|
148
|
+
}
|
|
149
|
+
return examples;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Render the unknown command message */
|
|
153
|
+
static renderUnknownCommandMessage(command: string): string {
|
|
154
|
+
const module = COMMAND_TO_MODULE[command];
|
|
155
|
+
if (module) {
|
|
156
|
+
return cliTpl`
|
|
157
|
+
${{ title: 'Missing Package' }}\n${'-'.repeat(20)}\nTo use ${{ input: command }} please run:\n
|
|
158
|
+
${{ identifier: Runtime.getInstallCommand(module) }}
|
|
159
|
+
`;
|
|
160
|
+
} else {
|
|
161
|
+
return cliTpl`${{ subtitle: 'Unknown command' }}: ${{ input: command }}`;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Render command-specific help
|
|
167
|
+
* @param command
|
|
168
|
+
*/
|
|
169
|
+
static async renderCommandHelp(command: CliCommandShape): Promise<string> {
|
|
170
|
+
return [
|
|
171
|
+
...this.getUsageMessage(command),
|
|
172
|
+
...this.getDescriptionMessage(command),
|
|
173
|
+
...this.getOptionsMessage(command),
|
|
174
|
+
...await this.getExtendedHelpMessage(command),
|
|
175
|
+
...this.getExamplesMessage(command)
|
|
123
176
|
].map(line => line.trimEnd()).join('\n');
|
|
124
177
|
}
|
|
125
178
|
|
|
@@ -136,11 +189,13 @@ ${{ identifier: install }}
|
|
|
136
189
|
for (const { command: cmd, schema } of resolved) {
|
|
137
190
|
try {
|
|
138
191
|
if (schema && !schema.private) {
|
|
139
|
-
|
|
192
|
+
const description = CodecUtil.readFirstLine(schema.description, '');
|
|
193
|
+
rows.push(cliTpl` ${{ param: cmd.padEnd(maxWidth, ' ') }} ${{ title: description }}`);
|
|
140
194
|
}
|
|
141
195
|
} catch (error) {
|
|
142
196
|
if (error instanceof Error) {
|
|
143
|
-
|
|
197
|
+
const failure = CodecUtil.readFirstLine(error.message);
|
|
198
|
+
rows.push(cliTpl` ${{ param: cmd.padEnd(maxWidth, ' ') }} ${{ failure }}`);
|
|
144
199
|
} else {
|
|
145
200
|
throw error;
|
|
146
201
|
}
|
|
@@ -175,13 +230,13 @@ ${{ identifier: install }}
|
|
|
175
230
|
process.exitCode ??= 1;
|
|
176
231
|
if (error instanceof ValidationResultError) {
|
|
177
232
|
console.error!(this.renderValidationError(error));
|
|
233
|
+
} else if (error instanceof Error) {
|
|
234
|
+
console.error!(cliTpl`${{ failure: error.stack }}\n`);
|
|
178
235
|
}
|
|
179
236
|
if (command) {
|
|
180
237
|
console.error!(await this.renderCommandHelp(command));
|
|
181
238
|
} else if (error === UNKNOWN_COMMAND) {
|
|
182
239
|
console.error!(this.renderUnknownCommandMessage(cmd));
|
|
183
|
-
} else {
|
|
184
|
-
console.error!(error);
|
|
185
240
|
}
|
|
186
241
|
console.error!();
|
|
187
242
|
}
|
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
|
|
|
@@ -93,7 +84,7 @@ export function CliModuleFlag(config: CliFlagOptions & { scope?: 'current' | 'co
|
|
|
93
84
|
...CliParseUtil.buildAliases(config, Env.TRV_MODULE.key),
|
|
94
85
|
description: 'Module to run for',
|
|
95
86
|
specifiers: ['module'],
|
|
96
|
-
required: { active: Runtime.monoRoot },
|
|
87
|
+
required: { active: Runtime.monoRoot && config.scope !== 'command' },
|
|
97
88
|
});
|
|
98
89
|
|
|
99
90
|
SchemaRegistryIndex.getForRegister(cls).register({
|
|
@@ -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
|
|
152
|
-
default: Runtime.localDevelopment,
|
|
153
|
-
required: { active: false },
|
|
150
|
+
description: 'Should the invocation support debugging via IPC (e.g. from VSCode)'
|
|
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,8 +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';
|
|
5
|
+
import type { CliCommandConfig, CliCommandShape, PreMainHandler } from '../types.ts';
|
|
6
6
|
import { CliCommandRegistryAdapter } from './registry-adapter.ts';
|
|
7
7
|
|
|
8
8
|
const CLI_FILE_REGEX = /\/cli[.](?<name>.{0,100}?)([.]tsx?)?$/;
|
|
@@ -28,6 +28,10 @@ export class CliCommandRegistryIndex implements RegistryIndex {
|
|
|
28
28
|
return this.#instance.load(names);
|
|
29
29
|
}
|
|
30
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
|
+
|
|
31
35
|
#fileMapping: Map<string, string>;
|
|
32
36
|
#instanceMapping: Map<string, CliCommandShape> = new Map();
|
|
33
37
|
|
package/src/schema-export.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { castTo, type Class, describeFunction } from '@travetto/runtime';
|
|
2
2
|
import { type SchemaInputConfig, SchemaRegistryIndex } from '@travetto/schema';
|
|
3
3
|
|
|
4
|
-
import { CliCommandRegistryIndex } from '
|
|
4
|
+
import { CliCommandRegistryIndex } from './registry/registry-index.ts';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* CLI Command argument/flag shape
|
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
|
/**
|
|
@@ -25,7 +25,10 @@ async function nameValidator(names?: string[]): Promise<ValidationError | undefi
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* Exports machine-readable command metadata for automation and tooling.
|
|
29
|
+
*
|
|
30
|
+
* Used by editor integrations to discover runnable commands and inputs.
|
|
31
|
+
* Used by guidance workflows to validate command signatures.
|
|
29
32
|
*/
|
|
30
33
|
@CliCommand()
|
|
31
34
|
@IsPrivate()
|
package/support/cli.main.ts
CHANGED
|
@@ -11,7 +11,10 @@ async function validateMain(fileOrImport: string): Promise<ValidationError | und
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Execute a module `main()` entrypoint directly.
|
|
15
|
+
*
|
|
16
|
+
* This internal command resolves an import/source target, invokes its exported
|
|
17
|
+
* `main` function, and forwards unknown CLI args to that function.
|
|
15
18
|
*/
|
|
16
19
|
@CliCommand()
|
|
17
20
|
@IsPrivate()
|
package/support/cli.service.ts
CHANGED
|
@@ -16,7 +16,10 @@ async function validateService(_: ServiceAction, services: string[]): Promise<Va
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
19
|
+
* Manage development services (start/stop/restart/status) across the workspace.
|
|
20
|
+
*
|
|
21
|
+
* Services are discovered from registered descriptors and executed with streamed
|
|
22
|
+
* terminal feedback, including optional quiet mode.
|
|
20
23
|
*/
|
|
21
24
|
@CliCommand()
|
|
22
25
|
export class CliServiceCommand implements CliCommandShape {
|
|
@@ -45,15 +48,15 @@ export class CliServiceCommand implements CliCommandShape {
|
|
|
45
48
|
const jobs = all.map(async (descriptor, i) => {
|
|
46
49
|
const identifier = descriptor.name.padEnd(maxName);
|
|
47
50
|
const type = `${descriptor.version}`.padStart(maxVersion - 3).padEnd(maxVersion);
|
|
48
|
-
let
|
|
51
|
+
let message: string;
|
|
49
52
|
for await (const [valueType, value] of new ServiceRunner(descriptor).action(action)) {
|
|
50
53
|
const details = { [valueType === 'message' ? 'subtitle' : valueType]: value };
|
|
51
|
-
queue.add({ idx: i, text:
|
|
54
|
+
queue.add({ idx: i, text: message = cliTpl`${{ identifier }} ${{ type }} ${details}` });
|
|
52
55
|
if (valueType === 'failure') {
|
|
53
|
-
failureMessages.push(
|
|
56
|
+
failureMessages.push(message);
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
|
-
queue.add({ idx: i, done: true, text:
|
|
59
|
+
queue.add({ idx: i, done: true, text: message! });
|
|
57
60
|
});
|
|
58
61
|
|
|
59
62
|
Promise.all(jobs).then(() => Util.queueMacroTask()).then(() => queue.close());
|
package/support/entry.trv.ts
CHANGED