@travetto/cli 8.0.0-alpha.2 → 8.0.0-alpha.20

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
@@ -59,7 +59,7 @@ This module also has a tight integration with the [VSCode plugin](https://market
59
59
  At it's heart, a cli command is the contract defined by what flags, and what arguments the command supports. Within the framework this requires three criteria to be met:
60
60
  * The file must be located in the `support/` folder, and have a name that matches `cli.*.ts`
61
61
  * The file must be a class that has a main method
62
- * The class must use the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L27) decorator
62
+ * The class must use the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L20) decorator
63
63
 
64
64
  **Code: Basic Command**
65
65
  ```typescript
@@ -75,12 +75,12 @@ export class BasicCommand {
75
75
 
76
76
  **Terminal: Basic Command Help**
77
77
  ```bash
78
- $ trv basic -h
78
+ $ trv basic --help
79
79
 
80
80
  Usage: basic [options]
81
81
 
82
82
  Options:
83
- -h, --help display help for command
83
+ --help display help for command
84
84
  ```
85
85
 
86
86
  ## Command Naming
@@ -94,7 +94,7 @@ Examples of mappings:
94
94
  The pattern is that underscores(_) translate to colons (:), and the `cli.` prefix, and `.ts` suffix are dropped.
95
95
 
96
96
  ## Binding Flags
97
- [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L27) is a wrapper for [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#L19), and so every class that uses the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L27) decorator is now a full [@Schema](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/schema.ts#L19) class. The fields of the class represent the flags that are available to the command.
97
+ [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L20) 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#L20) 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
@@ -113,13 +113,13 @@ export class BasicCommand {
113
113
 
114
114
  **Terminal: Basic Command with Flag Help**
115
115
  ```bash
116
- $ trv basic:flag -h
116
+ $ trv basic:flag --help
117
117
 
118
118
  Usage: basic:flag [options]
119
119
 
120
120
  Options:
121
121
  -l, --loud
122
- -h, --help display help for command
122
+ --help display help for command
123
123
  ```
124
124
 
125
125
  As you can see the command now has the support of a basic boolean flag to determine if the response should be loud or not. The default value here is undefined/false, and so is an opt-in experience.
@@ -131,7 +131,7 @@ $ trv basic:flag --loud
131
131
  HELLO
132
132
  ```
133
133
 
134
- The [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L27) supports the following data types for flags:
134
+ The [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L20) supports the following data types for flags:
135
135
  * Boolean values
136
136
  * Number values. The [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L172), [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L179), [@Precision](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L165), [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L99) and [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L110) decorators help provide additional validation.
137
137
  * String values. [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L99), [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L110), [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L90) and [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L64) provide additional constraints
@@ -158,12 +158,12 @@ export class BasicCommand {
158
158
 
159
159
  **Terminal: Basic Command**
160
160
  ```bash
161
- $ trv basic:arg -h
161
+ $ trv basic:arg --help
162
162
 
163
163
  Usage: basic:arg [options] [volume:number]
164
164
 
165
165
  Options:
166
- -h, --help display help for command
166
+ --help display help for command
167
167
  ```
168
168
 
169
169
  **Terminal: Basic Command with Invalid Loud Arg**
@@ -176,7 +176,7 @@ Execution failed:
176
176
  Usage: basic:arg [options] [volume:number]
177
177
 
178
178
  Options:
179
- -h, --help display help for command
179
+ --help display help for command
180
180
  ```
181
181
 
182
182
  **Terminal: Basic Command with Loud Arg > 7**
@@ -213,13 +213,13 @@ export class BasicCommand {
213
213
 
214
214
  **Terminal: Basic Command**
215
215
  ```bash
216
- $ trv basic:arg-list -h
216
+ $ trv basic:arg-list --help
217
217
 
218
218
  Usage: basic:arg-list [options] <volumes...:number>
219
219
 
220
220
  Options:
221
221
  -r, --reverse
222
- -h, --help display help for command
222
+ --help display help for command
223
223
  ```
224
224
 
225
225
  **Terminal: Basic Arg List**
@@ -240,7 +240,7 @@ Usage: basic:arg-list [options] <volumes...:number>
240
240
 
241
241
  Options:
242
242
  -r, --reverse
243
- -h, --help display help for command
243
+ --help display help for command
244
244
  ```
245
245
 
246
246
  **Terminal: Basic Arg List with Reverse**
@@ -279,13 +279,13 @@ export class CustomCommand {
279
279
 
280
280
  **Terminal: Custom Command Help**
281
281
  ```bash
282
- $ trv custom:arg -h
282
+ $ trv custom:arg --help
283
283
 
284
284
  Usage: custom:arg [options] [volume:number]
285
285
 
286
286
  Options:
287
287
  -m, --message <string> The message to send back to the user (default: "hello")
288
- -h, --help display help for command
288
+ --help display help for command
289
289
  ```
290
290
 
291
291
  **Terminal: Custom Command Help with overridden Text**
@@ -330,13 +330,13 @@ export class CustomCommand {
330
330
 
331
331
  **Terminal: Custom Command Help**
332
332
  ```bash
333
- $ trv custom:env-arg -h
333
+ $ trv custom:env-arg --help
334
334
 
335
335
  Usage: custom:env-arg [options] [volume:number]
336
336
 
337
337
  Options:
338
338
  -t, --text <string> The message to send back to the user (default: "hello")
339
- -h, --help display help for command
339
+ --help display help for command
340
340
  ```
341
341
 
342
342
  **Terminal: Custom Command Help with default Text**
@@ -390,7 +390,7 @@ npx trv call:db --host localhost --port 3306 --username app --password <custom>
390
390
  ```
391
391
 
392
392
  ## VSCode Integration
393
- By default, cli commands do not expose themselves to the VSCode extension, as the majority of them are not intended for that sort of operation. [Web API](https://github.com/travetto/travetto/tree/main/module/web#readme "Declarative support for creating Web Applications") does expose a cli target `web:http` that will show up, to help run/debug a web application. Any command can mark itself as being a run target, and will be eligible for running from within the [VSCode plugin](https://marketplace.visualstudio.com/items?itemName=arcsine.travetto-plugin). This is achieved by setting the `runTarget` field on the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L27) 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#L20) decorator. This means the target will be visible within the editor tooling.
394
394
 
395
395
  **Code: Simple Run Target**
396
396
  ```typescript
@@ -460,7 +460,7 @@ export class WebHttpCommand implements CliCommandShape {
460
460
  profile: string[];
461
461
 
462
462
  @CliRestartOnChangeFlag()
463
- restartOnChange: boolean = true;
463
+ restartOnChange: boolean = Runtime.localDevelopment;
464
464
 
465
465
  @CliDebugIpcFlag()
466
466
  debugIpc?: boolean;
@@ -542,7 +542,7 @@ A simple example of the validation can be found in the `doc` command:
542
542
  ```typescript
543
543
  @Validator(async (cmd) => {
544
544
  const docFile = path.resolve(cmd.input);
545
- if (!(await fs.stat(docFile).catch(() => false))) {
545
+ if (!(await fs.stat(docFile, { throwIfNoEntry: false }))) {
546
546
  return { message: `input: ${cmd.input} does not exist`, path: 'input', source: 'flag', kind: 'invalid' };
547
547
  }
548
548
  })
@@ -559,18 +559,18 @@ Usage: service [options] <action:restart|start|status|stop> [services...:string]
559
559
 
560
560
  Options:
561
561
  -q, --quiet (default: false)
562
- -h, --help display help for command
562
+ --help display help for command
563
563
 
564
564
  Available Services
565
565
  --------------------
566
566
  * dynamodb@3.3.0
567
- * elasticsearch@9.2.4
567
+ * elasticsearch@9.2.8
568
568
  * firestore@latest
569
569
  * mongodb@8.2
570
570
  * mysql@9.6
571
- * postgresql@18.1
571
+ * postgresql@18.3
572
572
  * redis@8.4
573
- * s3@4.11.0
573
+ * s3@4.12.4
574
574
  ```
575
575
 
576
576
  A sample of all services available to the entire framework:
@@ -582,13 +582,13 @@ $ trv service status
582
582
  Service Version Status
583
583
  -------------------------------------------------
584
584
  dynamodb 3.3.0 Running 93af422e793a
585
- elasticsearch 9.2.4 Running ed76ee063d13
585
+ elasticsearch 9.2.8 Running ed76ee063d13
586
586
  firestore latest Running feec2e5e95b4
587
587
  mongodb 8.2 Running 5513eba6734e
588
588
  mysql 9.6 Running 307bc66d442a
589
- postgresql 18.1 Running e78291e71040
589
+ postgresql 18.3 Running e78291e71040
590
590
  redis 8.4 Running 77ba279b4e30
591
- s3 4.11.0 Running fdacfc55b9e3
591
+ s3 4.12.4 Running fdacfc55b9e3
592
592
  ```
593
593
 
594
594
  ### Defining new Services
package/bin/trv.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // @ts-check
3
+ import '@travetto/runtime/support/patch.js';
3
4
  import '@travetto/compiler/bin/hook.js';
4
5
  const { invoke } = await import('@travetto/compiler/support/invoke.ts');
5
- await invoke('exec', ['@travetto/cli/support/entry.trv.ts', ...process.argv.slice(2)]);
6
+ await invoke('exec', '@travetto/cli/support/entry.trv.ts', ...process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/cli",
3
- "version": "8.0.0-alpha.2",
3
+ "version": "8.0.0-alpha.20",
4
4
  "type": "module",
5
5
  "description": "CLI infrastructure for Travetto framework",
6
6
  "keywords": [
@@ -29,8 +29,8 @@
29
29
  "directory": "module/cli"
30
30
  },
31
31
  "dependencies": {
32
- "@travetto/schema": "^8.0.0-alpha.1",
33
- "@travetto/terminal": "^8.0.0-alpha.1"
32
+ "@travetto/schema": "^8.0.0-alpha.15",
33
+ "@travetto/terminal": "^8.0.0-alpha.14"
34
34
  },
35
35
  "travetto": {
36
36
  "displayName": "Command Line Interface",
package/src/execute.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ConsoleManager, Runtime, ShutdownManager, Util } from '@travetto/runtime';
1
+ import { ConsoleManager, getClass, Runtime, ShutdownManager, Util } from '@travetto/runtime';
2
2
 
3
3
  import { HelpUtil } from './help.ts';
4
4
  import { CliCommandRegistryIndex } from './registry/registry-index.ts';
@@ -11,20 +11,32 @@ import type { CliCommandShape } from './types.ts';
11
11
  */
12
12
  export class ExecutionManager {
13
13
 
14
- /**
15
- * Execute the command line
16
- * @param args
17
- */
18
- static async run(argv: string[]): Promise<void> {
14
+ /** Command Execution */
15
+ static async execute(instance: CliCommandShape, args: unknown[]): Promise<void> {
16
+ const config = CliCommandRegistryIndex.get(getClass(instance));
17
+
18
+ for (const item of config.preMain) {
19
+ await item.handler(instance);
20
+ }
21
+
22
+ // Wait 50ms to allow stdout to flush on shutdown
23
+ ShutdownManager.signal.addEventListener('abort', () => Util.blockingTimeout(50));
24
+ ConsoleManager.debug(Runtime.debug);
25
+ await instance.main(...args);
26
+ }
27
+
28
+ /** Extract configuration and show help as needed */
29
+ static async getExecutionCommand(argv: string[]): Promise<(() => Promise<void>) | undefined> {
19
30
  let command: CliCommandShape | undefined;
20
31
 
21
32
  const { cmd, args, help } = CliParseUtil.getArgs(argv);
22
33
  if (!cmd) {
23
- return console.info!(await HelpUtil.renderAllHelp());
34
+ console.info!(await HelpUtil.renderAllHelp());
35
+ return;
24
36
  }
25
37
 
26
38
  try {
27
- const [{ instance, schema, config }] = await CliCommandRegistryIndex.load([cmd]);
39
+ const [{ instance, schema }] = await CliCommandRegistryIndex.load([cmd]);
28
40
  command = instance;
29
41
  const fullArgs = await CliParseUtil.expandArgs(schema, args);
30
42
 
@@ -35,22 +47,29 @@ export class ExecutionManager {
35
47
  await instance.finalize?.(help);
36
48
 
37
49
  if (help) {
38
- return console.log!(await HelpUtil.renderCommandHelp(instance));
50
+ console.log!(await HelpUtil.renderCommandHelp(instance));
51
+ return;
39
52
  }
40
53
 
41
54
  await CliCommandSchemaUtil.validate(command, boundArgs);
42
55
 
43
- // Wait 50ms to allow stdout to flush on shutdown
44
- ShutdownManager.signal.addEventListener('abort', () => Util.blockingTimeout(50));
45
-
46
- for (const preMain of config.preMain ?? []) {
47
- await preMain(instance);
48
- }
49
-
50
- ConsoleManager.debug(Runtime.debug);
51
- await instance.main(...boundArgs);
56
+ return this.execute.bind(this, instance, boundArgs);
52
57
  } catch (error) {
53
58
  await HelpUtil.renderError(error, cmd, command);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Execute the command line
64
+ * @param args
65
+ */
66
+ static async run(argv: string[]): Promise<void> {
67
+ try {
68
+ const execute = await this.getExecutionCommand(argv);
69
+ await execute?.();
70
+ } catch (error) {
71
+ console.error!(error);
72
+ process.exitCode ??= 1;
54
73
  } finally {
55
74
  await ShutdownManager.shutdown();
56
75
  }
package/src/help.ts CHANGED
@@ -4,32 +4,29 @@ import { castKey, getClass, JSONUtil, Runtime } from '@travetto/runtime';
4
4
  import { SchemaRegistryIndex, ValidationResultError } from '@travetto/schema';
5
5
 
6
6
  import { cliTpl } from './color.ts';
7
- import type { CliCommandShape } from './types.ts';
7
+ import { HELP_FLAG, type CliCommandShape } from './types.ts';
8
8
  import { CliCommandRegistryIndex, UNKNOWN_COMMAND } from './registry/registry-index.ts';
9
9
  import { CliSchemaExportUtil } from './schema-export.ts';
10
10
 
11
- const validationSourceMap: Record<string, string> = {
12
- arg: 'Argument',
13
- flag: 'Flag'
14
- };
11
+ const validationSourceMap: Record<string, string> = { arg: 'Argument', flag: 'Flag' };
15
12
 
16
13
  const ifDefined = <T>(value: T | null | '' | undefined): T | undefined =>
17
14
  (value === null || value === '' || value === undefined) ? undefined : value;
18
15
 
19
- const toItem = (name: string, pkg: string, prod?: boolean) => [name, Runtime.getInstallCommand(pkg, prod)] as const;
20
-
21
- const INSTALL_COMMANDS = new Map<string, string>([
22
- ...['test', 'test:watch', 'test:direct'].map(item => toItem(item, '@travetto/test')),
23
- ...['lint', 'lint:register', 'eslint', 'eslint:register'].map(item => toItem(item, '@travetto/eslint')),
24
- ...['model:install', 'model:export'].map(item => toItem(item, '@travetto/model', true)),
25
- ...['openapi:spec', 'openapi:client'].map(item => toItem(item, '@travetto/openapi', true)),
26
- ...['email:compile', 'email:test', 'email:editor'].map(item => toItem(item, '@travetto/email-compiler')),
27
- ...['pack', 'pack:zip', 'pack:docker'].map(item => toItem(item, '@travetto/pack')),
28
- ...['repo:publish', 'repo:version', 'repo:exec', 'repo:list', 'repo:version-sync'].map(item => toItem(item, '@travetto/repo')),
29
- toItem('web:http', '@travetto/web-http', true),
30
- toItem('doc', '@travetto/doc'),
31
- toItem('web:rpc-client', '@travetto/web-rpc', true),
32
- ]);
16
+ const MODULE_TO_COMMAND = {
17
+ '@travetto/doc': ['doc'],
18
+ '@travetto/email-compiler': ['email:compile', 'email:test', 'email:editor'],
19
+ '@travetto/eslint': ['eslint', 'eslint:register', 'lint', 'lint:register'],
20
+ '@travetto/model': ['model:install', 'model:export'],
21
+ '@travetto/openapi': ['openapi:spec', 'openapi:client'],
22
+ '@travetto/pack': ['pack', 'pack:zip', 'pack:docker'],
23
+ '@travetto/repo': ['repo:publish', 'repo:version', 'repo:exec', 'repo:list'],
24
+ '@travetto/test': ['test', 'test:watch', 'test:direct'],
25
+ '@travetto/web-http': ['web:http'],
26
+ '@travetto/web-rpc': ['web:rpc-client'],
27
+ };
28
+
29
+ const COMMAND_TO_MODULE = Object.fromEntries(Object.entries(MODULE_TO_COMMAND).flatMap(([k, v]) => v.map(sv => [sv, k])));
33
30
 
34
31
  /**
35
32
  * Utilities for showing help
@@ -37,15 +34,15 @@ const INSTALL_COMMANDS = new Map<string, string>([
37
34
  export class HelpUtil {
38
35
 
39
36
  /** Render the unknown command message */
40
- static renderUnknownCommandMessage(cmd: string): string {
41
- const install = INSTALL_COMMANDS.get(cmd);
42
- if (install) {
37
+ static renderUnknownCommandMessage(command: string): string {
38
+ const module = COMMAND_TO_MODULE[command];
39
+ if (module) {
43
40
  return cliTpl`
44
- ${{ title: 'Missing Package' }}\n${'-'.repeat(20)}\nTo use ${{ input: cmd }} please run:\n
45
- ${{ identifier: install }}
41
+ ${{ title: 'Missing Package' }}\n${'-'.repeat(20)}\nTo use ${{ input: command }} please run:\n
42
+ ${{ identifier: Runtime.getInstallCommand(module) }}
46
43
  `;
47
44
  } else {
48
- return cliTpl`${{ subtitle: 'Unknown command' }}: ${{ input: cmd }}`;
45
+ return cliTpl`${{ subtitle: 'Unknown command' }}: ${{ input: command }}`;
49
46
  }
50
47
  }
51
48
 
@@ -58,25 +55,24 @@ ${{ identifier: install }}
58
55
  const { name: commandName } = CliCommandRegistryIndex.get(getClass(command));
59
56
  const args = schema.methods.main?.parameters ?? [];
60
57
 
61
- // Ensure finalized
58
+ const usage = [cliTpl`${{ title: 'Usage:' }} ${{ param: commandName }} ${{ input: '[options]' }}`,];
59
+ const params: string[] = [];
60
+ const descriptions: string[] = [];
62
61
 
63
- const usage: string[] = [cliTpl`${{ title: 'Usage:' }} ${{ param: commandName }} ${{ input: '[options]' }}`,];
62
+ // Ensure finalized
64
63
  for (const field of args) {
65
64
  const type = field.type === String && field.enum && field.enum?.values.length <= 7 ? field.enum?.values?.join('|') : field.type.name.toLowerCase();
66
65
  const arg = `${field.name}${field.array ? '...' : ''}:${type}`;
67
66
  usage.push(cliTpl`${{ input: field.required?.active !== false ? `<${arg}>` : `[${arg}]` }}`);
68
67
  }
69
68
 
70
- const params: string[] = [];
71
- const descriptions: string[] = [];
72
-
73
69
  for (const field of Object.values(schema.fields)) {
74
70
  const key = castKey<CliCommandShape>(field.name);
75
71
  const defaultValue = ifDefined(command[key]) ?? ifDefined(field.default);
76
72
  const aliases = (field.aliases ?? [])
77
73
  .filter(flag => flag.startsWith('-'))
78
74
  .filter(flag =>
79
- (field.type !== Boolean) || ((defaultValue !== true || field.name === 'help') ? !flag.startsWith('--no-') : flag.startsWith('--'))
75
+ (field.type !== Boolean) || (defaultValue !== true ? !flag.startsWith('--no-') : flag.startsWith('--'))
80
76
  );
81
77
  let type: string | undefined;
82
78
 
@@ -86,29 +82,33 @@ ${{ identifier: install }}
86
82
  ({ type } = CliSchemaExportUtil.baseInputType(field));
87
83
  }
88
84
 
89
- const param = [
85
+ const parameter = [
90
86
  cliTpl`${{ param: aliases.join(', ') }}`,
91
87
  ...(type ? [cliTpl`${{ type: `<${type}>` }}`] : []),
92
88
  ];
93
89
 
94
- params.push(param.join(' '));
95
- const desc = [cliTpl`${{ title: field.description }}`];
90
+ params.push(parameter.join(' '));
91
+ const parts = [cliTpl`${{ title: field.description }}`];
96
92
 
97
- if (key !== 'help' && defaultValue !== undefined) {
98
- desc.push(cliTpl`(default: ${{ input: JSONUtil.toUTF8(defaultValue) }})`);
93
+ if (defaultValue !== undefined) {
94
+ parts.push(cliTpl`(default: ${{ input: JSONUtil.toUTF8(defaultValue) }})`);
99
95
  }
100
- descriptions.push(desc.join(' '));
96
+ descriptions.push(parts.join(' '));
101
97
  }
102
98
 
99
+ params.push(cliTpl`${{ param: HELP_FLAG }}`);
100
+ descriptions.push('display help for command');
101
+
102
+
103
103
  const paramWidths = params.map(item => util.stripVTControlCharacters(item).length);
104
104
  const descWidths = descriptions.map(item => util.stripVTControlCharacters(item).length);
105
105
 
106
106
  const paramWidth = Math.max(...paramWidths);
107
107
  const descWidth = Math.max(...descWidths);
108
108
 
109
- const helpText = await (command.help?.() ?? []);
110
- if (helpText.length && helpText.at(-1) !== '') {
111
- helpText.push('');
109
+ const extendedHelpText = await (command.help?.() ?? []);
110
+ if (extendedHelpText.length && extendedHelpText.at(-1) !== '') {
111
+ extendedHelpText.push('');
112
112
  }
113
113
 
114
114
  return [
@@ -119,7 +119,7 @@ ${{ identifier: install }}
119
119
  ` ${params[i]}${' '.repeat((paramWidth - paramWidths[i]))} ${descriptions[i].padEnd(descWidth)}${' '.repeat((descWidth - descWidths[i]))}`
120
120
  ),
121
121
  '',
122
- ...helpText
122
+ ...extendedHelpText
123
123
  ].map(line => line.trimEnd()).join('\n');
124
124
  }
125
125
 
@@ -175,13 +175,13 @@ ${{ identifier: install }}
175
175
  process.exitCode ??= 1;
176
176
  if (error instanceof ValidationResultError) {
177
177
  console.error!(this.renderValidationError(error));
178
+ } else if (error instanceof Error) {
179
+ console.error!(cliTpl`${{ failure: error.stack }}\n`);
178
180
  }
179
181
  if (command) {
180
182
  console.error!(await this.renderCommandHelp(command));
181
183
  } else if (error === UNKNOWN_COMMAND) {
182
184
  console.error!(this.renderUnknownCommandMessage(cmd));
183
- } else {
184
- console.error!(error);
185
185
  }
186
186
  console.error!();
187
187
  }
package/src/parse.ts CHANGED
@@ -4,13 +4,12 @@ import path from 'node:path';
4
4
  import { Runtime } from '@travetto/runtime';
5
5
  import type { SchemaClassConfig, SchemaFieldConfig, SchemaInputConfig } from '@travetto/schema';
6
6
 
7
- import type { ParsedState } from './types.ts';
7
+ import { HELP_FLAG, type ParsedState } from './types.ts';
8
8
 
9
9
  type ParsedInput = ParsedState['all'][number];
10
10
 
11
11
  const RAW_SEPARATOR = '--';
12
12
  const VALID_FLAG = /^-{1,2}[a-z]/i;
13
- const HELP_FLAG = /^(-h|--help)$/;
14
13
  const LONG_FLAG_WITH_EQ = /^--[a-z][^= ]+=\S+/i;
15
14
  const CONFIG_PREFIX = '+=';
16
15
  const SPACE = new Set([32, 7, 13, 10]);
@@ -95,7 +94,7 @@ export class CliParseUtil {
95
94
 
96
95
  const file = path.resolve(relativePath);
97
96
 
98
- if (!await fs.stat(file).catch(() => false)) {
97
+ if (!await fs.stat(file, { throwIfNoEntry: false })) {
99
98
  throw new Error(`Missing flag file: ${key}, unable to proceed`);
100
99
  }
101
100
 
@@ -124,9 +123,9 @@ export class CliParseUtil {
124
123
  const max = out.includes(RAW_SEPARATOR) ? out.indexOf(RAW_SEPARATOR) : out.length;
125
124
  const valid = out.slice(0, max);
126
125
  const cmd = valid.length > 0 && !valid[0].startsWith('-') ? valid[0] : undefined;
127
- const helpIdx = valid.findIndex(flag => HELP_FLAG.test(flag));
126
+ const help = valid.includes(HELP_FLAG);
128
127
  const args = out.slice(cmd ? 1 : 0);
129
- const result = { cmd, args, help: helpIdx >= 0 };
128
+ const result = { cmd, args, help };
130
129
  return result;
131
130
  }
132
131
 
@@ -10,13 +10,6 @@ import { CliUtil } from '../util.ts';
10
10
  type CliCommandConfigOptions = { runTarget?: boolean };
11
11
  type CliFlagOptions = { full?: string, short?: string, envVars?: string[] };
12
12
 
13
- function runBeforeMain<T>(cls: Class, handler: (item: T) => (unknown | Promise<unknown>), runTarget?: boolean): void {
14
- CliCommandRegistryIndex.getForRegister(cls).register({
15
- runTarget,
16
- preMain: [async (cmd): Promise<void> => { await handler(castTo(cmd)); }]
17
- });
18
- }
19
-
20
13
  /**
21
14
  * Decorator to register a CLI command
22
15
  *
@@ -72,9 +65,7 @@ export function CliProfilesFlag(config: CliFlagOptions = {}) {
72
65
  description: 'Application profiles'
73
66
  });
74
67
 
75
- runBeforeMain(cls, (cmd: typeof instance) =>
76
- Env.TRV_PROFILES.set([...cmd[property] ?? [], ...(Env.TRV_PROFILES.list ?? [])])
77
- );
68
+ CliCommandRegistryIndex.registerPreMain<typeof instance>(cls, 1, cmd => Env.TRV_PROFILES.add(...cmd[property] ?? []));
78
69
  };
79
70
  };
80
71
 
@@ -103,12 +94,8 @@ export function CliModuleFlag(config: CliFlagOptions & { scope?: 'current' | 'co
103
94
  const runModule = (config.scope === 'command' ? commandModule : providedModule) || Runtime.main.name;
104
95
 
105
96
  // If we need to run as a specific module
106
- if (runModule !== Runtime.main.name) {
107
- try {
108
- RuntimeIndex.reinitForModule(runModule);
109
- } catch {
110
- return { source: 'flag', message: `${runModule} is an unknown module`, kind: 'custom', path: property };
111
- }
97
+ if (runModule !== Runtime.main.name && RuntimeIndex.getModule(runModule) === undefined) {
98
+ return { source: 'flag', message: `${runModule} is an unknown module`, kind: 'custom', path: property };
112
99
  }
113
100
 
114
101
  if (!(await CliModuleUtil.moduleHasDependency(runModule, commandModule))) {
@@ -116,6 +103,15 @@ export function CliModuleFlag(config: CliFlagOptions & { scope?: 'current' | 'co
116
103
  }
117
104
  }],
118
105
  });
106
+
107
+ CliCommandRegistryIndex.registerPreMain<typeof instance>(cls, 5, cmd => {
108
+ const typed: (typeof cmd) & { [property]?: string } = castTo(cmd);
109
+ const providedModule = typed[property];
110
+ const runModule = (config.scope === 'command' ? commandModule : providedModule) || Runtime.main.name;
111
+ if (runModule !== Runtime.main.name) {
112
+ RuntimeIndex.reinitForModule(runModule);
113
+ }
114
+ });
119
115
  };
120
116
  }
121
117
 
@@ -127,14 +123,15 @@ export function CliModuleFlag(config: CliFlagOptions & { scope?: 'current' | 'co
127
123
  export function CliRestartOnChangeFlag(config: CliFlagOptions = {}) {
128
124
  return function <K extends string, T extends Partial<Record<K, boolean>>>(instance: T, property: K): void {
129
125
  const cls = getClass(instance);
126
+ if (Runtime.production) { return; }
127
+
130
128
  SchemaRegistryIndex.getForRegister(cls).registerField(property, {
131
129
  ...CliParseUtil.buildAliases(config),
132
- description: 'Should the invocation automatically restart on source changes',
133
- default: Runtime.localDevelopment,
134
- required: { active: false },
130
+ description: 'Should the invocation automatically restart on source changes'
135
131
  });
136
132
 
137
- runBeforeMain(cls, (cmd: typeof instance) => CliUtil.runWithRestartOnChange(cmd[property]), true);
133
+ CliCommandRegistryIndex.getForRegister(cls).register({ runTarget: true });
134
+ CliCommandRegistryIndex.registerPreMain<typeof instance>(cls, 20, cmd => CliUtil.runWithRestartOnChange(cmd[property]));
138
135
  };
139
136
  }
140
137
 
@@ -145,20 +142,18 @@ export function CliRestartOnChangeFlag(config: CliFlagOptions = {}) {
145
142
  */
146
143
  export function CliDebugIpcFlag(config: CliFlagOptions = {}) {
147
144
  return function <K extends string, T extends Partial<Record<K, boolean>>>(instance: T, property: K): void {
145
+ if (Runtime.production) { return; }
146
+
148
147
  const cls = getClass(instance);
149
148
  SchemaRegistryIndex.getForRegister(cls).registerField(property, {
150
149
  ...CliParseUtil.buildAliases(config, Env.TRV_DEBUG_IPC.key),
151
- description: 'Should the invocation automatically restart on source changes',
152
- default: Runtime.localDevelopment,
153
- required: { active: false },
150
+ description: 'Should the invocation automatically restart on source changes'
154
151
  });
155
152
 
156
- runBeforeMain(cls,
157
- (cmd: typeof instance & CliCommandShape) => {
158
- const cliConfig = CliCommandRegistryIndex.get(cls);
159
- return cmd[property] && CliUtil.runWithDebugIpc(cliConfig.name);
160
- },
161
- true
162
- );
153
+ CliCommandRegistryIndex.getForRegister(cls).register({ runTarget: true });
154
+ CliCommandRegistryIndex.registerPreMain<typeof instance>(cls, 10, cmd => {
155
+ const cliConfig = CliCommandRegistryIndex.get(cls);
156
+ return cmd[property] && CliUtil.runWithDebugIpc(cliConfig.name);
157
+ });
163
158
  };
164
159
  }
@@ -29,22 +29,10 @@ export class CliCommandRegistryAdapter implements RegistryAdapter<CliCommandConf
29
29
  this.#cls = cls;
30
30
  }
31
31
 
32
- finalize(): void {
32
+ // TODO: handle when aliases overlap/conflict
33
+ finalize(parent?: CliCommandConfig): void {
33
34
  // Add help command
34
35
  const schema = SchemaRegistryIndex.getConfig(this.#cls);
35
-
36
- // Add help to every command
37
- (schema.fields ??= {}).help = {
38
- type: Boolean,
39
- name: 'help',
40
- class: this.#cls,
41
- description: 'display help for command',
42
- required: { active: false },
43
- default: false,
44
- access: 'readonly',
45
- aliases: ['-h', '--help']
46
- };
47
-
48
36
  const used = new Set(Object.values(schema.fields)
49
37
  .flatMap(field => field.aliases ?? [])
50
38
  .filter(alias => !alias.startsWith(ENV_PREFIX))
@@ -75,6 +63,13 @@ export class CliCommandRegistryAdapter implements RegistryAdapter<CliCommandConf
75
63
  aliases.push(`--no-${long}`);
76
64
  }
77
65
  }
66
+
67
+ if (parent) {
68
+ this.#config.preMain = [...this.#config.preMain, ...parent?.preMain ?? []];
69
+ }
70
+
71
+ // Sort
72
+ this.#config.preMain = this.#config.preMain.toSorted((left, right) => left.priority - right.priority);
78
73
  }
79
74
 
80
75
  get(): CliCommandConfig {
@@ -86,7 +81,7 @@ export class CliCommandRegistryAdapter implements RegistryAdapter<CliCommandConf
86
81
  */
87
82
  register(...configs: Partial<CliCommandConfig>[]): CliCommandConfig {
88
83
  const metadata = describeFunction(this.#cls);
89
- this.#config ??= { cls: this.#cls, name: getName(metadata.import), preMain: [], runTarget: true };
84
+ this.#config ??= { cls: this.#cls, name: getName(metadata.import), preMain: [], runTarget: false };
90
85
  return combineClasses(this.#config, ...configs);
91
86
  }
92
87
 
@@ -1,8 +1,8 @@
1
- import { type Class, getClass, getParentClass, isClass, Runtime, RuntimeIndex } from '@travetto/runtime';
1
+ import { type Any, type Class, getClass, getParentClass, isClass, Runtime, RuntimeIndex } from '@travetto/runtime';
2
2
  import { type RegistryAdapter, type RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
3
3
  import { type SchemaClassConfig, SchemaRegistryIndex } from '@travetto/schema';
4
4
 
5
- import type { CliCommandConfig, CliCommandShape } from '../types.ts';
5
+ import type { CliCommandConfig, CliCommandShape, PreMainHandler } from '../types.ts';
6
6
  import { CliCommandRegistryAdapter } from './registry-adapter.ts';
7
7
 
8
8
  const CLI_FILE_REGEX = /\/cli[.](?<name>.{0,100}?)([.]tsx?)?$/;
@@ -28,6 +28,10 @@ export class CliCommandRegistryIndex implements RegistryIndex {
28
28
  return this.#instance.load(names);
29
29
  }
30
30
 
31
+ static registerPreMain<T = Any>(cls: Class, priority: number, handler: PreMainHandler<T>['handler']): void {
32
+ CliCommandRegistryIndex.getForRegister(cls).register({ preMain: [{ handler, priority }] });
33
+ }
34
+
31
35
  #fileMapping: Map<string, string>;
32
36
  #instanceMapping: Map<string, CliCommandShape> = new Map();
33
37
 
package/src/types.ts CHANGED
@@ -1,4 +1,6 @@
1
- import type { Class } from '@travetto/runtime';
1
+ import type { Any, Class } from '@travetto/runtime';
2
+
3
+ export const HELP_FLAG = '--help';
2
4
 
3
5
  type OrProm<T> = T | Promise<T>;
4
6
  type ParsedFlag = { type: 'flag', input: string, array?: boolean, fieldName: string, value?: unknown };
@@ -32,7 +34,7 @@ export interface CliCommandShape {
32
34
  help?(): OrProm<string[]>;
33
35
  }
34
36
 
35
- type PreMainHandler = (cmd: CliCommandShape) => (unknown | Promise<unknown>);
37
+ export type PreMainHandler<T extends Any = Any> = { priority: number, handler: (cmd: T) => Any };
36
38
 
37
39
  /**
38
40
  * CLI Command schema shape
@@ -41,5 +43,5 @@ export interface CliCommandConfig {
41
43
  cls: Class<CliCommandShape>;
42
44
  name: string;
43
45
  runTarget?: boolean;
44
- preMain?: PreMainHandler[];
46
+ preMain: PreMainHandler[];
45
47
  }
package/src/util.ts CHANGED
@@ -2,11 +2,12 @@ import { spawn, type ChildProcess } from 'node:child_process';
2
2
 
3
3
  import { RuntimeError, JSONUtil, Env, ExecUtil, Runtime, ShutdownManager, Util, WatchUtil } from '@travetto/runtime';
4
4
 
5
- const IPC_ALLOWED_ENV = new Set(['NODE_OPTIONS']);
6
- const IPC_INVALID_ENV = new Set(['PS1', 'INIT_CWD', 'COLOR', 'LANGUAGE', 'PROFILEHOME', '_']);
7
- const validEnv = (key: string): boolean => IPC_ALLOWED_ENV.has(key) || (
8
- !IPC_INVALID_ENV.has(key) && !/^(npm_|GTK|GDK|TRV|NODE|GIT|TERM_)/.test(key) && !/VSCODE/.test(key)
9
- );
5
+ const IPC_VALID_ENV = new Set(['NODE_OPTIONS', 'PATH', Env.DEBUG.key, Env.NODE_ENV.key]);
6
+ const IPC_INVALID_ENV = new Set([
7
+ Env.TRV_CLI_IPC, Env.TRV_DEBUG_IPC, Env.TRV_DEBUG_BREAK, Env.TRV_MANIFEST, Env.TRV_MODULE, Env.TRV_RESTART_TARGET
8
+ ].map(item => item.key));
9
+ const validEnv = ([key]: [key: string, value: unknown]): boolean =>
10
+ IPC_VALID_ENV.has(key) || (key.startsWith('TRV_') && !IPC_INVALID_ENV.has(key));
10
11
 
11
12
  export class CliUtil {
12
13
  /**
@@ -14,8 +15,7 @@ export class CliUtil {
14
15
  */
15
16
  static getSimpleModuleName(placeholder: string, module?: string): string {
16
17
  const simple = (module ?? Runtime.main.name).replace(/[\/]/, '_').replace(/@/, '');
17
- const targetModule = !simple || (!module && Runtime.monoRoot) ? '<module>' : simple;
18
- return placeholder.replace('<module>', targetModule);
18
+ return simple ? placeholder.replace('<module>', simple) : placeholder;
19
19
  }
20
20
 
21
21
  /**
@@ -33,10 +33,11 @@ export class CliUtil {
33
33
  ShutdownManager.disableInterrupt();
34
34
 
35
35
  let child: ChildProcess | undefined;
36
- void WatchUtil.watchCompilerEvents('file', () => ShutdownManager.shutdownChild(child!, { reason: 'restart', mode: 'exit' }));
36
+ await WatchUtil.watchCompilerEvents('file', () => ShutdownManager.shutdownChild(child!, { reason: 'restart', mode: 'exit' }));
37
+
37
38
  process
38
39
  .on('SIGINT', () => ShutdownManager.shutdownChild(child!, { mode: 'exit' }))
39
- .on('message', msg => child?.send?.(msg!));
40
+ .on('message', message => child?.send?.(message!));
40
41
 
41
42
  const env = { ...process.env, ...Env.TRV_RESTART_TARGET.export(true) };
42
43
 
@@ -77,32 +78,32 @@ export class CliUtil {
77
78
  return; // Server not running, run normal
78
79
  }
79
80
 
80
- const env: Record<string, string> = {};
81
81
  const request = {
82
82
  type: '@travetto/cli:run',
83
83
  data: {
84
84
  name,
85
- env,
85
+ env: Object.fromEntries(Object.entries(process.env).filter(validEnv)),
86
86
  cwd: process.cwd(),
87
87
  args: process.argv.slice(3),
88
88
  }
89
89
  };
90
- console.log('Triggering IPC request', request);
91
90
 
92
- Object.entries(process.env).forEach(([key, value]) => validEnv(key) && (env[key] = value!));
91
+ console.log('Triggering IPC request', request);
93
92
  const sent = await doFetch({ method: 'POST', body: JSONUtil.toUTF8(request) });
94
93
 
95
94
  if (!sent.ok) {
96
95
  throw new RuntimeError(`IPC Request failed: ${sent.status} ${await sent.text()}`);
97
96
  }
97
+
98
+ await ShutdownManager.shutdown({ mode: 'exit' });
98
99
  }
99
100
 
100
101
  /**
101
102
  * Write data to channel and ensure its flushed before continuing
102
103
  */
103
104
  static async writeAndEnsureComplete(data: unknown, channel: 'stdout' | 'stderr' = 'stdout'): Promise<void> {
104
- return await new Promise(resolve => process[channel].write(typeof data === 'string' ? data :
105
- JSONUtil.toUTF8Pretty(data), () => resolve()));
105
+ await new Promise<unknown>(resolve => process[channel].write(typeof data === 'string' ? data :
106
+ JSONUtil.toUTF8Pretty(data), resolve));
106
107
  }
107
108
 
108
109
  /**
@@ -45,15 +45,15 @@ export class CliServiceCommand implements CliCommandShape {
45
45
  const jobs = all.map(async (descriptor, i) => {
46
46
  const identifier = descriptor.name.padEnd(maxName);
47
47
  const type = `${descriptor.version}`.padStart(maxVersion - 3).padEnd(maxVersion);
48
- let msg: string;
48
+ let message: string;
49
49
  for await (const [valueType, value] of new ServiceRunner(descriptor).action(action)) {
50
50
  const details = { [valueType === 'message' ? 'subtitle' : valueType]: value };
51
- queue.add({ idx: i, text: msg = cliTpl`${{ identifier }} ${{ type }} ${details}` });
51
+ queue.add({ idx: i, text: message = cliTpl`${{ identifier }} ${{ type }} ${details}` });
52
52
  if (valueType === 'failure') {
53
- failureMessages.push(msg);
53
+ failureMessages.push(message);
54
54
  }
55
55
  }
56
- queue.add({ idx: i, done: true, text: msg! });
56
+ queue.add({ idx: i, done: true, text: message! });
57
57
  });
58
58
 
59
59
  Promise.all(jobs).then(() => Util.queueMacroTask()).then(() => queue.close());
@@ -1,4 +1,4 @@
1
1
  // @trv-no-transform
2
- import '@travetto/runtime/support/polyfill.js';
2
+ import '@travetto/runtime/support/patch.js';
3
3
  import { ExecutionManager } from '@travetto/cli';
4
4
  ExecutionManager.run(process.argv);