@travetto/cli 3.3.6 → 3.4.0-rc.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
@@ -420,7 +420,9 @@ If the goal is to run a more complex application, which may include depending on
420
420
  **Code: Simple Run Target**
421
421
  ```typescript
422
422
  import { DependencyRegistry } from '@travetto/di';
423
- import { CliCommand } from '@travetto/cli';
423
+ import { CliCommand, CliUtil } from '@travetto/cli';
424
+ import { GlobalEnv } from '@travetto/base';
425
+
424
426
  import { ServerHandle } from '../src/types';
425
427
 
426
428
  /**
@@ -429,6 +431,12 @@ import { ServerHandle } from '../src/types';
429
431
  @CliCommand({ runTarget: true, fields: ['module', 'env', 'profile'] })
430
432
  export class RunRestCommand {
431
433
 
434
+ /** IPC debug is enabled */
435
+ debugIpc = true;
436
+
437
+ /** Should the server be able to run with restart*/
438
+ canRestart = GlobalEnv.devMode;
439
+
432
440
  /** Port to run on */
433
441
  port?: number;
434
442
 
@@ -436,7 +444,11 @@ export class RunRestCommand {
436
444
  return this.port ? { REST_PORT: `${this.port}` } : {};
437
445
  }
438
446
 
439
- async main(): Promise<ServerHandle> {
447
+ async main(): Promise<ServerHandle | void> {
448
+ if (await CliUtil.debugIfIpc(this) || await CliUtil.runWithRestart(this)) {
449
+ return;
450
+ }
451
+
440
452
  const { RestApplication } = await import('../src/application/rest.js');
441
453
  return DependencyRegistry.runInstance(RestApplication);
442
454
  }
package/bin/trv.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+
3
+ // @ts-check
4
+ import { withContext } from '@travetto/compiler/bin/common.js';
5
+
6
+ withContext((ctx, compile) => compile(ctx, 'run')).then(load => load?.('@travetto/cli/support/entry.trv.js'));
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@travetto/cli",
3
- "version": "3.3.6",
3
+ "version": "3.4.0-rc.1",
4
4
  "description": "CLI infrastructure for Travetto framework",
5
5
  "keywords": [
6
6
  "cli",
7
7
  "travetto",
8
8
  "typescript"
9
9
  ],
10
+ "type": "module",
10
11
  "homepage": "https://travetto.io",
11
12
  "license": "MIT",
12
13
  "author": {
@@ -15,17 +16,21 @@
15
16
  },
16
17
  "files": [
17
18
  "__index__.ts",
19
+ "bin",
18
20
  "src",
19
21
  "support"
20
22
  ],
21
23
  "main": "__index__.ts",
24
+ "bin": {
25
+ "trv": "bin/trv.js"
26
+ },
22
27
  "repository": {
23
28
  "url": "https://github.com/travetto/travetto.git",
24
29
  "directory": "module/cli"
25
30
  },
26
31
  "dependencies": {
27
- "@travetto/schema": "^3.3.5",
28
- "@travetto/terminal": "^3.3.1"
32
+ "@travetto/schema": "^3.4.0-rc.0",
33
+ "@travetto/terminal": "^3.4.0-rc.0"
29
34
  },
30
35
  "travetto": {
31
36
  "displayName": "Command Line Interface"
package/src/decorators.ts CHANGED
@@ -3,7 +3,7 @@ import { RootIndex } from '@travetto/manifest';
3
3
  import { SchemaRegistry } from '@travetto/schema';
4
4
 
5
5
  import { CliCommandShape } from './types';
6
- import { CliCommandRegistry } from './registry';
6
+ import { CliCommandRegistry, CliCommandConfigOptions } from './registry';
7
7
  import { CliModuleUtil } from './module';
8
8
  import { CliUtil } from './util';
9
9
 
@@ -17,7 +17,7 @@ const getMod = (cls: Class): string => RootIndex.getModuleFromSource(RootIndex.g
17
17
  * @augments `@travetto/schema:Schema`
18
18
  * @augments `@travetto/cli:CliCommand`
19
19
  */
20
- export function CliCommand(cfg: { fields?: ExtraFields[], runTarget?: boolean, hidden?: boolean } = {}) {
20
+ export function CliCommand({ fields, ...cfg }: { fields?: ExtraFields[] } & CliCommandConfigOptions = {}) {
21
21
  return function <T extends CliCommandShape>(target: Class<T>): void {
22
22
  const meta = RootIndex.getFunctionMetadata(target);
23
23
  if (!meta || meta.abstract) {
@@ -25,17 +25,16 @@ export function CliCommand(cfg: { fields?: ExtraFields[], runTarget?: boolean, h
25
25
  }
26
26
 
27
27
  const name = getName(meta.source);
28
- const addEnv = cfg.fields?.includes('env');
29
- const addProfile = cfg.fields?.includes('profile');
30
- const addModule = cfg.fields?.includes('module');
28
+ const addEnv = fields?.includes('env');
29
+ const addProfile = fields?.includes('profile');
30
+ const addModule = fields?.includes('module');
31
31
 
32
32
  CliCommandRegistry.registerClass({
33
33
  module: getMod(target),
34
34
  name,
35
35
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
36
36
  cls: target as ConcreteClass<T>,
37
- hidden: cfg.hidden,
38
- runTarget: cfg.runTarget,
37
+ ...cfg,
39
38
  preMain: (cmd: CliCommandShape & { env?: string, profile?: string[], module?: string }) => {
40
39
  if (addEnv || addProfile) {
41
40
  defineGlobalEnv({
package/src/error.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { RootIndex } from '@travetto/manifest';
2
+
3
+ import { cliTpl } from './color';
4
+ import { CliValidationError } from './types';
5
+
6
+ const COMMAND_PACKAGE = [
7
+ [/^test(:watch)?$/, 'test', false],
8
+ [/^service$/, 'command', true],
9
+ [/^lint(:register)?$/, 'lint', true],
10
+ [/^model:(install|export)$/, 'model', true],
11
+ [/^openapi:(spec|client)$/, 'openapi', true],
12
+ [/^email:(compile|editor)$/, 'email-compiler', false],
13
+ [/^pack(:zip|:docker)?$/, 'pack', false],
14
+ [/^run:rest$/, 'rest', false],
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 matchedCfg = COMMAND_PACKAGE.find(([re]) => re.test(cmd));
24
+ if (matchedCfg) {
25
+ const [, pkg, prod] = matchedCfg;
26
+ let install: string;
27
+ switch (RootIndex.manifest.packageManager) {
28
+ case 'npm': install = `npm i ${prod ? '' : '--save-dev '}@travetto/${pkg}`; break;
29
+ case 'yarn': install = `yarn add ${prod ? '' : '--dev '}@travetto/${pkg}`; break;
30
+ }
31
+ return cliTpl`
32
+ ${{ title: 'Missing Package' }}\n${'-'.repeat(20)}\nTo use ${{ input: cmd }} please run:\n
33
+ ${{ identifier: install }}
34
+ `;
35
+ }
36
+ }
37
+
38
+ help?: string;
39
+ cmd: string;
40
+
41
+ constructor(cmd: string) {
42
+ super(`Unknown command: ${cmd}`);
43
+ this.cmd = cmd;
44
+ this.help = this.#getMissingCommandHelp(cmd);
45
+ }
46
+
47
+ get defaultMessage(): string {
48
+ return cliTpl`${{ subtitle: 'Unknown command' }}: ${{ input: this.cmd }}`;
49
+ }
50
+ }
51
+
52
+
53
+ /**
54
+ * Provides a basic error wrapper for cli validation issues
55
+ */
56
+ export class CliValidationResultError extends Error {
57
+ errors: CliValidationError[];
58
+
59
+ constructor(errors: CliValidationError[]) {
60
+ super('');
61
+ this.errors = errors;
62
+ }
63
+ }
package/src/execute.ts CHANGED
@@ -1,13 +1,11 @@
1
- import { appendFile, mkdir } from 'fs/promises';
2
-
3
1
  import { GlobalTerminal } from '@travetto/terminal';
4
- import { path } from '@travetto/manifest';
5
2
  import { ConsoleManager, defineGlobalEnv, ShutdownManager, GlobalEnv } from '@travetto/base';
6
3
 
7
4
  import { HelpUtil } from './help';
8
- import { CliCommandShape, CliValidationResultError } from './types';
5
+ import { CliCommandShape } from './types';
9
6
  import { CliCommandRegistry } from './registry';
10
7
  import { CliCommandSchemaUtil } from './schema';
8
+ import { CliUnknownCommandError, CliValidationResultError } from './error';
11
9
 
12
10
  /**
13
11
  * Execution manager
@@ -30,13 +28,6 @@ export class ExecutionManager {
30
28
  return known;
31
29
  }
32
30
 
33
- static #getAction(cmd: CliCommandShape, args: string[]): 'help' | 'ipc' | 'command' {
34
- const cfg = CliCommandRegistry.getConfig(cmd);
35
- return args.find(a => /^(-h|--help)$/.test(a)) ?
36
- 'help' :
37
- (process.env.TRV_CLI_IPC && cfg.runTarget) ? 'ipc' : 'command';
38
- }
39
-
40
31
  /**
41
32
  * Run help
42
33
  */
@@ -46,24 +37,6 @@ export class ExecutionManager {
46
37
  console.log!(await HelpUtil.renderHelp(cmd));
47
38
  }
48
39
 
49
- /**
50
- * Append IPC payload to provided file
51
- */
52
- static async ipc(cmd: CliCommandShape, args: string[]): Promise<void> {
53
- await this.#bindAndValidateArgs(cmd, args);
54
- const file = process.env.TRV_CLI_IPC!;
55
- const cfg = CliCommandRegistry.getConfig(cmd);
56
- const payload = JSON.stringify({
57
- type: '@travetto/cli:run', data: {
58
- name: cfg.name,
59
- module: cfg.module,
60
- args: process.argv.slice(3)
61
- }
62
- });
63
- await mkdir(path.dirname(file), { recursive: true });
64
- await appendFile(file, `${payload}\n`);
65
- }
66
-
67
40
  /**
68
41
  * Run the given command object with the given arguments
69
42
  */
@@ -103,15 +76,25 @@ export class ExecutionManager {
103
76
  try {
104
77
  // Load a single command
105
78
  command = (await CliCommandRegistry.getInstance(cmd, true));
106
- const action = this.#getAction(command, args);
107
- await this[action](command, args);
79
+ if (args.some(a => /^(-h|--help)$/.test(a))) {
80
+ await this.help(command, args);
81
+ } else {
82
+ await this.command(command, args);
83
+ }
108
84
  } catch (err) {
109
85
  process.exitCode ||= 1; // Trigger error state
110
86
  if (!(err instanceof Error)) {
111
87
  throw err;
112
- } else if (command && err instanceof CliValidationResultError) {
113
- console.error(await HelpUtil.renderValidationError(command, err));
88
+ } else if (err instanceof CliValidationResultError) {
89
+ console.error!(await HelpUtil.renderValidationError(command!, err));
114
90
  console.error!(await HelpUtil.renderHelp(command));
91
+ } else if (err instanceof CliUnknownCommandError) {
92
+ if (err.help) {
93
+ console.error!(err.help);
94
+ } else {
95
+ console.error!(err.defaultMessage, '\n');
96
+ console.error!(await HelpUtil.renderAllHelp(''));
97
+ }
115
98
  } else {
116
99
  console.error!(err);
117
100
  console.error!();
package/src/help.ts CHANGED
@@ -2,9 +2,10 @@ import { Primitive } from '@travetto/base';
2
2
  import { stripAnsiCodes } from '@travetto/terminal';
3
3
 
4
4
  import { cliTpl } from './color';
5
- import { CliCommandShape, CliValidationResultError } from './types';
5
+ import { CliCommandShape } from './types';
6
6
  import { CliCommandRegistry } from './registry';
7
7
  import { CliCommandSchemaUtil } from './schema';
8
+ import { CliValidationResultError } from './error';
8
9
 
9
10
  const validationSourceMap = {
10
11
  custom: '',
@@ -21,7 +22,7 @@ export class HelpUtil {
21
22
  * Render command-specific help
22
23
  * @param command
23
24
  */
24
- static async #renderCommandHelp(command: CliCommandShape): Promise<string> {
25
+ static async renderCommandHelp(command: CliCommandShape): Promise<string> {
25
26
  const commandName = CliCommandRegistry.getName(command);
26
27
 
27
28
  command.initialize?.();
@@ -93,7 +94,7 @@ export class HelpUtil {
93
94
  /**
94
95
  * Render help listing of all commands
95
96
  */
96
- static async #renderAllHelp(): Promise<string> {
97
+ static async renderAllHelp(title?: string): Promise<string> {
97
98
  const rows: string[] = [];
98
99
  const keys = [...CliCommandRegistry.getCommandMapping().keys()].sort((a, b) => a.localeCompare(b));
99
100
  const maxWidth = keys.reduce((a, b) => Math.max(a, stripAnsiCodes(b).length), 0);
@@ -116,20 +117,20 @@ export class HelpUtil {
116
117
  }
117
118
  }
118
119
  }
119
- return [
120
- cliTpl`${{ title: 'Usage:' }} ${{ param: '[options]' }} ${{ param: '[command]' }}`,
121
- '',
122
- cliTpl`${{ title: 'Commands:' }}`,
123
- ...rows,
124
- ''
125
- ].map(x => x.trimEnd()).join('\n');
120
+
121
+ const lines = [cliTpl`${{ title: 'Commands:' }}`, ...rows, ''];
122
+
123
+ if (title === undefined || title) {
124
+ lines.unshift(title ? cliTpl`${{ title }}` : cliTpl`${{ title: 'Usage:' }} ${{ param: '[options]' }} ${{ param: '[command]' }}`, '');
125
+ }
126
+ return lines.map(x => x.trimEnd()).join('\n');
126
127
  }
127
128
 
128
129
  /**
129
130
  * Render help
130
131
  */
131
132
  static async renderHelp(command?: CliCommandShape): Promise<string> {
132
- return command ? this.#renderCommandHelp(command) : this.#renderAllHelp();
133
+ return command ? this.renderCommandHelp(command) : this.renderAllHelp();
133
134
  }
134
135
 
135
136
  /**
package/src/registry.ts CHANGED
@@ -1,52 +1,27 @@
1
1
  import { Class, ConcreteClass } from '@travetto/base';
2
2
  import { RootIndex } from '@travetto/manifest';
3
3
 
4
- import { cliTpl } from './color';
5
4
  import { CliCommandShape } from './types';
5
+ import { CliUnknownCommandError } from './error';
6
6
 
7
- type CliCommandConfig = {
7
+ export type CliCommandConfigOptions = {
8
+ runTarget?: boolean;
9
+ hidden?: boolean;
10
+ };
11
+
12
+ export type CliCommandConfig = CliCommandConfigOptions & {
8
13
  name: string;
9
14
  module: string;
10
15
  cls: ConcreteClass<CliCommandShape>;
11
- runTarget?: boolean;
12
- hidden?: boolean;
13
16
  preMain?: (cmd: CliCommandShape) => void | Promise<void>;
14
17
  };
15
18
 
16
- const COMMAND_PACKAGE = [
17
- [/^test(:watch)?$/, 'test', false],
18
- [/^service$/, 'command', true],
19
- [/^lint(:register)?$/, 'lint', true],
20
- [/^model:(install|export)$/, 'model', true],
21
- [/^openapi:(spec|client)$/, 'openapi', true],
22
- [/^email:(compile|editor)$/, 'email-compiler', false],
23
- [/^pack(:zip|:docker)?$/, 'pack', false],
24
- ] as const;
25
-
26
19
  const CLI_REGEX = /\/cli[.]([^.]+)[.][^.]+?$/;
27
20
 
28
21
  class $CliCommandRegistry {
29
22
  #commands = new Map<Class, CliCommandConfig>();
30
23
  #fileMapping: Map<string, string>;
31
24
 
32
- #getMissingCommandHelp(cmd: string): string {
33
- const matchedCfg = COMMAND_PACKAGE.find(([re]) => re.test(cmd));
34
- if (matchedCfg) {
35
- const [, pkg, prod] = matchedCfg;
36
- let install: string;
37
- switch (RootIndex.manifest.packageManager) {
38
- case 'npm': install = `npm i ${prod ? '' : '--save-dev '}@travetto/${pkg}`; break;
39
- case 'yarn': install = `yarn add ${prod ? '' : '--dev '}@travetto/${pkg}`; break;
40
- }
41
- return cliTpl`
42
- ${{ title: 'Missing Package' }}\n${'-'.repeat(20)}\nTo use ${{ input: cmd }} please run:\n
43
- ${{ identifier: install }}
44
- `;
45
- } else {
46
- return `Unknown command: ${cmd}`;
47
- }
48
- }
49
-
50
25
  #get(cls: Class): CliCommandConfig | undefined {
51
26
  return this.#commands.get(cls);
52
27
  }
@@ -115,7 +90,7 @@ ${{ identifier: install }}
115
90
  return undefined;
116
91
  }
117
92
  }
118
- throw new Error(this.#getMissingCommandHelp(name));
93
+ throw new CliUnknownCommandError(name);
119
94
  }
120
95
  }
121
96
 
package/src/schema.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { Class, ConsoleManager, GlobalEnv } from '@travetto/base';
2
2
  import { BindUtil, FieldConfig, SchemaRegistry, SchemaValidator, ValidationResultError } from '@travetto/schema';
3
- import { CliCommandRegistry } from './registry';
4
3
 
5
- import { CliCommandInput, CliCommandSchema, CliCommandShape, CliValidationResultError } from './types';
4
+ import { CliCommandRegistry } from './registry';
5
+ import { CliCommandInput, CliCommandSchema, CliCommandShape } from './types';
6
+ import { CliValidationResultError } from './error';
6
7
 
7
8
  function fieldToInput(x: FieldConfig): CliCommandInput {
8
9
  const type = x.type === Date ? 'date' :
package/src/types.ts CHANGED
@@ -18,18 +18,6 @@ export type CliValidationError = {
18
18
  source?: 'flag' | 'arg' | 'custom';
19
19
  };
20
20
 
21
- /**
22
- * Provides a basic error wrapper for internal try/catch instanceof
23
- */
24
- export class CliValidationResultError extends Error {
25
- errors: CliValidationError[];
26
-
27
- constructor(errors: CliValidationError[]) {
28
- super('');
29
- this.errors = errors;
30
- }
31
- }
32
-
33
21
  /**
34
22
  * CLI Command Contract
35
23
  */
package/src/util.ts CHANGED
@@ -1,4 +1,10 @@
1
- import { RootIndex } from '@travetto/manifest';
1
+ import fs from 'fs/promises';
2
+
3
+ import { Env, ExecUtil } from '@travetto/base';
4
+ import { path, RootIndex } from '@travetto/manifest';
5
+
6
+ import { CliCommandShape } from './types';
7
+ import { CliCommandRegistry } from './registry';
2
8
 
3
9
  export class CliUtil {
4
10
  /**
@@ -15,4 +21,56 @@ export class CliUtil {
15
21
  static getSimpleModuleName(name = RootIndex.mainModuleName): string {
16
22
  return name.replace(/[\/]/, '_').replace(/@/, '');
17
23
  }
24
+
25
+ /**
26
+ * Run a command as restartable, linking into self
27
+ */
28
+ static runWithRestart<T extends CliCommandShape & { canRestart?: boolean }>(cmd: T): Promise<unknown> | undefined {
29
+ if (cmd.canRestart === false || Env.isFalse('TRV_CAN_RESTART')) {
30
+ delete process.env.TRV_CAN_RESTART;
31
+ return;
32
+ }
33
+ return ExecUtil.spawnWithRestart(process.argv0, process.argv.slice(1), {
34
+ env: { TRV_DYNAMIC: '1', TRV_CAN_RESTART: '0' },
35
+ stdio: [0, 1, 2, 'ipc']
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Dispatch IPC payload
41
+ */
42
+ static async triggerIpc<T extends CliCommandShape>(action: 'run', cmd: T): Promise<boolean> {
43
+ const file = process.env.TRV_CLI_IPC;
44
+
45
+ if (!file) {
46
+ return false;
47
+ }
48
+
49
+ const cfg = CliCommandRegistry.getConfig(cmd);
50
+ const payload = JSON.stringify({
51
+ type: `@travetto/cli:${action}`, data: {
52
+ name: cfg.name,
53
+ commandModule: cfg.module,
54
+ module: RootIndex.manifest.mainModule,
55
+ args: process.argv.slice(3)
56
+ }
57
+ });
58
+ await fs.mkdir(path.dirname(file), { recursive: true });
59
+ await fs.appendFile(file, `${payload}\n`);
60
+ return true;
61
+ }
62
+
63
+ /**
64
+ * Debug if IPC available
65
+ */
66
+ static async debugIfIpc<T extends CliCommandShape & { debugIpc?: boolean }>(cmd: T): Promise<boolean> {
67
+ return cmd.debugIpc !== false && this.triggerIpc('run', cmd);
68
+ }
69
+
70
+ /**
71
+ * Write data to channel and ensure its flushed before continuing
72
+ */
73
+ static async writeAndEnsureComplete(data: unknown, channel: 'stdout' | 'stderr' = 'stdout'): Promise<void> {
74
+ return await new Promise(r => process[channel].write(typeof data === 'string' ? data : JSON.stringify(data, null, 2), () => r()));
75
+ }
18
76
  }
@@ -2,6 +2,7 @@ import { CliCommand } from '../src/decorators';
2
2
  import { CliCommandSchema, CliValidationError } from '../src/types';
3
3
  import { CliCommandRegistry } from '../src/registry';
4
4
  import { CliCommandSchemaUtil } from '../src/schema';
5
+ import { CliUtil } from '../src/util';
5
6
 
6
7
  /**
7
8
  * Generates the schema for all CLI operations
@@ -24,12 +25,14 @@ export class CliSchemaCommand {
24
25
  }
25
26
 
26
27
  async main(name?: string): Promise<void> {
28
+ let output: unknown = undefined;
27
29
  if (name) {
28
- console.log(JSON.stringify(await this.#getSchema(name), null, 2));
30
+ output = await this.#getSchema(name);
29
31
  } else {
30
32
  const names = [...CliCommandRegistry.getCommandMapping().keys()];
31
33
  const schemas = await Promise.all(names.map(x => this.#getSchema(x)));
32
- console.log(JSON.stringify(schemas, null, 2));
34
+ output = schemas;
33
35
  }
36
+ await CliUtil.writeAndEnsureComplete(output);
34
37
  }
35
38
  }
File without changes