@travetto/cli 6.0.0 → 7.0.0-rc.0
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 +40 -46
- package/__index__.ts +4 -2
- package/package.json +3 -3
- package/src/error.ts +1 -1
- package/src/execute.ts +4 -4
- package/src/help.ts +54 -49
- 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 +8 -116
- package/src/service.ts +4 -4
- 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 -153
- package/src/registry.ts +0 -96
- package/support/transformer.cli.ts +0 -53
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
|
|
@@ -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/
|
|
62
|
+
* The class must use the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L85) 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/
|
|
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.
|
|
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/
|
|
134
|
+
The [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L85) 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/
|
|
137
|
-
* String values. [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/
|
|
138
|
-
* 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.
|
|
139
139
|
* String lists. Same as String, but allowing multiple values.
|
|
140
140
|
* Numeric lists. Same as Number, but allowing multiple values.
|
|
141
141
|
|
|
@@ -251,7 +251,7 @@ $ trv basic:arglist -r 10 5 3 9 8 1
|
|
|
251
251
|
```
|
|
252
252
|
|
|
253
253
|
## Customization
|
|
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#
|
|
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.
|
|
255
255
|
|
|
256
256
|
**Code: Custom Command with Metadata**
|
|
257
257
|
```typescript
|
|
@@ -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
|
|
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.
|
|
394
394
|
|
|
395
395
|
**Code: Simple Run Target**
|
|
396
396
|
```typescript
|
|
@@ -413,7 +413,6 @@ export class RunCommand {
|
|
|
413
413
|
**Code: Anatomy of a Command**
|
|
414
414
|
```typescript
|
|
415
415
|
export interface CliCommandShape<T extends unknown[] = unknown[]> {
|
|
416
|
-
|
|
417
416
|
/**
|
|
418
417
|
* Parsed state
|
|
419
418
|
*/
|
|
@@ -458,15 +457,15 @@ export interface CliCommandShape<T extends unknown[] = unknown[]> {
|
|
|
458
457
|
```
|
|
459
458
|
|
|
460
459
|
### Dependency Injection
|
|
461
|
-
If the goal is to run a more complex application, which may include depending on [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support."), we can take a look at [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative
|
|
460
|
+
If the goal is to run a more complex application, which may include depending on [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support."), we can take a look at [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications")'s target:
|
|
462
461
|
|
|
463
462
|
**Code: Simple Run Target**
|
|
464
463
|
```typescript
|
|
465
|
-
import { Runtime,
|
|
466
|
-
import {
|
|
464
|
+
import { Runtime, toConcrete, Util } from '@travetto/runtime';
|
|
465
|
+
import { DependencyRegistryIndex } from '@travetto/di';
|
|
467
466
|
import { CliCommand, CliCommandShape } from '@travetto/cli';
|
|
468
467
|
import { NetUtil } from '@travetto/web';
|
|
469
|
-
import {
|
|
468
|
+
import { Registry } from '@travetto/registry';
|
|
470
469
|
|
|
471
470
|
import type { WebHttpServer } from '../src/types.ts';
|
|
472
471
|
|
|
@@ -489,21 +488,16 @@ export class WebHttpCommand implements CliCommandShape {
|
|
|
489
488
|
}
|
|
490
489
|
|
|
491
490
|
async main(): Promise<void> {
|
|
492
|
-
await
|
|
493
|
-
const instance = await
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
}
|
|
503
|
-
throw err;
|
|
504
|
-
}
|
|
505
|
-
ShutdownManager.onGracefulShutdown(res.kill, this);
|
|
506
|
-
return res.wait;
|
|
491
|
+
await Registry.init();
|
|
492
|
+
const instance = await DependencyRegistryIndex.getInstance(toConcrete<WebHttpServer>());
|
|
493
|
+
|
|
494
|
+
const handle = await Util.acquireWithRetry(
|
|
495
|
+
() => instance.serve(),
|
|
496
|
+
NetUtil.freePortOnConflict,
|
|
497
|
+
this.killConflict && !Runtime.production ? 5 : 1
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
return handle.complete;
|
|
507
501
|
}
|
|
508
502
|
}
|
|
509
503
|
```
|
|
@@ -513,7 +507,7 @@ As noted in the example above, `fields` is specified in this execution, with sup
|
|
|
513
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.
|
|
514
508
|
|
|
515
509
|
### Custom Validation
|
|
516
|
-
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.
|
|
517
511
|
|
|
518
512
|
**Code: CliValidationError**
|
|
519
513
|
```typescript
|
|
@@ -555,14 +549,14 @@ Options:
|
|
|
555
549
|
|
|
556
550
|
Available Services
|
|
557
551
|
--------------------
|
|
558
|
-
* dynamodb@
|
|
559
|
-
* elasticsearch@
|
|
552
|
+
* dynamodb@3.1.0
|
|
553
|
+
* elasticsearch@9.2.1
|
|
560
554
|
* firestore@latest
|
|
561
|
-
* mongodb@8.
|
|
562
|
-
* mysql@9.
|
|
563
|
-
* postgresql@
|
|
564
|
-
* redis@
|
|
565
|
-
* s3@
|
|
555
|
+
* mongodb@8.2
|
|
556
|
+
* mysql@9.5
|
|
557
|
+
* postgresql@18.1
|
|
558
|
+
* redis@8.4
|
|
559
|
+
* s3@4.10.0
|
|
566
560
|
```
|
|
567
561
|
|
|
568
562
|
A sample of all services available to the entire framework:
|
|
@@ -573,14 +567,14 @@ $ trv service status
|
|
|
573
567
|
|
|
574
568
|
Service Version Status
|
|
575
569
|
-------------------------------------------------
|
|
576
|
-
dynamodb
|
|
577
|
-
elasticsearch
|
|
570
|
+
dynamodb 3.1.0 Running 93af422e793a
|
|
571
|
+
elasticsearch 9.2.1 Running ed76ee063d13
|
|
578
572
|
firestore latest Running feec2e5e95b4
|
|
579
|
-
mongodb 8.
|
|
580
|
-
mysql 9.
|
|
581
|
-
postgresql
|
|
582
|
-
redis
|
|
583
|
-
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
|
|
584
578
|
```
|
|
585
579
|
|
|
586
580
|
### Defining new Services
|
|
@@ -590,7 +584,7 @@ The services are defined as plain typescript files within the framework and can
|
|
|
590
584
|
```typescript
|
|
591
585
|
import type { ServiceDescriptor } from '@travetto/cli';
|
|
592
586
|
|
|
593
|
-
const version = process.env.MONGO_VERSION || '8.
|
|
587
|
+
const version = process.env.MONGO_VERSION || '8.2';
|
|
594
588
|
|
|
595
589
|
export const service: ServiceDescriptor = {
|
|
596
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.0",
|
|
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.0",
|
|
32
|
+
"@travetto/terminal": "^7.0.0-rc.0"
|
|
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:http$/, 'web-http
|
|
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?.();
|
|
@@ -75,7 +75,7 @@ export class ExecutionManager {
|
|
|
75
75
|
} catch (err) {
|
|
76
76
|
await this.#onError(err);
|
|
77
77
|
} finally {
|
|
78
|
-
await ShutdownManager.gracefulShutdown();
|
|
78
|
+
await ShutdownManager.gracefulShutdown('@travetto/cli:execute');
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
}
|
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
|
}
|
|
@@ -76,7 +82,7 @@ export class HelpUtil {
|
|
|
76
82
|
const descWidth = Math.max(...descWidths);
|
|
77
83
|
|
|
78
84
|
const helpText = await (command.help?.() ?? []);
|
|
79
|
-
if (helpText.length && helpText
|
|
85
|
+
if (helpText.length && helpText.at(-1) !== '') {
|
|
80
86
|
helpText.push('');
|
|
81
87
|
}
|
|
82
88
|
|
|
@@ -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
|
}
|