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