@travetto/cli 7.1.4 → 8.0.0-alpha.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
@@ -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#L98) decorator
62
+ * The class must use the [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L27) decorator
63
63
 
64
64
  **Code: Basic Command**
65
65
  ```typescript
@@ -94,7 +94,7 @@ Examples of mappings:
94
94
  The pattern is that underscores(_) translate to colons (:), and the `cli.` prefix, and `.ts` suffix are dropped.
95
95
 
96
96
  ## Binding Flags
97
- [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L98) 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#L98) 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#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.
98
98
 
99
99
  **Code: Basic Command with Flag**
100
100
  ```typescript
@@ -131,11 +131,11 @@ $ trv basic:flag --loud
131
131
  HELLO
132
132
  ```
133
133
 
134
- The [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L98) supports the following data types for flags:
134
+ The [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#L27) supports the following data types for flags:
135
135
  * Boolean values
136
- * Number values. The [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L166), [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L173), [@Precision](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L159), [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L93) and [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L104) decorators help provide additional validation.
137
- * String values. [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L93), [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L104), [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L84) and [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L58) provide additional constraints
138
- * Date values. The [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L93) and [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L104) decorators help provide additional validation.
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
+ * 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
138
+ * Date values. The [@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.
139
139
  * String lists. Same as String, but allowing multiple values.
140
140
  * Numeric lists. Same as Number, but allowing multiple values.
141
141
 
@@ -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#L98) 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#L27) decorator. This means the target will be visible within the editor tooling.
394
394
 
395
395
  **Code: Simple Run Target**
396
396
  ```typescript
@@ -412,47 +412,19 @@ export class RunCommand {
412
412
 
413
413
  **Code: Anatomy of a Command**
414
414
  ```typescript
415
- export interface CliCommandShape<T extends unknown[] = unknown[]> {
416
- /**
417
- * Parsed state
418
- */
419
- _parsed?: ParsedState;
420
- /**
421
- * Config
422
- */
423
- _cfg?: CliCommandConfig;
415
+ export interface CliCommandShape {
424
416
  /**
425
417
  * Action target of the command
426
418
  */
427
- main(...args: T): OrProm<undefined | void>;
419
+ main(...args: unknown[]): OrProm<undefined | void>;
428
420
  /**
429
421
  * Run before main runs
430
422
  */
431
- preMain?(): OrProm<void>;
423
+ finalize?(help?: boolean): OrProm<void>;
432
424
  /**
433
425
  * Extra help
434
426
  */
435
427
  help?(): OrProm<string[]>;
436
- /**
437
- * Run before help is displayed
438
- */
439
- preHelp?(): OrProm<void>;
440
- /**
441
- * Is the command active/eligible for usage
442
- */
443
- isActive?(): boolean;
444
- /**
445
- * Run before binding occurs
446
- */
447
- preBind?(): OrProm<void>;
448
- /**
449
- * Run before validation occurs
450
- */
451
- preValidate?(): OrProm<void>;
452
- /**
453
- * Validation method
454
- */
455
- validate?(...args: T): OrProm<CliValidationError | CliValidationError[] | undefined>;
456
428
  }
457
429
  ```
458
430
 
@@ -463,7 +435,7 @@ If the goal is to run a more complex application, which may include depending on
463
435
  ```typescript
464
436
  import { Runtime, toConcrete } from '@travetto/runtime';
465
437
  import { DependencyRegistryIndex } from '@travetto/di';
466
- import { CliCommand, type CliCommandShape } from '@travetto/cli';
438
+ import { CliCommand, CliDebugIpcFlag, CliModuleFlag, CliProfilesFlag, CliRestartOnChangeFlag, type CliCommandShape } from '@travetto/cli';
467
439
  import { NetUtil } from '@travetto/web';
468
440
  import { Registry } from '@travetto/registry';
469
441
 
@@ -472,7 +444,7 @@ import type { WebHttpServer } from '../src/types.ts';
472
444
  /**
473
445
  * Run a web server
474
446
  */
475
- @CliCommand({ runTarget: true, with: { debugIpc: 'optional', restartOnChange: true, module: true, profiles: true } })
447
+ @CliCommand()
476
448
  export class WebHttpCommand implements CliCommandShape {
477
449
 
478
450
  /** Port to run on */
@@ -481,7 +453,19 @@ export class WebHttpCommand implements CliCommandShape {
481
453
  /** Kill conflicting port owner */
482
454
  killConflict?: boolean = Runtime.localDevelopment;
483
455
 
484
- preMain(): void {
456
+ @CliModuleFlag({ short: 'm' })
457
+ module: string;
458
+
459
+ @CliProfilesFlag()
460
+ profile: string[];
461
+
462
+ @CliRestartOnChangeFlag()
463
+ restartOnChange: boolean = true;
464
+
465
+ @CliDebugIpcFlag()
466
+ debugIpc?: boolean;
467
+
468
+ finalize(): void {
485
469
  if (this.port) {
486
470
  process.env.WEB_HTTP_PORT = `${this.port}`;
487
471
  }
@@ -512,32 +496,56 @@ As noted in the example above, `fields` is specified in this execution, with sup
512
496
  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.
513
497
 
514
498
  ### Custom Validation
515
- 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.
499
+ 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 [ValidationError](https://github.com/travetto/travetto/tree/main/module/schema/src/validate/types.ts#L10) errors that are returned will be shared with the user, and fail to invoke the `main` method.
516
500
 
517
- **Code: CliValidationError**
501
+ **Code: ValidationError**
518
502
  ```typescript
519
- export interface CliValidationError {
503
+ export interface ValidationError {
520
504
  /**
521
505
  * The error message
522
506
  */
523
507
  message: string;
524
508
  /**
525
- * Source of validation
509
+ * The object path of the error
526
510
  */
527
- source?: 'flag' | 'arg' | 'custom';
528
- };
511
+ path: string;
512
+ /**
513
+ * The kind of validation
514
+ */
515
+ kind: ValidationKind;
516
+ /**
517
+ * The value provided
518
+ */
519
+ value?: unknown;
520
+ /**
521
+ * Regular expression to match
522
+ */
523
+ regex?: string;
524
+ /**
525
+ * Number to compare against
526
+ */
527
+ limit?: NumericLikeIntrinsic;
528
+ /**
529
+ * The type of the field
530
+ */
531
+ type?: string;
532
+ /**
533
+ * Source of the error
534
+ */
535
+ source?: string;
536
+ }
529
537
  ```
530
538
 
531
539
  A simple example of the validation can be found in the `doc` command:
532
540
 
533
541
  **Code: Simple Validation Example**
534
542
  ```typescript
535
- async validate(): Promise<CliValidationError | undefined> {
536
- const docFile = path.resolve(this.input);
537
- if (!(await fs.stat(docFile).catch(() => false))) {
538
- return { message: `input: ${this.input} does not exist`, source: 'flag' };
539
- }
543
+ @Validator(async (cmd) => {
544
+ const docFile = path.resolve(cmd.input);
545
+ if (!(await fs.stat(docFile).catch(() => false))) {
546
+ return { message: `input: ${cmd.input} does not exist`, path: 'input', source: 'flag', kind: 'invalid' };
540
547
  }
548
+ })
541
549
  ```
542
550
 
543
551
  ## CLI - service
@@ -554,11 +562,11 @@ Options:
554
562
 
555
563
  Available Services
556
564
  --------------------
557
- * dynamodb@3.1.0
558
- * elasticsearch@9.2.3
565
+ * dynamodb@3.3.0
566
+ * elasticsearch@9.2.4
559
567
  * firestore@latest
560
568
  * mongodb@8.2
561
- * mysql@9.5
569
+ * mysql@9.6
562
570
  * postgresql@18.1
563
571
  * redis@8.4
564
572
  * s3@4.11.0
@@ -572,11 +580,11 @@ $ trv service status
572
580
 
573
581
  Service Version Status
574
582
  -------------------------------------------------
575
- dynamodb 3.1.0 Running 93af422e793a
576
- elasticsearch 9.2.3 Running ed76ee063d13
583
+ dynamodb 3.3.0 Running 93af422e793a
584
+ elasticsearch 9.2.4 Running ed76ee063d13
577
585
  firestore latest Running feec2e5e95b4
578
586
  mongodb 8.2 Running 5513eba6734e
579
- mysql 9.5 Running 307bc66d442a
587
+ mysql 9.6 Running 307bc66d442a
580
588
  postgresql 18.1 Running e78291e71040
581
589
  redis 8.4 Running 77ba279b4e30
582
590
  s3 4.11.0 Running fdacfc55b9e3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/cli",
3
- "version": "7.1.4",
3
+ "version": "8.0.0-alpha.1",
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": "^7.1.4",
33
- "@travetto/terminal": "^7.1.4"
32
+ "@travetto/schema": "^8.0.0-alpha.1",
33
+ "@travetto/terminal": "^8.0.0-alpha.1"
34
34
  },
35
35
  "travetto": {
36
36
  "displayName": "Command Line Interface",
package/src/error.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { AppError, Runtime } from '@travetto/runtime';
1
+ import { RuntimeError, Runtime } from '@travetto/runtime';
2
+ import type { ValidationError } from '@travetto/schema';
2
3
 
3
4
  import { cliTpl } from './color.ts';
4
- import type { CliValidationError, CliCommandShape } from './types.ts';
5
+ import type { CliCommandShape } from './types.ts';
5
6
 
6
7
  const COMMAND_PACKAGE = [
7
8
  [/^test(:watch)?$/, 'test', false],
@@ -48,10 +49,10 @@ ${{ identifier: install }}
48
49
  /**
49
50
  * Provides a basic error wrapper for cli validation issues
50
51
  */
51
- export class CliValidationResultError extends AppError<{ errors: CliValidationError[] }> {
52
+ export class CliValidationResultError extends RuntimeError<{ errors: ValidationError[] }> {
52
53
  command: CliCommandShape;
53
54
 
54
- constructor(command: CliCommandShape, errors: CliValidationError[]) {
55
+ constructor(command: CliCommandShape, errors: ValidationError[]) {
55
56
  super('', { details: { errors } });
56
57
  this.command = command;
57
58
  }
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';
@@ -16,7 +16,7 @@ export class ExecutionManager {
16
16
  static async #onError(error: unknown): Promise<void> {
17
17
  process.exitCode ??= 1;
18
18
  if (error instanceof CliValidationResultError) {
19
- console.error!(await HelpUtil.renderValidationError(error));
19
+ console.error!(HelpUtil.renderValidationError(error));
20
20
  console.error!(await HelpUtil.renderCommandHelp(error.command));
21
21
  } else if (error instanceof CliUnknownCommandError) {
22
22
  if (error.help) {
@@ -36,10 +36,10 @@ export class ExecutionManager {
36
36
  const [{ instance: command, schema }] = await CliCommandRegistryIndex.load([cmd]);
37
37
  const fullArgs = await CliParseUtil.expandArgs(schema, args);
38
38
 
39
- const state = command._parsed = await CliParseUtil.parse(schema, fullArgs);
39
+ const state = await CliParseUtil.parse(schema, fullArgs);
40
+ CliParseUtil.setState(command, state);
40
41
 
41
- await command.preBind?.();
42
- const boundArgs = await CliCommandSchemaUtil.bindInput(command, state);
42
+ const boundArgs = CliCommandSchemaUtil.bindInput(command, state);
43
43
  return { command, boundArgs };
44
44
  }
45
45
 
@@ -47,11 +47,13 @@ export class ExecutionManager {
47
47
  static async #runCommand(cmd: string, args: string[]): Promise<void> {
48
48
  const { command, boundArgs } = await this.#bindCommand(cmd, args);
49
49
 
50
- await command.preValidate?.();
51
50
  await CliCommandSchemaUtil.validate(command, boundArgs);
51
+ const config = CliCommandRegistryIndex.get(getClass(command));
52
52
 
53
- await command._cfg!.preMain?.(command);
54
- await command.preMain?.();
53
+ for (const preMain of config.preMain ?? []) {
54
+ await preMain(command);
55
+ }
56
+ await command.finalize?.();
55
57
 
56
58
  ConsoleManager.debug(Runtime.debug);
57
59
  await command.main(...boundArgs);
package/src/help.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import util from 'node:util';
2
2
 
3
- import { castKey, getClass } from '@travetto/runtime';
3
+ import { castKey, getClass, JSONUtil } from '@travetto/runtime';
4
4
  import { SchemaRegistryIndex } from '@travetto/schema';
5
5
 
6
6
  import { cliTpl } from './color.ts';
@@ -31,7 +31,7 @@ export class HelpUtil {
31
31
  const { name: commandName } = CliCommandRegistryIndex.get(getClass(command));
32
32
  const args = schema.methods.main?.parameters ?? [];
33
33
 
34
- await command.preHelp?.();
34
+ await command.finalize?.(true);
35
35
 
36
36
  // Ensure finalized
37
37
 
@@ -70,7 +70,7 @@ export class HelpUtil {
70
70
  const desc = [cliTpl`${{ title: field.description }}`];
71
71
 
72
72
  if (key !== 'help' && defaultValue !== undefined) {
73
- desc.push(cliTpl`(default: ${{ input: JSON.stringify(defaultValue) }})`);
73
+ desc.push(cliTpl`(default: ${{ input: JSONUtil.toUTF8(defaultValue) }})`);
74
74
  }
75
75
  descriptions.push(desc.join(' '));
76
76
  }
package/src/module.ts CHANGED
@@ -28,7 +28,7 @@ export class CliModuleUtil {
28
28
  for (const module of await CliScmUtil.findChangedModules(fromHash, toHash)) {
29
29
  out.set(module.name, module);
30
30
  if (transitive) {
31
- for (const sub of await RuntimeIndex.getDependentModules(module, 'parents')) {
31
+ for (const sub of RuntimeIndex.getDependentModules(module, 'parents')) {
32
32
  out.set(sub.name, sub);
33
33
  }
34
34
  }
package/src/parse.ts CHANGED
@@ -10,7 +10,7 @@ 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$/;
13
+ const HELP_FLAG = /^(-h|--help)$/;
14
14
  const LONG_FLAG_WITH_EQ = /^--[a-z][^= ]+=\S+/i;
15
15
  const CONFIG_PREFIX = '+=';
16
16
  const SPACE = new Set([32, 7, 13, 10]);
@@ -20,6 +20,8 @@ export const isBoolFlag = (value?: SchemaInputConfig): boolean => value?.type ==
20
20
 
21
21
  export type AliasesParseResult = Record<'long' | 'short' | 'raw' | 'env', string[]>;
22
22
 
23
+ const STATE_SYMBOL = Symbol();
24
+
23
25
  /**
24
26
  * Parsing support for the cli
25
27
  */
@@ -134,8 +136,11 @@ export class CliParseUtil {
134
136
  static async expandArgs(schema: SchemaClassConfig, args: string[]): Promise<string[]> {
135
137
  const separatorIndex = args.includes(RAW_SEPARATOR) ? args.indexOf(RAW_SEPARATOR) : args.length;
136
138
  const module = this.getSpecifiedModule(schema, args);
137
- return (await Promise.all(args.map((arg, i) =>
138
- arg.startsWith(CONFIG_PREFIX) && (i < separatorIndex || separatorIndex < 0) ? this.readFlagFile(arg, module) : arg))).flat();
139
+ return Promise
140
+ .all(args.map(async (arg, i) =>
141
+ await (arg.startsWith(CONFIG_PREFIX) && (i < separatorIndex || separatorIndex < 0) ? this.readFlagFile(arg, module) : arg))
142
+ )
143
+ .then(expanded => expanded.flat());
139
144
  }
140
145
 
141
146
  /**
@@ -235,4 +240,34 @@ export class CliParseUtil {
235
240
  return result;
236
241
  }, { long: [], short: [], raw: [], env: [] });
237
242
  }
243
+
244
+ /**
245
+ * Build aliases for a schema config
246
+ */
247
+ static buildAliases(config: { full?: string, short?: string, envVars?: string[] }, ...extraEnvVars: string[]): Partial<SchemaFieldConfig> {
248
+ const envVars = [...config.envVars ?? [], ...extraEnvVars];
249
+ return {
250
+ aliases: [
251
+ ...(config.full ? [config.full.startsWith('-') ? config.full : `--${config.full}`] : []),
252
+ ...(config.short ? [config.short.startsWith('-') ? config.short : `-${config.short}`] : []),
253
+ ...(envVars.length ? envVars.map(CliParseUtil.toEnvField) : [])
254
+ ]
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Get the state from an object
260
+ */
261
+ static getState<T extends object>(item: T): ParsedState | undefined {
262
+ const local: T & { [STATE_SYMBOL]?: ParsedState } = item;
263
+ return local[STATE_SYMBOL];
264
+ }
265
+
266
+ /**
267
+ * Set the state
268
+ */
269
+ static setState<T extends object>(item: T, state: ParsedState): void {
270
+ const local: T & { [STATE_SYMBOL]?: ParsedState } = item;
271
+ local[STATE_SYMBOL] = state;
272
+ }
238
273
  }
@@ -1,5 +1,5 @@
1
- import { type Class, type ClassInstance, Env, Runtime, RuntimeIndex, TypedObject, castTo, describeFunction, getClass } from '@travetto/runtime';
2
- import { type SchemaFieldConfig, SchemaRegistryIndex, type ValidationError } from '@travetto/schema';
1
+ import { type Class, type ClassInstance, Env, Runtime, RuntimeIndex, castTo, describeFunction, getClass } from '@travetto/runtime';
2
+ import { SchemaRegistryIndex, type ValidationError } from '@travetto/schema';
3
3
 
4
4
  import type { CliCommandShape } from '../types.ts';
5
5
  import { CliCommandRegistryIndex } from './registry-index.ts';
@@ -7,86 +7,15 @@ import { CliModuleUtil } from '../module.ts';
7
7
  import { CliParseUtil } from '../parse.ts';
8
8
  import { CliUtil } from '../util.ts';
9
9
 
10
- type Cmd = CliCommandShape & { profiles?: string[] };
11
-
12
- type CliCommandConfigOptions = {
13
- runTarget?: boolean;
14
- runtimeModule?: 'current' | 'command';
15
- with?: {
16
- /** Application environment */
17
- profiles?: boolean;
18
- /** Module to run for */
19
- module?: boolean;
20
- /** Should debug invocation trigger via ipc */
21
- debugIpc?: boolean | 'optional';
22
- /** Should restart on source change */
23
- restartOnChange?: boolean | 'optional';
24
- };
25
- };
10
+ type CliCommandConfigOptions = { runTarget?: boolean };
11
+ type CliFlagOptions = { full?: string, short?: string, envVars?: string[] };
26
12
 
27
- type WithConfig = Required<Exclude<CliCommandConfigOptions['with'], undefined>>;
28
- type WithHandler<K extends keyof WithConfig> = (config?: WithConfig[K]) => ({
29
- name: K;
30
- field: Partial<SchemaFieldConfig>;
31
- run?: (cmd: Cmd) => (Promise<unknown> | unknown);
32
- } | undefined);
33
-
34
- const FIELD_CONFIG: { [K in keyof WithConfig]: WithHandler<K> } = {
35
- profiles: (config) => {
36
- if (!config) { return; }
37
- return {
38
- name: 'profiles',
39
- run: cmd => cmd.profiles && Env.TRV_PROFILES.set([...cmd.profiles, ...(Env.TRV_PROFILES.list ?? [])]),
40
- field: {
41
- type: String,
42
- aliases: ['--profile', '--profiles', CliParseUtil.toEnvField(Env.TRV_PROFILES.key)],
43
- description: 'Application profiles',
44
- required: { active: false },
45
- },
46
- };
47
- },
48
- module: (config) => {
49
- if (!config) { return; }
50
- return {
51
- name: 'module',
52
- field: {
53
- type: String,
54
- aliases: ['-m', CliParseUtil.toEnvField(Env.TRV_MODULE.key)],
55
- description: 'Module to run for',
56
- specifiers: ['module'],
57
- required: { active: Runtime.monoRoot },
58
- },
59
- };
60
- },
61
- debugIpc: (config) => {
62
- if (!config) { return; }
63
- return {
64
- name: 'debugIpc',
65
- run: cmd => CliUtil.runWithDebugIpc(cmd),
66
- field: {
67
- type: Boolean,
68
- aliases: ['-di', CliParseUtil.toEnvField(Env.TRV_DEBUG_IPC.key)],
69
- description: 'Should debug invocation trigger via ipc',
70
- default: config !== 'optional',
71
- required: { active: false },
72
- },
73
- };
74
- },
75
- restartOnChange: (config) => {
76
- if (!config) { return; }
77
- return {
78
- name: 'restartOnChange',
79
- run: cmd => CliUtil.runWithRestartOnChange(cmd),
80
- field: {
81
- type: Boolean,
82
- aliases: ['-rc'],
83
- description: 'Should the invocation automatically restart on source changes',
84
- default: config !== 'optional' && Runtime.localDevelopment,
85
- required: { active: false },
86
- },
87
- };
88
- }
89
- };
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
+ }
90
19
 
91
20
  /**
92
21
  * Decorator to register a CLI command
@@ -97,70 +26,139 @@ const FIELD_CONFIG: { [K in keyof WithConfig]: WithHandler<K> } = {
97
26
  */
98
27
  export function CliCommand(config: CliCommandConfigOptions = {}) {
99
28
  return function <T extends CliCommandShape>(target: Class<T>): void {
100
- const adapter = SchemaRegistryIndex.getForRegister(target);
101
- const description = describeFunction(target) ?? {};
102
-
103
- if (!target.Ⲑid || description.abstract) {
104
- return;
29
+ if (target.Ⲑid && !describeFunction(target)?.abstract) {
30
+ CliCommandRegistryIndex.getForRegister(target).register(config);
105
31
  }
32
+ };
33
+ }
106
34
 
107
- const VALID_FIELDS = TypedObject.keys(FIELD_CONFIG).map((name) => FIELD_CONFIG[name](castTo(config.with?.[name]))).filter(x => !!x);
35
+ /**
36
+ * Decorator to register a CLI command flag
37
+ * @augments `@travetto/schema:Input`
38
+ * @kind decorator
39
+ */
40
+ export function CliFlag(config: CliFlagOptions) {
41
+ return function (instance: ClassInstance, property: string): void {
42
+ SchemaRegistryIndex.getForRegister(getClass(instance))
43
+ .registerField(property, CliParseUtil.buildAliases(config));
44
+ };
45
+ }
108
46
 
109
- CliCommandRegistryIndex.getForRegister(target).register({
110
- runTarget: config.runTarget,
111
- preMain: async (cmd: Cmd) => {
112
- for (const field of VALID_FIELDS) {
113
- await field.run?.(cmd);
114
- }
115
- }
47
+ /**
48
+ * Decorator to register a CLI command file flag
49
+ * @augments `@travetto/schema:Input`
50
+ * @kind decorator
51
+ */
52
+ export function CliFileFlag(config: CliFlagOptions & { fileExtensions: string[] }) {
53
+ return function (instance: ClassInstance, property: string): void {
54
+ SchemaRegistryIndex.getForRegister(getClass(instance)).registerField(property, {
55
+ ...CliParseUtil.buildAliases(config),
56
+ specifiers: ['file', ...config.fileExtensions.map(ext => `ext:${ext.replace(/[*.]/g, '')}`)]
116
57
  });
58
+ };
59
+ }
117
60
 
118
- const commandModule = description.module;
61
+ /**
62
+ * Registers a flag to support profiles via the `TRV_PROFILES` environment variable
63
+ * @augments `@travetto/schema:Input`
64
+ * @kind decorator
65
+ */
66
+ export function CliProfilesFlag(config: CliFlagOptions = {}) {
67
+ return function <K extends string>(instance: Partial<Record<K, string[]>>, property: K): void {
68
+ const cls = getClass(instance);
69
+ SchemaRegistryIndex.getForRegister(cls).registerField(property, {
70
+ ...CliParseUtil.buildAliases(config, Env.TRV_PROFILES.key),
71
+ required: { active: false },
72
+ description: 'Application profiles'
73
+ });
119
74
 
120
- for (const { name, field: { type, ...field } } of VALID_FIELDS) {
121
- adapter.registerField(name, { type }, field);
122
- Object.defineProperty(target.prototype, name, { value: field.default, writable: true });
123
- }
75
+ runBeforeMain(cls, (cmd: typeof instance) =>
76
+ Env.TRV_PROFILES.set([...cmd[property] ?? [], ...(Env.TRV_PROFILES.list ?? [])])
77
+ );
78
+ };
79
+ };
124
80
 
125
- const runtimeModule = config.runtimeModule ?? (config.with?.module ? 'current' : undefined);
81
+ /**
82
+ * Registers a flag to support targeting a specific module
83
+ * @augments `@travetto/schema:Input`
84
+ * @kind decorator
85
+ */
86
+ export function CliModuleFlag(config: CliFlagOptions & { scope?: 'current' | 'command' } = { scope: 'current' }) {
87
+ return function <K extends string>(instance: Partial<Record<K, string>>, property: K): void {
88
+ const cls = getClass(instance);
89
+ const description = describeFunction(cls) ?? {};
90
+ const commandModule = description.module;
126
91
 
127
- if (runtimeModule) { // Validate module
128
- adapter.register({
129
- validators: [async ({ module }): Promise<ValidationError | undefined> => {
130
- const runModule = (runtimeModule === 'command' ? commandModule : module) || Runtime.main.name;
92
+ SchemaRegistryIndex.getForRegister(cls).registerField(property, {
93
+ ...CliParseUtil.buildAliases(config, Env.TRV_MODULE.key),
94
+ description: 'Module to run for',
95
+ specifiers: ['module'],
96
+ required: { active: Runtime.monoRoot },
97
+ });
131
98
 
132
- // If we need to run as a specific module
133
- if (runModule !== Runtime.main.name) {
134
- try {
135
- RuntimeIndex.reinitForModule(runModule);
136
- } catch {
137
- return { source: 'flag', message: `${runModule} is an unknown module`, kind: 'custom', path: '.' };
138
- }
99
+ SchemaRegistryIndex.getForRegister(cls).register({
100
+ validators: [async (cmd: CliCommandShape): Promise<ValidationError | undefined> => {
101
+ const typed: (typeof cmd) & { [property]?: string } = castTo(cmd);
102
+ const providedModule = typed[property];
103
+ const runModule = (config.scope === 'command' ? commandModule : providedModule) || Runtime.main.name;
104
+
105
+ // 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 };
139
111
  }
112
+ }
140
113
 
141
- if (!(await CliModuleUtil.moduleHasDependency(runModule, commandModule))) {
142
- return { source: 'flag', message: `${runModule} does not have ${commandModule} as a dependency`, kind: 'custom', path: '.' };
143
- }
144
- }],
145
- });
146
- }
114
+ if (!(await CliModuleUtil.moduleHasDependency(runModule, commandModule))) {
115
+ return { source: 'flag', message: `${runModule} does not have ${commandModule} as a dependency`, kind: 'custom', path: property };
116
+ }
117
+ }],
118
+ });
147
119
  };
148
120
  }
149
121
 
150
122
  /**
151
- * Decorator to register a CLI command flag
123
+ * Registers a flag to support restarting on source changes
152
124
  * @augments `@travetto/schema:Input`
153
125
  * @kind decorator
154
126
  */
155
- export function CliFlag(config: { full?: string, short?: string, fileExtensions?: string[], envVars?: string[] } = {}) {
156
- return function (instance: ClassInstance, property: string): void {
157
- const aliases = [
158
- ...(config.full ? [config.full.startsWith('-') ? config.full : `--${config.full}`] : []),
159
- ...(config.short ? [config.short.startsWith('-') ? config.short : `-${config.short}`] : []),
160
- ...(config.envVars ? config.envVars.map(CliParseUtil.toEnvField) : [])
161
- ];
162
- const specifiers = config.fileExtensions?.length ? ['file', ...config.fileExtensions.map(ext => `ext:${ext.replace(/[*.]/g, '')}`)] : [];
163
-
164
- SchemaRegistryIndex.getForRegister(getClass(instance)).registerField(property, { aliases, specifiers });
127
+ export function CliRestartOnChangeFlag(config: CliFlagOptions = {}) {
128
+ return function <K extends string, T extends Partial<Record<K, boolean>>>(instance: T, property: K): void {
129
+ const cls = getClass(instance);
130
+ SchemaRegistryIndex.getForRegister(cls).registerField(property, {
131
+ ...CliParseUtil.buildAliases(config),
132
+ description: 'Should the invocation automatically restart on source changes',
133
+ default: Runtime.localDevelopment,
134
+ required: { active: false },
135
+ });
136
+
137
+ runBeforeMain(cls, (cmd: typeof instance) => CliUtil.runWithRestartOnChange(cmd[property]), true);
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Registers a flag to support debugging invocations triggered via IPC
143
+ * @augments `@travetto/schema:Input`
144
+ * @kind decorator
145
+ */
146
+ export function CliDebugIpcFlag(config: CliFlagOptions = {}) {
147
+ return function <K extends string, T extends Partial<Record<K, boolean>>>(instance: T, property: K): void {
148
+ const cls = getClass(instance);
149
+ SchemaRegistryIndex.getForRegister(cls).registerField(property, {
150
+ ...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 },
154
+ });
155
+
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
+ );
165
163
  };
166
164
  }
@@ -11,6 +11,16 @@ const getName = (name: string): string => (name.match(CLI_FILE_REGEX)?.groups?.n
11
11
  const stripDashes = (flag?: string): string | undefined => flag?.replace(/^-+/, '');
12
12
  const toFlagName = (field: string): string => field.replace(/([a-z])([A-Z])/g, (_, left: string, right: string) => `${left}-${right.toLowerCase()}`);
13
13
 
14
+ function combineClasses(base: CliCommandConfig, ...configs: Partial<CliCommandConfig>[]): CliCommandConfig {
15
+ for (const config of configs) {
16
+ base.runTarget = config.runTarget ?? base.runTarget;
17
+ if (config.preMain) {
18
+ base.preMain = [...base.preMain ?? [], ...config.preMain ?? []];
19
+ }
20
+ }
21
+ return base;
22
+ }
23
+
14
24
  export class CliCommandRegistryAdapter implements RegistryAdapter<CliCommandConfig> {
15
25
  #cls: Class;
16
26
  #config: CliCommandConfig;
@@ -75,22 +85,15 @@ export class CliCommandRegistryAdapter implements RegistryAdapter<CliCommandConf
75
85
  * Registers a cli command
76
86
  */
77
87
  register(...configs: Partial<CliCommandConfig>[]): CliCommandConfig {
78
- const meta = describeFunction(this.#cls);
79
- this.#config ??= {
80
- cls: this.#cls,
81
- preMain: undefined,
82
- name: getName(meta.import),
83
- };
84
- Object.assign(this.#config, ...configs);
85
- return this.#config;
88
+ const metadata = describeFunction(this.#cls);
89
+ this.#config ??= { cls: this.#cls, name: getName(metadata.import), preMain: [], runTarget: true };
90
+ return combineClasses(this.#config, ...configs);
86
91
  }
87
92
 
88
93
  /**
89
94
  * Get instance of the command
90
95
  */
91
96
  getInstance(): CliCommandShape {
92
- const instance: CliCommandShape = classConstruct(this.#cls);
93
- instance._cfg = this.#config;
94
- return instance;
97
+ return classConstruct(this.#cls);
95
98
  }
96
99
  }
@@ -92,9 +92,6 @@ export class CliCommandRegistryIndex implements RegistryIndex {
92
92
  continue;
93
93
  }
94
94
  const result = config.getInstance();
95
- if (result.isActive !== undefined && !result.isActive()) {
96
- continue;
97
- }
98
95
  this.#instanceMapping.set(name, result);
99
96
  return result;
100
97
  }
package/src/schema.ts CHANGED
@@ -1,19 +1,30 @@
1
1
  import { castKey, castTo, getClass } from '@travetto/runtime';
2
- import { BindUtil, SchemaRegistryIndex, SchemaValidator, ValidationResultError } from '@travetto/schema';
2
+ import { BindUtil, SchemaRegistryIndex, SchemaValidator, ValidationResultError, type ValidationError } from '@travetto/schema';
3
3
 
4
- import type { ParsedState, CliCommandShape, CliValidationError } from './types.ts';
4
+ import type { ParsedState, CliCommandShape } from './types.ts';
5
5
  import { CliValidationResultError } from './error.ts';
6
6
 
7
- const getSource = (source: string | undefined, def: CliValidationError['source']): CliValidationError['source'] => {
7
+ const getSource = (source: string | undefined, defaultSource: ValidationError['source']): ValidationError['source'] => {
8
8
  switch (source) {
9
9
  case 'custom':
10
10
  case 'arg':
11
11
  case 'flag': return source;
12
- case undefined: return def;
12
+ case undefined: return defaultSource;
13
13
  default: return 'custom';
14
14
  }
15
15
  };
16
16
 
17
+ const transformErrors = (source: 'arg' | 'flag', error: unknown): ValidationError[] => {
18
+ if (error instanceof CliValidationResultError || error instanceof ValidationResultError) {
19
+ return error.details.errors.map(value => ({ source: getSource(value.source, source), ...value }));
20
+ } else {
21
+ throw error;
22
+ }
23
+ };
24
+
25
+ const transformArgErrors = (error: unknown): ValidationError[] => transformErrors('arg', error);
26
+ const transformFlagErrors = (error: unknown): ValidationError[] => transformErrors('flag', error);
27
+
17
28
  /**
18
29
  * Allows binding describing/binding inputs for commands
19
30
  */
@@ -59,27 +70,12 @@ export class CliCommandSchemaUtil {
59
70
  const cls = getClass(cmd);
60
71
  const paramNames = SchemaRegistryIndex.get(cls).getMethod('main').parameters.map(config => config.name!);
61
72
 
62
- const validators = [
63
- (): Promise<void> => SchemaValidator.validate(cls, cmd).then(() => { }),
64
- (): Promise<void> => SchemaValidator.validateMethod(cls, 'main', args, paramNames),
65
- async (): Promise<void> => {
66
- const result = await cmd.validate?.(...args);
67
- if (result) {
68
- throw new CliValidationResultError(cmd, Array.isArray(result) ? result : [result]);
69
- }
70
- },
71
- ];
72
-
73
- const SOURCES = ['flag', 'arg', 'custom'] as const;
74
-
75
- const results = validators.map((validator, i) => validator().catch(error => {
76
- if (!(error instanceof CliValidationResultError) && !(error instanceof ValidationResultError)) {
77
- throw error;
78
- }
79
- return error.details.errors.map(value => ({ ...value, source: getSource(value.source, SOURCES[i]) }));
80
- }));
73
+ const results = await Promise.all([
74
+ SchemaValidator.validate(cls, cmd).then(() => [], transformFlagErrors),
75
+ SchemaValidator.validateMethod(cls, 'main', args, paramNames).then(() => [], transformArgErrors),
76
+ ]);
81
77
 
82
- const errors = (await Promise.all(results)).flatMap(result => (result ?? []));
78
+ const errors = results.flat();
83
79
  if (errors.length) {
84
80
  throw new CliValidationResultError(cmd, errors);
85
81
  }
package/src/scm.ts CHANGED
@@ -2,7 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
 
5
- import { AppError, ExecUtil, Runtime, RuntimeIndex } from '@travetto/runtime';
5
+ import { RuntimeError, ExecUtil, Runtime, RuntimeIndex } from '@travetto/runtime';
6
6
  import type { IndexedModule } from '@travetto/manifest';
7
7
 
8
8
  export class CliScmUtil {
@@ -50,7 +50,7 @@ export class CliScmUtil {
50
50
  const rootPath = Runtime.workspace.path;
51
51
  const result = await ExecUtil.getResult(spawn('git', ['diff', '--name-only', `${fromHash}..${toHash}`, ':!**/DOC.*', ':!**/README.*'], { cwd: rootPath }), { catch: true });
52
52
  if (!result.valid) {
53
- throw new AppError('Unable to detect changes between', { category: 'data', details: { fromHash, toHash, output: (result.stderr || result.stdout) } });
53
+ throw new RuntimeError('Unable to detect changes between', { category: 'data', details: { fromHash, toHash, output: (result.stderr || result.stdout) } });
54
54
  }
55
55
  const out = new Set<string>();
56
56
  for (const line of result.stdout.split(/\n/g)) {
package/src/service.ts CHANGED
@@ -42,7 +42,7 @@ export class ServiceRunner {
42
42
  async #isRunning(full = false): Promise<boolean> {
43
43
  const port = ports(this.#descriptor.port!)[0];
44
44
  const start = Date.now();
45
- const timeoutMs = TimeUtil.asMillis(full ? this.#descriptor.startupTimeout ?? 5000 : 100);
45
+ const timeoutMs = TimeUtil.duration(full ? this.#descriptor.startupTimeout ?? 5000 : 100, 'ms');
46
46
  while ((Date.now() - start) < timeoutMs) {
47
47
  try {
48
48
  const sock = net.createConnection(port, 'localhost');
@@ -134,7 +134,7 @@ export class ServiceRunner {
134
134
  if (operation === 'restart' || operation === 'start') {
135
135
  if (!await this.#hasImage()) {
136
136
  yield ['message', 'Starting image download'];
137
- for await (const line of await this.#pullImage()) {
137
+ for await (const line of this.#pullImage()) {
138
138
  yield ['message', `Downloading: ${line}`];
139
139
  }
140
140
  yield ['message', 'Image download complete'];
package/src/types.ts CHANGED
@@ -13,89 +13,26 @@ export type ParsedState = {
13
13
  unknown: string[];
14
14
  };
15
15
 
16
- /**
17
- * Constrained version of Schema's Validation Error
18
- * @concrete
19
- */
20
- export interface CliValidationError {
21
- /**
22
- * The error message
23
- */
24
- message: string;
25
- /**
26
- * Source of validation
27
- */
28
- source?: 'flag' | 'arg' | 'custom';
29
- };
30
-
31
16
  /**
32
17
  * CLI Command Contract
33
18
  * @concrete
34
19
  */
35
- export interface CliCommandShape<T extends unknown[] = unknown[]> {
36
- /**
37
- * Parsed state
38
- */
39
- _parsed?: ParsedState;
40
- /**
41
- * Config
42
- */
43
- _cfg?: CliCommandConfig;
20
+ export interface CliCommandShape {
44
21
  /**
45
22
  * Action target of the command
46
23
  */
47
- main(...args: T): OrProm<undefined | void>;
24
+ main(...args: unknown[]): OrProm<undefined | void>;
48
25
  /**
49
26
  * Run before main runs
50
27
  */
51
- preMain?(): OrProm<void>;
28
+ finalize?(help?: boolean): OrProm<void>;
52
29
  /**
53
30
  * Extra help
54
31
  */
55
32
  help?(): OrProm<string[]>;
56
- /**
57
- * Run before help is displayed
58
- */
59
- preHelp?(): OrProm<void>;
60
- /**
61
- * Is the command active/eligible for usage
62
- */
63
- isActive?(): boolean;
64
- /**
65
- * Run before binding occurs
66
- */
67
- preBind?(): OrProm<void>;
68
- /**
69
- * Run before validation occurs
70
- */
71
- preValidate?(): OrProm<void>;
72
- /**
73
- * Validation method
74
- */
75
- validate?(...args: T): OrProm<CliValidationError | CliValidationError[] | undefined>;
76
33
  }
77
34
 
78
- /**
79
- * Command shape common fields
80
- */
81
- export type CliCommandShapeFields = {
82
- /**
83
- * Profiles to run the application under
84
- */
85
- profiles?: string[];
86
- /**
87
- * Should the cli invocation trigger a debug session, via IPC
88
- */
89
- debugIpc?: boolean;
90
- /**
91
- * Should the invocation run with auto-restart on source changes
92
- */
93
- restartOnChange?: boolean;
94
- /**
95
- * The module to run the command from
96
- */
97
- module?: string;
98
- };
35
+ type PreMainHandler = (cmd: CliCommandShape) => (unknown | Promise<unknown>);
99
36
 
100
37
  /**
101
38
  * CLI Command schema shape
@@ -104,5 +41,5 @@ export interface CliCommandConfig {
104
41
  cls: Class<CliCommandShape>;
105
42
  name: string;
106
43
  runTarget?: boolean;
107
- preMain?: (cmd: CliCommandShape) => void | Promise<void>;
44
+ preMain?: PreMainHandler[];
108
45
  }
package/src/util.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  import { spawn, type ChildProcess } from 'node:child_process';
2
2
 
3
- import { AppError, Env, ExecUtil, Runtime, ShutdownManager, Util, WatchUtil } from '@travetto/runtime';
4
-
5
- import type { CliCommandShape, CliCommandShapeFields } from './types.ts';
3
+ import { RuntimeError, JSONUtil, Env, ExecUtil, Runtime, ShutdownManager, Util, WatchUtil } from '@travetto/runtime';
6
4
 
7
5
  const IPC_ALLOWED_ENV = new Set(['NODE_OPTIONS']);
8
6
  const IPC_INVALID_ENV = new Set(['PS1', 'INIT_CWD', 'COLOR', 'LANGUAGE', 'PROFILEHOME', '_']);
@@ -16,24 +14,19 @@ export class CliUtil {
16
14
  */
17
15
  static getSimpleModuleName(placeholder: string, module?: string): string {
18
16
  const simple = (module ?? Runtime.main.name).replace(/[\/]/, '_').replace(/@/, '');
19
- if (!simple) {
20
- return placeholder;
21
- } else if (!module && Runtime.monoRoot) {
22
- return placeholder;
23
- } else {
24
- return placeholder.replace('<module>', simple);
25
- }
17
+ const targetModule = !simple || (!module && Runtime.monoRoot) ? '<module>' : simple;
18
+ return placeholder.replace('<module>', targetModule);
26
19
  }
27
20
 
28
21
  /**
29
22
  * Run a command as restartable, linking into self
30
23
  */
31
- static async runWithRestartOnChange<T extends CliCommandShapeFields>(cmd: T): Promise<void> {
24
+ static async runWithRestartOnChange(restartOnChange?: boolean): Promise<void> {
32
25
  if (Env.TRV_RESTART_TARGET.isTrue) {
33
26
  Env.TRV_RESTART_TARGET.clear();
34
27
  ShutdownManager.disableInterrupt();
35
28
  return;
36
- } else if (cmd.restartOnChange !== true) {
29
+ } else if (restartOnChange !== true) {
37
30
  return; // Not restarting, run normal
38
31
  }
39
32
 
@@ -72,13 +65,14 @@ export class CliUtil {
72
65
  /**
73
66
  * Dispatch IPC payload
74
67
  */
75
- static async runWithDebugIpc<T extends CliCommandShapeFields & CliCommandShape>(cmd: T): Promise<void> {
76
- if (cmd.debugIpc !== true || !Env.TRV_CLI_IPC.isSet) {
68
+ static async runWithDebugIpc(name: string): Promise<void> {
69
+ if (!Env.TRV_CLI_IPC.isSet) {
77
70
  return; // Not debugging, run normal
78
71
  }
79
72
 
80
- const info = await fetch(Env.TRV_CLI_IPC.value!).catch(() => ({ ok: false }));
73
+ const doFetch = fetch.bind(null, Env.TRV_CLI_IPC.value!);
81
74
 
75
+ const info = await doFetch().catch(() => ({ ok: false }));
82
76
  if (!info.ok) {
83
77
  return; // Server not running, run normal
84
78
  }
@@ -87,19 +81,19 @@ export class CliUtil {
87
81
  const request = {
88
82
  type: '@travetto/cli:run',
89
83
  data: {
90
- name: cmd._cfg!.name,
84
+ name,
91
85
  env,
92
- module: cmd.module ?? Runtime.main.name,
86
+ cwd: process.cwd(),
93
87
  args: process.argv.slice(3),
94
88
  }
95
89
  };
96
90
  console.log('Triggering IPC request', request);
97
91
 
98
92
  Object.entries(process.env).forEach(([key, value]) => validEnv(key) && (env[key] = value!));
99
- const sent = await fetch(Env.TRV_CLI_IPC.value!, { method: 'POST', body: JSON.stringify(request) });
93
+ const sent = await doFetch({ method: 'POST', body: JSONUtil.toUTF8(request) });
100
94
 
101
95
  if (!sent.ok) {
102
- throw new AppError(`IPC Request failed: ${sent.status} ${await sent.text()}`);
96
+ throw new RuntimeError(`IPC Request failed: ${sent.status} ${await sent.text()}`);
103
97
  }
104
98
  }
105
99
 
@@ -107,7 +101,8 @@ export class CliUtil {
107
101
  * Write data to channel and ensure its flushed before continuing
108
102
  */
109
103
  static async writeAndEnsureComplete(data: unknown, channel: 'stdout' | 'stderr' = 'stdout'): Promise<void> {
110
- return await new Promise(resolve => process[channel].write(typeof data === 'string' ? data : JSON.stringify(data, null, 2), () => resolve()));
104
+ return await new Promise(resolve => process[channel].write(typeof data === 'string' ? data :
105
+ JSONUtil.toUTF8Pretty(data), () => resolve()));
111
106
  }
112
107
 
113
108
  /**
@@ -0,0 +1,16 @@
1
+ import { RuntimeIndex, Runtime } from '@travetto/runtime';
2
+ import type { ServiceDescriptor } from '../../__index__.ts';
3
+
4
+ export async function getServices(services: string[]): Promise<ServiceDescriptor[]> {
5
+ return (await Promise.all(
6
+ RuntimeIndex.find({
7
+ module: module => module.roles.includes('std'),
8
+ folder: folder => folder === 'support',
9
+ file: file => /support\/service[.]/.test(file.sourceFile)
10
+ })
11
+ .map(file => Runtime.importFrom<{ service: ServiceDescriptor }>(file.import).then(value => value.service))
12
+ ))
13
+ .filter(file => !!file)
14
+ .filter(file => services?.length ? services.includes(file.name) : true)
15
+ .toSorted((a, b) => a.name.localeCompare(b.name));
16
+ }
@@ -1,12 +1,29 @@
1
1
  import { Env } from '@travetto/runtime';
2
- import { IsPrivate } from '@travetto/schema';
2
+ import { IsPrivate, MethodValidator, type ValidationError } from '@travetto/schema';
3
3
 
4
4
  import { CliCommand } from '../src/registry/decorator.ts';
5
- import type { CliCommandShape, CliValidationError } from '../src/types.ts';
5
+ import type { CliCommandShape } from '../src/types.ts';
6
6
  import { CliCommandRegistryIndex } from '../src/registry/registry-index.ts';
7
7
  import { CliUtil } from '../src/util.ts';
8
8
  import { CliSchemaExportUtil } from '../src/schema-export.ts';
9
9
 
10
+ async function nameValidator(names?: string[]): Promise<ValidationError | undefined> {
11
+ if (!names || names.length === 0) {
12
+ return;
13
+ }
14
+ const resolved = await CliCommandRegistryIndex.load(names);
15
+ const invalid = names.find(name => !resolved.find(result => result.command === name));
16
+
17
+ if (invalid) {
18
+ return {
19
+ source: 'arg',
20
+ kind: 'invalid',
21
+ path: 'names',
22
+ message: `name: ${invalid} is not a valid cli command`
23
+ };
24
+ }
25
+ }
26
+
10
27
  /**
11
28
  * Generates the schema for all CLI operations
12
29
  */
@@ -14,25 +31,11 @@ import { CliSchemaExportUtil } from '../src/schema-export.ts';
14
31
  @IsPrivate()
15
32
  export class CliSchemaCommand implements CliCommandShape {
16
33
 
17
- async validate(names?: string[]): Promise<CliValidationError | undefined> {
18
- if (!names || names.length === 0) {
19
- return;
20
- }
21
- const resolved = await CliCommandRegistryIndex.load(names);
22
- const invalid = names.find(name => !resolved.find(result => result.command === name));
23
-
24
- if (invalid) {
25
- return {
26
- source: 'arg',
27
- message: `name: ${invalid} is not a valid cli command`
28
- };
29
- }
30
- }
31
-
32
- preMain(): void {
34
+ finalize(): void {
33
35
  Env.DEBUG.set(false);
34
36
  }
35
37
 
38
+ @MethodValidator(nameValidator)
36
39
  async main(names?: string[]): Promise<void> {
37
40
  const resolved = await CliCommandRegistryIndex.load(names);
38
41
 
@@ -1,6 +1,14 @@
1
- import { Runtime } from '@travetto/runtime';
2
- import { type CliCommandShape, CliCommand, type CliValidationError, type ParsedState } from '@travetto/cli';
3
- import { Ignore, IsPrivate } from '@travetto/schema';
1
+ import { JSONUtil, Runtime } from '@travetto/runtime';
2
+ import { type CliCommandShape, CliCommand, CliParseUtil } from '@travetto/cli';
3
+ import { IsPrivate, MethodValidator, type ValidationError } from '@travetto/schema';
4
+
5
+ async function validateMain(fileOrImport: string): Promise<ValidationError | undefined> {
6
+ try {
7
+ await Runtime.importFrom(fileOrImport);
8
+ } catch {
9
+ return { message: `Unknown file: ${fileOrImport}`, source: 'arg', kind: 'invalid', path: 'fileOrImport' };
10
+ }
11
+ };
4
12
 
5
13
  /**
6
14
  * Allows for running of main entry points
@@ -9,22 +17,13 @@ import { Ignore, IsPrivate } from '@travetto/schema';
9
17
  @IsPrivate()
10
18
  export class MainCommand implements CliCommandShape {
11
19
 
12
- @Ignore()
13
- _parsed: ParsedState;
14
-
15
- async validate(fileOrImport: string): Promise<CliValidationError | undefined> {
16
- try {
17
- await Runtime.importFrom(fileOrImport);
18
- } catch {
19
- return { message: `Unknown file: ${fileOrImport}` };
20
- }
21
- }
22
-
20
+ @MethodValidator(validateMain)
23
21
  async main(fileOrImport: string, args: string[] = []): Promise<void> {
22
+ const parsed = CliParseUtil.getState(this);
24
23
  let result: unknown;
25
24
  try {
26
25
  const module = await Runtime.importFrom<{ main(..._: unknown[]): Promise<unknown> }>(fileOrImport);
27
- result = await module.main(...args, ...this._parsed.unknown);
26
+ result = await module.main(...args, ...parsed?.unknown ?? []);
28
27
  } catch (error) {
29
28
  result = error;
30
29
  process.exitCode = Math.max(process.exitCode ? +process.exitCode : 1, 1);
@@ -32,7 +31,7 @@ export class MainCommand implements CliCommandShape {
32
31
 
33
32
  if (result !== undefined) {
34
33
  if (process.connected) { process.send?.(result); }
35
- const payload = typeof result === 'string' ? result : (result instanceof Error ? result.stack : JSON.stringify(result));
34
+ const payload = typeof result === 'string' ? result : (result instanceof Error ? result.stack : JSONUtil.toUTF8(result));
36
35
  process[process.exitCode ? 'stderr' : 'stdout'].write(`${payload}\n`);
37
36
  }
38
37
  }
@@ -1,8 +1,18 @@
1
- import { type CliCommandShape, CliCommand, cliTpl, type CliValidationError } from '@travetto/cli';
1
+ import { type CliCommandShape, CliCommand, cliTpl } from '@travetto/cli';
2
2
  import { Terminal } from '@travetto/terminal';
3
- import { AsyncQueue, Runtime, RuntimeIndex, Util } from '@travetto/runtime';
3
+ import { AsyncQueue, Util } from '@travetto/runtime';
4
+ import { MethodValidator, type ValidationError } from '@travetto/schema';
4
5
 
5
- import { ServiceRunner, type ServiceDescriptor, type ServiceAction } from '../src/service.ts';
6
+ import { ServiceRunner, type ServiceAction } from '../src/service.ts';
7
+ import { getServices } from './bin/util.ts';
8
+
9
+ async function validateService(action: ServiceAction, services: string[]): Promise<ValidationError | undefined> {
10
+ const all = await getServices(services);
11
+
12
+ if (!all.length) {
13
+ return { message: 'No services found', source: 'arg', kind: 'invalid', path: 'services' };
14
+ }
15
+ }
6
16
 
7
17
  /**
8
18
  * Allows for running services
@@ -10,30 +20,8 @@ import { ServiceRunner, type ServiceDescriptor, type ServiceAction } from '../sr
10
20
  @CliCommand()
11
21
  export class CliServiceCommand implements CliCommandShape {
12
22
 
13
- async #getServices(services: string[]): Promise<ServiceDescriptor[]> {
14
- return (await Promise.all(
15
- RuntimeIndex.find({
16
- module: module => module.roles.includes('std'),
17
- folder: folder => folder === 'support',
18
- file: file => /support\/service[.]/.test(file.sourceFile)
19
- })
20
- .map(file => Runtime.importFrom<{ service: ServiceDescriptor }>(file.import).then(value => value.service))
21
- ))
22
- .filter(file => !!file)
23
- .filter(file => services?.length ? services.includes(file.name) : true)
24
- .toSorted((a, b) => a.name.localeCompare(b.name));
25
- }
26
-
27
- async validate(action: ServiceAction, services: string[]): Promise<CliValidationError | undefined> {
28
- const all = await this.#getServices(services);
29
-
30
- if (!all.length) {
31
- return { message: 'No services found' };
32
- }
33
- }
34
-
35
23
  async help(): Promise<string[]> {
36
- const all = await this.#getServices([]);
24
+ const all = await getServices([]);
37
25
  return [
38
26
  cliTpl`${{ title: 'Available Services' }}`,
39
27
  '-'.repeat(20),
@@ -41,8 +29,9 @@ export class CliServiceCommand implements CliCommandShape {
41
29
  ];
42
30
  }
43
31
 
32
+ @MethodValidator(validateService)
44
33
  async main(action: ServiceAction, services: string[] = []): Promise<void> {
45
- const all = await this.#getServices(services);
34
+ const all = await getServices(services);
46
35
  const maxName = Math.max(...all.map(service => service.name.length), 'Service'.length) + 3;
47
36
  const maxVersion = Math.max(...all.map(service => `${service.version}`.length), 'Version'.length) + 3;
48
37
  const maxStatus = 20;
@@ -1,3 +1,4 @@
1
1
  // @trv-no-transform
2
+ import '@travetto/runtime/support/polyfill.js';
2
3
  import { ExecutionManager } from '@travetto/cli';
3
4
  ExecutionManager.run(process.argv);