@travetto/cli 2.1.3 → 2.2.0

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/bin/cli.ts CHANGED
@@ -6,7 +6,7 @@ import { EnvUtil } from '@travetto/boot';
6
6
  /**
7
7
  * Entry point
8
8
  */
9
- export async function main() {
9
+ export async function main(): Promise<void> {
10
10
  if (!EnvUtil.isFalse('TRV_CLI_LOCAL') && !PathUtil.toUnix(__filename).includes(PathUtil.cwd)) { // If the current file is not under the working directory
11
11
  console.error(`
12
12
  The @travetto/cli is not intended to be installed globally. Please install it within your local project
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@travetto/cli",
3
3
  "displayName": "Command Line Interface",
4
- "version": "2.1.3",
4
+ "version": "2.2.0",
5
5
  "description": "CLI infrastructure for travetto framework",
6
6
  "keywords": [
7
7
  "cli",
@@ -27,7 +27,7 @@
27
27
  "directory": "module/cli"
28
28
  },
29
29
  "dependencies": {
30
- "@travetto/base": "^2.1.3",
30
+ "@travetto/base": "^2.2.0",
31
31
  "commander": "^9.4.0"
32
32
  },
33
33
  "docDependencies": {
package/src/color.ts CHANGED
@@ -1,14 +1,6 @@
1
1
  import { ColorUtil } from '@travetto/boot/src/color';
2
2
 
3
- /**
4
- * Colorize a string, as a string interpolation
5
- *
6
- * @example
7
- * ```
8
- * color`${{title: 'Main Title'}} is ${{subtitle: 'Sub Title}}`
9
- * ```
10
- */
11
- export const color = ColorUtil.makeTemplate({
3
+ const colorSet = {
12
4
  input: ColorUtil.makeColorer('yellow'),
13
5
  output: ColorUtil.makeColorer('magenta'),
14
6
  path: ColorUtil.makeColorer('cyan'),
@@ -21,4 +13,17 @@ export const color = ColorUtil.makeTemplate({
21
13
  identifier: ColorUtil.makeColorer('blue', 'bold'),
22
14
  subtitle: ColorUtil.makeColorer('white'),
23
15
  subsubtitle: ColorUtil.makeColorer('white', 'faint')
24
- });
16
+ } as const;
17
+
18
+ /**
19
+ * Colorize a string, as a string interpolation
20
+ *
21
+ * @example
22
+ * ```
23
+ * color`${{title: 'Main Title'}} is ${{subtitle: 'Sub Title}}`
24
+ * ```
25
+ */
26
+ export const color = ColorUtil.makeTemplate(colorSet);
27
+
28
+
29
+ export type ColoredElement = keyof typeof colorSet;
package/src/execute.ts CHANGED
@@ -1,4 +1,4 @@
1
- import * as commander from 'commander';
1
+ import { program as commander } from 'commander';
2
2
 
3
3
  import { CliUtil } from './util';
4
4
  import { CompletionConfig } from './types';
@@ -14,10 +14,10 @@ export class ExecutionManager {
14
14
  /**
15
15
  * Run tab completion given the full args list
16
16
  */
17
- static async runCompletion(args: string[]) {
18
- const compl: CompletionConfig = { all: [], task: {} };
19
- await PluginManager.loadAllPlugins(x => x.setupCompletion(compl));
20
- const res = await CliUtil.getCompletion(compl, args.slice(3));
17
+ static async runCompletion(args: string[]): Promise<void> {
18
+ const cfg: CompletionConfig = { all: [], task: {} };
19
+ await PluginManager.loadAllPlugins(x => x.setupCompletion(cfg));
20
+ const res = await CliUtil.getCompletion(cfg, args.slice(3));
21
21
  console.log(res.join(' '));
22
22
  return;
23
23
  }
@@ -25,7 +25,7 @@ export class ExecutionManager {
25
25
  /**
26
26
  * Run plugin
27
27
  */
28
- static async runPlugin(args: string[]) {
28
+ static async runPlugin(args: string[]): Promise<void> {
29
29
  const cmd = args[2];
30
30
 
31
31
  let plugin;
@@ -45,6 +45,9 @@ export class ExecutionManager {
45
45
  commander.parse(args);
46
46
  }
47
47
  } catch (err) {
48
+ if (!(err instanceof Error)) {
49
+ throw err;
50
+ }
48
51
  return plugin.showHelp(err);
49
52
  }
50
53
  }
@@ -53,7 +56,7 @@ export class ExecutionManager {
53
56
  * Execute the command line
54
57
  * @param args argv
55
58
  */
56
- static async run(args: string[]) {
59
+ static async run(args: string[]): Promise<void> {
57
60
  const width = +(process.env.TRV_CONSOLE_WIDTH ?? process.stdout.columns ?? 120);
58
61
  commander
59
62
  .version(version)
package/src/help.ts CHANGED
@@ -11,7 +11,7 @@ export class HelpUtil {
11
11
  * @param text Source text
12
12
  * @param key
13
13
  */
14
- static extractValue(text: string, key: string) {
14
+ static extractValue(text: string, key: string): readonly [string, string] {
15
15
  let sub = '';
16
16
  if (text.includes(key)) {
17
17
  const start = text.indexOf(key);
@@ -28,7 +28,7 @@ export class HelpUtil {
28
28
  /**
29
29
  * Colorize Usage
30
30
  */
31
- static colorizeOptions(option: string) {
31
+ static colorizeOptions(option: string): string {
32
32
  return option.replace(/(\s*)(-[^, ]+)(,?\s*)(--\S+)?((\s+)?((?:\[[^\]]+\])|(?:\<[^>]+>)))?((\s+)(.*))?/g, (
33
33
  p: string, spacing: string,
34
34
  simpleParam: string, pSep: string,
@@ -59,7 +59,7 @@ export class HelpUtil {
59
59
  /**
60
60
  * Colorize command section
61
61
  */
62
- static colorizeCommands(commands: string) {
62
+ static colorizeCommands(commands: string): string {
63
63
  return commands
64
64
  .replace(/\s([^\[\]]\S+)/g, param => color`${{ param }}`)
65
65
  .replace(/(\s*[^\x1b]\[[^\]]+\])/g, input => color`${{ input }}`) // eslint-disable-line no-control-regex
@@ -69,14 +69,14 @@ export class HelpUtil {
69
69
  /**
70
70
  * Colorize usage
71
71
  */
72
- static colorizeUsage(usage: string) {
72
+ static colorizeUsage(usage: string): string {
73
73
  return usage.replace(/Usage:/, title => color`${{ title }}`);
74
74
  }
75
75
 
76
76
  /**
77
77
  * Get full help text
78
78
  */
79
- static getHelpText(text: string, extraText?: string) {
79
+ static getHelpText(text: string, extraText?: string): string {
80
80
  const [usage, text2] = this.extractValue(text, 'Usage:');
81
81
  const [options, text3] = this.extractValue(text2, 'Options:');
82
82
  const [commands, textFinal] = this.extractValue(text3, 'Commands:');
@@ -6,7 +6,9 @@ import { CliUtil } from './util';
6
6
 
7
7
  type Completion = Record<string, string[]>;
8
8
 
9
- type ParamConfig<K> = {
9
+ type ParamPrimitive = string | number | boolean | string[] | number[];
10
+
11
+ type ParamConfig<K extends ParamPrimitive = ParamPrimitive> = {
10
12
  type?: Function;
11
13
  key?: string;
12
14
  short?: string | false;
@@ -14,12 +16,12 @@ type ParamConfig<K> = {
14
16
  desc: string;
15
17
  completion?: boolean;
16
18
  def?: K;
17
- choices?: K[];
19
+ choices?: K[] | readonly K[];
18
20
  combine?: (v: string, curr: K) => K;
19
21
  };
20
22
 
21
23
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
- type ParamMap<T = any> = { [key in keyof T]: ParamConfig<T[key]> };
24
+ type ParamMap<T = any> = { [key in keyof T]: T[key] extends ParamPrimitive ? ParamConfig<T[key]> : never };
23
25
 
24
26
  type Shape<M extends ParamMap> = { [k in keyof M]: Exclude<M[k]['def'], undefined> };
25
27
 
@@ -82,14 +84,16 @@ export abstract class BasePlugin<V extends ParamMap = ParamMap> {
82
84
  /**
83
85
  * Define option
84
86
  */
85
- choiceOption<K>({ choices, ...cfg }: ParamConfig<string> & { choices: K[] | readonly K[] }): ParamConfig<K> {
86
- return {
87
+ choiceOption<K extends string | number>({ choices, ...cfg }: ParamConfig<K> & { choices: K[] | readonly K[] }): ParamConfig<K> {
88
+ const config: ParamConfig<K> = {
87
89
  type: String,
88
- combine: (v, acc) => choices.includes(v as unknown as K) ? v as unknown as K : acc,
90
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
91
+ combine: (v: string, acc: K): K => choices.includes(v as K) ? v as K : acc,
89
92
  choices,
90
93
  completion: true,
91
94
  ...cfg
92
- } as ParamConfig<K>;
95
+ };
96
+ return config;
93
97
  }
94
98
 
95
99
  /**
@@ -131,7 +135,7 @@ export abstract class BasePlugin<V extends ParamMap = ParamMap> {
131
135
  /**
132
136
  * Pre-compile on every cli execution
133
137
  */
134
- async build() {
138
+ async build(): Promise<void> {
135
139
  await (await import('@travetto/base/bin/lib/'))
136
140
  .BuildUtil.build();
137
141
  }
@@ -139,14 +143,15 @@ export abstract class BasePlugin<V extends ParamMap = ParamMap> {
139
143
  /**
140
144
  * Expose configuration as constrained typed object
141
145
  */
142
- get cmd() {
146
+ get cmd(): Shape<ReturnType<Exclude<this['getOptions'], undefined>>> {
147
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
143
148
  return this.#cmd.opts() as Shape<ReturnType<Exclude<this['getOptions'], undefined>>>;
144
149
  }
145
150
 
146
151
  /**
147
152
  * Expose command line arguments
148
153
  */
149
- get args() {
154
+ get args(): string[] {
150
155
  return this.#cmd.args;
151
156
  }
152
157
 
@@ -164,11 +169,11 @@ export abstract class BasePlugin<V extends ParamMap = ParamMap> {
164
169
  * Process all options into final set before registering with commander
165
170
  * @returns
166
171
  */
167
- async finalizeOptions() {
168
- const opts = this.getOptions?.() ?? {};
172
+ async finalizeOptions(): Promise<ParamConfig[]> {
173
+ const opts = this.getOptions?.();
169
174
  const used = new Set();
170
175
 
171
- return Object.entries(opts as ParamMap).map(([k, cfg]) => {
176
+ return (opts ? Object.entries(opts) : []).map(([k, cfg]) => {
172
177
  cfg.key = k;
173
178
  cfg.name ??= k.replace(/([a-z])([A-Z])/g, (_, l, r: string) => `${l}-${r.toLowerCase()}`);
174
179
  if (cfg.short === undefined) {
@@ -185,7 +190,7 @@ export abstract class BasePlugin<V extends ParamMap = ParamMap> {
185
190
  /**
186
191
  * Receive the commander object, and process
187
192
  */
188
- async setup(cmd: commander.Command) {
193
+ async setup(cmd: commander.Command): Promise<commander.Command> {
189
194
  cmd = cmd.command(this.name);
190
195
  if (this.allowUnknownOptions) {
191
196
  cmd = cmd.allowUnknownOption(true);
@@ -201,7 +206,7 @@ export abstract class BasePlugin<V extends ParamMap = ParamMap> {
201
206
  if (cfg.type !== Boolean || cfg.def) {
202
207
  key = `${key} <${cfg.name}>`;
203
208
  }
204
- cmd = cfg.combine ? cmd.option(key, cfg.desc, cfg.combine, cfg.def) : cmd.option(key, cfg.desc, cfg.def);
209
+ cmd = cfg.combine ? cmd.option(key, cfg.desc, cfg.combine, cfg.def) : cmd.option(key, cfg.desc, (cur, acc) => cur, cfg.def);
205
210
  }
206
211
 
207
212
  cmd = cmd.action(this.runAction.bind(this));
@@ -211,7 +216,7 @@ export abstract class BasePlugin<V extends ParamMap = ParamMap> {
211
216
  /**
212
217
  * Runs the action at execution time
213
218
  */
214
- async runAction(...args: unknown[]) {
219
+ async runAction(...args: unknown[]): Promise<void> {
215
220
  await this.envInit?.();
216
221
  await this.build();
217
222
  return await this.action(...args);
@@ -220,7 +225,7 @@ export abstract class BasePlugin<V extends ParamMap = ParamMap> {
220
225
  /**
221
226
  * Collection tab completion information
222
227
  */
223
- async setupCompletion(config: CompletionConfig) {
228
+ async setupCompletion(config: CompletionConfig): Promise<void> {
224
229
  const task = await this.complete();
225
230
  config.all = [...config.all, this.name];
226
231
  if (task) {
@@ -232,16 +237,14 @@ export abstract class BasePlugin<V extends ParamMap = ParamMap> {
232
237
  * Return tab completion information
233
238
  */
234
239
  async complete(): Promise<Completion | void> {
235
- const out: Completion = {
236
- '': [] as string[]
237
- };
240
+ const out: Completion = { '': [] };
238
241
  for (const el of await this.finalizeOptions()) {
239
242
  if (el.completion) {
240
243
  out[''] = [...out['']!, `--${el.name} `];
241
244
  if (el.choices) {
242
- out[`--${el.name} `] = el.choices;
245
+ out[`--${el.name} `] = el.choices.map(x => `${x}`);
243
246
  if (el.short) {
244
- out[`- ${el.short} `] = el.choices;
247
+ out[`- ${el.short} `] = el.choices.map(x => `${x}`);
245
248
  }
246
249
  }
247
250
  }
package/src/plugin.ts CHANGED
@@ -12,7 +12,7 @@ const PLUGIN_PACKAGE = [
12
12
  [/^openapi:(spec|client)$/, 'openapi', true],
13
13
  [/^email:(compile|dev)$/, 'email-template', false],
14
14
  [/^pack(:assemble|:zip|:docker)?$/, 'pack', false],
15
- ] as [patt: RegExp, pkg: string, prod: boolean][];
15
+ ] as const;
16
16
 
17
17
  /**
18
18
  * Manages loading and finding all plugins
@@ -22,7 +22,7 @@ export class PluginManager {
22
22
  /**
23
23
  * Get list of all plugins available
24
24
  */
25
- static getPluginMapping() {
25
+ static getPluginMapping(): Map<string, string> {
26
26
  const all = new Map<string, string>();
27
27
  for (const { file } of SourceIndex.find({ folder: 'bin', filter: /bin\/cli-/ })) {
28
28
  all.set(file.replace(/^.*\/bin\/.+?-(.*?)[.][^.]*$/, (_, f) => f), file);
@@ -47,7 +47,8 @@ ${{ identifier: `npm i ${prod ? '' : '--save-dev '}@travetto/${pkg}` }}`);
47
47
  }
48
48
  throw new Error(`Unknown command: ${cmd}`);
49
49
  }
50
- for (const v of Object.values(await import(f)) as { new(...args: unknown[]): unknown }[]) {
50
+ const values = Object.values<{ new(...args: unknown[]): unknown }>(await import(f));
51
+ for (const v of values) {
51
52
  try {
52
53
  const inst = new v();
53
54
  if (inst instanceof BasePlugin) {
@@ -64,7 +65,7 @@ ${{ identifier: `npm i ${prod ? '' : '--save-dev '}@travetto/${pkg}` }}`);
64
65
  /**
65
66
  * Load all available plugins
66
67
  */
67
- static async loadAllPlugins(op?: (p: BasePlugin) => unknown | Promise<unknown>) {
68
+ static async loadAllPlugins(op?: (p: BasePlugin) => unknown | Promise<unknown>): Promise<BasePlugin[]> {
68
69
  return Promise.all(
69
70
  [...this.getPluginMapping().keys()]
70
71
  .sort((a, b) => a.localeCompare(b))
package/src/util.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import * as readline from 'readline';
2
+ import { Writable } from 'stream';
3
+
2
4
  import { CompletionConfig } from './types';
3
5
 
4
6
  /**
@@ -8,20 +10,20 @@ export class CliUtil {
8
10
 
9
11
  static #waitState = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'.split('');
10
12
 
11
- static isBoolean(x: string) {
13
+ static isBoolean(x: string): boolean {
12
14
  return /^(1|0|yes|no|on|off|auto|true|false)$/i.test(x);
13
15
  }
14
16
 
15
17
  static toBool(x: string | boolean, def: boolean): boolean;
16
18
  static toBool(x?: string | boolean, def?: boolean): boolean | undefined;
17
- static toBool(x?: string | boolean, def?: boolean) {
19
+ static toBool(x?: string | boolean, def?: boolean): boolean | undefined {
18
20
  return x === undefined ? true :
19
21
  (typeof x === 'boolean' ? x :
20
22
  (this.isBoolean(x) ? /^(1|yes|on|true)$/i.test(x) :
21
23
  def));
22
24
  }
23
25
 
24
- static toInt(l: number | undefined, u: number | undefined, v: string, d: number) {
26
+ static toInt(l: number | undefined, u: number | undefined, v: string, d: number): number {
25
27
  let n = +v;
26
28
  if (l === undefined && u === undefined) {
27
29
  return n;
@@ -38,7 +40,7 @@ export class CliUtil {
38
40
  /**
39
41
  * Get code completion values
40
42
  */
41
- static async getCompletion(compl: CompletionConfig, args: string[]) {
43
+ static async getCompletion(cfg: CompletionConfig, args: string[]): Promise<string[]> {
42
44
  args = args.slice(0); // Copy as we mutate
43
45
 
44
46
  const cmd = args.shift()!;
@@ -47,27 +49,27 @@ export class CliUtil {
47
49
  let opts: string[] = [];
48
50
 
49
51
  // List all commands
50
- if (!compl.task[cmd]) {
51
- opts = compl.all;
52
+ if (!cfg.task[cmd]) {
53
+ opts = cfg.all;
52
54
  } else {
53
55
  // Look available sub commands
54
56
  last = args.pop() ?? '';
55
57
  const second = args.pop() ?? '';
56
58
  let flag = '';
57
59
 
58
- if (last in compl.task[cmd]) {
60
+ if (last in cfg.task[cmd]) {
59
61
  flag = last;
60
62
  last = '';
61
- } else if (second in compl.task[cmd]) {
63
+ } else if (second in cfg.task[cmd]) {
62
64
  // Look for available flags
63
- if (compl.task[cmd][second].includes(last)) {
65
+ if (cfg.task[cmd][second].includes(last)) {
64
66
  flag = '';
65
67
  last = '';
66
68
  } else {
67
69
  flag = second;
68
70
  }
69
71
  }
70
- opts = compl.task[cmd][flag];
72
+ opts = cfg.task[cmd][flag];
71
73
  }
72
74
 
73
75
  return last ? opts.filter(x => x.startsWith(last)) : opts.filter(x => !x.startsWith('-'));
@@ -79,7 +81,7 @@ export class CliUtil {
79
81
  * @param text Text, if desired
80
82
  * @param clear Should the entire line be cleared?
81
83
  */
82
- static async rewriteLine(stream: NodeJS.WritableStream, text?: string, clear = false) {
84
+ static async rewriteLine(stream: Writable, text?: string, clear = false): Promise<void> {
83
85
  await new Promise<void>(r => readline.cursorTo(stream, 0, undefined, () => {
84
86
  if (clear) {
85
87
  readline.clearLine(stream, 0);
@@ -92,7 +94,7 @@ export class CliUtil {
92
94
  }));
93
95
  }
94
96
 
95
- static sleep(ms: number) {
97
+ static sleep(ms: number): Promise<void> {
96
98
  return new Promise(r => setTimeout(r, ms));
97
99
  }
98
100
 
@@ -103,8 +105,8 @@ export class CliUtil {
103
105
  * @param delay Delay duration
104
106
  */
105
107
  static async waiting<T>(message: string, work: Promise<T> | (() => Promise<T>),
106
- config: { completion?: string, delay?: number, stream?: NodeJS.WritableStream } = {}
107
- ) {
108
+ config: { completion?: string, delay?: number, stream?: Writable } = {}
109
+ ): Promise<T> {
108
110
  const { stream, delay, completion } = { delay: 1000, stream: process.stderr, ...config };
109
111
 
110
112
  const writeLine = this.rewriteLine.bind(this, stream);
@@ -120,10 +122,10 @@ export class CliUtil {
120
122
  let i = -1;
121
123
  let done = false;
122
124
  let value: T | undefined;
123
- let err: Error | undefined;
125
+ let capturedError: Error | undefined;
124
126
  const final = work
125
127
  .then(res => value = res)
126
- .catch(e => err = e)
128
+ .catch(err => capturedError = err)
127
129
  .finally(() => done = true);
128
130
 
129
131
  if (delay) {
@@ -138,8 +140,8 @@ export class CliUtil {
138
140
  if (i >= 0) {
139
141
  await writeLine(completion ? `${message} ${completion}\n` : '', true);
140
142
  }
141
- if (err) {
142
- throw err;
143
+ if (capturedError) {
144
+ throw capturedError;
143
145
  } else {
144
146
  return value!;
145
147
  }