@travetto/cli 6.0.1 → 7.0.0-rc.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 +31 -33
- package/__index__.ts +4 -2
- package/package.json +3 -3
- package/src/error.ts +1 -1
- package/src/execute.ts +3 -3
- package/src/help.ts +53 -48
- package/src/module.ts +20 -0
- package/src/parse.ts +64 -43
- package/src/registry/decorator.ts +153 -0
- package/src/registry/registry-adapter.ts +96 -0
- package/src/registry/registry-index.ts +124 -0
- package/src/schema-export.ts +90 -0
- package/src/schema.ts +16 -109
- package/src/types.ts +4 -36
- package/src/util.ts +5 -3
- package/support/cli.cli_schema.ts +25 -22
- package/support/cli.main.ts +3 -2
- package/src/decorators.ts +0 -154
- package/src/registry.ts +0 -96
package/README.md
CHANGED
|
@@ -28,8 +28,8 @@ Commands:
|
|
|
28
28
|
email:compile CLI Entry point for running the email server
|
|
29
29
|
email:editor The email editor compilation service and output serving
|
|
30
30
|
email:test CLI Entry point for running the email server
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
eslint Command line support for eslint
|
|
32
|
+
eslint:register Writes the eslint configuration file
|
|
33
33
|
model:export Exports model schemas
|
|
34
34
|
model:install Installing models
|
|
35
35
|
openapi:client CLI for generating the cli client
|
|
@@ -50,7 +50,6 @@ Commands:
|
|
|
50
50
|
test:watch Invoke the test watcher
|
|
51
51
|
web:http Run a web server
|
|
52
52
|
web:rpc-client Generate the web-rpc client
|
|
53
|
-
web:server Run a web server
|
|
54
53
|
```
|
|
55
54
|
|
|
56
55
|
This listing is from the [Travetto](https://travetto.dev) monorepo, and represents the majority of tools that can be invoked from the command line.
|
|
@@ -60,7 +59,7 @@ This module also has a tight integration with the [VSCode plugin](https://market
|
|
|
60
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:
|
|
61
60
|
* The file must be located in the `support/` folder, and have a name that matches `cli.*.ts`
|
|
62
61
|
* The file must be a class that has a main method
|
|
63
|
-
* The class must use the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/
|
|
62
|
+
* The class must use the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L85) decorator
|
|
64
63
|
|
|
65
64
|
**Code: Basic Command**
|
|
66
65
|
```typescript
|
|
@@ -95,7 +94,7 @@ Examples of mappings:
|
|
|
95
94
|
The pattern is that underscores(_) translate to colons (:), and the `cli.` prefix, and `.ts` suffix are dropped.
|
|
96
95
|
|
|
97
96
|
## Binding Flags
|
|
98
|
-
[@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/
|
|
97
|
+
[@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L85) 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#L85) 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.
|
|
99
98
|
|
|
100
99
|
**Code: Basic Command with Flag**
|
|
101
100
|
```typescript
|
|
@@ -132,11 +131,11 @@ $ trv basic:flag --loud
|
|
|
132
131
|
HELLO
|
|
133
132
|
```
|
|
134
133
|
|
|
135
|
-
The [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/
|
|
134
|
+
The [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L85) supports the following data types for flags:
|
|
136
135
|
* Boolean values
|
|
137
|
-
* Number values. The [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/
|
|
138
|
-
* String values. [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/
|
|
139
|
-
* Date values. The [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/
|
|
136
|
+
* Number values. The [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L164), [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L171), [@Precision](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L157), [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L91) and [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L102) decorators help provide additional validation.
|
|
137
|
+
* String values. [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L91), [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L102), [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L82) and [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L56) provide additional constraints
|
|
138
|
+
* Date values. The [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L91) and [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L102) decorators help provide additional validation.
|
|
140
139
|
* String lists. Same as String, but allowing multiple values.
|
|
141
140
|
* Numeric lists. Same as Number, but allowing multiple values.
|
|
142
141
|
|
|
@@ -252,7 +251,7 @@ $ trv basic:arglist -r 10 5 3 9 8 1
|
|
|
252
251
|
```
|
|
253
252
|
|
|
254
253
|
## Customization
|
|
255
|
-
By default, all fields are treated as flags and all parameters of `main()` are treated as arguments within the validation process. Like the standard [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#
|
|
254
|
+
By default, all fields are treated as flags and all parameters of `main()` are treated as arguments within the validation process. Like the standard [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#L19) behavior, we can leverage the metadata of the fields/parameters to help provide additional customization/context for the users of the commands.
|
|
256
255
|
|
|
257
256
|
**Code: Custom Command with Metadata**
|
|
258
257
|
```typescript
|
|
@@ -391,7 +390,7 @@ npx trv call:db --host localhost --port 3306 --username app --password <custom>
|
|
|
391
390
|
```
|
|
392
391
|
|
|
393
392
|
## VSCode Integration
|
|
394
|
-
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/
|
|
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#L85) decorator. This means the target will be visible within the editor tooling.
|
|
395
394
|
|
|
396
395
|
**Code: Simple Run Target**
|
|
397
396
|
```typescript
|
|
@@ -414,7 +413,6 @@ export class RunCommand {
|
|
|
414
413
|
**Code: Anatomy of a Command**
|
|
415
414
|
```typescript
|
|
416
415
|
export interface CliCommandShape<T extends unknown[] = unknown[]> {
|
|
417
|
-
|
|
418
416
|
/**
|
|
419
417
|
* Parsed state
|
|
420
418
|
*/
|
|
@@ -464,10 +462,10 @@ If the goal is to run a more complex application, which may include depending on
|
|
|
464
462
|
**Code: Simple Run Target**
|
|
465
463
|
```typescript
|
|
466
464
|
import { Runtime, toConcrete, Util } from '@travetto/runtime';
|
|
467
|
-
import {
|
|
465
|
+
import { DependencyRegistryIndex } from '@travetto/di';
|
|
468
466
|
import { CliCommand, CliCommandShape } from '@travetto/cli';
|
|
469
467
|
import { NetUtil } from '@travetto/web';
|
|
470
|
-
import {
|
|
468
|
+
import { Registry } from '@travetto/registry';
|
|
471
469
|
|
|
472
470
|
import type { WebHttpServer } from '../src/types.ts';
|
|
473
471
|
|
|
@@ -490,8 +488,8 @@ export class WebHttpCommand implements CliCommandShape {
|
|
|
490
488
|
}
|
|
491
489
|
|
|
492
490
|
async main(): Promise<void> {
|
|
493
|
-
await
|
|
494
|
-
const instance = await
|
|
491
|
+
await Registry.init();
|
|
492
|
+
const instance = await DependencyRegistryIndex.getInstance(toConcrete<WebHttpServer>());
|
|
495
493
|
|
|
496
494
|
const handle = await Util.acquireWithRetry(
|
|
497
495
|
() => instance.serve(),
|
|
@@ -499,7 +497,7 @@ export class WebHttpCommand implements CliCommandShape {
|
|
|
499
497
|
this.killConflict && !Runtime.production ? 5 : 1
|
|
500
498
|
);
|
|
501
499
|
|
|
502
|
-
|
|
500
|
+
return handle.complete;
|
|
503
501
|
}
|
|
504
502
|
}
|
|
505
503
|
```
|
|
@@ -509,7 +507,7 @@ As noted in the example above, `fields` is specified in this execution, with sup
|
|
|
509
507
|
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.
|
|
510
508
|
|
|
511
509
|
### Custom Validation
|
|
512
|
-
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 [CliValidationError](https://github.com/travetto/travetto/tree/main/module/cli/src/types.ts#
|
|
510
|
+
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 [CliValidationError](https://github.com/travetto/travetto/tree/main/module/cli/src/types.ts#L20) errors that are returned will be shared with the user, and fail to invoke the `main` method.
|
|
513
511
|
|
|
514
512
|
**Code: CliValidationError**
|
|
515
513
|
```typescript
|
|
@@ -551,14 +549,14 @@ Options:
|
|
|
551
549
|
|
|
552
550
|
Available Services
|
|
553
551
|
--------------------
|
|
554
|
-
* dynamodb@
|
|
555
|
-
* elasticsearch@
|
|
552
|
+
* dynamodb@3.1.0
|
|
553
|
+
* elasticsearch@9.2.1
|
|
556
554
|
* firestore@latest
|
|
557
|
-
* mongodb@8.
|
|
558
|
-
* mysql@9.
|
|
559
|
-
* postgresql@
|
|
560
|
-
* redis@8.
|
|
561
|
-
* s3@4.
|
|
555
|
+
* mongodb@8.2
|
|
556
|
+
* mysql@9.5
|
|
557
|
+
* postgresql@18.1
|
|
558
|
+
* redis@8.4
|
|
559
|
+
* s3@4.10.0
|
|
562
560
|
```
|
|
563
561
|
|
|
564
562
|
A sample of all services available to the entire framework:
|
|
@@ -569,14 +567,14 @@ $ trv service status
|
|
|
569
567
|
|
|
570
568
|
Service Version Status
|
|
571
569
|
-------------------------------------------------
|
|
572
|
-
dynamodb
|
|
573
|
-
elasticsearch
|
|
570
|
+
dynamodb 3.1.0 Running 93af422e793a
|
|
571
|
+
elasticsearch 9.2.1 Running ed76ee063d13
|
|
574
572
|
firestore latest Running feec2e5e95b4
|
|
575
|
-
mongodb 8.
|
|
576
|
-
mysql 9.
|
|
577
|
-
postgresql
|
|
578
|
-
redis 8.
|
|
579
|
-
s3
|
|
573
|
+
mongodb 8.2 Running 5513eba6734e
|
|
574
|
+
mysql 9.5 Running 307bc66d442a
|
|
575
|
+
postgresql 18.1 Running e78291e71040
|
|
576
|
+
redis 8.4 Running 77ba279b4e30
|
|
577
|
+
s3 4.10.0 Running fdacfc55b9e3
|
|
580
578
|
```
|
|
581
579
|
|
|
582
580
|
### Defining new Services
|
|
@@ -586,7 +584,7 @@ The services are defined as plain typescript files within the framework and can
|
|
|
586
584
|
```typescript
|
|
587
585
|
import type { ServiceDescriptor } from '@travetto/cli';
|
|
588
586
|
|
|
589
|
-
const version = process.env.MONGO_VERSION || '8.
|
|
587
|
+
const version = process.env.MONGO_VERSION || '8.2';
|
|
590
588
|
|
|
591
589
|
export const service: ServiceDescriptor = {
|
|
592
590
|
name: 'mongodb',
|
package/__index__.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { } from './src/trv.d.ts';
|
|
2
2
|
export * from './src/types.ts';
|
|
3
|
-
export * from './src/decorators.ts';
|
|
4
3
|
export * from './src/execute.ts';
|
|
5
4
|
export * from './src/error.ts';
|
|
6
5
|
export * from './src/schema.ts';
|
|
7
|
-
export * from './src/
|
|
6
|
+
export * from './src/schema-export.ts';
|
|
7
|
+
export * from './src/registry/decorator.ts';
|
|
8
|
+
export * from './src/registry/registry-index.ts';
|
|
9
|
+
export * from './src/registry/registry-adapter.ts';
|
|
8
10
|
export * from './src/help.ts';
|
|
9
11
|
export * from './src/color.ts';
|
|
10
12
|
export * from './src/module.ts';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0-rc.1",
|
|
4
4
|
"description": "CLI infrastructure for Travetto framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"directory": "module/cli"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@travetto/schema": "^
|
|
32
|
-
"@travetto/terminal": "^
|
|
31
|
+
"@travetto/schema": "^7.0.0-rc.1",
|
|
32
|
+
"@travetto/terminal": "^7.0.0-rc.1"
|
|
33
33
|
},
|
|
34
34
|
"travetto": {
|
|
35
35
|
"displayName": "Command Line Interface",
|
package/src/error.ts
CHANGED
|
@@ -11,7 +11,7 @@ const COMMAND_PACKAGE = [
|
|
|
11
11
|
[/^openapi:(spec|client)$/, 'openapi', true],
|
|
12
12
|
[/^email:(compile|editor)$/, 'email-compiler', false],
|
|
13
13
|
[/^pack(:zip|:docker)?$/, 'pack', false],
|
|
14
|
-
[/^web:
|
|
14
|
+
[/^web:http$/, 'web-http', true],
|
|
15
15
|
[/^web:rpc-client$/, 'web-rpc', true],
|
|
16
16
|
] as const;
|
|
17
17
|
|
package/src/execute.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ConsoleManager, Runtime, ShutdownManager } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import { HelpUtil } from './help.ts';
|
|
4
|
-
import {
|
|
4
|
+
import { CliCommandRegistryIndex } from './registry/registry-index.ts';
|
|
5
5
|
import { CliCommandSchemaUtil } from './schema.ts';
|
|
6
6
|
import { CliUnknownCommandError, CliValidationResultError } from './error.ts';
|
|
7
7
|
import { CliParseUtil } from './parse.ts';
|
|
@@ -33,9 +33,9 @@ export class ExecutionManager {
|
|
|
33
33
|
|
|
34
34
|
/** Bind command */
|
|
35
35
|
static async #bindCommand(cmd: string, args: string[]): Promise<{ command: CliCommandShape, boundArgs: unknown[] }> {
|
|
36
|
-
const command = await
|
|
37
|
-
const schema = await CliCommandSchemaUtil.getSchema(command);
|
|
36
|
+
const [{ instance: command, schema }] = await CliCommandRegistryIndex.load([cmd]);
|
|
38
37
|
const fullArgs = await CliParseUtil.expandArgs(schema, args);
|
|
38
|
+
|
|
39
39
|
const state = command._parsed = await CliParseUtil.parse(schema, fullArgs);
|
|
40
40
|
|
|
41
41
|
await command.preBind?.();
|
package/src/help.ts
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import util from 'node:util';
|
|
2
2
|
|
|
3
|
-
import { castKey,
|
|
3
|
+
import { castKey, getClass } from '@travetto/runtime';
|
|
4
|
+
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
4
5
|
|
|
5
6
|
import { cliTpl } from './color.ts';
|
|
6
7
|
import { CliCommandShape } from './types.ts';
|
|
7
|
-
import {
|
|
8
|
-
import { CliCommandSchemaUtil } from './schema.ts';
|
|
8
|
+
import { CliCommandRegistryIndex } from './registry/registry-index.ts';
|
|
9
9
|
import { CliValidationResultError } from './error.ts';
|
|
10
|
-
import {
|
|
10
|
+
import { CliSchemaExportUtil } from './schema-export.ts';
|
|
11
11
|
|
|
12
|
-
const validationSourceMap = {
|
|
13
|
-
custom: '',
|
|
12
|
+
const validationSourceMap: Record<string, string> = {
|
|
14
13
|
arg: 'Argument',
|
|
15
14
|
flag: 'Flag'
|
|
16
15
|
};
|
|
17
16
|
|
|
17
|
+
const ifDefined = <T>(v: T | null | '' | undefined): T | undefined =>
|
|
18
|
+
(v === null || v === '' || v === undefined) ? undefined : v;
|
|
19
|
+
|
|
18
20
|
/**
|
|
19
21
|
* Utilities for showing help
|
|
20
22
|
*/
|
|
@@ -24,47 +26,51 @@ export class HelpUtil {
|
|
|
24
26
|
* Render command-specific help
|
|
25
27
|
* @param command
|
|
26
28
|
*/
|
|
27
|
-
static async renderCommandHelp(
|
|
28
|
-
const
|
|
29
|
-
const commandName =
|
|
29
|
+
static async renderCommandHelp(command: CliCommandShape): Promise<string> {
|
|
30
|
+
const schema = SchemaRegistryIndex.getConfig(getClass(command));
|
|
31
|
+
const { name: commandName } = CliCommandRegistryIndex.get(getClass(command));
|
|
32
|
+
const args = schema.methods.main?.parameters ?? [];
|
|
30
33
|
|
|
31
34
|
await command.preHelp?.();
|
|
32
35
|
|
|
33
36
|
// Ensure finalized
|
|
34
|
-
const { flags, args } = await CliCommandSchemaUtil.getSchema(command);
|
|
35
37
|
|
|
36
|
-
const usage: string[] = [cliTpl`${{ title: 'Usage:' }} ${{ param: commandName }} ${{ input: '[options]' }}
|
|
38
|
+
const usage: string[] = [cliTpl`${{ title: 'Usage:' }} ${{ param: commandName }} ${{ input: '[options]' }}`,];
|
|
37
39
|
for (const field of args) {
|
|
38
|
-
const type = field.type ===
|
|
40
|
+
const type = field.type === String && field.enum && field.enum?.values.length <= 7 ? field.enum?.values?.join('|') : field.type.name.toLowerCase();
|
|
39
41
|
const arg = `${field.name}${field.array ? '...' : ''}:${type}`;
|
|
40
|
-
usage.push(cliTpl`${{ input: field.required ? `<${arg}>` : `[${arg}]` }}`);
|
|
42
|
+
usage.push(cliTpl`${{ input: field.required?.active !== false ? `<${arg}>` : `[${arg}]` }}`);
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
const params: string[] = [];
|
|
44
46
|
const descriptions: string[] = [];
|
|
45
47
|
|
|
46
|
-
for (const
|
|
47
|
-
const key = castKey<CliCommandShape>(
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const type = flag.type === 'string' && flag.choices && flag.choices.length <= 3 ? flag.choices?.join('|') : flag.type;
|
|
61
|
-
param.push(cliTpl`${{ type: `<${type}>` }}`);
|
|
48
|
+
for (const field of Object.values(schema.fields)) {
|
|
49
|
+
const key = castKey<CliCommandShape>(field.name);
|
|
50
|
+
const def = ifDefined(command[key]) ?? ifDefined(field.default);
|
|
51
|
+
const aliases = (field.aliases ?? [])
|
|
52
|
+
.filter(x => x.startsWith('-'))
|
|
53
|
+
.filter(x =>
|
|
54
|
+
(field.type !== Boolean) || ((def !== true || field.name === 'help') ? !x.startsWith('--no-') : x.startsWith('--'))
|
|
55
|
+
);
|
|
56
|
+
let type: string | undefined;
|
|
57
|
+
|
|
58
|
+
if (field.type === String && field.enum && field.enum.values.length <= 3) {
|
|
59
|
+
type = field.enum.values?.join('|');
|
|
60
|
+
} else if (field.type !== Boolean) {
|
|
61
|
+
({ type } = CliSchemaExportUtil.baseInputType(field));
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
const param = [
|
|
65
|
+
cliTpl`${{ param: aliases.join(', ') }}`,
|
|
66
|
+
...(type ? [cliTpl`${{ type: `<${type}>` }}`] : []),
|
|
67
|
+
];
|
|
68
|
+
|
|
63
69
|
params.push(param.join(' '));
|
|
64
|
-
const desc = [cliTpl`${{ title:
|
|
70
|
+
const desc = [cliTpl`${{ title: field.description }}`];
|
|
65
71
|
|
|
66
|
-
if (key !== 'help' &&
|
|
67
|
-
desc.push(cliTpl`(default: ${{ input: JSON.stringify(
|
|
72
|
+
if (key !== 'help' && def !== undefined) {
|
|
73
|
+
desc.push(cliTpl`(default: ${{ input: JSON.stringify(def) }})`);
|
|
68
74
|
}
|
|
69
75
|
descriptions.push(desc.join(' '));
|
|
70
76
|
}
|
|
@@ -97,18 +103,15 @@ export class HelpUtil {
|
|
|
97
103
|
*/
|
|
98
104
|
static async renderAllHelp(title?: string): Promise<string> {
|
|
99
105
|
const rows: string[] = [];
|
|
100
|
-
const keys = [...CliCommandRegistry.getCommandMapping().keys()].toSorted((a, b) => a.localeCompare(b));
|
|
101
|
-
const maxWidth = keys.reduce((a, b) => Math.max(a, util.stripVTControlCharacters(b).length), 0);
|
|
102
106
|
|
|
103
|
-
|
|
107
|
+
// All
|
|
108
|
+
const resolved = await CliCommandRegistryIndex.load();
|
|
109
|
+
const maxWidth = resolved.reduce((a, b) => Math.max(a, util.stripVTControlCharacters(b.command).length), 0);
|
|
110
|
+
|
|
111
|
+
for (const { command: cmd, schema } of resolved) {
|
|
104
112
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const cfg = await CliCommandRegistry.getConfig(inst);
|
|
108
|
-
if (!cfg.hidden) {
|
|
109
|
-
const schema = await CliCommandSchemaUtil.getSchema(cfg.cls);
|
|
110
|
-
rows.push(cliTpl` ${{ param: cmd.padEnd(maxWidth, ' ') }} ${{ title: schema.title }}`);
|
|
111
|
-
}
|
|
113
|
+
if (schema && !schema.private) {
|
|
114
|
+
rows.push(cliTpl` ${{ param: cmd.padEnd(maxWidth, ' ') }} ${{ title: schema.description || '' }}`);
|
|
112
115
|
}
|
|
113
116
|
} catch (err) {
|
|
114
117
|
if (err instanceof Error) {
|
|
@@ -121,9 +124,8 @@ export class HelpUtil {
|
|
|
121
124
|
|
|
122
125
|
const lines = [cliTpl`${{ title: 'Commands:' }}`, ...rows, ''];
|
|
123
126
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
+
lines.unshift(title ? cliTpl`${{ title }}` : cliTpl`${{ title: 'Usage:' }} ${{ param: '[options]' }} ${{ param: '[command]' }}`, '');
|
|
128
|
+
|
|
127
129
|
return lines.map(x => x.trimEnd()).join('\n');
|
|
128
130
|
}
|
|
129
131
|
|
|
@@ -133,9 +135,12 @@ export class HelpUtil {
|
|
|
133
135
|
static renderValidationError(err: CliValidationResultError): string {
|
|
134
136
|
return [
|
|
135
137
|
cliTpl`${{ failure: 'Execution failed' }}:`,
|
|
136
|
-
...err.details.errors.map(e =>
|
|
137
|
-
|
|
138
|
-
|
|
138
|
+
...err.details.errors.map(e => {
|
|
139
|
+
if (e.source && e.source in validationSourceMap) {
|
|
140
|
+
return cliTpl` * ${{ identifier: validationSourceMap[e.source] }} ${{ subtitle: e.message }}`;
|
|
141
|
+
}
|
|
142
|
+
return cliTpl` * ${{ failure: e.message }}`;
|
|
143
|
+
}),
|
|
139
144
|
'',
|
|
140
145
|
].join('\n');
|
|
141
146
|
}
|
package/src/module.ts
CHANGED
|
@@ -98,4 +98,24 @@ export class CliModuleUtil {
|
|
|
98
98
|
const graph = this.getDependencyGraph(mods);
|
|
99
99
|
return graph[modName].includes(depModName);
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Find changed paths, either files between two git commits, or all folders for changed modules
|
|
104
|
+
*/
|
|
105
|
+
static async findChangedPaths(config: { since?: string, changed?: boolean, logError?: boolean } = {}): Promise<string[]> {
|
|
106
|
+
if (config.since) {
|
|
107
|
+
try {
|
|
108
|
+
const files = await CliScmUtil.findChangedFiles(config.since, 'HEAD');
|
|
109
|
+
return files.filter(x => !x.endsWith('package.json') && !x.endsWith('package-lock.json'));
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (config.logError && err instanceof Error) {
|
|
112
|
+
console.error(err.message);
|
|
113
|
+
}
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
const mods = await this.findModules(config.changed ? 'changed' : 'workspace', undefined, 'HEAD');
|
|
118
|
+
return mods.map(x => x.sourcePath);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
101
121
|
}
|
package/src/parse.ts
CHANGED
|
@@ -2,7 +2,9 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { Runtime } from '@travetto/runtime';
|
|
5
|
-
import {
|
|
5
|
+
import { SchemaClassConfig, SchemaFieldConfig, SchemaInputConfig } from '@travetto/schema';
|
|
6
|
+
|
|
7
|
+
import { ParsedState } from './types.ts';
|
|
6
8
|
|
|
7
9
|
type ParsedInput = ParsedState['all'][number];
|
|
8
10
|
|
|
@@ -11,27 +13,12 @@ const VALID_FLAG = /^-{1,2}[a-z]/i;
|
|
|
11
13
|
const HELP_FLAG = /^-h|--help$/;
|
|
12
14
|
const LONG_FLAG_WITH_EQ = /^--[a-z][^= ]+=\S+/i;
|
|
13
15
|
const CONFIG_PRE = '+=';
|
|
14
|
-
const ENV_PRE = 'env.';
|
|
15
16
|
const SPACE = new Set([32, 7, 13, 10]);
|
|
16
17
|
|
|
17
|
-
export const
|
|
18
|
+
export const ENV_PREFIX = 'env.';
|
|
19
|
+
export const isBoolFlag = (x?: SchemaInputConfig): boolean => x?.type === Boolean && !x.array;
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
const { field, input, rawText = input, value, index } = cfg;
|
|
21
|
-
if (!field) {
|
|
22
|
-
return { type: 'unknown', input: rawText };
|
|
23
|
-
} else if (!field.flagNames?.length) {
|
|
24
|
-
return { type: 'arg', input: field ? input : rawText ?? input, array: field.array, index: index! };
|
|
25
|
-
} else {
|
|
26
|
-
return {
|
|
27
|
-
type: 'flag',
|
|
28
|
-
fieldName: field.name,
|
|
29
|
-
array: field.array,
|
|
30
|
-
input: field ? input : rawText ?? input,
|
|
31
|
-
value: value ?? (isBoolFlag(field) ? !input.startsWith('--no-') : undefined)
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
};
|
|
21
|
+
export type AliasesParseResult = Record<'long' | 'short' | 'raw' | 'env', string[]>;
|
|
35
22
|
|
|
36
23
|
/**
|
|
37
24
|
* Parsing support for the cli
|
|
@@ -39,7 +26,7 @@ const getInput = (cfg: { field?: CliCommandInput, rawText?: string, input: strin
|
|
|
39
26
|
export class CliParseUtil {
|
|
40
27
|
|
|
41
28
|
static toEnvField(k: string): string {
|
|
42
|
-
return `${
|
|
29
|
+
return k.startsWith(ENV_PREFIX) ? k : `${ENV_PREFIX}${k}`;
|
|
43
30
|
}
|
|
44
31
|
|
|
45
32
|
static readToken(text: string, start = 0): { next: number, value?: string } {
|
|
@@ -80,11 +67,11 @@ export class CliParseUtil {
|
|
|
80
67
|
/**
|
|
81
68
|
* Get a user-specified module if present
|
|
82
69
|
*/
|
|
83
|
-
static getSpecifiedModule(schema:
|
|
70
|
+
static getSpecifiedModule(schema: SchemaClassConfig, args: string[]): string | undefined {
|
|
84
71
|
const SEP = args.includes(RAW_SEP) ? args.indexOf(RAW_SEP) : args.length;
|
|
85
|
-
const input = schema.
|
|
86
|
-
const ENV_KEY = input?.
|
|
87
|
-
const flags = new Set(input?.
|
|
72
|
+
const input = Object.values(schema.fields).find(x => x.specifiers?.includes('module'));
|
|
73
|
+
const ENV_KEY = input?.aliases?.filter(x => x.startsWith(ENV_PREFIX)).map(x => x.replace(ENV_PREFIX, ''))[0] ?? '';
|
|
74
|
+
const flags = new Set(input?.aliases ?? []);
|
|
88
75
|
const check = (k?: string, v?: string): string | undefined => flags.has(k!) ? v : undefined;
|
|
89
76
|
return args.reduce(
|
|
90
77
|
(m, x, i, arr) =>
|
|
@@ -144,7 +131,7 @@ export class CliParseUtil {
|
|
|
144
131
|
/**
|
|
145
132
|
* Expand flag arguments into full argument list
|
|
146
133
|
*/
|
|
147
|
-
static async expandArgs(schema:
|
|
134
|
+
static async expandArgs(schema: SchemaClassConfig, args: string[]): Promise<string[]> {
|
|
148
135
|
const SEP = args.includes(RAW_SEP) ? args.indexOf(RAW_SEP) : args.length;
|
|
149
136
|
const mod = this.getSpecifiedModule(schema, args);
|
|
150
137
|
return (await Promise.all(args.map((x, i) =>
|
|
@@ -154,22 +141,23 @@ export class CliParseUtil {
|
|
|
154
141
|
/**
|
|
155
142
|
* Parse inputs to command
|
|
156
143
|
*/
|
|
157
|
-
static async parse(schema:
|
|
158
|
-
const flagMap = new Map<string,
|
|
159
|
-
schema.
|
|
144
|
+
static async parse(schema: SchemaClassConfig, inputs: string[]): Promise<ParsedState> {
|
|
145
|
+
const flagMap = new Map<string, SchemaFieldConfig>(
|
|
146
|
+
Object.values(schema.fields).flatMap(f => (f.aliases ?? []).map(name => [name, f]))
|
|
160
147
|
);
|
|
161
148
|
|
|
162
149
|
const out: ParsedInput[] = [];
|
|
163
150
|
|
|
164
151
|
// Load env vars to front
|
|
165
|
-
for (const field of schema.
|
|
166
|
-
for (const envName of field.
|
|
167
|
-
|
|
168
|
-
|
|
152
|
+
for (const field of Object.values(schema.fields)) {
|
|
153
|
+
for (const envName of (field.aliases ?? []).filter(x => x.startsWith(ENV_PREFIX))) {
|
|
154
|
+
const simple = envName.replace(ENV_PREFIX, '');
|
|
155
|
+
if (simple in process.env) {
|
|
156
|
+
const value: string = process.env[simple]!;
|
|
169
157
|
if (field.array) {
|
|
170
|
-
out.push(...value.split(/\s*,\s*/g).map(v =>
|
|
158
|
+
out.push(...value.split(/\s*,\s*/g).map(v => ({ type: 'flag', fieldName: field.name.toString(), input: envName, value: v }) as const));
|
|
171
159
|
} else {
|
|
172
|
-
out.push(
|
|
160
|
+
out.push({ type: 'flag', fieldName: field.name.toString(), input: envName, value });
|
|
173
161
|
}
|
|
174
162
|
}
|
|
175
163
|
}
|
|
@@ -181,24 +169,37 @@ export class CliParseUtil {
|
|
|
181
169
|
const input = inputs[i];
|
|
182
170
|
|
|
183
171
|
if (input === RAW_SEP) { // Raw separator
|
|
184
|
-
out.push(...inputs.slice(i + 1).map(x =>
|
|
172
|
+
out.push(...inputs.slice(i + 1).map((x, idx) => ({ type: 'unknown', input: x, index: argIdx + idx }) as const));
|
|
185
173
|
break;
|
|
186
174
|
} else if (LONG_FLAG_WITH_EQ.test(input)) {
|
|
187
175
|
const [k, ...v] = input.split('=');
|
|
188
176
|
const field = flagMap.get(k);
|
|
189
|
-
|
|
177
|
+
if (field) {
|
|
178
|
+
out.push({ type: 'flag', fieldName: field.name.toString(), input: k, value: v.join('=') });
|
|
179
|
+
} else {
|
|
180
|
+
out.push({ type: 'unknown', input });
|
|
181
|
+
}
|
|
190
182
|
} else if (VALID_FLAG.test(input)) { // Flag
|
|
191
183
|
const field = flagMap.get(input);
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
out.push(getInput({ field, input }));
|
|
184
|
+
if (!field) {
|
|
185
|
+
out.push({ type: 'unknown', input });
|
|
195
186
|
} else {
|
|
196
|
-
|
|
197
|
-
|
|
187
|
+
const next = inputs[i + 1];
|
|
188
|
+
const base = { type: 'flag', fieldName: field.name.toString(), input, array: field.array } as const;
|
|
189
|
+
if ((next && (VALID_FLAG.test(next) || next === RAW_SEP)) || isBoolFlag(field)) {
|
|
190
|
+
if (isBoolFlag(field)) {
|
|
191
|
+
out.push({ ...base, value: !input.startsWith('--no-') });
|
|
192
|
+
} else {
|
|
193
|
+
out.push(base);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
out.push({ ...base, value: next });
|
|
197
|
+
i += 1;
|
|
198
|
+
}
|
|
198
199
|
}
|
|
199
200
|
} else {
|
|
200
|
-
const field = schema.
|
|
201
|
-
out.push(
|
|
201
|
+
const field = schema.methods.main?.parameters[argIdx];
|
|
202
|
+
out.push({ type: 'arg', array: field?.array ?? false, input, index: argIdx });
|
|
202
203
|
// Move argIdx along if not in a var arg situation
|
|
203
204
|
if (!field?.array) {
|
|
204
205
|
argIdx += 1;
|
|
@@ -214,4 +215,24 @@ export class CliParseUtil {
|
|
|
214
215
|
flags: out.filter(x => x.type === 'flag')
|
|
215
216
|
};
|
|
216
217
|
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Parse aliases into categories for registration
|
|
221
|
+
*/
|
|
222
|
+
static parseAliases(aliases: string[]): AliasesParseResult {
|
|
223
|
+
return aliases.reduce<AliasesParseResult>((acc, curr) => {
|
|
224
|
+
if (VALID_FLAG.test(curr)) {
|
|
225
|
+
if (curr.startsWith('--')) {
|
|
226
|
+
acc.long.push(curr);
|
|
227
|
+
} else {
|
|
228
|
+
acc.short.push(curr);
|
|
229
|
+
}
|
|
230
|
+
} else if (curr.startsWith(ENV_PREFIX)) {
|
|
231
|
+
acc.env.push(curr);
|
|
232
|
+
} else {
|
|
233
|
+
acc.raw.push(curr);
|
|
234
|
+
}
|
|
235
|
+
return acc;
|
|
236
|
+
}, { long: [], short: [], raw: [], env: [] });
|
|
237
|
+
}
|
|
217
238
|
}
|