@travetto/cli 8.0.0-alpha.0 → 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,7 +131,7 @@ $ trv basic:flag --loud
131
131
  HELLO
132
132
  ```
133
133
 
134
- The [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/registry/decorator.ts#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
136
  * Number values. The [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L172), [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L179), [@Precision](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L165), [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L99) and [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L110) decorators help provide additional validation.
137
137
  * String values. [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L99), [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L110), [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L90) and [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L64) provide additional constraints
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/cli",
3
- "version": "8.0.0-alpha.0",
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": "^8.0.0-alpha.0",
33
- "@travetto/terminal": "^8.0.0-alpha.0"
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
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 RuntimeError<{ 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';
@@ -36,9 +36,9 @@ 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
42
  const boundArgs = CliCommandSchemaUtil.bindInput(command, state);
43
43
  return { command, boundArgs };
44
44
  }
@@ -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
@@ -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
 
package/src/parse.ts CHANGED
@@ -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
  */
@@ -238,4 +240,34 @@ export class CliParseUtil {
238
240
  return result;
239
241
  }, { long: [], short: [], raw: [], env: [] });
240
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
+ }
241
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;
@@ -76,17 +86,14 @@ export class CliCommandRegistryAdapter implements RegistryAdapter<CliCommandConf
76
86
  */
77
87
  register(...configs: Partial<CliCommandConfig>[]): CliCommandConfig {
78
88
  const metadata = describeFunction(this.#cls);
79
- this.#config ??= { cls: this.#cls, name: getName(metadata.import) };
80
- Object.assign(this.#config, ...configs);
81
- return this.#config;
89
+ this.#config ??= { cls: this.#cls, name: getName(metadata.import), preMain: [], runTarget: true };
90
+ return combineClasses(this.#config, ...configs);
82
91
  }
83
92
 
84
93
  /**
85
94
  * Get instance of the command
86
95
  */
87
96
  getInstance(): CliCommandShape {
88
- const instance: CliCommandShape = classConstruct(this.#cls);
89
- instance._cfg = this.#config;
90
- return instance;
97
+ return classConstruct(this.#cls);
91
98
  }
92
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/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
@@ -2,8 +2,6 @@ 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
- import type { CliCommandShape, CliCommandShapeFields } from './types.ts';
6
-
7
5
  const IPC_ALLOWED_ENV = new Set(['NODE_OPTIONS']);
8
6
  const IPC_INVALID_ENV = new Set(['PS1', 'INIT_CWD', 'COLOR', 'LANGUAGE', 'PROFILEHOME', '_']);
9
7
  const validEnv = (key: string): boolean => IPC_ALLOWED_ENV.has(key) || (
@@ -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,16 +81,16 @@ 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: JSONUtil.toUTF8(request) });
93
+ const sent = await doFetch({ method: 'POST', body: JSONUtil.toUTF8(request) });
100
94
 
101
95
  if (!sent.ok) {
102
96
  throw new RuntimeError(`IPC Request failed: ${sent.status} ${await sent.text()}`);
@@ -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
1
  import { JSONUtil, Runtime } from '@travetto/runtime';
2
- import { type CliCommandShape, CliCommand, type CliValidationError, type ParsedState } from '@travetto/cli';
3
- import { Ignore, IsPrivate } from '@travetto/schema';
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);
@@ -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;