@travetto/cli 3.4.0-rc.9 → 3.4.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020 ArcSine Technologies
3
+ Copyright (c) 2023 ArcSine Technologies
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -170,7 +170,7 @@ Options:
170
170
  $ trv basic:arg 20
171
171
 
172
172
  Execution failed:
173
- * Argument is bigger than (10)
173
+ * Argument volume is bigger than (10)
174
174
 
175
175
  Usage: basic:arg [options] [volume:number]
176
176
 
@@ -233,7 +233,7 @@ $ trv basic:arglist 10 5 3 9 8 1
233
233
  $ trv basic:arglist 10 5 3 9 20 1
234
234
 
235
235
  Execution failed:
236
- * Argument [4] is bigger than (10)
236
+ * Argument volumes[4] is bigger than (10)
237
237
 
238
238
  Usage: basic:arglist [options] <volumes...:number>
239
239
 
@@ -390,7 +390,7 @@ export interface CliCommandShape {
390
390
  /**
391
391
  * Setup environment before command runs
392
392
  */
393
- envInit?(): OrProm<GlobalEnvConfig>;
393
+ envInit?(): OrProm<EnvInit>;
394
394
  /**
395
395
  * Extra help
396
396
  */
@@ -454,7 +454,7 @@ export class RunRestCommand {
454
454
  }
455
455
  ```
456
456
 
457
- As noted in the example above, `fields` is specified in this execution, with support for `module`, and `env`. These env flag is directly tied to the [GlobalEnv](https://github.com/travetto/travetto/tree/main/module/base/src/global-env.ts#L17) flags defined in the [Base](https://github.com/travetto/travetto/tree/main/module/base#readme "Environment config and common utilities for travetto applications.") module.
457
+ As noted in the example above, `fields` is specified in this execution, with support for `module`, and `env`. These env flag is directly tied to the [GlobalEnv](https://github.com/travetto/travetto/tree/main/module/base/src/global-env.ts#L9) flags defined in the [Base](https://github.com/travetto/travetto/tree/main/module/base#readme "Environment config and common utilities for travetto applications.") module.
458
458
 
459
459
  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.
460
460
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/cli",
3
- "version": "3.4.0-rc.9",
3
+ "version": "3.4.1",
4
4
  "description": "CLI infrastructure for Travetto framework",
5
5
  "keywords": [
6
6
  "cli",
@@ -29,8 +29,8 @@
29
29
  "directory": "module/cli"
30
30
  },
31
31
  "dependencies": {
32
- "@travetto/schema": "^3.4.0-rc.7",
33
- "@travetto/terminal": "^3.4.0-rc.0"
32
+ "@travetto/schema": "^3.4.0",
33
+ "@travetto/terminal": "^3.4.0"
34
34
  },
35
35
  "travetto": {
36
36
  "displayName": "Command Line Interface"
package/src/decorators.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Class, ClassInstance, ConsoleManager, GlobalEnv, defineGlobalEnv } from '@travetto/base';
1
+ import { Class, ClassInstance, ConsoleManager, GlobalEnv, defineEnv } from '@travetto/base';
2
2
  import { RootIndex } from '@travetto/manifest';
3
3
  import { SchemaRegistry } from '@travetto/schema';
4
4
 
@@ -26,7 +26,7 @@ export function CliCommand(cfg: CliCommandConfigOptions = {}) {
26
26
  hidden: cfg.hidden,
27
27
  preMain: async (cmd) => {
28
28
  if (addEnv && 'env' in cmd && typeof cmd.env === 'string') {
29
- defineGlobalEnv({ envName: cmd.env });
29
+ defineEnv({ envName: cmd.env });
30
30
  ConsoleManager.setDebug(GlobalEnv.debug, GlobalEnv.devMode);
31
31
  }
32
32
  }
package/src/execute.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { GlobalTerminal } from '@travetto/terminal';
2
- import { ConsoleManager, defineGlobalEnv, ShutdownManager, GlobalEnv } from '@travetto/base';
2
+ import { ConsoleManager, defineEnv, ShutdownManager, GlobalEnv } from '@travetto/base';
3
3
 
4
4
  import { HelpUtil } from './help';
5
5
  import { CliCommandShape } from './types';
@@ -14,20 +14,11 @@ export class ExecutionManager {
14
14
 
15
15
  static async #envInit(cmd: CliCommandShape): Promise<void> {
16
16
  if (cmd.envInit) {
17
- defineGlobalEnv(await cmd.envInit());
17
+ defineEnv(await cmd.envInit());
18
18
  ConsoleManager.setDebug(GlobalEnv.debug, GlobalEnv.devMode);
19
19
  }
20
20
  }
21
21
 
22
- static async #bindAndValidateArgs(cmd: CliCommandShape, args: string[]): Promise<unknown[]> {
23
- await cmd.initialize?.();
24
- const remainingArgs = await CliCommandSchemaUtil.bindFlags(cmd, args);
25
- const [known, unknown] = await CliCommandSchemaUtil.bindArgs(cmd, remainingArgs);
26
- await cmd.finalize?.(unknown);
27
- await CliCommandSchemaUtil.validate(cmd, known);
28
- return known;
29
- }
30
-
31
22
  /**
32
23
  * Run help
33
24
  */
@@ -41,7 +32,7 @@ export class ExecutionManager {
41
32
  * Run the given command object with the given arguments
42
33
  */
43
34
  static async command(cmd: CliCommandShape, args: string[]): Promise<void> {
44
- const known = await this.#bindAndValidateArgs(cmd, args);
35
+ const known = await CliCommandSchemaUtil.bindAndValidateArgs(cmd, args);
45
36
  await this.#envInit(cmd);
46
37
  const cfg = CliCommandRegistry.getConfig(cmd);
47
38
  await cfg?.preMain?.(cmd);
package/src/help.ts CHANGED
@@ -105,7 +105,7 @@ export class HelpUtil {
105
105
  if (inst) {
106
106
  const cfg = await CliCommandRegistry.getConfig(inst);
107
107
  if (!cfg.hidden) {
108
- const schema = await CliCommandSchemaUtil.getSchema(inst);
108
+ const schema = await CliCommandSchemaUtil.getSchema(cfg.cls);
109
109
  rows.push(cliTpl` ${{ param: cmd.padEnd(maxWidth, ' ') }} ${{ title: schema.title }}`);
110
110
  }
111
111
  }
package/src/registry.ts CHANGED
@@ -29,7 +29,7 @@ class $CliCommandRegistry {
29
29
  #commands = new Map<Class, CliCommandConfig>();
30
30
  #fileMapping: Map<string, string>;
31
31
 
32
- #get(cls: Class): CliCommandConfig | undefined {
32
+ getByClass(cls: Class): CliCommandConfig | undefined {
33
33
  return this.#commands.get(cls);
34
34
  }
35
35
 
@@ -75,7 +75,7 @@ class $CliCommandRegistry {
75
75
  * Get config for a given instance
76
76
  */
77
77
  getConfig(cmd: CliCommandShape): CliCommandConfig {
78
- return this.#get(this.#getClass(cmd))!;
78
+ return this.getByClass(this.#getClass(cmd))!;
79
79
  }
80
80
 
81
81
  /**
@@ -97,7 +97,7 @@ class $CliCommandRegistry {
97
97
  if (found) {
98
98
  const values = Object.values<Class>(await import(found));
99
99
  for (const v of values) {
100
- const cfg = this.#get(v);
100
+ const cfg = this.getByClass(v);
101
101
  if (cfg) {
102
102
  const inst = new cfg.cls();
103
103
  if (!inst.isActive || inst.isActive()) {
package/src/schema.ts CHANGED
@@ -5,14 +5,34 @@ import { CliCommandRegistry } from './registry';
5
5
  import { CliCommandInput, CliCommandSchema, CliCommandShape } from './types';
6
6
  import { CliValidationResultError } from './error';
7
7
 
8
- function split(args: string[]): [core: string[], extra: string[]] {
9
- const restIdx = args.indexOf('--');
10
- if (restIdx >= 0) {
11
- return [args.slice(0, restIdx), args.slice(restIdx + 1)];
8
+ const VALID_FLAG = /^-{1,2}[a-z]/i;
9
+ const LONG_FLAG = /^--[a-z][^= ]+/i;
10
+ const LONG_FLAG_WITH_EQ = /^--[a-z][^= ]+=\S+/i;
11
+ const SHORT_FLAG = /^-[a-z]/i;
12
+
13
+ type ParsedInput =
14
+ { type: 'unknown', input: string } |
15
+ { type: 'arg', input: string, array?: boolean } |
16
+ { type: 'flag', input: string, array?: boolean, fieldName: string, value?: unknown };
17
+
18
+ const isBoolFlag = (x?: CliCommandInput): boolean => x?.type === 'boolean' && !x.array;
19
+
20
+ const getInput = (cfg: { field?: CliCommandInput, rawText?: string, input: string, value?: string }): ParsedInput => {
21
+ const { field, input, rawText = input, value } = cfg;
22
+ if (!field) {
23
+ return { type: 'unknown', input: rawText };
24
+ } else if (!field.flagNames?.length) {
25
+ return { type: 'arg', input: field ? input : rawText ?? input, array: field.array };
12
26
  } else {
13
- return [args, []];
27
+ return {
28
+ type: 'flag',
29
+ fieldName: field.name,
30
+ array: field.array,
31
+ input: field ? input : rawText ?? input,
32
+ value: value ?? (isBoolFlag(field) ? !input.startsWith('--no-') : undefined)
33
+ };
14
34
  }
15
- }
35
+ };
16
36
 
17
37
  function fieldToInput(x: FieldConfig): CliCommandInput {
18
38
  const type = x.type === Date ? 'date' :
@@ -34,12 +54,6 @@ function fieldToInput(x: FieldConfig): CliCommandInput {
34
54
  });
35
55
  }
36
56
 
37
- const VALID_FLAG = /^-{1,2}[a-z]/i;
38
- const LONG_FLAG = /^--[a-z]/i;
39
- const SHORT_FLAG = /^-[a-z]/i;
40
-
41
- const isBoolFlag = (x: CliCommandInput): boolean => x.type === 'boolean' && !x.array;
42
-
43
57
  /**
44
58
  * Allows binding describing/binding inputs for commands
45
59
  */
@@ -50,10 +64,9 @@ export class CliCommandSchemaUtil {
50
64
  /**
51
65
  * Get schema for a given command
52
66
  */
53
- static async getSchema(cmd: CliCommandShape): Promise<CliCommandSchema> {
67
+ static async getSchema(src: Class | CliCommandShape): Promise<CliCommandSchema> {
54
68
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
55
- const cls = cmd.constructor as Class<CliCommandShape>;
56
-
69
+ const cls = 'main' in src ? src.constructor as Class : src;
57
70
  if (this.#schemas.has(cls)) {
58
71
  return this.#schemas.get(cls)!;
59
72
  }
@@ -110,7 +123,7 @@ export class CliCommandSchemaUtil {
110
123
  }
111
124
 
112
125
  const fullSchema = SchemaRegistry.get(cls);
113
- const { cls: _cls, preMain: _preMain, ...meta } = CliCommandRegistry.getConfig(cmd);
126
+ const { cls: _cls, preMain: _preMain, ...meta } = CliCommandRegistry.getByClass(cls)!;
114
127
  const cfg: CliCommandSchema = {
115
128
  ...meta,
116
129
  args: method,
@@ -123,111 +136,107 @@ export class CliCommandSchemaUtil {
123
136
  }
124
137
 
125
138
  /**
126
- * Produce the arguments into the final argument set
139
+ * Parse inputs to command
127
140
  */
128
- static async bindArgs(cmd: CliCommandShape, args: string[]): Promise<[known: unknown[], unknown: string[]]> {
129
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
130
- const cls = cmd.constructor as Class<CliCommandShape>;
131
- const [copy, extra] = split(args);
132
- const schema = await this.getSchema(cmd);
133
- const out: unknown[] = [];
134
- const found: boolean[] = copy.map(x => false);
135
- let i = 0;
136
-
137
- for (const el of schema.args) {
138
- // Siphon off unrecognized flags, in order
139
- while (i < copy.length && VALID_FLAG.test(copy[i])) {
140
- i += 1;
141
- }
141
+ static async parse<T extends CliCommandShape>(cls: Class<T>, inputs: string[]): Promise<ParsedInput[]> {
142
+ const schema = await this.getSchema(cls);
143
+ const flagMap = new Map<string, CliCommandInput>(
144
+ schema.flags.flatMap(f => (f.flagNames ?? []).map(name => [name, f]))
145
+ );
146
+
147
+ const out: ParsedInput[] = [];
142
148
 
143
- if (i >= copy.length) {
144
- out.push(el.array ? [] : undefined);
145
- } else if (el.array) {
146
- const sub: string[] = [];
147
- while (i < copy.length) {
148
- if (!VALID_FLAG.test(copy[i])) {
149
- sub.push(copy[i]);
150
- found[i] = true;
149
+ // Load env vars to front
150
+ for (const field of schema.flags) {
151
+ for (const envName of field.envVars ?? []) {
152
+ if (envName in process.env) {
153
+ const value: string = process.env[envName]!;
154
+ if (field.array) {
155
+ out.push(...value.split(/\s*,\s*/g).map(v => getInput({ field, input: `env.${envName}`, value: v })));
156
+ } else {
157
+ out.push(getInput({ field, input: `env.${envName}`, value }));
151
158
  }
159
+ }
160
+ }
161
+ }
162
+
163
+ let argIdx = 0;
164
+
165
+ for (let i = 0; i < inputs.length; i += 1) {
166
+ const input = inputs[i];
167
+
168
+ if (input === '--') { // Raw separator
169
+ out.push(...inputs.slice(i + 1).map(x => getInput({ input: x })));
170
+ break;
171
+ } else if (LONG_FLAG_WITH_EQ.test(input)) {
172
+ const [k, ...v] = input.split('=');
173
+ const field = flagMap.get(k);
174
+ out.push(getInput({ field, rawText: input, input: k, value: v.join('=') }));
175
+ } else if (VALID_FLAG.test(input)) { // Flag
176
+ const field = flagMap.get(input);
177
+ const next = inputs[i + 1];
178
+ if ((next && (VALID_FLAG.test(next) || next === '--')) || isBoolFlag(field)) {
179
+ out.push(getInput({ field, input }));
180
+ } else {
181
+ out.push(getInput({ field, input, value: next }));
152
182
  i += 1;
153
183
  }
154
- out.push(sub);
155
184
  } else {
156
- out.push(copy[i]);
157
- found[i] = true;
158
- i += 1;
185
+ const field = schema.args[argIdx];
186
+ out.push(getInput({ field, input }));
187
+ // Move argIdx along if not in a vararg situation
188
+ if (!field?.array) {
189
+ argIdx += 1;
190
+ }
159
191
  }
160
192
  }
161
193
 
162
- const final = [...copy.filter((_, idx) => !found[idx]), ...extra];
163
- return [
164
- BindUtil.coerceMethodParams(cls, 'main', out, true),
165
- final
166
- ];
194
+ return out;
167
195
  }
168
196
 
169
197
  /**
170
198
  * Bind arguments to command
171
199
  */
172
- static async bindFlags<T extends CliCommandShape>(cmd: T, args: string[]): Promise<string[]> {
173
- const schema = await this.getSchema(cmd);
174
-
175
- const [base, extra] = split(args);
176
- const copy = base.flatMap(k => (k.startsWith('--') && k.includes('=')) ? k.split('=') : [k]);
177
-
200
+ static async bindFlags<T extends CliCommandShape>(cmd: T, args: ParsedInput[]): Promise<void> {
178
201
  const template: Partial<T> = {};
179
202
 
180
- const flagMap = new Map<string, CliCommandInput>();
181
- for (const flag of schema.flags) {
182
- for (const name of flag.flagNames ?? []) {
183
- flagMap.set(name, flag);
184
- }
185
- for (const envName of flag.envVars ?? []) {
186
- if (envName in process.env) {
187
- let val: string | string[] = process.env[envName]!;
188
- if (flag.array) {
189
- val = val.split(/\s*,\s*/g);
190
- }
203
+ for (const arg of args) {
204
+ switch (arg.type) {
205
+ case 'flag': {
191
206
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
192
- template[flag.name as keyof T] = val as T[keyof T];
207
+ const key = arg.fieldName as keyof T;
208
+ const value = arg.value!;
209
+ if (arg.array) {
210
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
211
+ ((template[key] as unknown[]) ??= []).push(value);
212
+ } else {
213
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
214
+ template[key] = value as unknown as T[typeof key];
215
+ }
193
216
  }
194
217
  }
195
218
  }
196
219
 
197
- const out = [];
198
- for (let i = 0; i < copy.length; i += 1) {
199
- const arg = copy[i];
200
- const next = copy[i + 1];
201
-
202
- const input = flagMap.get(arg);
203
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
204
- const key = input?.name as keyof T;
205
- if (!input) {
206
- out.push(arg);
207
- } else if (isBoolFlag(input)) {
208
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
209
- template[key] = !arg.startsWith('--no') as T[typeof key];
210
- } else if (next === undefined || VALID_FLAG.test(next)) {
211
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
212
- template[key] = null as T[typeof key];
213
- } else if (input.array) {
214
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
215
- const arr = template[key] ??= [] as T[typeof key];
216
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
217
- (arr as unknown[]).push(next);
218
- i += 1; // Skip next
219
- } else {
220
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
221
- template[key] = next as T[typeof key];
222
- i += 1; // Skip next
223
- }
224
- }
225
-
226
220
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
227
221
  const cls = cmd.constructor as Class<CliCommandShape>;
228
222
  BindUtil.bindSchemaToObject(cls, cmd, template);
223
+ }
224
+
225
+ /**
226
+ * Produce the arguments into the final argument set
227
+ */
228
+ static async bindArgs(cmd: CliCommandShape, args: ParsedInput[]): Promise<unknown[]> {
229
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
230
+ const cls = cmd.constructor as Class<CliCommandShape>;
231
+ const out = args.filter(x => x.type === 'arg').map(x => x.input);
232
+ return BindUtil.coerceMethodParams(cls, 'main', out, true);
233
+ }
229
234
 
230
- return [...out, '--', ...extra];
235
+ /**
236
+ * Get the unused arguments
237
+ */
238
+ static getUnusedArgs(args: ParsedInput[]): string[] {
239
+ return args.filter(x => x.type === 'unknown').map(x => x.input);
231
240
  }
232
241
 
233
242
  /**
@@ -237,9 +246,11 @@ export class CliCommandSchemaUtil {
237
246
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
238
247
  const cls = cmd.constructor as Class<CliCommandShape>;
239
248
 
249
+ const paramNames = SchemaRegistry.getMethodSchema(cls, 'main').map(x => x.name);
250
+
240
251
  const validators = [
241
252
  (): Promise<void> => SchemaValidator.validate(cls, cmd).then(() => { }),
242
- (): Promise<void> => SchemaValidator.validateMethod(cls, 'main', args),
253
+ (): Promise<void> => SchemaValidator.validateMethod(cls, 'main', args, paramNames),
243
254
  async (): Promise<void> => {
244
255
  const res = await cmd.validate?.(...args);
245
256
  if (res) {
@@ -263,4 +274,19 @@ export class CliCommandSchemaUtil {
263
274
  }
264
275
  return cmd;
265
276
  }
277
+
278
+ /**
279
+ * Bind and validate a command with a given set of arguments
280
+ */
281
+ static async bindAndValidateArgs(cmd: CliCommandShape, args: string[]): Promise<unknown[]> {
282
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
283
+ const cls = cmd.constructor as Class;
284
+ await cmd.initialize?.();
285
+ const parsed = await this.parse(cls, args);
286
+ await this.bindFlags(cmd, parsed);
287
+ const known = await this.bindArgs(cmd, parsed);
288
+ await cmd.finalize?.(this.getUnusedArgs(parsed));
289
+ await this.validate(cmd, known);
290
+ return known;
291
+ }
266
292
  }
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Closeable, GlobalEnvConfig } from '@travetto/base';
1
+ import { Closeable, EnvInit } from '@travetto/base';
2
2
 
3
3
  type OrProm<T> = T | Promise<T>;
4
4
 
@@ -29,7 +29,7 @@ export interface CliCommandShape {
29
29
  /**
30
30
  * Setup environment before command runs
31
31
  */
32
- envInit?(): OrProm<GlobalEnvConfig>;
32
+ envInit?(): OrProm<EnvInit>;
33
33
  /**
34
34
  * Extra help
35
35
  */