@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 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
- lint Command line support for linting
32
- lint:register Writes the lint configuration file
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/decorators.ts#L84) decorator
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/decorators.ts#L84) is a wrapper for [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#L13), and so every class that uses the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/decorators.ts#L84) decorator is now a full [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#L13) class. The fields of the class represent the flags that are available to the command.
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/decorators.ts#L84) supports the following data types for flags:
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/field.ts#L164), [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L170), [@Precision](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L158), [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L99) and [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L109) decorators help provide additional validation.
137
- * String values. [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L99), [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L109), [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L91) and [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L70) provide additional constraints
138
- * Date values. The [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L99) and [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L109) decorators help provide additional validation.
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#L13) behavior, we can leverage the metadata of the fields/parameters to help provide additional customization/context for the users of the commands.
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 api for Web Applications with support for the dependency injection.") 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/decorators.ts#L84) decorator. This means the target will be visible within the editor tooling.
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 api for Web Applications with support for the dependency injection.")'s target:
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, ShutdownManager, toConcrete } from '@travetto/runtime';
466
- import { DependencyRegistry } from '@travetto/di';
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 { RootRegistry } from '@travetto/registry';
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 RootRegistry.init();
493
- const instance = await DependencyRegistry.getInstance(toConcrete<WebHttpServer>());
494
-
495
- let res;
496
- try {
497
- res = await instance.serve();
498
- } catch (err) {
499
- if (NetUtil.isPortUsedError(err) && !Runtime.production && this.killConflict) {
500
- await NetUtil.freePort(err.port);
501
- res = await instance.serve();
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#L32) errors that are returned will be shared with the user, and fail to invoke the `main` method.
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@2.5.3
559
- * elasticsearch@8.17.0
552
+ * dynamodb@3.1.0
553
+ * elasticsearch@9.2.1
560
554
  * firestore@latest
561
- * mongodb@8.0
562
- * mysql@9.1
563
- * postgresql@17.2
564
- * redis@7.4
565
- * s3@3.12.0
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 2.5.3 Running 93af422e793a
577
- elasticsearch 8.17.0 Running ed76ee063d13
570
+ dynamodb 3.1.0 Running 93af422e793a
571
+ elasticsearch 9.2.1 Running ed76ee063d13
578
572
  firestore latest Running feec2e5e95b4
579
- mongodb 8.0 Running 5513eba6734e
580
- mysql 9.1 Running 307bc66d442a
581
- postgresql 17.2 Running e78291e71040
582
- redis 7.4 Running 77ba279b4e30
583
- s3 3.12.0 Running fdacfc55b9e3
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.0';
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/registry.ts';
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": "6.0.0",
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": "^6.0.0",
32
- "@travetto/terminal": "^6.0.0"
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-server', true],
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 { CliCommandRegistry } from './registry.ts';
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 CliCommandRegistry.getInstance(cmd, true);
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, castTo, Primitive } from '@travetto/runtime';
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 { CliCommandRegistry } from './registry.ts';
8
- import { CliCommandSchemaUtil } from './schema.ts';
8
+ import { CliCommandRegistryIndex } from './registry/registry-index.ts';
9
9
  import { CliValidationResultError } from './error.ts';
10
- import { isBoolFlag } from './parse.ts';
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(cmd: CliCommandShape | string): Promise<string> {
28
- const command = typeof cmd === 'string' ? await CliCommandRegistry.getInstance(cmd, true) : cmd;
29
- const commandName = CliCommandRegistry.getName(command);
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 === 'string' && field.choices && field.choices.length <= 7 ? field.choices?.join('|') : 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 flag of flags) {
47
- const key = castKey<CliCommandShape>(flag.name);
48
- const flagVal: Primitive = castTo(command[key]);
49
-
50
- let aliases = flag.flagNames ?? [];
51
- if (isBoolFlag(flag)) {
52
- if (flagVal === true) {
53
- aliases = (flag.flagNames ?? []).filter(x => !/^[-][^-]/.test(x));
54
- } else {
55
- aliases = (flag.flagNames ?? []).filter(x => !x.startsWith('--no-'));
56
- }
57
- }
58
- const param = [cliTpl`${{ param: aliases.join(', ') }}`];
59
- if (!isBoolFlag(flag)) {
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: flag.description }}`];
70
+ const desc = [cliTpl`${{ title: field.description }}`];
65
71
 
66
- if (key !== 'help' && flagVal !== null && flagVal !== undefined && flagVal !== '') {
67
- desc.push(cliTpl`(default: ${{ input: JSON.stringify(flagVal) }})`);
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[helpText.length - 1] !== '') {
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
- for (const cmd of keys) {
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
- const inst = await CliCommandRegistry.getInstance(cmd);
106
- if (inst) {
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
- if (title === undefined || title) {
125
- lines.unshift(title ? cliTpl`${{ title }}` : cliTpl`${{ title: 'Usage:' }} ${{ param: '[options]' }} ${{ param: '[command]' }}`, '');
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 => e.source && e.source !== 'custom' ?
137
- cliTpl` * ${{ identifier: validationSourceMap[e.source] }} ${{ subtitle: e.message }}` :
138
- cliTpl` * ${{ failure: e.message }}`),
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 { CliCommandInput, CliCommandSchema, ParsedState } from './types.ts';
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 isBoolFlag = (x?: CliCommandInput): boolean => x?.type === 'boolean' && !x.array;
18
+ export const ENV_PREFIX = 'env.';
19
+ export const isBoolFlag = (x?: SchemaInputConfig): boolean => x?.type === Boolean && !x.array;
18
20
 
19
- const getInput = (cfg: { field?: CliCommandInput, rawText?: string, input: string, index?: number, value?: string }): ParsedInput => {
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 `${ENV_PRE}${k}`;
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: CliCommandSchema, args: string[]): string | undefined {
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.flags.find(x => x.type === 'module');
86
- const ENV_KEY = input?.flagNames?.filter(x => x.startsWith(ENV_PRE)).map(x => x.replace(ENV_PRE, ''))[0] ?? '';
87
- const flags = new Set(input?.flagNames ?? []);
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: CliCommandSchema, args: string[]): Promise<string[]> {
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: CliCommandSchema, inputs: string[]): Promise<ParsedState> {
158
- const flagMap = new Map<string, CliCommandInput>(
159
- schema.flags.flatMap(f => (f.flagNames ?? []).map(name => [name, f]))
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.flags) {
166
- for (const envName of field.envVars ?? []) {
167
- if (envName in process.env) {
168
- const value: string = process.env[envName]!;
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 => getInput({ field, input: `${ENV_PRE}${envName}`, value: 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(getInput({ field, input: `${ENV_PRE}${envName}`, value }));
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 => getInput({ input: 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
- out.push(getInput({ field, rawText: input, input: k, value: v.join('=') }));
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
- const next = inputs[i + 1];
193
- if ((next && (VALID_FLAG.test(next) || next === RAW_SEP)) || isBoolFlag(field)) {
194
- out.push(getInput({ field, input }));
184
+ if (!field) {
185
+ out.push({ type: 'unknown', input });
195
186
  } else {
196
- out.push(getInput({ field, input, value: next }));
197
- i += 1;
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.args[argIdx];
201
- out.push(getInput({ field, input, index: argIdx }));
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
  }