@travetto/cli 3.4.0 → 3.4.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/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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/cli",
3
- "version": "3.4.0",
3
+ "version": "3.4.2",
4
4
  "description": "CLI infrastructure for Travetto framework",
5
5
  "keywords": [
6
6
  "cli",
package/src/execute.ts CHANGED
@@ -19,15 +19,6 @@ export class ExecutionManager {
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/module.ts CHANGED
@@ -11,19 +11,20 @@ export class CliModuleUtil {
11
11
 
12
12
  /**
13
13
  * Find modules that changed, and the dependent modules
14
- * @param hash
14
+ * @param fromHash
15
+ * @param toHash
15
16
  * @param transitive
16
17
  * @returns
17
18
  */
18
- static async findChangedModulesRecursive(hash?: string, transitive = true): Promise<IndexedModule[]> {
19
- hash ??= await CliScmUtil.findLastRelease();
19
+ static async findChangedModulesRecursive(fromHash?: string, toHash?: string, transitive = true): Promise<IndexedModule[]> {
20
+ fromHash ??= await CliScmUtil.findLastRelease();
20
21
 
21
- if (!hash) {
22
+ if (!fromHash) {
22
23
  return RootIndex.getLocalModules();
23
24
  }
24
25
 
25
26
  const out = new Map<string, IndexedModule>();
26
- for (const mod of await CliScmUtil.findChangedModulesSince(hash)) {
27
+ for (const mod of await CliScmUtil.findChangedModules(fromHash, toHash)) {
27
28
  out.set(mod.name, mod);
28
29
  if (transitive) {
29
30
  for (const sub of await RootIndex.getDependentModules(mod)) {
@@ -42,9 +43,9 @@ export class CliModuleUtil {
42
43
  * @param transitive
43
44
  * @returns
44
45
  */
45
- static async findModules(mode: 'all' | 'changed', sinceHash?: string): Promise<IndexedModule[]> {
46
+ static async findModules(mode: 'all' | 'changed', fromHash?: string, toHash?: string): Promise<IndexedModule[]> {
46
47
  return (mode === 'changed' ?
47
- await this.findChangedModulesRecursive(sinceHash) :
48
+ await this.findChangedModulesRecursive(fromHash, toHash) :
48
49
  [...RootIndex.getModuleList('all')].map(x => RootIndex.getModule(x)!)
49
50
  ).filter(x => x.sourcePath !== RootIndex.manifest.workspacePath);
50
51
  }
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
+ }
229
224
 
230
- return [...out, '--', ...extra];
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
+ }
234
+
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
  /**
@@ -265,4 +274,19 @@ export class CliCommandSchemaUtil {
265
274
  }
266
275
  return cmd;
267
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
+ }
268
292
  }
package/src/scm.ts CHANGED
@@ -41,13 +41,13 @@ export class CliScmUtil {
41
41
  }
42
42
 
43
43
  /**
44
- * Find all source files that changed since hash
45
- * @param hash
44
+ * Find all source files that changed between from and to hashes
45
+ * @param fromHash
46
46
  * @returns
47
47
  */
48
- static async findChangedFilesSince(hash: string): Promise<string[]> {
48
+ static async findChangedFiles(fromHash: string, toHash: string = 'HEAD'): Promise<string[]> {
49
49
  const ws = RootIndex.manifest.workspacePath;
50
- const res = await ExecUtil.spawn('git', ['diff', '--name-only', `HEAD..${hash}`, ':!**/DOC.*', ':!**/README.*'], { cwd: ws }).result;
50
+ const res = await ExecUtil.spawn('git', ['diff', '--name-only', `${fromHash}..${toHash}`, ':!**/DOC.*', ':!**/README.*'], { cwd: ws }).result;
51
51
  const out = new Set<string>();
52
52
  for (const line of res.stdout.split(/\n/g)) {
53
53
  const entry = RootIndex.getEntry(path.resolve(ws, line));
@@ -59,12 +59,13 @@ export class CliScmUtil {
59
59
  }
60
60
 
61
61
  /**
62
- * Find all modules that changed since hash
63
- * @param hash
62
+ * Find all modules that changed between from and to hashes
63
+ * @param fromHash
64
+ * @param toHash
64
65
  * @returns
65
66
  */
66
- static async findChangedModulesSince(hash: string): Promise<IndexedModule[]> {
67
- const files = await this.findChangedFilesSince(hash);
67
+ static async findChangedModules(fromHash: string, toHash?: string): Promise<IndexedModule[]> {
68
+ const files = await this.findChangedFiles(fromHash, toHash);
68
69
  const mods = files
69
70
  .map(x => RootIndex.getFromSource(x))
70
71
  .filter((x): x is IndexedFile => !!x)