@travetto/cli 3.1.0-rc.0 → 3.1.0-rc.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.
package/README.md CHANGED
@@ -130,9 +130,9 @@ HELLO
130
130
 
131
131
  The [@CliCommand](https://github.com/travetto/travetto/tree/main/module/cli/src/decorators.ts#L20) supports the following data types for flags:
132
132
  * Boolean values
133
- * Number values. The [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L179), [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L185), [@Precision](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L173), [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L114) and [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L124) decorators help provide additional validation.
134
- * String values. [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L114), [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L124), [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L106) and [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L85) provide additional constraints
135
- * Date values. The [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L114) and [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L124) decorators help provide additional validation.
133
+ * Number values. The [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L172), [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L178), [@Precision](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L166), [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L107) and [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L117) decorators help provide additional validation.
134
+ * String values. [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L107), [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L117), [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L99) and [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L78) provide additional constraints
135
+ * Date values. The [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L107) and [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L117) decorators help provide additional validation.
136
136
  * String lists. Same as String, but allowing multiple values.
137
137
  * Numeric lists. Same as Number, but allowing multiple values.
138
138
 
@@ -168,7 +168,7 @@ Options:
168
168
  $ trv basic:arg 20
169
169
 
170
170
  Execution failed:
171
- * volume is bigger than (10). [1]
171
+ * Argument volume is bigger than (10)
172
172
 
173
173
  Usage: doc/cli.basic:arg [options] [volume:number]
174
174
 
@@ -231,7 +231,7 @@ $ trv basic:arglist 10 5 3 9 8 1
231
231
  $ trv basic:arglist 10 5 3 9 20 1
232
232
 
233
233
  Execution failed:
234
- * volumes[4] is bigger than (10). [1]
234
+ * Argument volumes[4] is bigger than (10)
235
235
 
236
236
  Usage: doc/cli.basic:arglist [options] <volumes...:number>
237
237
 
@@ -358,7 +358,7 @@ CuStOm
358
358
  ```
359
359
 
360
360
  ## VSCode Integration
361
- 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. [RESTful API](https://github.com/travetto/travetto/tree/main/module/rest#readme "Declarative api for RESTful APIs with support for the dependency injection module.") does expose a cli target `run:rest` that will show up, to help run/debug a rest 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).
361
+ 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. [RESTful API](https://github.com/travetto/travetto/tree/main/module/rest#readme "Declarative api for RESTful APIs with support for the dependency injection module.") does expose a cli target `run:rest` that will show up, to help run/debug a rest 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/decorators.ts#L20) decorator. This means the target will be visible within the editor tooling.
362
362
 
363
363
  **Code: Simple Run Target**
364
364
  ```typescript
@@ -376,9 +376,43 @@ export class RunCommand {
376
376
  }
377
377
  ```
378
378
 
379
- Also, any command name that starts with `run:` (i.e. `support/cli.run_*.ts`), will be opted-in to the run behavior unless explicitly disabled.
380
-
381
379
  ## Advanced Usage
380
+
381
+ **Code: Anatomy of a Command**
382
+ ```typescript
383
+ export interface CliCommandShape {
384
+ /**
385
+ * Action target of the command
386
+ */
387
+ main(...args: unknown[]): OrProm<RunResponse>;
388
+ /**
389
+ * Setup environment before command runs
390
+ */
391
+ envInit?(): OrProm<GlobalEnvConfig>;
392
+ /**
393
+ * Extra help
394
+ */
395
+ help?(): OrProm<string[]>;
396
+ /**
397
+ * Is the command active/eligible for usage
398
+ */
399
+ isActive?(): boolean;
400
+ /**
401
+ * Run before binding occurs
402
+ */
403
+ initialize?(): OrProm<void>;
404
+ /**
405
+ * Run before validation occurs
406
+ */
407
+ finalize?(unknownArgs: string[]): OrProm<void>;
408
+ /**
409
+ * Validation method
410
+ */
411
+ validate?(...unknownArgs: unknown[]): OrProm<CliValidationError | CliValidationError[] | undefined>;
412
+ }
413
+ ```
414
+
415
+ ### Dependency Injection
382
416
  If the goal is to run a more complex application, which may include depending on [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support."), we can take a look at [RESTful API](https://github.com/travetto/travetto/tree/main/module/rest#readme "Declarative api for RESTful APIs with support for the dependency injection module.")'s target:
383
417
 
384
418
  **Code: Simple Run Target**
@@ -390,7 +424,7 @@ import { ServerHandle } from '../src/types';
390
424
  /**
391
425
  * Run a rest server as an application
392
426
  */
393
- @CliCommand({ fields: ['module', 'env', 'profile'] })
427
+ @CliCommand({ runTarget: true, fields: ['module', 'env', 'profile'] })
394
428
  export class RunRestCommand {
395
429
 
396
430
  /** Port to run on */
@@ -410,3 +444,32 @@ export class RunRestCommand {
410
444
  As noted in the example above, `fields` is specified in this execution, with support for `module`, `env`, and `profile`. These env and profile flags are directly tied to the GlobalEnv flags defined in the [Base](https://github.com/travetto/travetto/tree/main/module/base#readme "Environment config and common utilities for travetto applications.") module.
411
445
 
412
446
  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.
447
+
448
+ ### Custom Validation
449
+ 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#L10) errors that are returned will be shared with the user, and fail to invoke the `main` method.
450
+
451
+ **Code: CliValidationError**
452
+ ```typescript
453
+ export type CliValidationError = {
454
+ /**
455
+ * The error message
456
+ */
457
+ message: string;
458
+ /**
459
+ * Source of validation
460
+ */
461
+ source?: 'flag' | 'arg' | 'custom';
462
+ };
463
+ ```
464
+
465
+ A simple example of the validation can be found in the `doc` command:
466
+
467
+ **Code: Simple Validation Example**
468
+ ```typescript
469
+ async validate(...args: unknown[]): Promise<CliValidationError | undefined> {
470
+ const docFile = path.resolve(this.input);
471
+ if (!(await fs.stat(docFile).catch(() => false))) {
472
+ return { message: `input: ${this.input} does not exist`, source: 'flag' };
473
+ }
474
+ }
475
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/cli",
3
- "version": "3.1.0-rc.0",
3
+ "version": "3.1.0-rc.2",
4
4
  "description": "CLI infrastructure for Travetto framework",
5
5
  "keywords": [
6
6
  "cli",
@@ -24,7 +24,7 @@
24
24
  "directory": "module/cli"
25
25
  },
26
26
  "dependencies": {
27
- "@travetto/schema": "^3.1.0-rc.0",
27
+ "@travetto/schema": "^3.1.0-rc.2",
28
28
  "@travetto/terminal": "^3.1.0-rc.0",
29
29
  "@travetto/worker": "^3.1.0-rc.0"
30
30
  },
package/src/decorators.ts CHANGED
@@ -35,7 +35,6 @@ export function CliCommand(cfg: { fields?: ExtraFields[], runTarget?: boolean, h
35
35
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
36
36
  cls: target as ConcreteClass<T>,
37
37
  hidden: cfg.hidden,
38
- runTarget: cfg.runTarget ?? name.startsWith('run:'),
39
38
  preMain: (cmd: CliCommandShape & { env?: string, profile?: string[], module?: string }) => {
40
39
  if (addEnv) { defineGlobalEnv({ envName: cmd.env }); }
41
40
  if (addProfile) { defineGlobalEnv({ profiles: cmd.profile }); }
@@ -72,10 +71,11 @@ export function CliCommand(cfg: { fields?: ExtraFields[], runTarget?: boolean, h
72
71
  });
73
72
 
74
73
  // Register validator for module
75
- (pendingCls.validators ??= []).push(item =>
74
+ (pendingCls.validators ??= []).push(async item => {
76
75
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
77
- CliModuleUtil.validateCommandModule(getMod(target), item as { module?: string })
78
- );
76
+ const res = await CliModuleUtil.validateCommandModule(getMod(target), item as { module?: string });
77
+ return res ? { ...res, kind: 'custom', path: '.' } : res;
78
+ });
79
79
  }
80
80
  };
81
81
  }
package/src/help.ts CHANGED
@@ -6,6 +6,12 @@ import { CliCommandShape, CliValidationResultError } from './types';
6
6
  import { CliCommandRegistry } from './registry';
7
7
  import { CliCommandSchemaUtil } from './schema';
8
8
 
9
+ const validationSourceMap = {
10
+ custom: '',
11
+ arg: 'Argument',
12
+ flag: 'Flag'
13
+ };
14
+
9
15
  /**
10
16
  * Utilities for showing help
11
17
  */
@@ -93,12 +99,20 @@ export class HelpUtil {
93
99
  const maxWidth = keys.reduce((a, b) => Math.max(a, stripAnsiCodes(b).length), 0);
94
100
 
95
101
  for (const cmd of keys) {
96
- const inst = await CliCommandRegistry.getInstance(cmd);
97
- if (inst) {
98
- const cfg = await CliCommandRegistry.getConfig(inst);
99
- if (!cfg.hidden) {
100
- const schema = await CliCommandSchemaUtil.getSchema(inst);
101
- rows.push(cliTpl` ${{ param: cmd.padEnd(maxWidth, ' ') }} ${{ title: schema.title }}`);
102
+ try {
103
+ const inst = await CliCommandRegistry.getInstance(cmd);
104
+ if (inst) {
105
+ const cfg = await CliCommandRegistry.getConfig(inst);
106
+ if (!cfg.hidden) {
107
+ const schema = await CliCommandSchemaUtil.getSchema(inst);
108
+ rows.push(cliTpl` ${{ param: cmd.padEnd(maxWidth, ' ') }} ${{ title: schema.title }}`);
109
+ }
110
+ }
111
+ } catch (err) {
112
+ if (err instanceof Error) {
113
+ rows.push(cliTpl` ${{ param: cmd.padEnd(maxWidth, ' ') }} ${{ failure: err.message.split(/\n/)[0] }}`);
114
+ } else {
115
+ throw err;
102
116
  }
103
117
  }
104
118
  }
@@ -124,7 +138,9 @@ export class HelpUtil {
124
138
  static renderValidationError(cmd: CliCommandShape, err: CliValidationResultError): string {
125
139
  return [
126
140
  cliTpl`${{ failure: 'Execution failed' }}:`,
127
- ...err.errors.map(e => cliTpl` * ${{ failure: e.message }}`),
141
+ ...err.errors.map(e => e.source && e.source !== 'custom' ?
142
+ cliTpl` * ${{ identifier: validationSourceMap[e.source] }} ${{ subtitle: e.message }}` :
143
+ cliTpl` * ${{ failure: e.message }}`),
128
144
  '',
129
145
  ].join('\n');
130
146
  }
package/src/module.ts CHANGED
@@ -27,7 +27,7 @@ const COLORS = TypedObject.keys(NAMED_COLORS)
27
27
 
28
28
  const colorize = (val: string, idx: number): string => COLORS[idx % COLORS.length](val);
29
29
 
30
- const modError = (message: string): CliValidationError => ({ kind: 'required', path: 'module', message });
30
+ const modError = (message: string): CliValidationError => ({ source: 'flag', message: `module: ${message}` });
31
31
 
32
32
  /**
33
33
  * Simple utilities for understanding modules for CLI use cases
package/src/schema.ts CHANGED
@@ -22,6 +22,10 @@ function fieldToInput(x: FieldConfig): CliCommandInput {
22
22
  });
23
23
  }
24
24
 
25
+ const VALID_FLAG = /^-{1,2}[a-z]/i;
26
+ const LONG_FLAG = /^--[a-z]/i;
27
+ const SHORT_FLAG = /^-[a-z]/i;
28
+
25
29
  const isBoolFlag = (x: CliCommandInput): boolean => x.type === 'boolean' && !x.array;
26
30
 
27
31
  /**
@@ -55,7 +59,7 @@ export class CliCommandSchemaUtil {
55
59
  }
56
60
 
57
61
  const schema = await SchemaRegistry.getViewSchema(cls);
58
- const flags = [...Object.values(schema.schema)].filter(v => !v.forMethod).map(fieldToInput);
62
+ const flags = Object.values(schema.schema).map(fieldToInput);
59
63
 
60
64
  // Add help command
61
65
  flags.push({ name: 'help', flagNames: ['h'], description: 'display help for command', type: 'boolean' });
@@ -64,13 +68,13 @@ export class CliCommandSchemaUtil {
64
68
 
65
69
  const used = new Set(flags
66
70
  .flatMap(f => f.flagNames ?? [])
67
- .filter(x => /^-[^-]/.test(x) || x.replaceAll('-', '').length < 3)
71
+ .filter(x => SHORT_FLAG.test(x) || x.replaceAll('-', '').length < 3)
68
72
  .map(x => x.replace(/^-+/, ''))
69
73
  );
70
74
 
71
75
  for (const flag of flags) {
72
- let short = (flag.flagNames ?? []).find(x => /^-[^-]/.test(x) || x.replaceAll('-', '').length < 3)?.replace(/^-+/, '');
73
- const long = (flag.flagNames ?? []).find(x => /^--[^-]/.test(x) || x.replaceAll('-', '').length > 2)?.replace(/^-+/, '') ??
76
+ let short = (flag.flagNames ?? []).find(x => SHORT_FLAG.test(x) || x.replaceAll('-', '').length < 3)?.replace(/^-+/, '');
77
+ const long = (flag.flagNames ?? []).find(x => LONG_FLAG.test(x) || x.replaceAll('-', '').length > 2)?.replace(/^-+/, '') ??
74
78
  flag.name.replace(/([a-z])([A-Z])/g, (_, l, r: string) => `${l}-${r.toLowerCase()}`);
75
79
  const aliases: string[] = flag.flagNames = [];
76
80
 
@@ -123,7 +127,7 @@ export class CliCommandSchemaUtil {
123
127
 
124
128
  for (const el of schema.args) {
125
129
  // Siphon off unrecognized flags, in order
126
- while (i < copy.length && copy[i].startsWith('-')) {
130
+ while (i < copy.length && VALID_FLAG.test(copy[i])) {
127
131
  i += 1;
128
132
  }
129
133
 
@@ -132,7 +136,7 @@ export class CliCommandSchemaUtil {
132
136
  } else if (el.array) {
133
137
  const sub: string[] = [];
134
138
  while (i < copy.length) {
135
- if (!copy[i].startsWith('-')) {
139
+ if (!VALID_FLAG.test(copy[i])) {
136
140
  sub.push(copy[i]);
137
141
  found[i] = true;
138
142
  }
@@ -196,7 +200,7 @@ export class CliCommandSchemaUtil {
196
200
  } else if (isBoolFlag(input)) {
197
201
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
198
202
  template[key] = !arg.startsWith('--no') as T[typeof key];
199
- } else if (next === undefined || next.startsWith('-')) {
203
+ } else if (next === undefined || VALID_FLAG.test(next)) {
200
204
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
201
205
  template[key] = null as T[typeof key];
202
206
  } else if (input.array) {
@@ -232,16 +236,18 @@ export class CliCommandSchemaUtil {
232
236
  async (): Promise<void> => {
233
237
  const res = await cmd.validate?.(...args);
234
238
  if (res) {
235
- throw new ValidationResultError(Array.isArray(res) ? res : [res]);
239
+ throw new CliValidationResultError(Array.isArray(res) ? res : [res]);
236
240
  }
237
241
  },
238
242
  ];
239
243
 
244
+ const SOURCES = ['flag', 'arg', 'custom'] as const;
245
+
240
246
  const results = validators.map((x, i) => x().catch(err => {
241
- if (!(err instanceof ValidationResultError)) {
247
+ if (!(err instanceof CliValidationResultError) && !(err instanceof ValidationResultError)) {
242
248
  throw err;
243
249
  }
244
- return err.errors.map(v => ({ ...v, message: `${v.message}. [${i}]`, index: i }));
250
+ return err.errors.map(v => ({ source: SOURCES[i], ...v }));
245
251
  }));
246
252
 
247
253
  const errors = (await Promise.all(results)).flatMap(x => (x ?? []));
package/src/types.ts CHANGED
@@ -13,13 +13,9 @@ export type CliValidationError = {
13
13
  */
14
14
  message: string;
15
15
  /**
16
- * The object path of the error
16
+ * Source of validation
17
17
  */
18
- path: string;
19
- /**
20
- * The kind of validation
21
- */
22
- kind: string;
18
+ source?: 'flag' | 'arg' | 'custom';
23
19
  };
24
20
 
25
21
  /**
@@ -65,7 +61,7 @@ export interface CliCommandShape {
65
61
  /**
66
62
  * Validation method
67
63
  */
68
- validate?(...args: unknown[]): OrProm<CliValidationError | CliValidationError[] | undefined>;
64
+ validate?(...unknownArgs: unknown[]): OrProm<CliValidationError | CliValidationError[] | undefined>;
69
65
  }
70
66
 
71
67
  /**
@@ -17,9 +17,8 @@ export class CliSchemaCommand {
17
17
  async validate(name?: string): Promise<CliValidationError | undefined> {
18
18
  if (name && !CliCommandRegistry.getCommandMapping().has(name)) {
19
19
  return {
20
- kind: 'invalid',
21
- path: 'name',
22
- message: `${name} is not a valid cli command`
20
+ source: 'arg',
21
+ message: `name: ${name} is not a valid cli command`
23
22
  };
24
23
  }
25
24
  }
@@ -31,7 +31,7 @@ export class ListModuleCommand implements CliCommandShape {
31
31
  }
32
32
  case 'json': {
33
33
  const outputMap = CliModuleUtil.getDependencyGraph(mods);
34
- await write(JSON.stringify(Object.entries(outputMap).map(([name, children]) => ({ name, children })), null, 2));
34
+ await write(JSON.stringify(Object.entries(outputMap).map(([name, children]) => ({ name, children, local: RootIndex.getModule(name)?.local })), null, 2));
35
35
  break;
36
36
  }
37
37
  case 'graph': {
@@ -25,11 +25,7 @@ export class MainCommand implements CliCommandShape {
25
25
  async validate(fileOrImport: string): Promise<CliValidationError | undefined> {
26
26
  const imp = await this.#getImport(fileOrImport);
27
27
  if (!imp) {
28
- return {
29
- message: `Unknown file: ${fileOrImport}`,
30
- kind: 'required',
31
- path: 'fileOrImport'
32
- };
28
+ return { message: `Unknown file: ${fileOrImport}` };
33
29
  }
34
30
  }
35
31