@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 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
@@ -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/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
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/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.
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/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:
136
135
  * Boolean values
137
- * 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.
138
- * 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
139
- * 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.
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#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.
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/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.
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 { DependencyRegistry } from '@travetto/di';
465
+ import { DependencyRegistryIndex } from '@travetto/di';
468
466
  import { CliCommand, CliCommandShape } from '@travetto/cli';
469
467
  import { NetUtil } from '@travetto/web';
470
- import { RootRegistry } from '@travetto/registry';
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 RootRegistry.init();
494
- const instance = await DependencyRegistry.getInstance(toConcrete<WebHttpServer>());
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
- await handle.complete;
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#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.
513
511
 
514
512
  **Code: CliValidationError**
515
513
  ```typescript
@@ -551,14 +549,14 @@ Options:
551
549
 
552
550
  Available Services
553
551
  --------------------
554
- * dynamodb@2.6.1
555
- * elasticsearch@8.17.0
552
+ * dynamodb@3.1.0
553
+ * elasticsearch@9.2.1
556
554
  * firestore@latest
557
- * mongodb@8.0
558
- * mysql@9.3
559
- * postgresql@17.5
560
- * redis@8.0
561
- * s3@4.2.0
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 2.6.1 Running 93af422e793a
573
- elasticsearch 8.17.0 Running ed76ee063d13
570
+ dynamodb 3.1.0 Running 93af422e793a
571
+ elasticsearch 9.2.1 Running ed76ee063d13
574
572
  firestore latest Running feec2e5e95b4
575
- mongodb 8.0 Running 5513eba6734e
576
- mysql 9.3 Running 307bc66d442a
577
- postgresql 17.5 Running e78291e71040
578
- redis 8.0 Running 77ba279b4e30
579
- s3 4.2.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
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.0';
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/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.1",
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": "^6.0.1",
32
- "@travetto/terminal": "^6.0.1"
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:(http|server)$/, '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?.();
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
  }
@@ -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
  }