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

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
@@ -22,34 +22,38 @@ $ trv --help
22
22
  Usage: [options] [command]
23
23
 
24
24
  Commands:
25
- doc Command line support for generating module docs.
26
- doc:angular Generate documentation into the angular webapp under related/travetto.github.io
27
- doc:mapping Generate module mapping for
28
- email:compile CLI Entry point for running the email server
29
- email:editor The email editor compilation service and output serving
30
- email:test CLI Entry point for running the email server
31
- eslint Command line support for eslint
32
- eslint:register Writes the eslint configuration file
33
- model:export Exports model schemas
34
- model:install Installing models
35
- openapi:client CLI for generating the cli client
36
- openapi:spec CLI for outputting the open api spec to a local file
37
- pack Standard pack support
38
- pack:docker Standard docker support for pack
39
- pack:lambda Standard lambda support for pack
40
- pack:zip Standard zip support for pack
41
- repo:exec Repo execution
42
- repo:list Allows for listing of modules
43
- repo:publish Publish all pending modules
44
- repo:version Version all changed dependencies
45
- repo:version-sync Enforces all packages to write out their versions and dependencies
46
- run:double Doubles a number
47
- scaffold Command to run scaffolding
48
- service Allows for running services
49
- test Launch test framework and execute tests
50
- test:watch Invoke the test watcher
51
- web:http Run a web server
52
- web:rpc-client Generate the web-rpc client
25
+ doc Generate documentation outputs from a module `DOC.tsx` entry file.
26
+ doc:angular Generate documentation into the angular webapp under related/travetto.github.io
27
+ doc:mapping Generate module mapping for
28
+ email:compile Compile all email templates into generated runtime artifacts.
29
+ email:editor Start the email template editor service for interactive preview and testing.
30
+ email:test Render and send a template file to a target recipient for quick validation.
31
+ eslint Run ESLint for the workspace or changed files.
32
+ eslint:register Generate the workspace ESLint configuration entry file.
33
+ llm:support:execute Execute llm-support operations with dry-run by default.
34
+ llm:support:mcp Minimal MCP stdio server for llm-support tools.
35
+ llm:support:plan Build plan-first execution details for llm-support operations.
36
+ llm:support:recommend Recommend llm-support bundles, workflows, and operations.
37
+ llm:support:status Show llm-support execution coverage status.
38
+ model:export Export model definitions for a selected provider and model set.
39
+ model:install Install or update model definitions for a selected provider.
40
+ openapi:client Generate API clients from an OpenAPI specification using the generator image.
41
+ openapi:spec Generate the OpenAPI specification for the selected module.
42
+ pack Build a standard module package artifact.
43
+ pack:docker Build container-ready artifacts and optionally publish Docker images.
44
+ pack:lambda Build an AWS Lambda-ready zip package using the pack pipeline.
45
+ pack:zip Build a deployable zip artifact using the standard pack pipeline.
46
+ repo:exec Execute a shell command across workspace modules.
47
+ repo:list List workspace modules and their relationships.
48
+ repo:publish Publish unpublished workspace modules to the package registry.
49
+ repo:version Bump workspace module versions and optionally commit/tag release metadata.
50
+ repo:version-sync Synchronize package versions and dependency ranges across the monorepo.
51
+ run:double Doubles a number
52
+ service Manage development services (start/stop/restart/status) across the workspace.
53
+ test Execute the test framework for targeted files, suites, or methods.
54
+ test:watch Start the test watcher for continuous test execution.
55
+ web:http Start the configured web HTTP server for a module.
56
+ web:rpc-client Generate web-rpc client artifacts from a specified provider or leveraging local config.
53
57
  ```
54
58
 
55
59
  This listing is from the [Travetto](https://travetto.dev) monorepo, and represents the majority of tools that can be invoked from the command line.
@@ -59,7 +63,7 @@ This module also has a tight integration with the [VSCode plugin](https://market
59
63
  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
64
  * The file must be located in the `support/` folder, and have a name that matches `cli.*.ts`
61
65
  * 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
66
+ * The class must use the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L20) decorator
63
67
 
64
68
  **Code: Basic Command**
65
69
  ```typescript
@@ -75,12 +79,12 @@ export class BasicCommand {
75
79
 
76
80
  **Terminal: Basic Command Help**
77
81
  ```bash
78
- $ trv basic -h
82
+ $ trv basic --help
79
83
 
80
84
  Usage: basic [options]
81
85
 
82
86
  Options:
83
- -h, --help display help for command
87
+ --help display help for command
84
88
  ```
85
89
 
86
90
  ## Command Naming
@@ -94,7 +98,7 @@ Examples of mappings:
94
98
  The pattern is that underscores(_) translate to colons (:), and the `cli.` prefix, and `.ts` suffix are dropped.
95
99
 
96
100
  ## 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.
101
+ [@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
102
 
99
103
  **Code: Basic Command with Flag**
100
104
  ```typescript
@@ -113,13 +117,13 @@ export class BasicCommand {
113
117
 
114
118
  **Terminal: Basic Command with Flag Help**
115
119
  ```bash
116
- $ trv basic:flag -h
120
+ $ trv basic:flag --help
117
121
 
118
122
  Usage: basic:flag [options]
119
123
 
120
124
  Options:
121
125
  -l, --loud
122
- -h, --help display help for command
126
+ --help display help for command
123
127
  ```
124
128
 
125
129
  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 +135,7 @@ $ trv basic:flag --loud
131
135
  HELLO
132
136
  ```
133
137
 
134
- The [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L27) supports the following data types for flags:
138
+ The [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L20) supports the following data types for flags:
135
139
  * Boolean values
136
140
  * 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
141
  * 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 +162,12 @@ export class BasicCommand {
158
162
 
159
163
  **Terminal: Basic Command**
160
164
  ```bash
161
- $ trv basic:arg -h
165
+ $ trv basic:arg --help
162
166
 
163
167
  Usage: basic:arg [options] [volume:number]
164
168
 
165
169
  Options:
166
- -h, --help display help for command
170
+ --help display help for command
167
171
  ```
168
172
 
169
173
  **Terminal: Basic Command with Invalid Loud Arg**
@@ -176,7 +180,7 @@ Execution failed:
176
180
  Usage: basic:arg [options] [volume:number]
177
181
 
178
182
  Options:
179
- -h, --help display help for command
183
+ --help display help for command
180
184
  ```
181
185
 
182
186
  **Terminal: Basic Command with Loud Arg > 7**
@@ -213,13 +217,13 @@ export class BasicCommand {
213
217
 
214
218
  **Terminal: Basic Command**
215
219
  ```bash
216
- $ trv basic:arg-list -h
220
+ $ trv basic:arg-list --help
217
221
 
218
222
  Usage: basic:arg-list [options] <volumes...:number>
219
223
 
220
224
  Options:
221
225
  -r, --reverse
222
- -h, --help display help for command
226
+ --help display help for command
223
227
  ```
224
228
 
225
229
  **Terminal: Basic Arg List**
@@ -240,7 +244,7 @@ Usage: basic:arg-list [options] <volumes...:number>
240
244
 
241
245
  Options:
242
246
  -r, --reverse
243
- -h, --help display help for command
247
+ --help display help for command
244
248
  ```
245
249
 
246
250
  **Terminal: Basic Arg List with Reverse**
@@ -259,7 +263,7 @@ import { CliCommand } from '@travetto/cli';
259
263
  import { Max, Min } from '@travetto/schema';
260
264
 
261
265
  /**
262
- * Custom Argument Command
266
+ * Example command with a custom argument
263
267
  */
264
268
  @CliCommand()
265
269
  export class CustomCommand {
@@ -279,13 +283,15 @@ export class CustomCommand {
279
283
 
280
284
  **Terminal: Custom Command Help**
281
285
  ```bash
282
- $ trv custom:arg -h
286
+ $ trv custom:arg --help
283
287
 
284
288
  Usage: custom:arg [options] [volume:number]
285
289
 
290
+ Example command with a custom argument
291
+
286
292
  Options:
287
293
  -m, --message <string> The message to send back to the user (default: "hello")
288
- -h, --help display help for command
294
+ --help display help for command
289
295
  ```
290
296
 
291
297
  **Terminal: Custom Command Help with overridden Text**
@@ -311,7 +317,7 @@ import { CliCommand } from '@travetto/cli';
311
317
  import { Max, Min } from '@travetto/schema';
312
318
 
313
319
  /**
314
- * Custom Argument Command
320
+ * Example of a command with a custom environment variable argument
315
321
  */
316
322
  @CliCommand()
317
323
  export class CustomCommand {
@@ -330,13 +336,15 @@ export class CustomCommand {
330
336
 
331
337
  **Terminal: Custom Command Help**
332
338
  ```bash
333
- $ trv custom:env-arg -h
339
+ $ trv custom:env-arg --help
334
340
 
335
341
  Usage: custom:env-arg [options] [volume:number]
336
342
 
343
+ Example of a command with a custom environment variable argument
344
+
337
345
  Options:
338
346
  -t, --text <string> The message to send back to the user (default: "hello")
339
- -h, --help display help for command
347
+ --help display help for command
340
348
  ```
341
349
 
342
350
  **Terminal: Custom Command Help with default Text**
@@ -390,7 +398,7 @@ npx trv call:db --host localhost --port 3306 --username app --password <custom>
390
398
  ```
391
399
 
392
400
  ## 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.
401
+ 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
402
 
395
403
  **Code: Simple Run Target**
396
404
  ```typescript
@@ -442,7 +450,12 @@ import { Registry } from '@travetto/registry';
442
450
  import type { WebHttpServer } from '../src/types.ts';
443
451
 
444
452
  /**
445
- * Run a web server
453
+ * Start the configured web HTTP server for a module.
454
+ *
455
+ * Initializes registry and server bindings, supports restart-aware development
456
+ * flags, and can attempt to clear conflicting port owners in local workflows.
457
+ *
458
+ * @example trv web:http -m <MODULE> -p 3000
446
459
  */
447
460
  @CliCommand()
448
461
  export class WebHttpCommand implements CliCommandShape {
@@ -460,7 +473,7 @@ export class WebHttpCommand implements CliCommandShape {
460
473
  profile: string[];
461
474
 
462
475
  @CliRestartOnChangeFlag()
463
- restartOnChange: boolean = true;
476
+ restartOnChange: boolean = Runtime.localDevelopment;
464
477
 
465
478
  @CliDebugIpcFlag()
466
479
  debugIpc?: boolean;
@@ -542,7 +555,7 @@ A simple example of the validation can be found in the `doc` command:
542
555
  ```typescript
543
556
  @Validator(async (cmd) => {
544
557
  const docFile = path.resolve(cmd.input);
545
- if (!(await fs.stat(docFile).catch(() => false))) {
558
+ if (!(await fs.stat(docFile, { throwIfNoEntry: false }))) {
546
559
  return { message: `input: ${cmd.input} does not exist`, path: 'input', source: 'flag', kind: 'invalid' };
547
560
  }
548
561
  })
@@ -551,26 +564,31 @@ A simple example of the validation can be found in the `doc` command:
551
564
  ## CLI - service
552
565
  The module provides the ability to start/stop/restart services as [docker](https://www.docker.com/community-edition) containers. This is meant to be used for development purposes, to minimize the effort of getting an application up and running. Services can be targeted individually or handled as a group.
553
566
 
554
- **Terminal: Command Service**
567
+ **Terminal: Help for service**
555
568
  ```bash
556
569
  $ trv service --help
557
570
 
558
571
  Usage: service [options] <action:restart|start|status|stop> [services...:string]
559
572
 
573
+ Manage development services (start/stop/restart/status) across the workspace.
574
+
575
+ Services are discovered from registered descriptors and executed with streamed
576
+ terminal feedback, including optional quiet mode.
577
+
560
578
  Options:
561
579
  -q, --quiet (default: false)
562
- -h, --help display help for command
580
+ --help display help for command
563
581
 
564
582
  Available Services
565
583
  --------------------
566
584
  * dynamodb@3.3.0
567
- * elasticsearch@9.2.4
585
+ * elasticsearch@9.2.8
568
586
  * firestore@latest
569
- * mongodb@8.2
587
+ * mongodb@8.3
570
588
  * mysql@9.6
571
- * postgresql@18.1
589
+ * postgresql@18.3
572
590
  * redis@8.4
573
- * s3@4.11.0
591
+ * s3@4.12.4
574
592
  ```
575
593
 
576
594
  A sample of all services available to the entire framework:
@@ -582,13 +600,13 @@ $ trv service status
582
600
  Service Version Status
583
601
  -------------------------------------------------
584
602
  dynamodb 3.3.0 Running 93af422e793a
585
- elasticsearch 9.2.4 Running ed76ee063d13
603
+ elasticsearch 9.2.8 Running ed76ee063d13
586
604
  firestore latest Running feec2e5e95b4
587
- mongodb 8.2 Running 5513eba6734e
605
+ mongodb 8.3 Running 5513eba6734e
588
606
  mysql 9.6 Running 307bc66d442a
589
- postgresql 18.1 Running e78291e71040
607
+ postgresql 18.3 Running e78291e71040
590
608
  redis 8.4 Running 77ba279b4e30
591
- s3 4.11.0 Running fdacfc55b9e3
609
+ s3 4.12.4 Running fdacfc55b9e3
592
610
  ```
593
611
 
594
612
  ### Defining new Services
@@ -598,12 +616,16 @@ The services are defined as plain typescript files within the framework and can
598
616
  ```typescript
599
617
  import type { ServiceDescriptor } from '@travetto/cli';
600
618
 
601
- const version = process.env.MONGO_VERSION || '8.2';
619
+ const version = process.env.MONGO_VERSION || '8.3';
602
620
 
603
621
  export const service: ServiceDescriptor = {
604
622
  name: 'mongodb',
605
623
  version,
606
624
  port: 27017,
607
- image: `mongo:${version}`
625
+ image: `mongo:${version}`,
626
+ env: {
627
+ // Temp until mongo image fixes orbstack issue
628
+ GLIBC_TUNABLES: 'glibc.pthread.rseq=1'
629
+ }
608
630
  };
609
631
  ```
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.22",
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.17",
33
+ "@travetto/terminal": "^8.0.0-alpha.16"
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
@@ -1,72 +1,73 @@
1
1
  import util from 'node:util';
2
2
 
3
- import { castKey, getClass, JSONUtil, Runtime } from '@travetto/runtime';
3
+ import { castKey, CodecUtil, 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
36
33
  */
37
34
  export class HelpUtil {
38
35
 
39
- /** Render the unknown command message */
40
- static renderUnknownCommandMessage(cmd: string): string {
41
- const install = INSTALL_COMMANDS.get(cmd);
42
- if (install) {
43
- return cliTpl`
44
- ${{ title: 'Missing Package' }}\n${'-'.repeat(20)}\nTo use ${{ input: cmd }} please run:\n
45
- ${{ identifier: install }}
46
- `;
47
- } else {
48
- return cliTpl`${{ subtitle: 'Unknown command' }}: ${{ input: cmd }}`;
49
- }
50
- }
51
-
52
- /**
53
- * Render command-specific help
54
- * @param command
55
- */
56
- static async renderCommandHelp(command: CliCommandShape): Promise<string> {
36
+ /** Get usage help for a command */
37
+ static getUsageMessage(command: CliCommandShape): string[] {
57
38
  const schema = SchemaRegistryIndex.getConfig(getClass(command));
58
39
  const { name: commandName } = CliCommandRegistryIndex.get(getClass(command));
59
- const args = schema.methods.main?.parameters ?? [];
60
40
 
61
- // Ensure finalized
41
+ const usage: string[] = [];
42
+
43
+ usage.push(
44
+ cliTpl`${{ title: 'Usage:' }} ${{ param: commandName }} ${{ input: '[options]' }}`
45
+ );
62
46
 
63
- const usage: string[] = [cliTpl`${{ title: 'Usage:' }} ${{ param: commandName }} ${{ input: '[options]' }}`,];
64
- for (const field of args) {
47
+ // Ensure finalized
48
+ for (const field of schema.methods.main?.parameters ?? []) {
65
49
  const type = field.type === String && field.enum && field.enum?.values.length <= 7 ? field.enum?.values?.join('|') : field.type.name.toLowerCase();
66
50
  const arg = `${field.name}${field.array ? '...' : ''}:${type}`;
67
51
  usage.push(cliTpl`${{ input: field.required?.active !== false ? `<${arg}>` : `[${arg}]` }}`);
68
52
  }
53
+ usage.push('');
54
+ return usage;
55
+ }
56
+
57
+ /** Get description help for a command */
58
+ static getDescriptionMessage(command: CliCommandShape): string[] {
59
+ const schema = SchemaRegistryIndex.getConfig(getClass(command));
60
+ const description: string[] = [];
61
+
62
+ if (schema.description) {
63
+ description.push(cliTpl`${{ title: 'Description:' }}`, ...schema.description.split('\n').map(line => ` ${line}`), '');
64
+ }
65
+ return description;
66
+ }
69
67
 
68
+ /** Get options help for a command */
69
+ static getOptionsMessage(command: CliCommandShape): string[] {
70
+ const schema = SchemaRegistryIndex.getConfig(getClass(command));
70
71
  const params: string[] = [];
71
72
  const descriptions: string[] = [];
72
73
 
@@ -76,7 +77,7 @@ ${{ identifier: install }}
76
77
  const aliases = (field.aliases ?? [])
77
78
  .filter(flag => flag.startsWith('-'))
78
79
  .filter(flag =>
79
- (field.type !== Boolean) || ((defaultValue !== true || field.name === 'help') ? !flag.startsWith('--no-') : flag.startsWith('--'))
80
+ (field.type !== Boolean) || (defaultValue !== true ? !flag.startsWith('--no-') : flag.startsWith('--'))
80
81
  );
81
82
  let type: string | undefined;
82
83
 
@@ -86,40 +87,92 @@ ${{ identifier: install }}
86
87
  ({ type } = CliSchemaExportUtil.baseInputType(field));
87
88
  }
88
89
 
89
- const param = [
90
+ const parameter = [
90
91
  cliTpl`${{ param: aliases.join(', ') }}`,
91
92
  ...(type ? [cliTpl`${{ type: `<${type}>` }}`] : []),
92
93
  ];
93
94
 
94
- params.push(param.join(' '));
95
- const desc = [cliTpl`${{ title: field.description }}`];
95
+ params.push(parameter.join(' '));
96
+ const parts = [cliTpl`${{ title: field.description }}`];
96
97
 
97
- if (key !== 'help' && defaultValue !== undefined) {
98
- desc.push(cliTpl`(default: ${{ input: JSONUtil.toUTF8(defaultValue) }})`);
98
+ if (defaultValue !== undefined) {
99
+ parts.push(cliTpl`(default: ${{ input: JSONUtil.toUTF8(defaultValue) }})`);
99
100
  }
100
- descriptions.push(desc.join(' '));
101
+ descriptions.push(parts.join(' '));
101
102
  }
102
103
 
104
+ params.push(cliTpl`${{ param: HELP_FLAG }}`);
105
+ descriptions.push('display help for command');
106
+
107
+
103
108
  const paramWidths = params.map(item => util.stripVTControlCharacters(item).length);
104
109
  const descWidths = descriptions.map(item => util.stripVTControlCharacters(item).length);
105
110
 
106
111
  const paramWidth = Math.max(...paramWidths);
107
112
  const descWidth = Math.max(...descWidths);
108
113
 
109
- const helpText = await (command.help?.() ?? []);
110
- if (helpText.length && helpText.at(-1) !== '') {
111
- helpText.push('');
112
- }
113
-
114
- return [
115
- usage.join(' '),
116
- '',
114
+ const options: string[] = [
117
115
  cliTpl`${{ title: 'Options:' }}`,
118
116
  ...params.map((_, i) =>
119
117
  ` ${params[i]}${' '.repeat((paramWidth - paramWidths[i]))} ${descriptions[i].padEnd(descWidth)}${' '.repeat((descWidth - descWidths[i]))}`
120
118
  ),
121
- '',
122
- ...helpText
119
+ ''
120
+ ];
121
+ return options;
122
+ }
123
+
124
+ /** Get extended help for a command */
125
+ static async getExtendedHelpMessage(command: CliCommandShape): Promise<string[]> {
126
+ const extendedHelpText = await (command.help?.() ?? []);
127
+ if (extendedHelpText.length && extendedHelpText.at(-1) !== '') {
128
+ extendedHelpText.push('');
129
+ }
130
+ return extendedHelpText;
131
+ }
132
+
133
+ /** Get examples for a command */
134
+ static getExamplesMessage(command: CliCommandShape): string[] {
135
+ const schema = SchemaRegistryIndex.getConfig(getClass(command));
136
+ const examples: string[] = [];
137
+ if (schema.examples) {
138
+ examples.push(cliTpl`${{ title: 'Examples:' }}`);
139
+ for (const example of schema.examples) {
140
+ for (const line of example.split('\n')) {
141
+ examples.push(
142
+ line.trim().startsWith('>') ?
143
+ cliTpl` ${{ input: line.substring(line.indexOf('> ') + 2).trim() }}` :
144
+ cliTpl` ${{ subtitle: line.trim() }}`);
145
+ }
146
+ }
147
+ examples.push('');
148
+ }
149
+ return examples;
150
+ }
151
+
152
+ /** Render the unknown command message */
153
+ static renderUnknownCommandMessage(command: string): string {
154
+ const module = COMMAND_TO_MODULE[command];
155
+ if (module) {
156
+ return cliTpl`
157
+ ${{ title: 'Missing Package' }}\n${'-'.repeat(20)}\nTo use ${{ input: command }} please run:\n
158
+ ${{ identifier: Runtime.getInstallCommand(module) }}
159
+ `;
160
+ } else {
161
+ return cliTpl`${{ subtitle: 'Unknown command' }}: ${{ input: command }}`;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Render command-specific help
167
+ * @param command
168
+ */
169
+ static async renderCommandHelp(command: CliCommandShape): Promise<string> {
170
+ return [
171
+ ...this.getUsageMessage(command),
172
+ ...this.getDescriptionMessage(command),
173
+ ...this.getOptionsMessage(command),
174
+ ...await this.getExtendedHelpMessage(command),
175
+ ...this.getExamplesMessage(command)
123
176
  ].map(line => line.trimEnd()).join('\n');
124
177
  }
125
178
 
@@ -136,11 +189,13 @@ ${{ identifier: install }}
136
189
  for (const { command: cmd, schema } of resolved) {
137
190
  try {
138
191
  if (schema && !schema.private) {
139
- rows.push(cliTpl` ${{ param: cmd.padEnd(maxWidth, ' ') }} ${{ title: schema.description || '' }}`);
192
+ const description = CodecUtil.readFirstLine(schema.description, '');
193
+ rows.push(cliTpl` ${{ param: cmd.padEnd(maxWidth, ' ') }} ${{ title: description }}`);
140
194
  }
141
195
  } catch (error) {
142
196
  if (error instanceof Error) {
143
- rows.push(cliTpl` ${{ param: cmd.padEnd(maxWidth, ' ') }} ${{ failure: error.message.split(/\n/)[0] }}`);
197
+ const failure = CodecUtil.readFirstLine(error.message);
198
+ rows.push(cliTpl` ${{ param: cmd.padEnd(maxWidth, ' ') }} ${{ failure }}`);
144
199
  } else {
145
200
  throw error;
146
201
  }
@@ -175,13 +230,13 @@ ${{ identifier: install }}
175
230
  process.exitCode ??= 1;
176
231
  if (error instanceof ValidationResultError) {
177
232
  console.error!(this.renderValidationError(error));
233
+ } else if (error instanceof Error) {
234
+ console.error!(cliTpl`${{ failure: error.stack }}\n`);
178
235
  }
179
236
  if (command) {
180
237
  console.error!(await this.renderCommandHelp(command));
181
238
  } else if (error === UNKNOWN_COMMAND) {
182
239
  console.error!(this.renderUnknownCommandMessage(cmd));
183
- } else {
184
- console.error!(error);
185
240
  }
186
241
  console.error!();
187
242
  }
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
 
@@ -93,7 +84,7 @@ export function CliModuleFlag(config: CliFlagOptions & { scope?: 'current' | 'co
93
84
  ...CliParseUtil.buildAliases(config, Env.TRV_MODULE.key),
94
85
  description: 'Module to run for',
95
86
  specifiers: ['module'],
96
- required: { active: Runtime.monoRoot },
87
+ required: { active: Runtime.monoRoot && config.scope !== 'command' },
97
88
  });
98
89
 
99
90
  SchemaRegistryIndex.getForRegister(cls).register({
@@ -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 support debugging via IPC (e.g. from VSCode)'
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
 
@@ -1,7 +1,7 @@
1
1
  import { castTo, type Class, describeFunction } from '@travetto/runtime';
2
2
  import { type SchemaInputConfig, SchemaRegistryIndex } from '@travetto/schema';
3
3
 
4
- import { CliCommandRegistryIndex } from '../src/registry/registry-index.ts';
4
+ import { CliCommandRegistryIndex } from './registry/registry-index.ts';
5
5
 
6
6
  /**
7
7
  * CLI Command argument/flag shape
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
  /**
@@ -25,7 +25,10 @@ async function nameValidator(names?: string[]): Promise<ValidationError | undefi
25
25
  }
26
26
 
27
27
  /**
28
- * Generates the schema for all CLI operations
28
+ * Exports machine-readable command metadata for automation and tooling.
29
+ *
30
+ * Used by editor integrations to discover runnable commands and inputs.
31
+ * Used by guidance workflows to validate command signatures.
29
32
  */
30
33
  @CliCommand()
31
34
  @IsPrivate()
@@ -11,7 +11,10 @@ async function validateMain(fileOrImport: string): Promise<ValidationError | und
11
11
  };
12
12
 
13
13
  /**
14
- * Allows for running of main entry points
14
+ * Execute a module `main()` entrypoint directly.
15
+ *
16
+ * This internal command resolves an import/source target, invokes its exported
17
+ * `main` function, and forwards unknown CLI args to that function.
15
18
  */
16
19
  @CliCommand()
17
20
  @IsPrivate()
@@ -16,7 +16,10 @@ async function validateService(_: ServiceAction, services: string[]): Promise<Va
16
16
  }
17
17
 
18
18
  /**
19
- * Allows for running services
19
+ * Manage development services (start/stop/restart/status) across the workspace.
20
+ *
21
+ * Services are discovered from registered descriptors and executed with streamed
22
+ * terminal feedback, including optional quiet mode.
20
23
  */
21
24
  @CliCommand()
22
25
  export class CliServiceCommand implements CliCommandShape {
@@ -45,15 +48,15 @@ export class CliServiceCommand implements CliCommandShape {
45
48
  const jobs = all.map(async (descriptor, i) => {
46
49
  const identifier = descriptor.name.padEnd(maxName);
47
50
  const type = `${descriptor.version}`.padStart(maxVersion - 3).padEnd(maxVersion);
48
- let msg: string;
51
+ let message: string;
49
52
  for await (const [valueType, value] of new ServiceRunner(descriptor).action(action)) {
50
53
  const details = { [valueType === 'message' ? 'subtitle' : valueType]: value };
51
- queue.add({ idx: i, text: msg = cliTpl`${{ identifier }} ${{ type }} ${details}` });
54
+ queue.add({ idx: i, text: message = cliTpl`${{ identifier }} ${{ type }} ${details}` });
52
55
  if (valueType === 'failure') {
53
- failureMessages.push(msg);
56
+ failureMessages.push(message);
54
57
  }
55
58
  }
56
- queue.add({ idx: i, done: true, text: msg! });
59
+ queue.add({ idx: i, done: true, text: message! });
57
60
  });
58
61
 
59
62
  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);