@travetto/cli 7.1.4 → 8.0.0-alpha.1
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 +66 -58
- package/package.json +3 -3
- package/src/error.ts +5 -4
- package/src/execute.ts +10 -8
- package/src/help.ts +3 -3
- package/src/module.ts +1 -1
- package/src/parse.ts +38 -3
- package/src/registry/decorator.ts +126 -128
- package/src/registry/registry-adapter.ts +14 -11
- package/src/registry/registry-index.ts +0 -3
- package/src/schema.ts +20 -24
- package/src/scm.ts +2 -2
- package/src/service.ts +2 -2
- package/src/types.ts +5 -68
- package/src/util.ts +15 -20
- package/support/bin/util.ts +16 -0
- package/support/cli.cli_schema.ts +21 -18
- package/support/cli.main.ts +15 -16
- package/support/cli.service.ts +16 -27
- package/support/entry.trv.ts +1 -0
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#L27) decorator
|
|
63
63
|
|
|
64
64
|
**Code: Basic Command**
|
|
65
65
|
```typescript
|
|
@@ -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#L27) 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#L27) 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
|
|
@@ -131,11 +131,11 @@ $ 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#L27) supports the following data types for flags:
|
|
135
135
|
* Boolean values
|
|
136
|
-
* Number values. The [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
137
|
-
* String values. [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
138
|
-
* Date values. The [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#
|
|
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
|
+
* 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
|
|
138
|
+
* Date values. The [@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.
|
|
139
139
|
* String lists. Same as String, but allowing multiple values.
|
|
140
140
|
* Numeric lists. Same as Number, but allowing multiple values.
|
|
141
141
|
|
|
@@ -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#L27) decorator. This means the target will be visible within the editor tooling.
|
|
394
394
|
|
|
395
395
|
**Code: Simple Run Target**
|
|
396
396
|
```typescript
|
|
@@ -412,47 +412,19 @@ export class RunCommand {
|
|
|
412
412
|
|
|
413
413
|
**Code: Anatomy of a Command**
|
|
414
414
|
```typescript
|
|
415
|
-
export interface CliCommandShape
|
|
416
|
-
/**
|
|
417
|
-
* Parsed state
|
|
418
|
-
*/
|
|
419
|
-
_parsed?: ParsedState;
|
|
420
|
-
/**
|
|
421
|
-
* Config
|
|
422
|
-
*/
|
|
423
|
-
_cfg?: CliCommandConfig;
|
|
415
|
+
export interface CliCommandShape {
|
|
424
416
|
/**
|
|
425
417
|
* Action target of the command
|
|
426
418
|
*/
|
|
427
|
-
main(...args:
|
|
419
|
+
main(...args: unknown[]): OrProm<undefined | void>;
|
|
428
420
|
/**
|
|
429
421
|
* Run before main runs
|
|
430
422
|
*/
|
|
431
|
-
|
|
423
|
+
finalize?(help?: boolean): OrProm<void>;
|
|
432
424
|
/**
|
|
433
425
|
* Extra help
|
|
434
426
|
*/
|
|
435
427
|
help?(): OrProm<string[]>;
|
|
436
|
-
/**
|
|
437
|
-
* Run before help is displayed
|
|
438
|
-
*/
|
|
439
|
-
preHelp?(): OrProm<void>;
|
|
440
|
-
/**
|
|
441
|
-
* Is the command active/eligible for usage
|
|
442
|
-
*/
|
|
443
|
-
isActive?(): boolean;
|
|
444
|
-
/**
|
|
445
|
-
* Run before binding occurs
|
|
446
|
-
*/
|
|
447
|
-
preBind?(): OrProm<void>;
|
|
448
|
-
/**
|
|
449
|
-
* Run before validation occurs
|
|
450
|
-
*/
|
|
451
|
-
preValidate?(): OrProm<void>;
|
|
452
|
-
/**
|
|
453
|
-
* Validation method
|
|
454
|
-
*/
|
|
455
|
-
validate?(...args: T): OrProm<CliValidationError | CliValidationError[] | undefined>;
|
|
456
428
|
}
|
|
457
429
|
```
|
|
458
430
|
|
|
@@ -463,7 +435,7 @@ If the goal is to run a more complex application, which may include depending on
|
|
|
463
435
|
```typescript
|
|
464
436
|
import { Runtime, toConcrete } from '@travetto/runtime';
|
|
465
437
|
import { DependencyRegistryIndex } from '@travetto/di';
|
|
466
|
-
import { CliCommand, type CliCommandShape } from '@travetto/cli';
|
|
438
|
+
import { CliCommand, CliDebugIpcFlag, CliModuleFlag, CliProfilesFlag, CliRestartOnChangeFlag, type CliCommandShape } from '@travetto/cli';
|
|
467
439
|
import { NetUtil } from '@travetto/web';
|
|
468
440
|
import { Registry } from '@travetto/registry';
|
|
469
441
|
|
|
@@ -472,7 +444,7 @@ import type { WebHttpServer } from '../src/types.ts';
|
|
|
472
444
|
/**
|
|
473
445
|
* Run a web server
|
|
474
446
|
*/
|
|
475
|
-
@CliCommand(
|
|
447
|
+
@CliCommand()
|
|
476
448
|
export class WebHttpCommand implements CliCommandShape {
|
|
477
449
|
|
|
478
450
|
/** Port to run on */
|
|
@@ -481,7 +453,19 @@ export class WebHttpCommand implements CliCommandShape {
|
|
|
481
453
|
/** Kill conflicting port owner */
|
|
482
454
|
killConflict?: boolean = Runtime.localDevelopment;
|
|
483
455
|
|
|
484
|
-
|
|
456
|
+
@CliModuleFlag({ short: 'm' })
|
|
457
|
+
module: string;
|
|
458
|
+
|
|
459
|
+
@CliProfilesFlag()
|
|
460
|
+
profile: string[];
|
|
461
|
+
|
|
462
|
+
@CliRestartOnChangeFlag()
|
|
463
|
+
restartOnChange: boolean = true;
|
|
464
|
+
|
|
465
|
+
@CliDebugIpcFlag()
|
|
466
|
+
debugIpc?: boolean;
|
|
467
|
+
|
|
468
|
+
finalize(): void {
|
|
485
469
|
if (this.port) {
|
|
486
470
|
process.env.WEB_HTTP_PORT = `${this.port}`;
|
|
487
471
|
}
|
|
@@ -512,32 +496,56 @@ As noted in the example above, `fields` is specified in this execution, with sup
|
|
|
512
496
|
The `module` field is slightly more complex, but is geared towards supporting commands within a monorepo context. This flag ensures that a module is specified if running from the root of the monorepo, and that the module provided is real, and can run the desired command. When running from an explicit module folder in the monorepo, the module flag is ignored.
|
|
513
497
|
|
|
514
498
|
### Custom Validation
|
|
515
|
-
In addition to dependency injection, the command contract also allows for a custom validation function, which will have access to bound command (flags, and args) as well as the unknown arguments. When a command implements this method, any [
|
|
499
|
+
In addition to dependency injection, the command contract also allows for a custom validation function, which will have access to bound command (flags, and args) as well as the unknown arguments. When a command implements this method, any [ValidationError](https://github.com/travetto/travetto/tree/main/module/schema/src/validate/types.ts#L10) errors that are returned will be shared with the user, and fail to invoke the `main` method.
|
|
516
500
|
|
|
517
|
-
**Code:
|
|
501
|
+
**Code: ValidationError**
|
|
518
502
|
```typescript
|
|
519
|
-
export interface
|
|
503
|
+
export interface ValidationError {
|
|
520
504
|
/**
|
|
521
505
|
* The error message
|
|
522
506
|
*/
|
|
523
507
|
message: string;
|
|
524
508
|
/**
|
|
525
|
-
*
|
|
509
|
+
* The object path of the error
|
|
526
510
|
*/
|
|
527
|
-
|
|
528
|
-
|
|
511
|
+
path: string;
|
|
512
|
+
/**
|
|
513
|
+
* The kind of validation
|
|
514
|
+
*/
|
|
515
|
+
kind: ValidationKind;
|
|
516
|
+
/**
|
|
517
|
+
* The value provided
|
|
518
|
+
*/
|
|
519
|
+
value?: unknown;
|
|
520
|
+
/**
|
|
521
|
+
* Regular expression to match
|
|
522
|
+
*/
|
|
523
|
+
regex?: string;
|
|
524
|
+
/**
|
|
525
|
+
* Number to compare against
|
|
526
|
+
*/
|
|
527
|
+
limit?: NumericLikeIntrinsic;
|
|
528
|
+
/**
|
|
529
|
+
* The type of the field
|
|
530
|
+
*/
|
|
531
|
+
type?: string;
|
|
532
|
+
/**
|
|
533
|
+
* Source of the error
|
|
534
|
+
*/
|
|
535
|
+
source?: string;
|
|
536
|
+
}
|
|
529
537
|
```
|
|
530
538
|
|
|
531
539
|
A simple example of the validation can be found in the `doc` command:
|
|
532
540
|
|
|
533
541
|
**Code: Simple Validation Example**
|
|
534
542
|
```typescript
|
|
535
|
-
async
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
543
|
+
@Validator(async (cmd) => {
|
|
544
|
+
const docFile = path.resolve(cmd.input);
|
|
545
|
+
if (!(await fs.stat(docFile).catch(() => false))) {
|
|
546
|
+
return { message: `input: ${cmd.input} does not exist`, path: 'input', source: 'flag', kind: 'invalid' };
|
|
540
547
|
}
|
|
548
|
+
})
|
|
541
549
|
```
|
|
542
550
|
|
|
543
551
|
## CLI - service
|
|
@@ -554,11 +562,11 @@ Options:
|
|
|
554
562
|
|
|
555
563
|
Available Services
|
|
556
564
|
--------------------
|
|
557
|
-
* dynamodb@3.
|
|
558
|
-
* elasticsearch@9.2.
|
|
565
|
+
* dynamodb@3.3.0
|
|
566
|
+
* elasticsearch@9.2.4
|
|
559
567
|
* firestore@latest
|
|
560
568
|
* mongodb@8.2
|
|
561
|
-
* mysql@9.
|
|
569
|
+
* mysql@9.6
|
|
562
570
|
* postgresql@18.1
|
|
563
571
|
* redis@8.4
|
|
564
572
|
* s3@4.11.0
|
|
@@ -572,11 +580,11 @@ $ trv service status
|
|
|
572
580
|
|
|
573
581
|
Service Version Status
|
|
574
582
|
-------------------------------------------------
|
|
575
|
-
dynamodb 3.
|
|
576
|
-
elasticsearch 9.2.
|
|
583
|
+
dynamodb 3.3.0 Running 93af422e793a
|
|
584
|
+
elasticsearch 9.2.4 Running ed76ee063d13
|
|
577
585
|
firestore latest Running feec2e5e95b4
|
|
578
586
|
mongodb 8.2 Running 5513eba6734e
|
|
579
|
-
mysql 9.
|
|
587
|
+
mysql 9.6 Running 307bc66d442a
|
|
580
588
|
postgresql 18.1 Running e78291e71040
|
|
581
589
|
redis 8.4 Running 77ba279b4e30
|
|
582
590
|
s3 4.11.0 Running fdacfc55b9e3
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0-alpha.1",
|
|
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": "^
|
|
33
|
-
"@travetto/terminal": "^
|
|
32
|
+
"@travetto/schema": "^8.0.0-alpha.1",
|
|
33
|
+
"@travetto/terminal": "^8.0.0-alpha.1"
|
|
34
34
|
},
|
|
35
35
|
"travetto": {
|
|
36
36
|
"displayName": "Command Line Interface",
|
package/src/error.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RuntimeError, Runtime } from '@travetto/runtime';
|
|
2
|
+
import type { ValidationError } from '@travetto/schema';
|
|
2
3
|
|
|
3
4
|
import { cliTpl } from './color.ts';
|
|
4
|
-
import type {
|
|
5
|
+
import type { CliCommandShape } from './types.ts';
|
|
5
6
|
|
|
6
7
|
const COMMAND_PACKAGE = [
|
|
7
8
|
[/^test(:watch)?$/, 'test', false],
|
|
@@ -48,10 +49,10 @@ ${{ identifier: install }}
|
|
|
48
49
|
/**
|
|
49
50
|
* Provides a basic error wrapper for cli validation issues
|
|
50
51
|
*/
|
|
51
|
-
export class CliValidationResultError extends
|
|
52
|
+
export class CliValidationResultError extends RuntimeError<{ errors: ValidationError[] }> {
|
|
52
53
|
command: CliCommandShape;
|
|
53
54
|
|
|
54
|
-
constructor(command: CliCommandShape, errors:
|
|
55
|
+
constructor(command: CliCommandShape, errors: ValidationError[]) {
|
|
55
56
|
super('', { details: { errors } });
|
|
56
57
|
this.command = command;
|
|
57
58
|
}
|
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';
|
|
@@ -16,7 +16,7 @@ export class ExecutionManager {
|
|
|
16
16
|
static async #onError(error: unknown): Promise<void> {
|
|
17
17
|
process.exitCode ??= 1;
|
|
18
18
|
if (error instanceof CliValidationResultError) {
|
|
19
|
-
console.error!(
|
|
19
|
+
console.error!(HelpUtil.renderValidationError(error));
|
|
20
20
|
console.error!(await HelpUtil.renderCommandHelp(error.command));
|
|
21
21
|
} else if (error instanceof CliUnknownCommandError) {
|
|
22
22
|
if (error.help) {
|
|
@@ -36,10 +36,10 @@ export class ExecutionManager {
|
|
|
36
36
|
const [{ instance: command, schema }] = await CliCommandRegistryIndex.load([cmd]);
|
|
37
37
|
const fullArgs = await CliParseUtil.expandArgs(schema, args);
|
|
38
38
|
|
|
39
|
-
const state =
|
|
39
|
+
const state = await CliParseUtil.parse(schema, fullArgs);
|
|
40
|
+
CliParseUtil.setState(command, state);
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
const boundArgs = await CliCommandSchemaUtil.bindInput(command, state);
|
|
42
|
+
const boundArgs = CliCommandSchemaUtil.bindInput(command, state);
|
|
43
43
|
return { command, boundArgs };
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -47,11 +47,13 @@ export class ExecutionManager {
|
|
|
47
47
|
static async #runCommand(cmd: string, args: string[]): Promise<void> {
|
|
48
48
|
const { command, boundArgs } = await this.#bindCommand(cmd, args);
|
|
49
49
|
|
|
50
|
-
await command.preValidate?.();
|
|
51
50
|
await CliCommandSchemaUtil.validate(command, boundArgs);
|
|
51
|
+
const config = CliCommandRegistryIndex.get(getClass(command));
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
for (const preMain of config.preMain ?? []) {
|
|
54
|
+
await preMain(command);
|
|
55
|
+
}
|
|
56
|
+
await command.finalize?.();
|
|
55
57
|
|
|
56
58
|
ConsoleManager.debug(Runtime.debug);
|
|
57
59
|
await command.main(...boundArgs);
|
package/src/help.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import util from 'node:util';
|
|
2
2
|
|
|
3
|
-
import { castKey, getClass } from '@travetto/runtime';
|
|
3
|
+
import { castKey, getClass, JSONUtil } from '@travetto/runtime';
|
|
4
4
|
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
5
5
|
|
|
6
6
|
import { cliTpl } from './color.ts';
|
|
@@ -31,7 +31,7 @@ export class HelpUtil {
|
|
|
31
31
|
const { name: commandName } = CliCommandRegistryIndex.get(getClass(command));
|
|
32
32
|
const args = schema.methods.main?.parameters ?? [];
|
|
33
33
|
|
|
34
|
-
await command.
|
|
34
|
+
await command.finalize?.(true);
|
|
35
35
|
|
|
36
36
|
// Ensure finalized
|
|
37
37
|
|
|
@@ -70,7 +70,7 @@ export class HelpUtil {
|
|
|
70
70
|
const desc = [cliTpl`${{ title: field.description }}`];
|
|
71
71
|
|
|
72
72
|
if (key !== 'help' && defaultValue !== undefined) {
|
|
73
|
-
desc.push(cliTpl`(default: ${{ input:
|
|
73
|
+
desc.push(cliTpl`(default: ${{ input: JSONUtil.toUTF8(defaultValue) }})`);
|
|
74
74
|
}
|
|
75
75
|
descriptions.push(desc.join(' '));
|
|
76
76
|
}
|
package/src/module.ts
CHANGED
|
@@ -28,7 +28,7 @@ export class CliModuleUtil {
|
|
|
28
28
|
for (const module of await CliScmUtil.findChangedModules(fromHash, toHash)) {
|
|
29
29
|
out.set(module.name, module);
|
|
30
30
|
if (transitive) {
|
|
31
|
-
for (const sub of
|
|
31
|
+
for (const sub of RuntimeIndex.getDependentModules(module, 'parents')) {
|
|
32
32
|
out.set(sub.name, sub);
|
|
33
33
|
}
|
|
34
34
|
}
|
package/src/parse.ts
CHANGED
|
@@ -10,7 +10,7 @@ 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 =
|
|
13
|
+
const HELP_FLAG = /^(-h|--help)$/;
|
|
14
14
|
const LONG_FLAG_WITH_EQ = /^--[a-z][^= ]+=\S+/i;
|
|
15
15
|
const CONFIG_PREFIX = '+=';
|
|
16
16
|
const SPACE = new Set([32, 7, 13, 10]);
|
|
@@ -20,6 +20,8 @@ export const isBoolFlag = (value?: SchemaInputConfig): boolean => value?.type ==
|
|
|
20
20
|
|
|
21
21
|
export type AliasesParseResult = Record<'long' | 'short' | 'raw' | 'env', string[]>;
|
|
22
22
|
|
|
23
|
+
const STATE_SYMBOL = Symbol();
|
|
24
|
+
|
|
23
25
|
/**
|
|
24
26
|
* Parsing support for the cli
|
|
25
27
|
*/
|
|
@@ -134,8 +136,11 @@ export class CliParseUtil {
|
|
|
134
136
|
static async expandArgs(schema: SchemaClassConfig, args: string[]): Promise<string[]> {
|
|
135
137
|
const separatorIndex = args.includes(RAW_SEPARATOR) ? args.indexOf(RAW_SEPARATOR) : args.length;
|
|
136
138
|
const module = this.getSpecifiedModule(schema, args);
|
|
137
|
-
return
|
|
138
|
-
|
|
139
|
+
return Promise
|
|
140
|
+
.all(args.map(async (arg, i) =>
|
|
141
|
+
await (arg.startsWith(CONFIG_PREFIX) && (i < separatorIndex || separatorIndex < 0) ? this.readFlagFile(arg, module) : arg))
|
|
142
|
+
)
|
|
143
|
+
.then(expanded => expanded.flat());
|
|
139
144
|
}
|
|
140
145
|
|
|
141
146
|
/**
|
|
@@ -235,4 +240,34 @@ export class CliParseUtil {
|
|
|
235
240
|
return result;
|
|
236
241
|
}, { long: [], short: [], raw: [], env: [] });
|
|
237
242
|
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Build aliases for a schema config
|
|
246
|
+
*/
|
|
247
|
+
static buildAliases(config: { full?: string, short?: string, envVars?: string[] }, ...extraEnvVars: string[]): Partial<SchemaFieldConfig> {
|
|
248
|
+
const envVars = [...config.envVars ?? [], ...extraEnvVars];
|
|
249
|
+
return {
|
|
250
|
+
aliases: [
|
|
251
|
+
...(config.full ? [config.full.startsWith('-') ? config.full : `--${config.full}`] : []),
|
|
252
|
+
...(config.short ? [config.short.startsWith('-') ? config.short : `-${config.short}`] : []),
|
|
253
|
+
...(envVars.length ? envVars.map(CliParseUtil.toEnvField) : [])
|
|
254
|
+
]
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get the state from an object
|
|
260
|
+
*/
|
|
261
|
+
static getState<T extends object>(item: T): ParsedState | undefined {
|
|
262
|
+
const local: T & { [STATE_SYMBOL]?: ParsedState } = item;
|
|
263
|
+
return local[STATE_SYMBOL];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Set the state
|
|
268
|
+
*/
|
|
269
|
+
static setState<T extends object>(item: T, state: ParsedState): void {
|
|
270
|
+
const local: T & { [STATE_SYMBOL]?: ParsedState } = item;
|
|
271
|
+
local[STATE_SYMBOL] = state;
|
|
272
|
+
}
|
|
238
273
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type Class, type ClassInstance, Env, Runtime, RuntimeIndex,
|
|
2
|
-
import {
|
|
1
|
+
import { type Class, type ClassInstance, Env, Runtime, RuntimeIndex, castTo, describeFunction, getClass } from '@travetto/runtime';
|
|
2
|
+
import { SchemaRegistryIndex, type ValidationError } from '@travetto/schema';
|
|
3
3
|
|
|
4
4
|
import type { CliCommandShape } from '../types.ts';
|
|
5
5
|
import { CliCommandRegistryIndex } from './registry-index.ts';
|
|
@@ -7,86 +7,15 @@ import { CliModuleUtil } from '../module.ts';
|
|
|
7
7
|
import { CliParseUtil } from '../parse.ts';
|
|
8
8
|
import { CliUtil } from '../util.ts';
|
|
9
9
|
|
|
10
|
-
type
|
|
11
|
-
|
|
12
|
-
type CliCommandConfigOptions = {
|
|
13
|
-
runTarget?: boolean;
|
|
14
|
-
runtimeModule?: 'current' | 'command';
|
|
15
|
-
with?: {
|
|
16
|
-
/** Application environment */
|
|
17
|
-
profiles?: boolean;
|
|
18
|
-
/** Module to run for */
|
|
19
|
-
module?: boolean;
|
|
20
|
-
/** Should debug invocation trigger via ipc */
|
|
21
|
-
debugIpc?: boolean | 'optional';
|
|
22
|
-
/** Should restart on source change */
|
|
23
|
-
restartOnChange?: boolean | 'optional';
|
|
24
|
-
};
|
|
25
|
-
};
|
|
10
|
+
type CliCommandConfigOptions = { runTarget?: boolean };
|
|
11
|
+
type CliFlagOptions = { full?: string, short?: string, envVars?: string[] };
|
|
26
12
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const FIELD_CONFIG: { [K in keyof WithConfig]: WithHandler<K> } = {
|
|
35
|
-
profiles: (config) => {
|
|
36
|
-
if (!config) { return; }
|
|
37
|
-
return {
|
|
38
|
-
name: 'profiles',
|
|
39
|
-
run: cmd => cmd.profiles && Env.TRV_PROFILES.set([...cmd.profiles, ...(Env.TRV_PROFILES.list ?? [])]),
|
|
40
|
-
field: {
|
|
41
|
-
type: String,
|
|
42
|
-
aliases: ['--profile', '--profiles', CliParseUtil.toEnvField(Env.TRV_PROFILES.key)],
|
|
43
|
-
description: 'Application profiles',
|
|
44
|
-
required: { active: false },
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
},
|
|
48
|
-
module: (config) => {
|
|
49
|
-
if (!config) { return; }
|
|
50
|
-
return {
|
|
51
|
-
name: 'module',
|
|
52
|
-
field: {
|
|
53
|
-
type: String,
|
|
54
|
-
aliases: ['-m', CliParseUtil.toEnvField(Env.TRV_MODULE.key)],
|
|
55
|
-
description: 'Module to run for',
|
|
56
|
-
specifiers: ['module'],
|
|
57
|
-
required: { active: Runtime.monoRoot },
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
},
|
|
61
|
-
debugIpc: (config) => {
|
|
62
|
-
if (!config) { return; }
|
|
63
|
-
return {
|
|
64
|
-
name: 'debugIpc',
|
|
65
|
-
run: cmd => CliUtil.runWithDebugIpc(cmd),
|
|
66
|
-
field: {
|
|
67
|
-
type: Boolean,
|
|
68
|
-
aliases: ['-di', CliParseUtil.toEnvField(Env.TRV_DEBUG_IPC.key)],
|
|
69
|
-
description: 'Should debug invocation trigger via ipc',
|
|
70
|
-
default: config !== 'optional',
|
|
71
|
-
required: { active: false },
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
},
|
|
75
|
-
restartOnChange: (config) => {
|
|
76
|
-
if (!config) { return; }
|
|
77
|
-
return {
|
|
78
|
-
name: 'restartOnChange',
|
|
79
|
-
run: cmd => CliUtil.runWithRestartOnChange(cmd),
|
|
80
|
-
field: {
|
|
81
|
-
type: Boolean,
|
|
82
|
-
aliases: ['-rc'],
|
|
83
|
-
description: 'Should the invocation automatically restart on source changes',
|
|
84
|
-
default: config !== 'optional' && Runtime.localDevelopment,
|
|
85
|
-
required: { active: false },
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
};
|
|
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
|
+
}
|
|
90
19
|
|
|
91
20
|
/**
|
|
92
21
|
* Decorator to register a CLI command
|
|
@@ -97,70 +26,139 @@ const FIELD_CONFIG: { [K in keyof WithConfig]: WithHandler<K> } = {
|
|
|
97
26
|
*/
|
|
98
27
|
export function CliCommand(config: CliCommandConfigOptions = {}) {
|
|
99
28
|
return function <T extends CliCommandShape>(target: Class<T>): void {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (!target.Ⲑid || description.abstract) {
|
|
104
|
-
return;
|
|
29
|
+
if (target.Ⲑid && !describeFunction(target)?.abstract) {
|
|
30
|
+
CliCommandRegistryIndex.getForRegister(target).register(config);
|
|
105
31
|
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
106
34
|
|
|
107
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Decorator to register a CLI command flag
|
|
37
|
+
* @augments `@travetto/schema:Input`
|
|
38
|
+
* @kind decorator
|
|
39
|
+
*/
|
|
40
|
+
export function CliFlag(config: CliFlagOptions) {
|
|
41
|
+
return function (instance: ClassInstance, property: string): void {
|
|
42
|
+
SchemaRegistryIndex.getForRegister(getClass(instance))
|
|
43
|
+
.registerField(property, CliParseUtil.buildAliases(config));
|
|
44
|
+
};
|
|
45
|
+
}
|
|
108
46
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Decorator to register a CLI command file flag
|
|
49
|
+
* @augments `@travetto/schema:Input`
|
|
50
|
+
* @kind decorator
|
|
51
|
+
*/
|
|
52
|
+
export function CliFileFlag(config: CliFlagOptions & { fileExtensions: string[] }) {
|
|
53
|
+
return function (instance: ClassInstance, property: string): void {
|
|
54
|
+
SchemaRegistryIndex.getForRegister(getClass(instance)).registerField(property, {
|
|
55
|
+
...CliParseUtil.buildAliases(config),
|
|
56
|
+
specifiers: ['file', ...config.fileExtensions.map(ext => `ext:${ext.replace(/[*.]/g, '')}`)]
|
|
116
57
|
});
|
|
58
|
+
};
|
|
59
|
+
}
|
|
117
60
|
|
|
118
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Registers a flag to support profiles via the `TRV_PROFILES` environment variable
|
|
63
|
+
* @augments `@travetto/schema:Input`
|
|
64
|
+
* @kind decorator
|
|
65
|
+
*/
|
|
66
|
+
export function CliProfilesFlag(config: CliFlagOptions = {}) {
|
|
67
|
+
return function <K extends string>(instance: Partial<Record<K, string[]>>, property: K): void {
|
|
68
|
+
const cls = getClass(instance);
|
|
69
|
+
SchemaRegistryIndex.getForRegister(cls).registerField(property, {
|
|
70
|
+
...CliParseUtil.buildAliases(config, Env.TRV_PROFILES.key),
|
|
71
|
+
required: { active: false },
|
|
72
|
+
description: 'Application profiles'
|
|
73
|
+
});
|
|
119
74
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
75
|
+
runBeforeMain(cls, (cmd: typeof instance) =>
|
|
76
|
+
Env.TRV_PROFILES.set([...cmd[property] ?? [], ...(Env.TRV_PROFILES.list ?? [])])
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
};
|
|
124
80
|
|
|
125
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Registers a flag to support targeting a specific module
|
|
83
|
+
* @augments `@travetto/schema:Input`
|
|
84
|
+
* @kind decorator
|
|
85
|
+
*/
|
|
86
|
+
export function CliModuleFlag(config: CliFlagOptions & { scope?: 'current' | 'command' } = { scope: 'current' }) {
|
|
87
|
+
return function <K extends string>(instance: Partial<Record<K, string>>, property: K): void {
|
|
88
|
+
const cls = getClass(instance);
|
|
89
|
+
const description = describeFunction(cls) ?? {};
|
|
90
|
+
const commandModule = description.module;
|
|
126
91
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
92
|
+
SchemaRegistryIndex.getForRegister(cls).registerField(property, {
|
|
93
|
+
...CliParseUtil.buildAliases(config, Env.TRV_MODULE.key),
|
|
94
|
+
description: 'Module to run for',
|
|
95
|
+
specifiers: ['module'],
|
|
96
|
+
required: { active: Runtime.monoRoot },
|
|
97
|
+
});
|
|
131
98
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
99
|
+
SchemaRegistryIndex.getForRegister(cls).register({
|
|
100
|
+
validators: [async (cmd: CliCommandShape): Promise<ValidationError | undefined> => {
|
|
101
|
+
const typed: (typeof cmd) & { [property]?: string } = castTo(cmd);
|
|
102
|
+
const providedModule = typed[property];
|
|
103
|
+
const runModule = (config.scope === 'command' ? commandModule : providedModule) || Runtime.main.name;
|
|
104
|
+
|
|
105
|
+
// If we need to run as a specific module
|
|
106
|
+
if (runModule !== Runtime.main.name) {
|
|
107
|
+
try {
|
|
108
|
+
RuntimeIndex.reinitForModule(runModule);
|
|
109
|
+
} catch {
|
|
110
|
+
return { source: 'flag', message: `${runModule} is an unknown module`, kind: 'custom', path: property };
|
|
139
111
|
}
|
|
112
|
+
}
|
|
140
113
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
114
|
+
if (!(await CliModuleUtil.moduleHasDependency(runModule, commandModule))) {
|
|
115
|
+
return { source: 'flag', message: `${runModule} does not have ${commandModule} as a dependency`, kind: 'custom', path: property };
|
|
116
|
+
}
|
|
117
|
+
}],
|
|
118
|
+
});
|
|
147
119
|
};
|
|
148
120
|
}
|
|
149
121
|
|
|
150
122
|
/**
|
|
151
|
-
*
|
|
123
|
+
* Registers a flag to support restarting on source changes
|
|
152
124
|
* @augments `@travetto/schema:Input`
|
|
153
125
|
* @kind decorator
|
|
154
126
|
*/
|
|
155
|
-
export function
|
|
156
|
-
return function (instance:
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
...
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
127
|
+
export function CliRestartOnChangeFlag(config: CliFlagOptions = {}) {
|
|
128
|
+
return function <K extends string, T extends Partial<Record<K, boolean>>>(instance: T, property: K): void {
|
|
129
|
+
const cls = getClass(instance);
|
|
130
|
+
SchemaRegistryIndex.getForRegister(cls).registerField(property, {
|
|
131
|
+
...CliParseUtil.buildAliases(config),
|
|
132
|
+
description: 'Should the invocation automatically restart on source changes',
|
|
133
|
+
default: Runtime.localDevelopment,
|
|
134
|
+
required: { active: false },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
runBeforeMain(cls, (cmd: typeof instance) => CliUtil.runWithRestartOnChange(cmd[property]), true);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Registers a flag to support debugging invocations triggered via IPC
|
|
143
|
+
* @augments `@travetto/schema:Input`
|
|
144
|
+
* @kind decorator
|
|
145
|
+
*/
|
|
146
|
+
export function CliDebugIpcFlag(config: CliFlagOptions = {}) {
|
|
147
|
+
return function <K extends string, T extends Partial<Record<K, boolean>>>(instance: T, property: K): void {
|
|
148
|
+
const cls = getClass(instance);
|
|
149
|
+
SchemaRegistryIndex.getForRegister(cls).registerField(property, {
|
|
150
|
+
...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 },
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
runBeforeMain(cls,
|
|
157
|
+
(cmd: typeof instance & CliCommandShape) => {
|
|
158
|
+
const cliConfig = CliCommandRegistryIndex.get(cls);
|
|
159
|
+
return cmd[property] && CliUtil.runWithDebugIpc(cliConfig.name);
|
|
160
|
+
},
|
|
161
|
+
true
|
|
162
|
+
);
|
|
165
163
|
};
|
|
166
164
|
}
|
|
@@ -11,6 +11,16 @@ const getName = (name: string): string => (name.match(CLI_FILE_REGEX)?.groups?.n
|
|
|
11
11
|
const stripDashes = (flag?: string): string | undefined => flag?.replace(/^-+/, '');
|
|
12
12
|
const toFlagName = (field: string): string => field.replace(/([a-z])([A-Z])/g, (_, left: string, right: string) => `${left}-${right.toLowerCase()}`);
|
|
13
13
|
|
|
14
|
+
function combineClasses(base: CliCommandConfig, ...configs: Partial<CliCommandConfig>[]): CliCommandConfig {
|
|
15
|
+
for (const config of configs) {
|
|
16
|
+
base.runTarget = config.runTarget ?? base.runTarget;
|
|
17
|
+
if (config.preMain) {
|
|
18
|
+
base.preMain = [...base.preMain ?? [], ...config.preMain ?? []];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return base;
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
export class CliCommandRegistryAdapter implements RegistryAdapter<CliCommandConfig> {
|
|
15
25
|
#cls: Class;
|
|
16
26
|
#config: CliCommandConfig;
|
|
@@ -75,22 +85,15 @@ export class CliCommandRegistryAdapter implements RegistryAdapter<CliCommandConf
|
|
|
75
85
|
* Registers a cli command
|
|
76
86
|
*/
|
|
77
87
|
register(...configs: Partial<CliCommandConfig>[]): CliCommandConfig {
|
|
78
|
-
const
|
|
79
|
-
this.#config ??= {
|
|
80
|
-
|
|
81
|
-
preMain: undefined,
|
|
82
|
-
name: getName(meta.import),
|
|
83
|
-
};
|
|
84
|
-
Object.assign(this.#config, ...configs);
|
|
85
|
-
return this.#config;
|
|
88
|
+
const metadata = describeFunction(this.#cls);
|
|
89
|
+
this.#config ??= { cls: this.#cls, name: getName(metadata.import), preMain: [], runTarget: true };
|
|
90
|
+
return combineClasses(this.#config, ...configs);
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
/**
|
|
89
94
|
* Get instance of the command
|
|
90
95
|
*/
|
|
91
96
|
getInstance(): CliCommandShape {
|
|
92
|
-
|
|
93
|
-
instance._cfg = this.#config;
|
|
94
|
-
return instance;
|
|
97
|
+
return classConstruct(this.#cls);
|
|
95
98
|
}
|
|
96
99
|
}
|
|
@@ -92,9 +92,6 @@ export class CliCommandRegistryIndex implements RegistryIndex {
|
|
|
92
92
|
continue;
|
|
93
93
|
}
|
|
94
94
|
const result = config.getInstance();
|
|
95
|
-
if (result.isActive !== undefined && !result.isActive()) {
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
95
|
this.#instanceMapping.set(name, result);
|
|
99
96
|
return result;
|
|
100
97
|
}
|
package/src/schema.ts
CHANGED
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
import { castKey, castTo, getClass } from '@travetto/runtime';
|
|
2
|
-
import { BindUtil, SchemaRegistryIndex, SchemaValidator, ValidationResultError } from '@travetto/schema';
|
|
2
|
+
import { BindUtil, SchemaRegistryIndex, SchemaValidator, ValidationResultError, type ValidationError } from '@travetto/schema';
|
|
3
3
|
|
|
4
|
-
import type { ParsedState, CliCommandShape
|
|
4
|
+
import type { ParsedState, CliCommandShape } from './types.ts';
|
|
5
5
|
import { CliValidationResultError } from './error.ts';
|
|
6
6
|
|
|
7
|
-
const getSource = (source: string | undefined,
|
|
7
|
+
const getSource = (source: string | undefined, defaultSource: ValidationError['source']): ValidationError['source'] => {
|
|
8
8
|
switch (source) {
|
|
9
9
|
case 'custom':
|
|
10
10
|
case 'arg':
|
|
11
11
|
case 'flag': return source;
|
|
12
|
-
case undefined: return
|
|
12
|
+
case undefined: return defaultSource;
|
|
13
13
|
default: return 'custom';
|
|
14
14
|
}
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
const transformErrors = (source: 'arg' | 'flag', error: unknown): ValidationError[] => {
|
|
18
|
+
if (error instanceof CliValidationResultError || error instanceof ValidationResultError) {
|
|
19
|
+
return error.details.errors.map(value => ({ source: getSource(value.source, source), ...value }));
|
|
20
|
+
} else {
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const transformArgErrors = (error: unknown): ValidationError[] => transformErrors('arg', error);
|
|
26
|
+
const transformFlagErrors = (error: unknown): ValidationError[] => transformErrors('flag', error);
|
|
27
|
+
|
|
17
28
|
/**
|
|
18
29
|
* Allows binding describing/binding inputs for commands
|
|
19
30
|
*/
|
|
@@ -59,27 +70,12 @@ export class CliCommandSchemaUtil {
|
|
|
59
70
|
const cls = getClass(cmd);
|
|
60
71
|
const paramNames = SchemaRegistryIndex.get(cls).getMethod('main').parameters.map(config => config.name!);
|
|
61
72
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const result = await cmd.validate?.(...args);
|
|
67
|
-
if (result) {
|
|
68
|
-
throw new CliValidationResultError(cmd, Array.isArray(result) ? result : [result]);
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
];
|
|
72
|
-
|
|
73
|
-
const SOURCES = ['flag', 'arg', 'custom'] as const;
|
|
74
|
-
|
|
75
|
-
const results = validators.map((validator, i) => validator().catch(error => {
|
|
76
|
-
if (!(error instanceof CliValidationResultError) && !(error instanceof ValidationResultError)) {
|
|
77
|
-
throw error;
|
|
78
|
-
}
|
|
79
|
-
return error.details.errors.map(value => ({ ...value, source: getSource(value.source, SOURCES[i]) }));
|
|
80
|
-
}));
|
|
73
|
+
const results = await Promise.all([
|
|
74
|
+
SchemaValidator.validate(cls, cmd).then(() => [], transformFlagErrors),
|
|
75
|
+
SchemaValidator.validateMethod(cls, 'main', args, paramNames).then(() => [], transformArgErrors),
|
|
76
|
+
]);
|
|
81
77
|
|
|
82
|
-
const errors =
|
|
78
|
+
const errors = results.flat();
|
|
83
79
|
if (errors.length) {
|
|
84
80
|
throw new CliValidationResultError(cmd, errors);
|
|
85
81
|
}
|
package/src/scm.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { RuntimeError, ExecUtil, Runtime, RuntimeIndex } from '@travetto/runtime';
|
|
6
6
|
import type { IndexedModule } from '@travetto/manifest';
|
|
7
7
|
|
|
8
8
|
export class CliScmUtil {
|
|
@@ -50,7 +50,7 @@ export class CliScmUtil {
|
|
|
50
50
|
const rootPath = Runtime.workspace.path;
|
|
51
51
|
const result = await ExecUtil.getResult(spawn('git', ['diff', '--name-only', `${fromHash}..${toHash}`, ':!**/DOC.*', ':!**/README.*'], { cwd: rootPath }), { catch: true });
|
|
52
52
|
if (!result.valid) {
|
|
53
|
-
throw new
|
|
53
|
+
throw new RuntimeError('Unable to detect changes between', { category: 'data', details: { fromHash, toHash, output: (result.stderr || result.stdout) } });
|
|
54
54
|
}
|
|
55
55
|
const out = new Set<string>();
|
|
56
56
|
for (const line of result.stdout.split(/\n/g)) {
|
package/src/service.ts
CHANGED
|
@@ -42,7 +42,7 @@ export class ServiceRunner {
|
|
|
42
42
|
async #isRunning(full = false): Promise<boolean> {
|
|
43
43
|
const port = ports(this.#descriptor.port!)[0];
|
|
44
44
|
const start = Date.now();
|
|
45
|
-
const timeoutMs = TimeUtil.
|
|
45
|
+
const timeoutMs = TimeUtil.duration(full ? this.#descriptor.startupTimeout ?? 5000 : 100, 'ms');
|
|
46
46
|
while ((Date.now() - start) < timeoutMs) {
|
|
47
47
|
try {
|
|
48
48
|
const sock = net.createConnection(port, 'localhost');
|
|
@@ -134,7 +134,7 @@ export class ServiceRunner {
|
|
|
134
134
|
if (operation === 'restart' || operation === 'start') {
|
|
135
135
|
if (!await this.#hasImage()) {
|
|
136
136
|
yield ['message', 'Starting image download'];
|
|
137
|
-
for await (const line of
|
|
137
|
+
for await (const line of this.#pullImage()) {
|
|
138
138
|
yield ['message', `Downloading: ${line}`];
|
|
139
139
|
}
|
|
140
140
|
yield ['message', 'Image download complete'];
|
package/src/types.ts
CHANGED
|
@@ -13,89 +13,26 @@ export type ParsedState = {
|
|
|
13
13
|
unknown: string[];
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
* Constrained version of Schema's Validation Error
|
|
18
|
-
* @concrete
|
|
19
|
-
*/
|
|
20
|
-
export interface CliValidationError {
|
|
21
|
-
/**
|
|
22
|
-
* The error message
|
|
23
|
-
*/
|
|
24
|
-
message: string;
|
|
25
|
-
/**
|
|
26
|
-
* Source of validation
|
|
27
|
-
*/
|
|
28
|
-
source?: 'flag' | 'arg' | 'custom';
|
|
29
|
-
};
|
|
30
|
-
|
|
31
16
|
/**
|
|
32
17
|
* CLI Command Contract
|
|
33
18
|
* @concrete
|
|
34
19
|
*/
|
|
35
|
-
export interface CliCommandShape
|
|
36
|
-
/**
|
|
37
|
-
* Parsed state
|
|
38
|
-
*/
|
|
39
|
-
_parsed?: ParsedState;
|
|
40
|
-
/**
|
|
41
|
-
* Config
|
|
42
|
-
*/
|
|
43
|
-
_cfg?: CliCommandConfig;
|
|
20
|
+
export interface CliCommandShape {
|
|
44
21
|
/**
|
|
45
22
|
* Action target of the command
|
|
46
23
|
*/
|
|
47
|
-
main(...args:
|
|
24
|
+
main(...args: unknown[]): OrProm<undefined | void>;
|
|
48
25
|
/**
|
|
49
26
|
* Run before main runs
|
|
50
27
|
*/
|
|
51
|
-
|
|
28
|
+
finalize?(help?: boolean): OrProm<void>;
|
|
52
29
|
/**
|
|
53
30
|
* Extra help
|
|
54
31
|
*/
|
|
55
32
|
help?(): OrProm<string[]>;
|
|
56
|
-
/**
|
|
57
|
-
* Run before help is displayed
|
|
58
|
-
*/
|
|
59
|
-
preHelp?(): OrProm<void>;
|
|
60
|
-
/**
|
|
61
|
-
* Is the command active/eligible for usage
|
|
62
|
-
*/
|
|
63
|
-
isActive?(): boolean;
|
|
64
|
-
/**
|
|
65
|
-
* Run before binding occurs
|
|
66
|
-
*/
|
|
67
|
-
preBind?(): OrProm<void>;
|
|
68
|
-
/**
|
|
69
|
-
* Run before validation occurs
|
|
70
|
-
*/
|
|
71
|
-
preValidate?(): OrProm<void>;
|
|
72
|
-
/**
|
|
73
|
-
* Validation method
|
|
74
|
-
*/
|
|
75
|
-
validate?(...args: T): OrProm<CliValidationError | CliValidationError[] | undefined>;
|
|
76
33
|
}
|
|
77
34
|
|
|
78
|
-
|
|
79
|
-
* Command shape common fields
|
|
80
|
-
*/
|
|
81
|
-
export type CliCommandShapeFields = {
|
|
82
|
-
/**
|
|
83
|
-
* Profiles to run the application under
|
|
84
|
-
*/
|
|
85
|
-
profiles?: string[];
|
|
86
|
-
/**
|
|
87
|
-
* Should the cli invocation trigger a debug session, via IPC
|
|
88
|
-
*/
|
|
89
|
-
debugIpc?: boolean;
|
|
90
|
-
/**
|
|
91
|
-
* Should the invocation run with auto-restart on source changes
|
|
92
|
-
*/
|
|
93
|
-
restartOnChange?: boolean;
|
|
94
|
-
/**
|
|
95
|
-
* The module to run the command from
|
|
96
|
-
*/
|
|
97
|
-
module?: string;
|
|
98
|
-
};
|
|
35
|
+
type PreMainHandler = (cmd: CliCommandShape) => (unknown | Promise<unknown>);
|
|
99
36
|
|
|
100
37
|
/**
|
|
101
38
|
* CLI Command schema shape
|
|
@@ -104,5 +41,5 @@ export interface CliCommandConfig {
|
|
|
104
41
|
cls: Class<CliCommandShape>;
|
|
105
42
|
name: string;
|
|
106
43
|
runTarget?: boolean;
|
|
107
|
-
preMain?:
|
|
44
|
+
preMain?: PreMainHandler[];
|
|
108
45
|
}
|
package/src/util.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from 'node:child_process';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import type { CliCommandShape, CliCommandShapeFields } from './types.ts';
|
|
3
|
+
import { RuntimeError, JSONUtil, Env, ExecUtil, Runtime, ShutdownManager, Util, WatchUtil } from '@travetto/runtime';
|
|
6
4
|
|
|
7
5
|
const IPC_ALLOWED_ENV = new Set(['NODE_OPTIONS']);
|
|
8
6
|
const IPC_INVALID_ENV = new Set(['PS1', 'INIT_CWD', 'COLOR', 'LANGUAGE', 'PROFILEHOME', '_']);
|
|
@@ -16,24 +14,19 @@ export class CliUtil {
|
|
|
16
14
|
*/
|
|
17
15
|
static getSimpleModuleName(placeholder: string, module?: string): string {
|
|
18
16
|
const simple = (module ?? Runtime.main.name).replace(/[\/]/, '_').replace(/@/, '');
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} else if (!module && Runtime.monoRoot) {
|
|
22
|
-
return placeholder;
|
|
23
|
-
} else {
|
|
24
|
-
return placeholder.replace('<module>', simple);
|
|
25
|
-
}
|
|
17
|
+
const targetModule = !simple || (!module && Runtime.monoRoot) ? '<module>' : simple;
|
|
18
|
+
return placeholder.replace('<module>', targetModule);
|
|
26
19
|
}
|
|
27
20
|
|
|
28
21
|
/**
|
|
29
22
|
* Run a command as restartable, linking into self
|
|
30
23
|
*/
|
|
31
|
-
static async runWithRestartOnChange
|
|
24
|
+
static async runWithRestartOnChange(restartOnChange?: boolean): Promise<void> {
|
|
32
25
|
if (Env.TRV_RESTART_TARGET.isTrue) {
|
|
33
26
|
Env.TRV_RESTART_TARGET.clear();
|
|
34
27
|
ShutdownManager.disableInterrupt();
|
|
35
28
|
return;
|
|
36
|
-
} else if (
|
|
29
|
+
} else if (restartOnChange !== true) {
|
|
37
30
|
return; // Not restarting, run normal
|
|
38
31
|
}
|
|
39
32
|
|
|
@@ -72,13 +65,14 @@ export class CliUtil {
|
|
|
72
65
|
/**
|
|
73
66
|
* Dispatch IPC payload
|
|
74
67
|
*/
|
|
75
|
-
static async runWithDebugIpc
|
|
76
|
-
if (
|
|
68
|
+
static async runWithDebugIpc(name: string): Promise<void> {
|
|
69
|
+
if (!Env.TRV_CLI_IPC.isSet) {
|
|
77
70
|
return; // Not debugging, run normal
|
|
78
71
|
}
|
|
79
72
|
|
|
80
|
-
const
|
|
73
|
+
const doFetch = fetch.bind(null, Env.TRV_CLI_IPC.value!);
|
|
81
74
|
|
|
75
|
+
const info = await doFetch().catch(() => ({ ok: false }));
|
|
82
76
|
if (!info.ok) {
|
|
83
77
|
return; // Server not running, run normal
|
|
84
78
|
}
|
|
@@ -87,19 +81,19 @@ export class CliUtil {
|
|
|
87
81
|
const request = {
|
|
88
82
|
type: '@travetto/cli:run',
|
|
89
83
|
data: {
|
|
90
|
-
name
|
|
84
|
+
name,
|
|
91
85
|
env,
|
|
92
|
-
|
|
86
|
+
cwd: process.cwd(),
|
|
93
87
|
args: process.argv.slice(3),
|
|
94
88
|
}
|
|
95
89
|
};
|
|
96
90
|
console.log('Triggering IPC request', request);
|
|
97
91
|
|
|
98
92
|
Object.entries(process.env).forEach(([key, value]) => validEnv(key) && (env[key] = value!));
|
|
99
|
-
const sent = await
|
|
93
|
+
const sent = await doFetch({ method: 'POST', body: JSONUtil.toUTF8(request) });
|
|
100
94
|
|
|
101
95
|
if (!sent.ok) {
|
|
102
|
-
throw new
|
|
96
|
+
throw new RuntimeError(`IPC Request failed: ${sent.status} ${await sent.text()}`);
|
|
103
97
|
}
|
|
104
98
|
}
|
|
105
99
|
|
|
@@ -107,7 +101,8 @@ export class CliUtil {
|
|
|
107
101
|
* Write data to channel and ensure its flushed before continuing
|
|
108
102
|
*/
|
|
109
103
|
static async writeAndEnsureComplete(data: unknown, channel: 'stdout' | 'stderr' = 'stdout'): Promise<void> {
|
|
110
|
-
return await new Promise(resolve => process[channel].write(typeof data === 'string' ? data :
|
|
104
|
+
return await new Promise(resolve => process[channel].write(typeof data === 'string' ? data :
|
|
105
|
+
JSONUtil.toUTF8Pretty(data), () => resolve()));
|
|
111
106
|
}
|
|
112
107
|
|
|
113
108
|
/**
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
}
|
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import { Env } from '@travetto/runtime';
|
|
2
|
-
import { IsPrivate } from '@travetto/schema';
|
|
2
|
+
import { IsPrivate, MethodValidator, type ValidationError } from '@travetto/schema';
|
|
3
3
|
|
|
4
4
|
import { CliCommand } from '../src/registry/decorator.ts';
|
|
5
|
-
import type { CliCommandShape
|
|
5
|
+
import type { CliCommandShape } from '../src/types.ts';
|
|
6
6
|
import { CliCommandRegistryIndex } from '../src/registry/registry-index.ts';
|
|
7
7
|
import { CliUtil } from '../src/util.ts';
|
|
8
8
|
import { CliSchemaExportUtil } from '../src/schema-export.ts';
|
|
9
9
|
|
|
10
|
+
async function nameValidator(names?: string[]): Promise<ValidationError | undefined> {
|
|
11
|
+
if (!names || names.length === 0) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const resolved = await CliCommandRegistryIndex.load(names);
|
|
15
|
+
const invalid = names.find(name => !resolved.find(result => result.command === name));
|
|
16
|
+
|
|
17
|
+
if (invalid) {
|
|
18
|
+
return {
|
|
19
|
+
source: 'arg',
|
|
20
|
+
kind: 'invalid',
|
|
21
|
+
path: 'names',
|
|
22
|
+
message: `name: ${invalid} is not a valid cli command`
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
10
27
|
/**
|
|
11
28
|
* Generates the schema for all CLI operations
|
|
12
29
|
*/
|
|
@@ -14,25 +31,11 @@ import { CliSchemaExportUtil } from '../src/schema-export.ts';
|
|
|
14
31
|
@IsPrivate()
|
|
15
32
|
export class CliSchemaCommand implements CliCommandShape {
|
|
16
33
|
|
|
17
|
-
|
|
18
|
-
if (!names || names.length === 0) {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
const resolved = await CliCommandRegistryIndex.load(names);
|
|
22
|
-
const invalid = names.find(name => !resolved.find(result => result.command === name));
|
|
23
|
-
|
|
24
|
-
if (invalid) {
|
|
25
|
-
return {
|
|
26
|
-
source: 'arg',
|
|
27
|
-
message: `name: ${invalid} is not a valid cli command`
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
preMain(): void {
|
|
34
|
+
finalize(): void {
|
|
33
35
|
Env.DEBUG.set(false);
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
@MethodValidator(nameValidator)
|
|
36
39
|
async main(names?: string[]): Promise<void> {
|
|
37
40
|
const resolved = await CliCommandRegistryIndex.load(names);
|
|
38
41
|
|
package/support/cli.main.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import { Runtime } from '@travetto/runtime';
|
|
2
|
-
import { type CliCommandShape, CliCommand,
|
|
3
|
-
import {
|
|
1
|
+
import { JSONUtil, Runtime } from '@travetto/runtime';
|
|
2
|
+
import { type CliCommandShape, CliCommand, CliParseUtil } from '@travetto/cli';
|
|
3
|
+
import { IsPrivate, MethodValidator, type ValidationError } from '@travetto/schema';
|
|
4
|
+
|
|
5
|
+
async function validateMain(fileOrImport: string): Promise<ValidationError | undefined> {
|
|
6
|
+
try {
|
|
7
|
+
await Runtime.importFrom(fileOrImport);
|
|
8
|
+
} catch {
|
|
9
|
+
return { message: `Unknown file: ${fileOrImport}`, source: 'arg', kind: 'invalid', path: 'fileOrImport' };
|
|
10
|
+
}
|
|
11
|
+
};
|
|
4
12
|
|
|
5
13
|
/**
|
|
6
14
|
* Allows for running of main entry points
|
|
@@ -9,22 +17,13 @@ import { Ignore, IsPrivate } from '@travetto/schema';
|
|
|
9
17
|
@IsPrivate()
|
|
10
18
|
export class MainCommand implements CliCommandShape {
|
|
11
19
|
|
|
12
|
-
@
|
|
13
|
-
_parsed: ParsedState;
|
|
14
|
-
|
|
15
|
-
async validate(fileOrImport: string): Promise<CliValidationError | undefined> {
|
|
16
|
-
try {
|
|
17
|
-
await Runtime.importFrom(fileOrImport);
|
|
18
|
-
} catch {
|
|
19
|
-
return { message: `Unknown file: ${fileOrImport}` };
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
20
|
+
@MethodValidator(validateMain)
|
|
23
21
|
async main(fileOrImport: string, args: string[] = []): Promise<void> {
|
|
22
|
+
const parsed = CliParseUtil.getState(this);
|
|
24
23
|
let result: unknown;
|
|
25
24
|
try {
|
|
26
25
|
const module = await Runtime.importFrom<{ main(..._: unknown[]): Promise<unknown> }>(fileOrImport);
|
|
27
|
-
result = await module.main(...args, ...
|
|
26
|
+
result = await module.main(...args, ...parsed?.unknown ?? []);
|
|
28
27
|
} catch (error) {
|
|
29
28
|
result = error;
|
|
30
29
|
process.exitCode = Math.max(process.exitCode ? +process.exitCode : 1, 1);
|
|
@@ -32,7 +31,7 @@ export class MainCommand implements CliCommandShape {
|
|
|
32
31
|
|
|
33
32
|
if (result !== undefined) {
|
|
34
33
|
if (process.connected) { process.send?.(result); }
|
|
35
|
-
const payload = typeof result === 'string' ? result : (result instanceof Error ? result.stack :
|
|
34
|
+
const payload = typeof result === 'string' ? result : (result instanceof Error ? result.stack : JSONUtil.toUTF8(result));
|
|
36
35
|
process[process.exitCode ? 'stderr' : 'stdout'].write(`${payload}\n`);
|
|
37
36
|
}
|
|
38
37
|
}
|
package/support/cli.service.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import { type CliCommandShape, CliCommand, cliTpl
|
|
1
|
+
import { type CliCommandShape, CliCommand, cliTpl } from '@travetto/cli';
|
|
2
2
|
import { Terminal } from '@travetto/terminal';
|
|
3
|
-
import { AsyncQueue,
|
|
3
|
+
import { AsyncQueue, Util } from '@travetto/runtime';
|
|
4
|
+
import { MethodValidator, type ValidationError } from '@travetto/schema';
|
|
4
5
|
|
|
5
|
-
import { ServiceRunner, type
|
|
6
|
+
import { ServiceRunner, type ServiceAction } from '../src/service.ts';
|
|
7
|
+
import { getServices } from './bin/util.ts';
|
|
8
|
+
|
|
9
|
+
async function validateService(action: ServiceAction, services: string[]): Promise<ValidationError | undefined> {
|
|
10
|
+
const all = await getServices(services);
|
|
11
|
+
|
|
12
|
+
if (!all.length) {
|
|
13
|
+
return { message: 'No services found', source: 'arg', kind: 'invalid', path: 'services' };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
6
16
|
|
|
7
17
|
/**
|
|
8
18
|
* Allows for running services
|
|
@@ -10,30 +20,8 @@ import { ServiceRunner, type ServiceDescriptor, type ServiceAction } from '../sr
|
|
|
10
20
|
@CliCommand()
|
|
11
21
|
export class CliServiceCommand implements CliCommandShape {
|
|
12
22
|
|
|
13
|
-
async #getServices(services: string[]): Promise<ServiceDescriptor[]> {
|
|
14
|
-
return (await Promise.all(
|
|
15
|
-
RuntimeIndex.find({
|
|
16
|
-
module: module => module.roles.includes('std'),
|
|
17
|
-
folder: folder => folder === 'support',
|
|
18
|
-
file: file => /support\/service[.]/.test(file.sourceFile)
|
|
19
|
-
})
|
|
20
|
-
.map(file => Runtime.importFrom<{ service: ServiceDescriptor }>(file.import).then(value => value.service))
|
|
21
|
-
))
|
|
22
|
-
.filter(file => !!file)
|
|
23
|
-
.filter(file => services?.length ? services.includes(file.name) : true)
|
|
24
|
-
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async validate(action: ServiceAction, services: string[]): Promise<CliValidationError | undefined> {
|
|
28
|
-
const all = await this.#getServices(services);
|
|
29
|
-
|
|
30
|
-
if (!all.length) {
|
|
31
|
-
return { message: 'No services found' };
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
23
|
async help(): Promise<string[]> {
|
|
36
|
-
const all = await
|
|
24
|
+
const all = await getServices([]);
|
|
37
25
|
return [
|
|
38
26
|
cliTpl`${{ title: 'Available Services' }}`,
|
|
39
27
|
'-'.repeat(20),
|
|
@@ -41,8 +29,9 @@ export class CliServiceCommand implements CliCommandShape {
|
|
|
41
29
|
];
|
|
42
30
|
}
|
|
43
31
|
|
|
32
|
+
@MethodValidator(validateService)
|
|
44
33
|
async main(action: ServiceAction, services: string[] = []): Promise<void> {
|
|
45
|
-
const all = await
|
|
34
|
+
const all = await getServices(services);
|
|
46
35
|
const maxName = Math.max(...all.map(service => service.name.length), 'Service'.length) + 3;
|
|
47
36
|
const maxVersion = Math.max(...all.map(service => `${service.version}`.length), 'Version'.length) + 3;
|
|
48
37
|
const maxStatus = 20;
|
package/support/entry.trv.ts
CHANGED