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

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/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Class } from '@travetto/runtime';
1
+ import type { Any, Class } from '@travetto/runtime';
2
2
 
3
3
  type OrProm<T> = T | Promise<T>;
4
4
  type ParsedFlag = { type: 'flag', input: string, array?: boolean, fieldName: string, value?: unknown };
@@ -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
+ export type PreMainHandler<T extends Any = Any> = { priority: number, handler: (cmd: T) => Any };
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,13 +2,12 @@ import { spawn, type ChildProcess } from 'node:child_process';
2
2
 
3
3
  import { RuntimeError, JSONUtil, Env, ExecUtil, Runtime, ShutdownManager, Util, WatchUtil } from '@travetto/runtime';
4
4
 
5
- import type { CliCommandShape, CliCommandShapeFields } from './types.ts';
6
-
7
- const IPC_ALLOWED_ENV = new Set(['NODE_OPTIONS']);
8
- const IPC_INVALID_ENV = new Set(['PS1', 'INIT_CWD', 'COLOR', 'LANGUAGE', 'PROFILEHOME', '_']);
9
- const validEnv = (key: string): boolean => IPC_ALLOWED_ENV.has(key) || (
10
- !IPC_INVALID_ENV.has(key) && !/^(npm_|GTK|GDK|TRV|NODE|GIT|TERM_)/.test(key) && !/VSCODE/.test(key)
11
- );
5
+ const IPC_VALID_ENV = new Set(['NODE_OPTIONS', 'PATH', Env.DEBUG.key, Env.NODE_ENV.key]);
6
+ const IPC_INVALID_ENV = new Set([
7
+ Env.TRV_CLI_IPC, Env.TRV_DEBUG_IPC, Env.TRV_DEBUG_BREAK, Env.TRV_MANIFEST, Env.TRV_MODULE, Env.TRV_RESTART_TARGET
8
+ ].map(item => item.key));
9
+ const validEnv = ([key]: [key: string, value: unknown]): boolean =>
10
+ IPC_VALID_ENV.has(key) || (key.startsWith('TRV_') && !IPC_INVALID_ENV.has(key));
12
11
 
13
12
  export class CliUtil {
14
13
  /**
@@ -16,34 +15,29 @@ export class CliUtil {
16
15
  */
17
16
  static getSimpleModuleName(placeholder: string, module?: string): string {
18
17
  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
- }
18
+ return simple ? placeholder.replace('<module>', simple) : placeholder;
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
 
40
33
  ShutdownManager.disableInterrupt();
41
34
 
42
35
  let child: ChildProcess | undefined;
43
- void WatchUtil.watchCompilerEvents('file', () => ShutdownManager.shutdownChild(child!, { reason: 'restart', mode: 'exit' }));
36
+ await WatchUtil.watchCompilerEvents('file', () => ShutdownManager.shutdownChild(child!, { reason: 'restart', mode: 'exit' }));
37
+
44
38
  process
45
39
  .on('SIGINT', () => ShutdownManager.shutdownChild(child!, { mode: 'exit' }))
46
- .on('message', msg => child?.send?.(msg!));
40
+ .on('message', message => child?.send?.(message!));
47
41
 
48
42
  const env = { ...process.env, ...Env.TRV_RESTART_TARGET.export(true) };
49
43
 
@@ -72,43 +66,44 @@ export class CliUtil {
72
66
  /**
73
67
  * Dispatch IPC payload
74
68
  */
75
- static async runWithDebugIpc<T extends CliCommandShapeFields & CliCommandShape>(cmd: T): Promise<void> {
76
- if (cmd.debugIpc !== true || !Env.TRV_CLI_IPC.isSet) {
69
+ static async runWithDebugIpc(name: string): Promise<void> {
70
+ if (!Env.TRV_CLI_IPC.isSet) {
77
71
  return; // Not debugging, run normal
78
72
  }
79
73
 
80
- const info = await fetch(Env.TRV_CLI_IPC.value!).catch(() => ({ ok: false }));
74
+ const doFetch = fetch.bind(null, Env.TRV_CLI_IPC.value!);
81
75
 
76
+ const info = await doFetch().catch(() => ({ ok: false }));
82
77
  if (!info.ok) {
83
78
  return; // Server not running, run normal
84
79
  }
85
80
 
86
- const env: Record<string, string> = {};
87
81
  const request = {
88
82
  type: '@travetto/cli:run',
89
83
  data: {
90
- name: cmd._cfg!.name,
91
- env,
92
- module: cmd.module ?? Runtime.main.name,
84
+ name,
85
+ env: Object.fromEntries(Object.entries(process.env).filter(validEnv)),
86
+ cwd: process.cwd(),
93
87
  args: process.argv.slice(3),
94
88
  }
95
89
  };
96
- console.log('Triggering IPC request', request);
97
90
 
98
- 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) });
91
+ console.log('Triggering IPC request', request);
92
+ const sent = await doFetch({ method: 'POST', body: JSONUtil.toUTF8(request) });
100
93
 
101
94
  if (!sent.ok) {
102
95
  throw new RuntimeError(`IPC Request failed: ${sent.status} ${await sent.text()}`);
103
96
  }
97
+
98
+ await ShutdownManager.shutdown({ mode: 'exit' });
104
99
  }
105
100
 
106
101
  /**
107
102
  * Write data to channel and ensure its flushed before continuing
108
103
  */
109
104
  static async writeAndEnsureComplete(data: unknown, channel: 'stdout' | 'stderr' = 'stdout'): Promise<void> {
110
- return await new Promise(resolve => process[channel].write(typeof data === 'string' ? data :
111
- JSONUtil.toUTF8Pretty(data), () => resolve()));
105
+ await new Promise<unknown>(resolve => process[channel].write(typeof data === 'string' ? data :
106
+ JSONUtil.toUTF8Pretty(data), resolve));
112
107
  }
113
108
 
114
109
  /**
@@ -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,19 @@
1
- import { type CliCommandShape, CliCommand, cliTpl, type CliValidationError } from '@travetto/cli';
1
+ import { stripVTControlCharacters } from 'node:util';
2
+
3
+ import { type CliCommandShape, CliCommand, cliTpl } from '@travetto/cli';
2
4
  import { Terminal } from '@travetto/terminal';
3
- import { AsyncQueue, Runtime, RuntimeIndex, Util } from '@travetto/runtime';
5
+ import { AsyncQueue, Util } from '@travetto/runtime';
6
+ import { MethodValidator, type ValidationError } from '@travetto/schema';
7
+
8
+ import { ServiceRunner, type ServiceAction } from '../src/service.ts';
4
9
 
5
- import { ServiceRunner, type ServiceDescriptor, type ServiceAction } from '../src/service.ts';
10
+ async function validateService(_: ServiceAction, services: string[]): Promise<ValidationError | undefined> {
11
+ const all = await ServiceRunner.findServices(services);
12
+
13
+ if (!all.length) {
14
+ return { message: 'No services found', source: 'arg', kind: 'invalid', path: 'services' };
15
+ }
16
+ }
6
17
 
7
18
  /**
8
19
  * Allows for running services
@@ -10,30 +21,10 @@ import { ServiceRunner, type ServiceDescriptor, type ServiceAction } from '../sr
10
21
  @CliCommand()
11
22
  export class CliServiceCommand implements CliCommandShape {
12
23
 
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
- }
24
+ quiet = false;
34
25
 
35
26
  async help(): Promise<string[]> {
36
- const all = await this.#getServices([]);
27
+ const all = await ServiceRunner.findServices([]);
37
28
  return [
38
29
  cliTpl`${{ title: 'Available Services' }}`,
39
30
  '-'.repeat(20),
@@ -41,33 +32,50 @@ export class CliServiceCommand implements CliCommandShape {
41
32
  ];
42
33
  }
43
34
 
35
+ @MethodValidator(validateService)
44
36
  async main(action: ServiceAction, services: string[] = []): Promise<void> {
45
- const all = await this.#getServices(services);
37
+ const all = await ServiceRunner.findServices(services);
46
38
  const maxName = Math.max(...all.map(service => service.name.length), 'Service'.length) + 3;
47
39
  const maxVersion = Math.max(...all.map(service => `${service.version}`.length), 'Version'.length) + 3;
48
40
  const maxStatus = 20;
49
41
  const queue = new AsyncQueue<{ idx: number, text: string, done?: boolean }>();
50
42
 
43
+ const failureMessages: string[] = [];
44
+
51
45
  const jobs = all.map(async (descriptor, i) => {
52
46
  const identifier = descriptor.name.padEnd(maxName);
53
47
  const type = `${descriptor.version}`.padStart(maxVersion - 3).padEnd(maxVersion);
54
- let msg: string;
48
+ let message: string;
55
49
  for await (const [valueType, value] of new ServiceRunner(descriptor).action(action)) {
56
50
  const details = { [valueType === 'message' ? 'subtitle' : valueType]: value };
57
- queue.add({ idx: i, text: msg = cliTpl`${{ identifier }} ${{ type }} ${details}` });
51
+ queue.add({ idx: i, text: message = cliTpl`${{ identifier }} ${{ type }} ${details}` });
52
+ if (valueType === 'failure') {
53
+ failureMessages.push(message);
54
+ }
58
55
  }
59
- queue.add({ idx: i, done: true, text: msg! });
56
+ queue.add({ idx: i, done: true, text: message! });
60
57
  });
61
58
 
62
59
  Promise.all(jobs).then(() => Util.queueMacroTask()).then(() => queue.close());
63
60
 
64
- const term = new Terminal();
65
- await term.writer.writeLines([
66
- '',
67
- cliTpl`${{ title: 'Service'.padEnd(maxName) }} ${{ title: 'Version'.padEnd(maxVersion) }} ${{ title: 'Status' }}`,
68
- ''.padEnd(maxName + maxVersion + maxStatus + 3, '-'),
69
- ]).commit();
70
61
 
71
- await term.streamList(queue);
62
+ if (this.quiet) {
63
+ for await (const _ of queue) { }
64
+ if (failureMessages.length) {
65
+ console.error('Failure');
66
+ failureMessages.map(stripVTControlCharacters).map(item => console.error(item));
67
+ }
68
+ } else {
69
+ const term = new Terminal();
70
+ await term.writer.writeLines([
71
+ '',
72
+ cliTpl`${{ title: 'Service'.padEnd(maxName) }} ${{ title: 'Version'.padEnd(maxVersion) }} ${{ title: 'Status' }}`,
73
+ ''.padEnd(maxName + maxVersion + maxStatus + 3, '-'),
74
+ ]).commit();
75
+
76
+ await term.streamList(queue);
77
+ }
78
+
79
+ process.exitCode = failureMessages.length ? 1 : 0;
72
80
  }
73
81
  }
@@ -1,4 +1,4 @@
1
1
  // @trv-no-transform
2
- import '@travetto/runtime/support/polyfill.js';
2
+ import '@travetto/runtime/support/patch.js';
3
3
  import { ExecutionManager } from '@travetto/cli';
4
4
  ExecutionManager.run(process.argv);
package/src/error.ts DELETED
@@ -1,58 +0,0 @@
1
- import { RuntimeError, Runtime } from '@travetto/runtime';
2
-
3
- import { cliTpl } from './color.ts';
4
- import type { CliValidationError, CliCommandShape } from './types.ts';
5
-
6
- const COMMAND_PACKAGE = [
7
- [/^test(:watch)?$/, 'test', false],
8
- [/^lint(:register)?$/, 'eslint', false],
9
- [/^model:(install|export)$/, 'model', true],
10
- [/^openapi:(spec|client)$/, 'openapi', true],
11
- [/^email:(compile|editor)$/, 'email-compiler', false],
12
- [/^pack(:zip|:docker)?$/, 'pack', false],
13
- [/^web:http$/, 'web-http', true],
14
- [/^web:rpc-client$/, 'web-rpc', true],
15
- ] as const;
16
-
17
- /**
18
- * Provides a contract for unknown commands
19
- */
20
- export class CliUnknownCommandError extends Error {
21
-
22
- #getMissingCommandHelp(cmd: string): string | undefined {
23
- const matchedConfig = COMMAND_PACKAGE.find(([regex]) => regex.test(cmd));
24
- if (matchedConfig) {
25
- const [, pkg, production] = matchedConfig;
26
- const install = Runtime.getInstallCommand(`@travetto/${pkg}`, production);
27
- return cliTpl`
28
- ${{ title: 'Missing Package' }}\n${'-'.repeat(20)}\nTo use ${{ input: cmd }} please run:\n
29
- ${{ identifier: install }}
30
- `;
31
- }
32
- }
33
-
34
- help?: string;
35
- cmd: string;
36
-
37
- constructor(cmd: string) {
38
- super(`Unknown command: ${cmd}`);
39
- this.cmd = cmd;
40
- this.help = this.#getMissingCommandHelp(cmd);
41
- }
42
-
43
- get defaultMessage(): string {
44
- return cliTpl`${{ subtitle: 'Unknown command' }}: ${{ input: this.cmd }}`;
45
- }
46
- }
47
-
48
- /**
49
- * Provides a basic error wrapper for cli validation issues
50
- */
51
- export class CliValidationResultError extends RuntimeError<{ errors: CliValidationError[] }> {
52
- command: CliCommandShape;
53
-
54
- constructor(command: CliCommandShape, errors: CliValidationError[]) {
55
- super('', { details: { errors } });
56
- this.command = command;
57
- }
58
- }