@wooksjs/event-cli 0.2.22 → 0.3.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/README.md CHANGED
@@ -13,7 +13,7 @@ As a part of `wooks` event processing framework, `@wooksjs/event-cli` implements
13
13
 
14
14
  - access flags (- or --)
15
15
 
16
- ## Install
16
+ ## Installation
17
17
 
18
18
  `npm install wooks @wooksjs/event-cli`
19
19
 
package/dist/index.cjs CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  var eventCore = require('@wooksjs/event-core');
4
4
  var wooks = require('wooks');
5
+ var cliHelp = require('@prostojs/cli-help');
5
6
  var minimist = require('minimist');
6
7
 
7
8
  function createCliContext(data, options) {
@@ -48,22 +49,138 @@ class WooksCli extends wooks.WooksAdapterBase {
48
49
  constructor(opts, wooks) {
49
50
  super(wooks, opts === null || opts === void 0 ? void 0 : opts.logger);
50
51
  this.opts = opts;
52
+ this.alreadyComputedAliases = false;
51
53
  this.logger = (opts === null || opts === void 0 ? void 0 : opts.logger) || this.getLogger('wooks-cli');
54
+ this.cliHelp =
55
+ (opts === null || opts === void 0 ? void 0 : opts.cliHelp) instanceof cliHelp.CliHelpRenderer
56
+ ? opts.cliHelp
57
+ : new cliHelp.CliHelpRenderer(opts === null || opts === void 0 ? void 0 : opts.cliHelp);
52
58
  }
53
- cli(path, handler) {
54
- return this.on('CLI', path, handler);
59
+ /**
60
+ * ### Register CLI Command
61
+ * Command path segments may be separated by / or space.
62
+ *
63
+ * For example the folowing path are interpreted the same:
64
+ * - "command test use:dev :name"
65
+ * - "command/test/use:dev/:name"
66
+ *
67
+ * Where name will become an argument
68
+ *
69
+ * ```js
70
+ * // example without options
71
+ * app.cli('command/:arg', () => 'arg = ' + useRouteParams().params.arg )
72
+ *
73
+ * // example with options
74
+ * app.cli('command/:arg', {
75
+ * description: 'Description of the command',
76
+ * options: [{ keys: ['project', 'p'], description: 'Description of the option', value: 'myProject' }],
77
+ * args: { arg: 'Description of the arg' },
78
+ * aliases: ['cmd'], // alias "cmd/:arg" will be registered
79
+ * examples: [{
80
+ * description: 'Example of usage with someProject',
81
+ * cmd: 'argValue -p=someProject',
82
+ * // will result in help display:
83
+ * // "# Example of usage with someProject\n" +
84
+ * // "$ myCli command argValue -p=someProject\n"
85
+ * }],
86
+ * handler: () => 'arg = ' + useRouteParams().params.arg
87
+ * })
88
+ * ```
89
+ *
90
+ * @param path command path
91
+ * @param _options handler or options
92
+ *
93
+ * @returns
94
+ */
95
+ cli(path, _options) {
96
+ const options = typeof _options === 'function' ? { handler: _options } : _options;
97
+ const handler = typeof _options === 'function' ? _options : _options.handler;
98
+ const makePath = (s) => '/' + s.replace(/\s+/g, '/');
99
+ // register handler
100
+ const targetPath = makePath(path);
101
+ const routed = this.on('CLI', targetPath, handler);
102
+ if (options.onRegister) {
103
+ options.onRegister(targetPath, 0);
104
+ }
105
+ // register direct aliases
106
+ for (const alias of options.aliases || []) {
107
+ const vars = routed.getArgs().map((k) => ':' + k).join('/');
108
+ const targetPath = makePath(alias) + (vars ? '/' + vars : '');
109
+ this.on('CLI', targetPath, handler);
110
+ if (options.onRegister) {
111
+ options.onRegister(targetPath, 1);
112
+ }
113
+ }
114
+ // register helpCli entry
115
+ const command = routed.getStaticPart().replace(/\//g, ' ').trim();
116
+ const args = Object.assign({}, (options.args || {}));
117
+ for (const arg of routed.getArgs()) {
118
+ if (!args[arg]) {
119
+ args[arg] = '';
120
+ }
121
+ }
122
+ this.cliHelp.addEntry({
123
+ command,
124
+ aliases: options.aliases,
125
+ args,
126
+ description: options.description,
127
+ examples: options.examples,
128
+ options: options.options,
129
+ custom: { handler: options.handler, cb: options.onRegister },
130
+ });
131
+ return routed;
132
+ }
133
+ computeAliases() {
134
+ if (!this.alreadyComputedAliases) {
135
+ this.alreadyComputedAliases = true;
136
+ const aliases = this.cliHelp.getComputedAliases();
137
+ for (const [alias, entry] of Object.entries(aliases)) {
138
+ if (entry.custom) {
139
+ const vars = Object.keys(entry.args || {})
140
+ .map((k) => ':' + k)
141
+ .join('/');
142
+ const path = '/' +
143
+ alias.replace(/\s+/g, '/').replace(/:/g, '\\:') +
144
+ (vars ? '/' + vars : '');
145
+ this.on('CLI', path, entry.custom.handler);
146
+ if (entry.custom.cb) {
147
+ entry.custom.cb(path, 3);
148
+ }
149
+ }
150
+ }
151
+ }
55
152
  }
153
+ /**
154
+ * ## run
155
+ * ### Start command processing
156
+ * Triggers command processing
157
+ *
158
+ * By default takes `process.argv.slice(2)` as a command
159
+ *
160
+ * It's possible to replace the command by passing an argument
161
+ *
162
+ * @param _argv optionally overwrite `process.argv.slice(2)` with your `argv` array
163
+ */
56
164
  run(_argv) {
57
165
  var _a, _b;
58
166
  return __awaiter(this, void 0, void 0, function* () {
59
- const argv = process.argv.slice(2) || _argv;
167
+ const argv = _argv || process.argv.slice(2);
60
168
  const firstFlagIndex = argv.findIndex((a) => a.startsWith('-')) + 1;
61
- const pathParams = (firstFlagIndex
169
+ const pathParams = firstFlagIndex
62
170
  ? argv.slice(0, firstFlagIndex - 1)
63
- : argv);
64
- const path = '/' + pathParams.map((v) => encodeURIComponent(v)).join('/');
65
- const { restoreCtx, clearCtx } = createCliContext({ argv, pathParams }, this.mergeEventOptions((_a = this.opts) === null || _a === void 0 ? void 0 : _a.eventOptions));
66
- const handlers = this.wooks.lookup('CLI', path) || ((_b = this.opts) === null || _b === void 0 ? void 0 : _b.onNotFound) && [this.opts.onNotFound] || null;
171
+ : argv;
172
+ const path = '/' +
173
+ pathParams.map((v) => encodeURI(v).replace(/\//g, '%2F')).join('/');
174
+ const { restoreCtx, clearCtx, store } = createCliContext({ argv, pathParams, cliHelp: this.cliHelp, command: path.replace(/\//g, ' ').trim() }, this.mergeEventOptions((_a = this.opts) === null || _a === void 0 ? void 0 : _a.eventOptions));
175
+ this.computeAliases();
176
+ const { handlers: foundHandlers, firstStatic } = this.wooks.lookup('CLI', path);
177
+ if (typeof firstStatic === 'string') {
178
+ // overwriting command with firstStatic to properly search for help
179
+ store('event').set('command', firstStatic.replace(/\//g, ' ').trim());
180
+ }
181
+ const handlers = foundHandlers ||
182
+ (((_b = this.opts) === null || _b === void 0 ? void 0 : _b.onNotFound) && [this.opts.onNotFound]) ||
183
+ null;
67
184
  if (handlers) {
68
185
  try {
69
186
  for (const handler of handlers) {
@@ -108,16 +225,21 @@ class WooksCli extends wooks.WooksAdapterBase {
108
225
  process.exit(1);
109
226
  }
110
227
  }
228
+ /**
229
+ * Triggers `unknown command` processing and callbacks
230
+ * @param pathParams `string[]` containing command
231
+ */
111
232
  onUnknownCommand(pathParams) {
112
233
  var _a;
234
+ const raiseError = () => {
235
+ this.error('' + 'Unknown command: ' + pathParams.join(' '));
236
+ process.exit(1);
237
+ };
113
238
  if ((_a = this.opts) === null || _a === void 0 ? void 0 : _a.onUnknownCommand) {
114
- this.opts.onUnknownCommand(pathParams);
239
+ this.opts.onUnknownCommand(pathParams, raiseError);
115
240
  }
116
241
  else {
117
- this.error('' +
118
- 'Unknown command: ' +
119
- pathParams.join(' '));
120
- process.exit(1);
242
+ raiseError();
121
243
  }
122
244
  }
123
245
  error(e) {
@@ -133,6 +255,142 @@ function createCliApp(opts, wooks) {
133
255
  return new WooksCli(opts, wooks);
134
256
  }
135
257
 
258
+ /**
259
+ * ## useCliHelp
260
+ * ### Composable
261
+ * ```js
262
+ * // example of printing cli instructions
263
+ * const { print } = useCliHelp()
264
+ * // print with colors
265
+ * print(true)
266
+ * // print with no colors
267
+ * // print(false)
268
+ * ```
269
+ * @returns
270
+ */
271
+ function useCliHelp() {
272
+ const event = useCliContext().store('event');
273
+ const getCliHelp = () => event.get('cliHelp');
274
+ const getEntry = () => { var _a; return (_a = getCliHelp().match(event.get('command'))) === null || _a === void 0 ? void 0 : _a.main; };
275
+ return {
276
+ getCliHelp,
277
+ getEntry,
278
+ render: (width, withColors) => getCliHelp().render(event.get('command'), width, withColors),
279
+ print: (withColors) => getCliHelp().print(event.get('command'), withColors),
280
+ };
281
+ }
282
+ /**
283
+ * ## useAutoHelp
284
+ * ### Composable
285
+ *
286
+ * Prints help if `--help` option provided.
287
+ *
288
+ * ```js
289
+ * // example of use: print help and exit
290
+ * app.cli('test', () => {
291
+ * useAutoHelp() && process.exit(0)
292
+ * return 'hit test command'
293
+ * })
294
+ *
295
+ * // add option -h to print help, no colors
296
+ * app.cli('test/nocolors', () => {
297
+ * useAutoHelp(['help', 'h'], false) && process.exit(0)
298
+ * return 'hit test nocolors command'
299
+ * })
300
+ * ```
301
+ * @param keys default `['help']` - list of options to trigger help render
302
+ * @param colors default `true`, prints with colors when true
303
+ * @returns true when --help was provided. Otherwise returns false
304
+ */
305
+ function useAutoHelp(keys = ['help'], colors = true) {
306
+ for (const option of keys) {
307
+ if (useFlag(option) === true) {
308
+ // try {
309
+ useCliHelp().print(colors);
310
+ return true;
311
+ // } catch (e) {
312
+ // throw new
313
+ // }
314
+ }
315
+ }
316
+ }
317
+ /**
318
+ * ##useCommandLookupHelp
319
+ * ### Composable
320
+ *
321
+ * Tries to find valid command based on provided command.
322
+ *
323
+ * If manages to find a valid command, throws an error
324
+ * suggesting a list of valid commands
325
+ *
326
+ * Best to use in `onUnknownCommand` callback:
327
+ *
328
+ * ```js
329
+ * const app = createCliApp({
330
+ * onUnknownCommand: (path, raiseError) => {
331
+ * // will throw an error suggesting a list
332
+ * // of valid commands if could find some
333
+ * useCommandLookupHelp()
334
+ * // fallback to a regular error handler
335
+ * raiseError()
336
+ * },
337
+ * })
338
+ * ```
339
+ *
340
+ * @param lookupDepth depth of search in backwards
341
+ * @example
342
+ *
343
+ * For provided command `run test:drive dir`
344
+ * - lookup1: `run test:drive dir` (deep = 0)
345
+ * - lookup2: `run test:drive` (deep = 1)
346
+ * - lookup3: `run test` (deep = 2)
347
+ * - lookup4: `run` (deep = 3)
348
+ * ...
349
+ */
350
+ function useCommandLookupHelp(lookupDepth = 3) {
351
+ const parts = useCliContext()
352
+ .store('event')
353
+ .get('pathParams')
354
+ .map((p) => (p + ' ').split(':').map((s, i) => (i ? ':' + s : s)))
355
+ .flat();
356
+ const cliHelp = useCliHelp().getCliHelp();
357
+ const cmd = cliHelp.getCliName();
358
+ let data;
359
+ for (let i = 0; i < Math.min(parts.length, lookupDepth + 1); i++) {
360
+ const pathParams = parts
361
+ .slice(0, i ? -i : parts.length)
362
+ .join('')
363
+ .trim();
364
+ try {
365
+ data = cliHelp.match(pathParams);
366
+ break;
367
+ }
368
+ catch (e) {
369
+ const variants = cliHelp.lookup(pathParams);
370
+ if (variants.length) {
371
+ throw new Error(`Wrong command, did you mean:\n${variants
372
+ .slice(0, 7)
373
+ .map((c) => ` $ ${cmd} ${c.main.command}`)
374
+ .join('\n')}`);
375
+ }
376
+ }
377
+ }
378
+ if (data) {
379
+ const { main, children } = data;
380
+ if (main.args && Object.keys(main.args).length) {
381
+ throw new Error(`Arguments expected: ${Object.keys(main.args)
382
+ .map((l) => `<${l}>`)
383
+ .join(', ')}`);
384
+ }
385
+ else if (children && children.length) {
386
+ throw new Error(`Wrong command, did you mean:\n${children
387
+ .slice(0, 7)
388
+ .map((c) => ` $ ${cmd} ${c.command}`)
389
+ .join('\n')}`);
390
+ }
391
+ }
392
+ }
393
+
136
394
  function useFlags() {
137
395
  const { store } = useCliContext();
138
396
  const flags = store('flags');
@@ -142,6 +400,21 @@ function useFlags() {
142
400
  return flags.value;
143
401
  }
144
402
  function useFlag(name) {
403
+ var _a;
404
+ try {
405
+ const options = ((_a = useCliHelp().getEntry()) === null || _a === void 0 ? void 0 : _a.options) || [];
406
+ const opt = options.find(o => o.keys.includes(name));
407
+ if (opt) {
408
+ for (const key of opt.keys) {
409
+ if (useFlags()[key]) {
410
+ return useFlags()[key];
411
+ }
412
+ }
413
+ }
414
+ }
415
+ catch (e) {
416
+ //
417
+ }
145
418
  return useFlags()[name];
146
419
  }
147
420
 
@@ -155,7 +428,10 @@ exports.WooksCli = WooksCli;
155
428
  exports.cliShortcuts = cliShortcuts;
156
429
  exports.createCliApp = createCliApp;
157
430
  exports.createCliContext = createCliContext;
431
+ exports.useAutoHelp = useAutoHelp;
158
432
  exports.useCliContext = useCliContext;
433
+ exports.useCliHelp = useCliHelp;
434
+ exports.useCommandLookupHelp = useCommandLookupHelp;
159
435
  exports.useFlag = useFlag;
160
436
  exports.useFlags = useFlags;
161
437
  exports.usePathParams = usePathParams;
package/dist/index.d.ts CHANGED
@@ -1,9 +1,13 @@
1
+ import { CliHelpRenderer } from '@prostojs/cli-help';
2
+ import { TCliEntry } from '@prostojs/cli-help';
3
+ import { TCliHelpOptions } from '@prostojs/cli-help';
1
4
  import { TConsoleBase } from '@prostojs/logger';
2
5
  import { TEmpty } from '@wooksjs/event-core';
3
6
  import { TEventOptions } from '@wooksjs/event-core';
4
7
  import { TGenericContextStore } from '@wooksjs/event-core';
5
8
  import { TProstoRouterPathHandle } from '@prostojs/router';
6
- import { TWooksHandler } from 'wooks';
9
+ import { TWooksHandler } from '@wooksjs/wooks';
10
+ import { TWooksHandler as TWooksHandler_2 } from 'wooks';
7
11
  import { Wooks } from 'wooks';
8
12
  import { WooksAdapterBase } from 'wooks';
9
13
 
@@ -44,17 +48,63 @@ export declare interface TCliContextStore {
44
48
  export declare interface TCliEventData {
45
49
  argv: string[];
46
50
  pathParams: string[];
51
+ command: string;
47
52
  type: 'CLI';
53
+ cliHelp: TCliHelpRenderer;
54
+ }
55
+
56
+ export declare type TCliHelpCustom = {
57
+ handler: TWooksHandler<any>;
58
+ /**
59
+ * ### Callback for registered path
60
+ *
61
+ * @param path registered path
62
+ * @param aliasType 0 - direct command, 1 - direct alias, 2 - computed alias
63
+ */
64
+ cb?: (path: string, aliasType: number) => void;
65
+ };
66
+
67
+ export declare type TCliHelpRenderer = CliHelpRenderer<TCliHelpCustom>;
68
+
69
+ export declare interface TWooksCliEntry<T> extends Omit<TCliEntry<TWooksHandler_2<T>>, 'custom' | 'command'> {
70
+ onRegister?: TCliHelpCustom['cb'];
71
+ handler: TWooksHandler_2<T>;
48
72
  }
49
73
 
50
74
  export declare interface TWooksCliOptions {
51
75
  onError?(e: Error): void;
52
- onNotFound?: TWooksHandler<unknown>;
53
- onUnknownCommand?: ((params: string[]) => unknown);
76
+ onNotFound?: TWooksHandler_2<unknown>;
77
+ onUnknownCommand?: (params: string[], raiseError: () => void) => unknown;
54
78
  logger?: TConsoleBase;
55
79
  eventOptions?: TEventOptions;
80
+ cliHelp?: TCliHelpRenderer | TCliHelpOptions;
56
81
  }
57
82
 
83
+ /**
84
+ * ## useAutoHelp
85
+ * ### Composable
86
+ *
87
+ * Prints help if `--help` option provided.
88
+ *
89
+ * ```js
90
+ * // example of use: print help and exit
91
+ * app.cli('test', () => {
92
+ * useAutoHelp() && process.exit(0)
93
+ * return 'hit test command'
94
+ * })
95
+ *
96
+ * // add option -h to print help, no colors
97
+ * app.cli('test/nocolors', () => {
98
+ * useAutoHelp(['help', 'h'], false) && process.exit(0)
99
+ * return 'hit test nocolors command'
100
+ * })
101
+ * ```
102
+ * @param keys default `['help']` - list of options to trigger help render
103
+ * @param colors default `true`, prints with colors when true
104
+ * @returns true when --help was provided. Otherwise returns false
105
+ */
106
+ export declare function useAutoHelp(keys?: string[], colors?: boolean): true | undefined;
107
+
58
108
  export declare function useCliContext<T extends TEmpty>(): {
59
109
  getCtx: () => TCliContextStore & T & TGenericContextStore<TCliEventData>;
60
110
  restoreCtx: () => TGenericContextStore<TEmpty>;
@@ -77,6 +127,61 @@ export declare function useCliContext<T extends TEmpty>(): {
77
127
  setStore: <K_2 extends "flags" | keyof TGenericContextStore<TCliEventData> | keyof T>(key: K_2, v: (TCliContextStore & T & TGenericContextStore<TCliEventData>)[K_2]) => void;
78
128
  };
79
129
 
130
+ /**
131
+ * ## useCliHelp
132
+ * ### Composable
133
+ * ```js
134
+ * // example of printing cli instructions
135
+ * const { print } = useCliHelp()
136
+ * // print with colors
137
+ * print(true)
138
+ * // print with no colors
139
+ * // print(false)
140
+ * ```
141
+ * @returns
142
+ */
143
+ export declare function useCliHelp(): {
144
+ getCliHelp: () => TCliHelpRenderer;
145
+ getEntry: () => TCliEntry<TCliHelpCustom>;
146
+ render: (width?: number, withColors?: boolean) => string[];
147
+ print: (withColors?: boolean) => void;
148
+ };
149
+
150
+ /**
151
+ * ##useCommandLookupHelp
152
+ * ### Composable
153
+ *
154
+ * Tries to find valid command based on provided command.
155
+ *
156
+ * If manages to find a valid command, throws an error
157
+ * suggesting a list of valid commands
158
+ *
159
+ * Best to use in `onUnknownCommand` callback:
160
+ *
161
+ * ```js
162
+ * const app = createCliApp({
163
+ * onUnknownCommand: (path, raiseError) => {
164
+ * // will throw an error suggesting a list
165
+ * // of valid commands if could find some
166
+ * useCommandLookupHelp()
167
+ * // fallback to a regular error handler
168
+ * raiseError()
169
+ * },
170
+ * })
171
+ * ```
172
+ *
173
+ * @param lookupDepth depth of search in backwards
174
+ * @example
175
+ *
176
+ * For provided command `run test:drive dir`
177
+ * - lookup1: `run test:drive dir` (deep = 0)
178
+ * - lookup2: `run test:drive` (deep = 1)
179
+ * - lookup3: `run test` (deep = 2)
180
+ * - lookup4: `run` (deep = 3)
181
+ * ...
182
+ */
183
+ export declare function useCommandLookupHelp(lookupDepth?: number): void;
184
+
80
185
  export declare function useFlag(name: string): string | boolean;
81
186
 
82
187
  export declare function useFlags(): {
@@ -88,12 +193,66 @@ export declare function usePathParams(): string[];
88
193
  export declare class WooksCli extends WooksAdapterBase {
89
194
  protected opts?: TWooksCliOptions | undefined;
90
195
  protected logger: TConsoleBase;
196
+ protected cliHelp: TCliHelpRenderer;
91
197
  constructor(opts?: TWooksCliOptions | undefined, wooks?: Wooks | WooksAdapterBase);
92
- cli<ResType = unknown, ParamsType = Record<string, string | string[]>>(path: string, handler: TWooksHandler<ResType>): TProstoRouterPathHandle<ParamsType>;
198
+ /**
199
+ * ### Register CLI Command
200
+ * Command path segments may be separated by / or space.
201
+ *
202
+ * For example the folowing path are interpreted the same:
203
+ * - "command test use:dev :name"
204
+ * - "command/test/use:dev/:name"
205
+ *
206
+ * Where name will become an argument
207
+ *
208
+ * ```js
209
+ * // example without options
210
+ * app.cli('command/:arg', () => 'arg = ' + useRouteParams().params.arg )
211
+ *
212
+ * // example with options
213
+ * app.cli('command/:arg', {
214
+ * description: 'Description of the command',
215
+ * options: [{ keys: ['project', 'p'], description: 'Description of the option', value: 'myProject' }],
216
+ * args: { arg: 'Description of the arg' },
217
+ * aliases: ['cmd'], // alias "cmd/:arg" will be registered
218
+ * examples: [{
219
+ * description: 'Example of usage with someProject',
220
+ * cmd: 'argValue -p=someProject',
221
+ * // will result in help display:
222
+ * // "# Example of usage with someProject\n" +
223
+ * // "$ myCli command argValue -p=someProject\n"
224
+ * }],
225
+ * handler: () => 'arg = ' + useRouteParams().params.arg
226
+ * })
227
+ * ```
228
+ *
229
+ * @param path command path
230
+ * @param _options handler or options
231
+ *
232
+ * @returns
233
+ */
234
+ cli<ResType = unknown, ParamsType = Record<string, string | string[]>>(path: string, _options: TWooksCliEntry<ResType> | TWooksHandler_2<ResType>): TProstoRouterPathHandle<ParamsType>;
235
+ protected alreadyComputedAliases: boolean;
236
+ protected computeAliases(): void;
237
+ /**
238
+ * ## run
239
+ * ### Start command processing
240
+ * Triggers command processing
241
+ *
242
+ * By default takes `process.argv.slice(2)` as a command
243
+ *
244
+ * It's possible to replace the command by passing an argument
245
+ *
246
+ * @param _argv optionally overwrite `process.argv.slice(2)` with your `argv` array
247
+ */
93
248
  run(_argv?: string[]): Promise<void>;
94
- onError(e: Error): void;
249
+ protected onError(e: Error): void;
250
+ /**
251
+ * Triggers `unknown command` processing and callbacks
252
+ * @param pathParams `string[]` containing command
253
+ */
95
254
  onUnknownCommand(pathParams: string[]): void;
96
- error(e: string | Error): void;
255
+ protected error(e: string | Error): void;
97
256
  }
98
257
 
99
258
  export { }
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createEventContext, useEventContext } from '@wooksjs/event-core';
2
2
  import { WooksAdapterBase } from 'wooks';
3
+ import { CliHelpRenderer } from '@prostojs/cli-help';
3
4
  import minimist from 'minimist';
4
5
 
5
6
  function createCliContext(data, options) {
@@ -46,22 +47,138 @@ class WooksCli extends WooksAdapterBase {
46
47
  constructor(opts, wooks) {
47
48
  super(wooks, opts === null || opts === void 0 ? void 0 : opts.logger);
48
49
  this.opts = opts;
50
+ this.alreadyComputedAliases = false;
49
51
  this.logger = (opts === null || opts === void 0 ? void 0 : opts.logger) || this.getLogger('wooks-cli');
52
+ this.cliHelp =
53
+ (opts === null || opts === void 0 ? void 0 : opts.cliHelp) instanceof CliHelpRenderer
54
+ ? opts.cliHelp
55
+ : new CliHelpRenderer(opts === null || opts === void 0 ? void 0 : opts.cliHelp);
50
56
  }
51
- cli(path, handler) {
52
- return this.on('CLI', path, handler);
57
+ /**
58
+ * ### Register CLI Command
59
+ * Command path segments may be separated by / or space.
60
+ *
61
+ * For example the folowing path are interpreted the same:
62
+ * - "command test use:dev :name"
63
+ * - "command/test/use:dev/:name"
64
+ *
65
+ * Where name will become an argument
66
+ *
67
+ * ```js
68
+ * // example without options
69
+ * app.cli('command/:arg', () => 'arg = ' + useRouteParams().params.arg )
70
+ *
71
+ * // example with options
72
+ * app.cli('command/:arg', {
73
+ * description: 'Description of the command',
74
+ * options: [{ keys: ['project', 'p'], description: 'Description of the option', value: 'myProject' }],
75
+ * args: { arg: 'Description of the arg' },
76
+ * aliases: ['cmd'], // alias "cmd/:arg" will be registered
77
+ * examples: [{
78
+ * description: 'Example of usage with someProject',
79
+ * cmd: 'argValue -p=someProject',
80
+ * // will result in help display:
81
+ * // "# Example of usage with someProject\n" +
82
+ * // "$ myCli command argValue -p=someProject\n"
83
+ * }],
84
+ * handler: () => 'arg = ' + useRouteParams().params.arg
85
+ * })
86
+ * ```
87
+ *
88
+ * @param path command path
89
+ * @param _options handler or options
90
+ *
91
+ * @returns
92
+ */
93
+ cli(path, _options) {
94
+ const options = typeof _options === 'function' ? { handler: _options } : _options;
95
+ const handler = typeof _options === 'function' ? _options : _options.handler;
96
+ const makePath = (s) => '/' + s.replace(/\s+/g, '/');
97
+ // register handler
98
+ const targetPath = makePath(path);
99
+ const routed = this.on('CLI', targetPath, handler);
100
+ if (options.onRegister) {
101
+ options.onRegister(targetPath, 0);
102
+ }
103
+ // register direct aliases
104
+ for (const alias of options.aliases || []) {
105
+ const vars = routed.getArgs().map((k) => ':' + k).join('/');
106
+ const targetPath = makePath(alias) + (vars ? '/' + vars : '');
107
+ this.on('CLI', targetPath, handler);
108
+ if (options.onRegister) {
109
+ options.onRegister(targetPath, 1);
110
+ }
111
+ }
112
+ // register helpCli entry
113
+ const command = routed.getStaticPart().replace(/\//g, ' ').trim();
114
+ const args = Object.assign({}, (options.args || {}));
115
+ for (const arg of routed.getArgs()) {
116
+ if (!args[arg]) {
117
+ args[arg] = '';
118
+ }
119
+ }
120
+ this.cliHelp.addEntry({
121
+ command,
122
+ aliases: options.aliases,
123
+ args,
124
+ description: options.description,
125
+ examples: options.examples,
126
+ options: options.options,
127
+ custom: { handler: options.handler, cb: options.onRegister },
128
+ });
129
+ return routed;
130
+ }
131
+ computeAliases() {
132
+ if (!this.alreadyComputedAliases) {
133
+ this.alreadyComputedAliases = true;
134
+ const aliases = this.cliHelp.getComputedAliases();
135
+ for (const [alias, entry] of Object.entries(aliases)) {
136
+ if (entry.custom) {
137
+ const vars = Object.keys(entry.args || {})
138
+ .map((k) => ':' + k)
139
+ .join('/');
140
+ const path = '/' +
141
+ alias.replace(/\s+/g, '/').replace(/:/g, '\\:') +
142
+ (vars ? '/' + vars : '');
143
+ this.on('CLI', path, entry.custom.handler);
144
+ if (entry.custom.cb) {
145
+ entry.custom.cb(path, 3);
146
+ }
147
+ }
148
+ }
149
+ }
53
150
  }
151
+ /**
152
+ * ## run
153
+ * ### Start command processing
154
+ * Triggers command processing
155
+ *
156
+ * By default takes `process.argv.slice(2)` as a command
157
+ *
158
+ * It's possible to replace the command by passing an argument
159
+ *
160
+ * @param _argv optionally overwrite `process.argv.slice(2)` with your `argv` array
161
+ */
54
162
  run(_argv) {
55
163
  var _a, _b;
56
164
  return __awaiter(this, void 0, void 0, function* () {
57
- const argv = process.argv.slice(2) || _argv;
165
+ const argv = _argv || process.argv.slice(2);
58
166
  const firstFlagIndex = argv.findIndex((a) => a.startsWith('-')) + 1;
59
- const pathParams = (firstFlagIndex
167
+ const pathParams = firstFlagIndex
60
168
  ? argv.slice(0, firstFlagIndex - 1)
61
- : argv);
62
- const path = '/' + pathParams.map((v) => encodeURIComponent(v)).join('/');
63
- const { restoreCtx, clearCtx } = createCliContext({ argv, pathParams }, this.mergeEventOptions((_a = this.opts) === null || _a === void 0 ? void 0 : _a.eventOptions));
64
- const handlers = this.wooks.lookup('CLI', path) || ((_b = this.opts) === null || _b === void 0 ? void 0 : _b.onNotFound) && [this.opts.onNotFound] || null;
169
+ : argv;
170
+ const path = '/' +
171
+ pathParams.map((v) => encodeURI(v).replace(/\//g, '%2F')).join('/');
172
+ const { restoreCtx, clearCtx, store } = createCliContext({ argv, pathParams, cliHelp: this.cliHelp, command: path.replace(/\//g, ' ').trim() }, this.mergeEventOptions((_a = this.opts) === null || _a === void 0 ? void 0 : _a.eventOptions));
173
+ this.computeAliases();
174
+ const { handlers: foundHandlers, firstStatic } = this.wooks.lookup('CLI', path);
175
+ if (typeof firstStatic === 'string') {
176
+ // overwriting command with firstStatic to properly search for help
177
+ store('event').set('command', firstStatic.replace(/\//g, ' ').trim());
178
+ }
179
+ const handlers = foundHandlers ||
180
+ (((_b = this.opts) === null || _b === void 0 ? void 0 : _b.onNotFound) && [this.opts.onNotFound]) ||
181
+ null;
65
182
  if (handlers) {
66
183
  try {
67
184
  for (const handler of handlers) {
@@ -106,16 +223,21 @@ class WooksCli extends WooksAdapterBase {
106
223
  process.exit(1);
107
224
  }
108
225
  }
226
+ /**
227
+ * Triggers `unknown command` processing and callbacks
228
+ * @param pathParams `string[]` containing command
229
+ */
109
230
  onUnknownCommand(pathParams) {
110
231
  var _a;
232
+ const raiseError = () => {
233
+ this.error('' + 'Unknown command: ' + pathParams.join(' '));
234
+ process.exit(1);
235
+ };
111
236
  if ((_a = this.opts) === null || _a === void 0 ? void 0 : _a.onUnknownCommand) {
112
- this.opts.onUnknownCommand(pathParams);
237
+ this.opts.onUnknownCommand(pathParams, raiseError);
113
238
  }
114
239
  else {
115
- this.error('' +
116
- 'Unknown command: ' +
117
- pathParams.join(' '));
118
- process.exit(1);
240
+ raiseError();
119
241
  }
120
242
  }
121
243
  error(e) {
@@ -131,6 +253,142 @@ function createCliApp(opts, wooks) {
131
253
  return new WooksCli(opts, wooks);
132
254
  }
133
255
 
256
+ /**
257
+ * ## useCliHelp
258
+ * ### Composable
259
+ * ```js
260
+ * // example of printing cli instructions
261
+ * const { print } = useCliHelp()
262
+ * // print with colors
263
+ * print(true)
264
+ * // print with no colors
265
+ * // print(false)
266
+ * ```
267
+ * @returns
268
+ */
269
+ function useCliHelp() {
270
+ const event = useCliContext().store('event');
271
+ const getCliHelp = () => event.get('cliHelp');
272
+ const getEntry = () => { var _a; return (_a = getCliHelp().match(event.get('command'))) === null || _a === void 0 ? void 0 : _a.main; };
273
+ return {
274
+ getCliHelp,
275
+ getEntry,
276
+ render: (width, withColors) => getCliHelp().render(event.get('command'), width, withColors),
277
+ print: (withColors) => getCliHelp().print(event.get('command'), withColors),
278
+ };
279
+ }
280
+ /**
281
+ * ## useAutoHelp
282
+ * ### Composable
283
+ *
284
+ * Prints help if `--help` option provided.
285
+ *
286
+ * ```js
287
+ * // example of use: print help and exit
288
+ * app.cli('test', () => {
289
+ * useAutoHelp() && process.exit(0)
290
+ * return 'hit test command'
291
+ * })
292
+ *
293
+ * // add option -h to print help, no colors
294
+ * app.cli('test/nocolors', () => {
295
+ * useAutoHelp(['help', 'h'], false) && process.exit(0)
296
+ * return 'hit test nocolors command'
297
+ * })
298
+ * ```
299
+ * @param keys default `['help']` - list of options to trigger help render
300
+ * @param colors default `true`, prints with colors when true
301
+ * @returns true when --help was provided. Otherwise returns false
302
+ */
303
+ function useAutoHelp(keys = ['help'], colors = true) {
304
+ for (const option of keys) {
305
+ if (useFlag(option) === true) {
306
+ // try {
307
+ useCliHelp().print(colors);
308
+ return true;
309
+ // } catch (e) {
310
+ // throw new
311
+ // }
312
+ }
313
+ }
314
+ }
315
+ /**
316
+ * ##useCommandLookupHelp
317
+ * ### Composable
318
+ *
319
+ * Tries to find valid command based on provided command.
320
+ *
321
+ * If manages to find a valid command, throws an error
322
+ * suggesting a list of valid commands
323
+ *
324
+ * Best to use in `onUnknownCommand` callback:
325
+ *
326
+ * ```js
327
+ * const app = createCliApp({
328
+ * onUnknownCommand: (path, raiseError) => {
329
+ * // will throw an error suggesting a list
330
+ * // of valid commands if could find some
331
+ * useCommandLookupHelp()
332
+ * // fallback to a regular error handler
333
+ * raiseError()
334
+ * },
335
+ * })
336
+ * ```
337
+ *
338
+ * @param lookupDepth depth of search in backwards
339
+ * @example
340
+ *
341
+ * For provided command `run test:drive dir`
342
+ * - lookup1: `run test:drive dir` (deep = 0)
343
+ * - lookup2: `run test:drive` (deep = 1)
344
+ * - lookup3: `run test` (deep = 2)
345
+ * - lookup4: `run` (deep = 3)
346
+ * ...
347
+ */
348
+ function useCommandLookupHelp(lookupDepth = 3) {
349
+ const parts = useCliContext()
350
+ .store('event')
351
+ .get('pathParams')
352
+ .map((p) => (p + ' ').split(':').map((s, i) => (i ? ':' + s : s)))
353
+ .flat();
354
+ const cliHelp = useCliHelp().getCliHelp();
355
+ const cmd = cliHelp.getCliName();
356
+ let data;
357
+ for (let i = 0; i < Math.min(parts.length, lookupDepth + 1); i++) {
358
+ const pathParams = parts
359
+ .slice(0, i ? -i : parts.length)
360
+ .join('')
361
+ .trim();
362
+ try {
363
+ data = cliHelp.match(pathParams);
364
+ break;
365
+ }
366
+ catch (e) {
367
+ const variants = cliHelp.lookup(pathParams);
368
+ if (variants.length) {
369
+ throw new Error(`Wrong command, did you mean:\n${variants
370
+ .slice(0, 7)
371
+ .map((c) => ` $ ${cmd} ${c.main.command}`)
372
+ .join('\n')}`);
373
+ }
374
+ }
375
+ }
376
+ if (data) {
377
+ const { main, children } = data;
378
+ if (main.args && Object.keys(main.args).length) {
379
+ throw new Error(`Arguments expected: ${Object.keys(main.args)
380
+ .map((l) => `<${l}>`)
381
+ .join(', ')}`);
382
+ }
383
+ else if (children && children.length) {
384
+ throw new Error(`Wrong command, did you mean:\n${children
385
+ .slice(0, 7)
386
+ .map((c) => ` $ ${cmd} ${c.command}`)
387
+ .join('\n')}`);
388
+ }
389
+ }
390
+ }
391
+
134
392
  function useFlags() {
135
393
  const { store } = useCliContext();
136
394
  const flags = store('flags');
@@ -140,6 +398,21 @@ function useFlags() {
140
398
  return flags.value;
141
399
  }
142
400
  function useFlag(name) {
401
+ var _a;
402
+ try {
403
+ const options = ((_a = useCliHelp().getEntry()) === null || _a === void 0 ? void 0 : _a.options) || [];
404
+ const opt = options.find(o => o.keys.includes(name));
405
+ if (opt) {
406
+ for (const key of opt.keys) {
407
+ if (useFlags()[key]) {
408
+ return useFlags()[key];
409
+ }
410
+ }
411
+ }
412
+ }
413
+ catch (e) {
414
+ //
415
+ }
143
416
  return useFlags()[name];
144
417
  }
145
418
 
@@ -149,4 +422,4 @@ function usePathParams() {
149
422
  return event.get('pathParams');
150
423
  }
151
424
 
152
- export { WooksCli, cliShortcuts, createCliApp, createCliContext, useCliContext, useFlag, useFlags, usePathParams };
425
+ export { WooksCli, cliShortcuts, createCliApp, createCliContext, useAutoHelp, useCliContext, useCliHelp, useCommandLookupHelp, useFlag, useFlags, usePathParams };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wooksjs/event-cli",
3
- "version": "0.2.22",
3
+ "version": "0.3.0",
4
4
  "description": "@wooksjs/event-cli",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",
@@ -31,11 +31,12 @@
31
31
  "url": "https://github.com/wooksjs/wooksjs/issues"
32
32
  },
33
33
  "peerDependencies": {
34
- "wooks": "0.2.22",
35
- "@wooksjs/event-core": "0.2.22"
34
+ "wooks": "0.3.0",
35
+ "@wooksjs/event-core": "0.3.0"
36
36
  },
37
37
  "dependencies": {
38
38
  "minimist": "^1.2.6",
39
+ "@prostojs/cli-help": "^0.0.10",
39
40
  "@prostojs/logger": "^0.3.6"
40
41
  },
41
42
  "homepage": "https://github.com/wooksjs/wooksjs/tree/main/packages/event-cli#readme"