@travetto/cli 3.4.8 → 3.4.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/README.md CHANGED
@@ -411,6 +411,10 @@ export class RunCommand {
411
411
  **Code: Anatomy of a Command**
412
412
  ```typescript
413
413
  export interface CliCommandShape<T extends unknown[] = unknown[]> {
414
+ /**
415
+ * Parsed state
416
+ */
417
+ _parsed?: ParsedState;
414
418
  /**
415
419
  * Action target of the command
416
420
  */
@@ -493,7 +497,7 @@ As noted in the example above, `fields` is specified in this execution, with sup
493
497
  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.
494
498
 
495
499
  ### Custom Validation
496
- 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.
500
+ 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#L22) errors that are returned will be shared with the user, and fail to invoke the `main` method.
497
501
 
498
502
  **Code: CliValidationError**
499
503
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/cli",
3
- "version": "3.4.8",
3
+ "version": "3.4.10",
4
4
  "description": "CLI infrastructure for Travetto framework",
5
5
  "keywords": [
6
6
  "cli",
package/src/decorators.ts CHANGED
@@ -26,8 +26,9 @@ export function CliCommand(cfg: CliCommandConfigOptions = {}) {
26
26
  const { commandModule } = CliCommandRegistry.registerClass(target, {
27
27
  hidden: cfg.hidden,
28
28
  preMain: async (cmd) => {
29
- if (addEnv && 'env' in cmd && typeof cmd.env === 'string') {
30
- defineEnv({ envName: cmd.env });
29
+ if (addEnv) {
30
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
31
+ defineEnv({ envName: (cmd as { env?: string }).env ?? 'dev' });
31
32
  }
32
33
  }
33
34
  });
@@ -47,6 +48,7 @@ export function CliCommand(cfg: CliCommandConfigOptions = {}) {
47
48
  SchemaRegistry.registerPendingFieldConfig(target, 'module', String, {
48
49
  aliases: ['m', CliParseUtil.toEnvField('TRV_MODULE')],
49
50
  description: 'Module to run for',
51
+ specifiers: ['module'],
50
52
  required: { active: CliUtil.monoRoot }
51
53
  });
52
54
  }
package/src/execute.ts CHANGED
@@ -19,13 +19,12 @@ export class ExecutionManager {
19
19
  */
20
20
  static async #runCommand(cmd: CliCommandShape, args: string[]): Promise<RunResponse> {
21
21
  const schema = await CliCommandSchemaUtil.getSchema(cmd);
22
- const state = await CliParseUtil.parse(schema, args);
22
+ args = await CliParseUtil.expandArgs(schema, args);
23
+ cmd._parsed = await CliParseUtil.parse(schema, args);
23
24
  const cfg = CliCommandRegistry.getConfig(cmd);
24
25
 
25
- CliParseUtil.setState(cmd, state);
26
-
27
26
  await cmd.preBind?.();
28
- const known = await CliCommandSchemaUtil.bindInput(cmd, state);
27
+ const known = await CliCommandSchemaUtil.bindInput(cmd, cmd._parsed);
29
28
 
30
29
  await cmd.preValidate?.();
31
30
  await CliCommandSchemaUtil.validate(cmd, known);
@@ -75,7 +74,7 @@ export class ExecutionManager {
75
74
 
76
75
  let command: CliCommandShape | undefined;
77
76
  try {
78
- const { cmd, args, help } = await CliParseUtil.getArgs(argv);
77
+ const { cmd, args, help } = CliParseUtil.getArgs(argv);
79
78
 
80
79
  if (!cmd) {
81
80
  console.info!(await HelpUtil.renderAllHelp());
package/src/help.ts CHANGED
@@ -6,6 +6,7 @@ import { CliCommandShape } from './types';
6
6
  import { CliCommandRegistry } from './registry';
7
7
  import { CliCommandSchemaUtil } from './schema';
8
8
  import { CliValidationResultError } from './error';
9
+ import { isBoolFlag } from './parse';
9
10
 
10
11
  const validationSourceMap = {
11
12
  custom: '',
@@ -47,7 +48,7 @@ export class HelpUtil {
47
48
  const flagVal = command[key] as unknown as Exclude<Primitive, Error>;
48
49
 
49
50
  let aliases = flag.flagNames ?? [];
50
- if (flag.type === 'boolean' && !flag.array) {
51
+ if (isBoolFlag(flag)) {
51
52
  if (flagVal === true) {
52
53
  aliases = (flag.flagNames ?? []).filter(x => !/^[-][^-]/.test(x));
53
54
  } else {
@@ -55,7 +56,7 @@ export class HelpUtil {
55
56
  }
56
57
  }
57
58
  const param = [cliTpl`${{ param: aliases.join(', ') }}`];
58
- if (!(flag.type === 'boolean' && !flag.array)) {
59
+ if (!isBoolFlag(flag)) {
59
60
  const type = flag.type === 'string' && flag.choices && flag.choices.length <= 3 ? flag.choices?.join('|') : flag.type;
60
61
  param.push(cliTpl`${{ type: `<${type}>` }}`);
61
62
  }
package/src/parse.ts CHANGED
@@ -1,19 +1,9 @@
1
1
  import fs from 'fs/promises';
2
2
 
3
3
  import { RootIndex, path } from '@travetto/manifest';
4
- import { CliCommandInput, CliCommandSchema, CliCommandShape } from './types';
5
-
6
- type ParsedFlag = { type: 'flag', input: string, array?: boolean, fieldName: string, value?: unknown };
7
- type ParsedArg = { type: 'arg', input: string, array?: boolean, index: number };
8
- type ParsedUnknown = { type: 'unknown', input: string };
9
- type ParsedInput = ParsedUnknown | ParsedFlag | ParsedArg;
10
-
11
- export type ParsedState = {
12
- inputs: string[];
13
- all: ParsedInput[];
14
- flags: ParsedFlag[];
15
- unknown: string[];
16
- };
4
+ import { CliCommandInput, CliCommandSchema, ParsedState } from './types';
5
+
6
+ type ParsedInput = ParsedState['all'][number];
17
7
 
18
8
  const RAW_SEP = '--';
19
9
  const VALID_FLAG = /^-{1,2}[a-z]/i;
@@ -21,10 +11,9 @@ const HELP_FLAG = /^-h|--help$/;
21
11
  const LONG_FLAG_WITH_EQ = /^--[a-z][^= ]+=\S+/i;
22
12
  const CONFIG_PRE = '+=';
23
13
  const ENV_PRE = 'env.';
24
- const ParsedⲐ = Symbol.for('@travetto/cli:parsed');
25
14
  const SPACE = new Set([32, 7, 13, 10]);
26
15
 
27
- const isBoolFlag = (x?: CliCommandInput): boolean => x?.type === 'boolean' && !x.array;
16
+ export const isBoolFlag = (x?: CliCommandInput): boolean => x?.type === 'boolean' && !x.array;
28
17
 
29
18
  const getInput = (cfg: { field?: CliCommandInput, rawText?: string, input: string, index?: number, value?: string }): ParsedInput => {
30
19
  const { field, input, rawText = input, value, index } = cfg;
@@ -43,6 +32,7 @@ const getInput = (cfg: { field?: CliCommandInput, rawText?: string, input: strin
43
32
  }
44
33
  };
45
34
 
35
+
46
36
  /**
47
37
  * Parsing support for the cli
48
38
  */
@@ -90,20 +80,19 @@ export class CliParseUtil {
90
80
  /**
91
81
  * Read configuration file given flag
92
82
  */
93
- static async readFlagFile(flag: string): Promise<string[]> {
83
+ static async readFlagFile(flag: string, mod: string): Promise<string[]> {
94
84
  const key = flag.replace(CONFIG_PRE, '');
95
- const mod = RootIndex.mainModuleName;
96
85
 
97
86
  // We have a file
98
87
  const rel = (key.includes('/') ? key : `@/support/pack.${key}.flags`)
99
88
  .replace('@@/', `${RootIndex.manifest.workspacePath}/`)
100
89
  .replace('@/', `${mod}/`)
101
- .replace(/^(@[^\/]+\/[^\/]+)/, (_, imp) => {
90
+ .replace(/^(@[^\/]+\/[^\/]+)(\/.*)$/, (_, imp, rest) => {
102
91
  const val = RootIndex.getModule(imp);
103
92
  if (!val) {
104
- throw new Error(`Unknown module file: ${key}, unable to proceed`);
93
+ throw new Error(`Unknown module file: ${_}, unable to proceed`);
105
94
  }
106
- return val.sourcePath;
95
+ return `${val.sourcePath}${rest}`;
107
96
  });
108
97
 
109
98
  const file = path.resolve(rel);
@@ -128,7 +117,7 @@ export class CliParseUtil {
128
117
  * Parse args to extract command from argv along with other params. Will skip
129
118
  * argv[0] and argv[1] if equal to process.argv[0:2]
130
119
  */
131
- static async getArgs(argv: string[]): Promise<{ cmd?: string, args: string[], help?: boolean }> {
120
+ static getArgs(argv: string[]): { cmd?: string, args: string[], help?: boolean } {
132
121
  let offset = 0;
133
122
  if (argv[0] === process.argv[0] && argv[1] === process.argv[1]) {
134
123
  offset = 2;
@@ -138,19 +127,28 @@ export class CliParseUtil {
138
127
  const valid = out.slice(0, max);
139
128
  const cmd = valid.length > 0 && !valid[0].startsWith('-') ? valid[0] : undefined;
140
129
  const helpIdx = valid.findIndex(x => HELP_FLAG.test(x));
141
- const args = [];
142
- for (const item of out.slice(cmd ? 1 : 0)) {
143
- if (item.startsWith(CONFIG_PRE)) {
144
- args.push(...await this.readFlagFile(item));
145
- } else {
146
- args.push(item);
147
- }
148
- }
149
- args.push(...out.slice(max));
130
+ const args = out.slice(cmd ? 1 : 0);
150
131
  const res = { cmd, args, help: helpIdx >= 0 };
151
132
  return res;
152
133
  }
153
134
 
135
+ /**
136
+ * Expand flag arguments into full argument list
137
+ */
138
+ static async expandArgs(schema: CliCommandSchema, args: string[]): Promise<string[]> {
139
+ const SEP = args.includes(RAW_SEP) ? args.indexOf(RAW_SEP) : args.length;
140
+ const input = schema.flags.find(x => x.type === 'module');
141
+ const ENV_KEY = input?.flagNames?.filter(x => x.startsWith(ENV_PRE)).map(x => x.replace(ENV_PRE, ''))[0] ?? '';
142
+ const flags = new Set(input?.flagNames ?? []);
143
+ const check = (k?: string, v?: string): string | undefined => flags.has(k!) ? v : undefined;
144
+ const mod = args.reduce(
145
+ (m, x, i, arr) =>
146
+ (i < SEP ? check(arr[i - 1], x) ?? check(...x.split('=')) : undefined) ?? m,
147
+ process.env[ENV_KEY] || RootIndex.mainModuleName
148
+ );
149
+ return (await Promise.all(args.map((x, i) => x.startsWith(CONFIG_PRE) && (i < SEP || SEP < 0) ? this.readFlagFile(x, mod) : x))).flat();
150
+ }
151
+
154
152
  /**
155
153
  * Parse inputs to command
156
154
  */
@@ -210,22 +208,8 @@ export class CliParseUtil {
210
208
  return {
211
209
  inputs,
212
210
  all: out,
213
- unknown: out.filter((x): x is ParsedUnknown => x.type === 'unknown').map(x => x.input),
214
- flags: out.filter((x): x is ParsedFlag => x.type === 'flag')
211
+ unknown: out.filter(x => x.type === 'unknown').map(x => x.input),
212
+ flags: out.filter((x): x is ParsedInput & { type: 'flag' } => x.type === 'flag')
215
213
  };
216
214
  }
217
-
218
- /**
219
- * Get parse state from the command
220
- */
221
- static getState(cmd: CliCommandShape & { [ParsedⲐ]?: ParsedState }): ParsedState | undefined {
222
- return cmd[ParsedⲐ];
223
- }
224
-
225
- /**
226
- * Set state for a command
227
- */
228
- static setState(cmd: CliCommandShape & { [ParsedⲐ]?: ParsedState }, state: ParsedState): void {
229
- cmd[ParsedⲐ] = state;
230
- }
231
215
  }
package/src/schema.ts CHANGED
@@ -2,35 +2,45 @@ import { Class, ConsoleManager, GlobalEnv } from '@travetto/base';
2
2
  import { BindUtil, FieldConfig, SchemaRegistry, SchemaValidator, ValidationResultError } from '@travetto/schema';
3
3
 
4
4
  import { CliCommandRegistry } from './registry';
5
- import { CliCommandInput, CliCommandSchema, CliCommandShape } from './types';
5
+ import { ParsedState, CliCommandInput, CliCommandSchema, CliCommandShape } from './types';
6
6
  import { CliValidationResultError } from './error';
7
- import { ParsedState } from './parse';
8
7
 
9
8
  const LONG_FLAG = /^--[a-z][^= ]+/i;
10
9
  const SHORT_FLAG = /^-[a-z]/i;
11
10
 
12
11
  const isBoolFlag = (x?: CliCommandInput): boolean => x?.type === 'boolean' && !x.array;
13
12
 
14
- function fieldToInput(x: FieldConfig): CliCommandInput {
15
- const type = x.type === Date ? 'date' :
16
- x.type === Boolean ? 'boolean' :
17
- x.type === String ? (x.specifiers?.includes('file') ? 'file' : 'string') :
18
- x.type === Number ? 'number' :
19
- x.type === RegExp ? 'regex' : 'string';
20
- return ({
21
- name: x.name,
22
- description: x.description,
23
- array: x.array,
24
- required: x.required?.active,
25
- choices: x.enum?.values,
26
- fileExtensions: type === 'file' ? x.specifiers?.filter(s => s.startsWith('ext:')).map(s => s.split('ext:')[1]) : undefined,
27
- type,
28
- default: Array.isArray(x.default) ? x.default.slice(0) : x.default,
29
- flagNames: (x.aliases ?? []).slice(0).filter(v => !v.startsWith('env.')),
30
- envVars: (x.aliases ?? []).slice(0).filter(v => v.startsWith('env.')).map(v => v.replace('env.', ''))
31
- });
13
+ function baseType(x: FieldConfig): Pick<CliCommandInput, 'type' | 'fileExtensions'> {
14
+ switch (x.type) {
15
+ case Date: return { type: 'date' };
16
+ case Boolean: return { type: 'boolean' };
17
+ case Number: return { type: 'number' };
18
+ case RegExp: return { type: 'regex' };
19
+ case String: {
20
+ switch (true) {
21
+ case x.specifiers?.includes('module'): return { type: 'module' };
22
+ case x.specifiers?.includes('file'): return {
23
+ type: 'file',
24
+ fileExtensions: x.specifiers?.map(s => s.split('ext:')[1]).filter(s => !!s)
25
+ };
26
+ }
27
+ }
28
+ }
29
+ return { type: 'string' };
32
30
  }
33
31
 
32
+ const fieldToInput = (x: FieldConfig): CliCommandInput => ({
33
+ ...baseType(x),
34
+ name: x.name,
35
+ description: x.description,
36
+ array: x.array,
37
+ required: x.required?.active,
38
+ choices: x.enum?.values,
39
+ default: Array.isArray(x.default) ? x.default.slice(0) : x.default,
40
+ flagNames: (x.aliases ?? []).slice(0).filter(v => !v.startsWith('env.')),
41
+ envVars: (x.aliases ?? []).slice(0).filter(v => v.startsWith('env.')).map(v => v.replace('env.', ''))
42
+ });
43
+
34
44
  /**
35
45
  * Allows binding describing/binding inputs for commands
36
46
  */
package/src/types.ts CHANGED
@@ -4,6 +4,18 @@ type OrProm<T> = T | Promise<T>;
4
4
 
5
5
  export type RunResponse = { wait(): Promise<unknown> } | { on(event: 'close', cb: Function): unknown } | Closeable | void | undefined;
6
6
 
7
+ type ParsedFlag = { type: 'flag', input: string, array?: boolean, fieldName: string, value?: unknown };
8
+ type ParsedArg = { type: 'arg', input: string, array?: boolean, index: number };
9
+ type ParsedUnknown = { type: 'unknown', input: string };
10
+ type ParsedInput = ParsedUnknown | ParsedFlag | ParsedArg;
11
+
12
+ export type ParsedState = {
13
+ inputs: string[];
14
+ all: ParsedInput[];
15
+ flags: ParsedFlag[];
16
+ unknown: string[];
17
+ };
18
+
7
19
  /**
8
20
  * Constrained version of Schema's Validation Error
9
21
  */
@@ -22,6 +34,10 @@ export type CliValidationError = {
22
34
  * CLI Command Contract
23
35
  */
24
36
  export interface CliCommandShape<T extends unknown[] = unknown[]> {
37
+ /**
38
+ * Parsed state
39
+ */
40
+ _parsed?: ParsedState;
25
41
  /**
26
42
  * Action target of the command
27
43
  */
@@ -84,7 +100,7 @@ export type CliCommandShapeFields = {
84
100
  export type CliCommandInput = {
85
101
  name: string;
86
102
  description?: string;
87
- type: 'string' | 'file' | 'number' | 'boolean' | 'date' | 'regex';
103
+ type: 'string' | 'file' | 'number' | 'boolean' | 'date' | 'regex' | 'module';
88
104
  fileExtensions?: string[];
89
105
  choices?: unknown[];
90
106
  required?: boolean;
@@ -1,8 +1,9 @@
1
1
  import fs from 'fs/promises';
2
2
 
3
3
  import { ShutdownManager } from '@travetto/base';
4
- import { CliCommandShape, CliCommand, CliValidationError, CliParseUtil } from '@travetto/cli';
4
+ import { CliCommandShape, CliCommand, CliValidationError, ParsedState } from '@travetto/cli';
5
5
  import { path, RootIndex } from '@travetto/manifest';
6
+ import { Ignore } from '@travetto/schema';
6
7
 
7
8
  /**
8
9
  * Allows for running of main entry points
@@ -10,6 +11,9 @@ import { path, RootIndex } from '@travetto/manifest';
10
11
  @CliCommand({ hidden: true })
11
12
  export class MainCommand implements CliCommandShape {
12
13
 
14
+ @Ignore()
15
+ _parsed: ParsedState;
16
+
13
17
  async #getImport(fileOrImport: string): Promise<string | undefined> {
14
18
  // If referenced file exists
15
19
  let file = fileOrImport;
@@ -32,7 +36,7 @@ export class MainCommand implements CliCommandShape {
32
36
  const imp = await this.#getImport(fileOrImport);
33
37
  const mod = await import(imp!);
34
38
 
35
- await ShutdownManager.exitWithResponse(await mod.main(...args, ...CliParseUtil.getState(this)?.unknown ?? []));
39
+ await ShutdownManager.exitWithResponse(await mod.main(...args, ...this._parsed.unknown));
36
40
  } catch (err) {
37
41
  await ShutdownManager.exitWithResponse(err, true);
38
42
  }