@travetto/cli 3.1.0-rc.0 → 3.1.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
@@ -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
 
@@ -379,6 +379,42 @@ export class RunCommand {
379
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
380
 
381
381
  ## Advanced Usage
382
+
383
+ **Code: Anatomy of a Command**
384
+ ```typescript
385
+ export interface CliCommandShape {
386
+ /**
387
+ * Action target of the command
388
+ */
389
+ main(...args: unknown[]): OrProm<RunResponse>;
390
+ /**
391
+ * Setup environment before command runs
392
+ */
393
+ envInit?(): OrProm<GlobalEnvConfig>;
394
+ /**
395
+ * Extra help
396
+ */
397
+ help?(): OrProm<string[]>;
398
+ /**
399
+ * Is the command active/eligible for usage
400
+ */
401
+ isActive?(): boolean;
402
+ /**
403
+ * Run before binding occurs
404
+ */
405
+ initialize?(): OrProm<void>;
406
+ /**
407
+ * Run before validation occurs
408
+ */
409
+ finalize?(unknownArgs: string[]): OrProm<void>;
410
+ /**
411
+ * Validation method
412
+ */
413
+ validate?(...unknownArgs: unknown[]): OrProm<CliValidationError | CliValidationError[] | undefined>;
414
+ }
415
+ ```
416
+
417
+ ### Dependency Injection
382
418
  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
419
 
384
420
  **Code: Simple Run Target**
@@ -410,3 +446,32 @@ export class RunRestCommand {
410
446
  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
447
 
412
448
  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.
449
+
450
+ ### Custom Validation
451
+ 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.
452
+
453
+ **Code: CliValidationError**
454
+ ```typescript
455
+ export type CliValidationError = {
456
+ /**
457
+ * The error message
458
+ */
459
+ message: string;
460
+ /**
461
+ * Source of validation
462
+ */
463
+ source?: 'flag' | 'arg' | 'custom';
464
+ };
465
+ ```
466
+
467
+ A simple example of the validation can be found in the `doc` command:
468
+
469
+ **Code: Simple Validation Example**
470
+ ```typescript
471
+ async validate(...args: unknown[]): Promise<CliValidationError | undefined> {
472
+ const docFile = path.resolve(this.input);
473
+ if (!(await fs.stat(docFile).catch(() => false))) {
474
+ return { message: `input: ${this.input} does not exist`, source: 'flag' };
475
+ }
476
+ }
477
+ ```
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.1",
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.1",
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
@@ -72,10 +72,11 @@ export function CliCommand(cfg: { fields?: ExtraFields[], runTarget?: boolean, h
72
72
  });
73
73
 
74
74
  // Register validator for module
75
- (pendingCls.validators ??= []).push(item =>
75
+ (pendingCls.validators ??= []).push(async item => {
76
76
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
77
- CliModuleUtil.validateCommandModule(getMod(target), item as { module?: string })
78
- );
77
+ const res = await CliModuleUtil.validateCommandModule(getMod(target), item as { module?: string });
78
+ return res ? { ...res, kind: 'custom', path: '.' } : res;
79
+ });
79
80
  }
80
81
  };
81
82
  }
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