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

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.
@@ -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
  }
@@ -3,7 +3,6 @@ import { type RegistryAdapter, type RegistryIndex, RegistryIndexStore, Registry
3
3
  import { type SchemaClassConfig, SchemaRegistryIndex } from '@travetto/schema';
4
4
 
5
5
  import type { CliCommandConfig, CliCommandShape } from '../types.ts';
6
- import { CliUnknownCommandError } from '../error.ts';
7
6
  import { CliCommandRegistryAdapter } from './registry-adapter.ts';
8
7
 
9
8
  const CLI_FILE_REGEX = /\/cli[.](?<name>.{0,100}?)([.]tsx?)?$/;
@@ -11,6 +10,8 @@ const getName = (field: string): string => (field.match(CLI_FILE_REGEX)?.groups?
11
10
 
12
11
  type CliCommandLoadResult = { command: string, config: CliCommandConfig, instance: CliCommandShape, schema: SchemaClassConfig };
13
12
 
13
+ export const UNKNOWN_COMMAND = Symbol();
14
+
14
15
  export class CliCommandRegistryIndex implements RegistryIndex {
15
16
 
16
17
  static #instance = Registry.registerIndex(this);
@@ -57,7 +58,7 @@ export class CliCommandRegistryIndex implements RegistryIndex {
57
58
  */
58
59
  async #getInstance(name: string): Promise<CliCommandShape> {
59
60
  if (!this.hasCommand(name)) {
60
- throw new CliUnknownCommandError(name);
61
+ throw UNKNOWN_COMMAND;
61
62
  }
62
63
 
63
64
  if (this.#instanceMapping.has(name)) {
@@ -92,13 +93,10 @@ export class CliCommandRegistryIndex implements RegistryIndex {
92
93
  continue;
93
94
  }
94
95
  const result = config.getInstance();
95
- if (result.isActive !== undefined && !result.isActive()) {
96
- continue;
97
- }
98
96
  this.#instanceMapping.set(name, result);
99
97
  return result;
100
98
  }
101
- throw new CliUnknownCommandError(name);
99
+ throw UNKNOWN_COMMAND;
102
100
  }
103
101
 
104
102
  hasCommand(name: string): boolean {
@@ -1,4 +1,4 @@
1
- import { type Class, describeFunction } from '@travetto/runtime';
1
+ import { castTo, type Class, describeFunction } from '@travetto/runtime';
2
2
  import { type SchemaInputConfig, SchemaRegistryIndex } from '@travetto/schema';
3
3
 
4
4
  import { CliCommandRegistryIndex } from '../src/registry/registry-index.ts';
@@ -9,7 +9,7 @@ import { CliCommandRegistryIndex } from '../src/registry/registry-index.ts';
9
9
  export type CliCommandInput<K extends string = string> = {
10
10
  name: string;
11
11
  description?: string;
12
- type: 'string' | 'file' | 'number' | 'boolean' | 'date' | 'regex' | 'module';
12
+ type: 'string' | 'file' | 'number' | 'bigint' | 'boolean' | 'date' | 'regex' | 'module';
13
13
  fileExtensions?: string[];
14
14
  choices?: unknown[];
15
15
  required?: boolean;
@@ -37,11 +37,12 @@ export class CliSchemaExportUtil {
37
37
  * Get the base type for a CLI command input
38
38
  */
39
39
  static baseInputType(config: SchemaInputConfig): Pick<CliCommandInput, 'type' | 'fileExtensions'> {
40
- switch (config.type) {
40
+ switch (castTo<Function>(config.type)) {
41
41
  case Date: return { type: 'date' };
42
42
  case Boolean: return { type: 'boolean' };
43
43
  case Number: return { type: 'number' };
44
44
  case RegExp: return { type: 'regex' };
45
+ case BigInt: return { type: 'bigint' };
45
46
  case String: {
46
47
  switch (true) {
47
48
  case config.specifiers?.includes('module'): return { type: 'module' };
package/src/schema.ts CHANGED
@@ -1,19 +1,29 @@
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';
5
- import { CliValidationResultError } from './error.ts';
4
+ import type { ParsedState, CliCommandShape } from './types.ts';
6
5
 
7
- const getSource = (source: string | undefined, def: CliValidationError['source']): CliValidationError['source'] => {
6
+ const getSource = (source: string | undefined, defaultSource: ValidationError['source']): ValidationError['source'] => {
8
7
  switch (source) {
9
8
  case 'custom':
10
9
  case 'arg':
11
10
  case 'flag': return source;
12
- case undefined: return def;
11
+ case undefined: return defaultSource;
13
12
  default: return 'custom';
14
13
  }
15
14
  };
16
15
 
16
+ const transformErrors = (source: 'arg' | 'flag', error: unknown): ValidationError[] => {
17
+ if (error instanceof ValidationResultError) {
18
+ return error.details.errors.map(value => ({ source: getSource(value.source, source), ...value }));
19
+ } else {
20
+ throw error;
21
+ }
22
+ };
23
+
24
+ const transformArgErrors = (error: unknown): ValidationError[] => transformErrors('arg', error);
25
+ const transformFlagErrors = (error: unknown): ValidationError[] => transformErrors('flag', error);
26
+
17
27
  /**
18
28
  * Allows binding describing/binding inputs for commands
19
29
  */
@@ -21,16 +31,16 @@ export class CliCommandSchemaUtil {
21
31
  /**
22
32
  * Bind parsed inputs to command
23
33
  */
24
- static bindInput<T extends CliCommandShape>(cmd: T, state: ParsedState): unknown[] {
34
+ static bindInput<T extends CliCommandShape>(command: T, state: ParsedState): unknown[] {
25
35
  const template: Partial<T> = {};
26
36
  const bound: unknown[] = [];
27
37
 
28
- for (const arg of state.all) {
29
- switch (arg.type) {
38
+ for (const item of state.all) {
39
+ switch (item.type) {
30
40
  case 'flag': {
31
- const key = castKey<T>(arg.fieldName);
32
- const value = arg.value!;
33
- if (arg.array) {
41
+ const key = castKey<T>(item.fieldName);
42
+ const value = item.value!;
43
+ if (item.array) {
34
44
  castTo<unknown[]>(template[key] ??= castTo([])).push(value);
35
45
  } else {
36
46
  template[key] = castTo(value);
@@ -38,51 +48,36 @@ export class CliCommandSchemaUtil {
38
48
  break;
39
49
  }
40
50
  case 'arg': {
41
- if (arg.array) {
42
- castTo<unknown[]>(bound[arg.index] ??= []).push(arg.input);
51
+ if (item.array) {
52
+ castTo<unknown[]>(bound[item.index] ??= []).push(item.input);
43
53
  } else {
44
- bound[arg.index] = arg.input;
54
+ bound[item.index] = item.input;
45
55
  }
46
56
  }
47
57
  }
48
58
  }
49
59
 
50
- const cls = getClass(cmd);
51
- BindUtil.bindSchemaToObject(cls, cmd, template);
60
+ const cls = getClass(command);
61
+ BindUtil.bindSchemaToObject(cls, command, template);
52
62
  return BindUtil.coerceMethodParams(cls, 'main', bound);
53
63
  }
54
64
 
55
65
  /**
56
66
  * Validate command shape with the given arguments
57
67
  */
58
- static async validate(cmd: CliCommandShape, args: unknown[]): Promise<typeof cmd> {
59
- const cls = getClass(cmd);
68
+ static async validate(command: CliCommandShape, args: unknown[]): Promise<typeof command> {
69
+ const cls = getClass(command);
60
70
  const paramNames = SchemaRegistryIndex.get(cls).getMethod('main').parameters.map(config => config.name!);
61
71
 
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
- }));
72
+ const results = await Promise.all([
73
+ SchemaValidator.validate(cls, command).then(() => [], transformFlagErrors),
74
+ SchemaValidator.validateMethod(cls, 'main', args, paramNames).then(() => [], transformArgErrors),
75
+ ]);
81
76
 
82
- const errors = (await Promise.all(results)).flatMap(result => (result ?? []));
77
+ const errors = results.flat();
83
78
  if (errors.length) {
84
- throw new CliValidationResultError(cmd, errors);
79
+ throw new ValidationResultError(errors);
85
80
  }
86
- return cmd;
81
+ return command;
87
82
  }
88
83
  }
package/src/service.ts CHANGED
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
3
3
  import rl from 'node:readline/promises';
4
4
  import net from 'node:net';
5
5
 
6
- import { ExecUtil, TimeUtil, Util } from '@travetto/runtime';
6
+ import { ExecUtil, Runtime, RuntimeIndex, TimeUtil, Util } from '@travetto/runtime';
7
7
 
8
8
  const ports = (value: number | `${number}:${number}`): [number, number] =>
9
9
  typeof value === 'number' ?
@@ -36,6 +36,23 @@ export type ServiceAction = 'start' | 'stop' | 'status' | 'restart';
36
36
  */
37
37
  export class ServiceRunner {
38
38
 
39
+ /**
40
+ * Find all services
41
+ */
42
+ static async findServices(services: string[]): Promise<ServiceDescriptor[]> {
43
+ return (await Promise.all(
44
+ RuntimeIndex.find({
45
+ module: module => module.roles.includes('std'),
46
+ folder: folder => folder === 'support',
47
+ file: file => /support\/service[.]/.test(file.sourceFile)
48
+ })
49
+ .map(file => Runtime.importFrom<{ service: ServiceDescriptor }>(file.import).then(value => value.service))
50
+ ))
51
+ .filter(file => !!file)
52
+ .filter(file => services?.length ? services.includes(file.name) : true)
53
+ .toSorted((a, b) => a.name.localeCompare(b.name));
54
+ }
55
+
39
56
  #descriptor: ServiceDescriptor;
40
57
  constructor(descriptor: ServiceDescriptor) { this.#descriptor = descriptor; }
41
58
 
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
  }