@zhennann/common-bin 2.9.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017-present node-modules and other contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,430 @@
1
+ # common-bin
2
+
3
+ [![NPM version][npm-image]][npm-url]
4
+ [![Test coverage][codecov-image]][codecov-url]
5
+ [![Known Vulnerabilities][snyk-image]][snyk-url]
6
+ [![npm download][download-image]][download-url]
7
+
8
+ [npm-image]: https://img.shields.io/npm/v/common-bin.svg?style=flat-square
9
+ [npm-url]: https://npmjs.org/package/common-bin
10
+ [codecov-image]: https://codecov.io/gh/node-modules/common-bin/branch/master/graph/badge.svg
11
+ [codecov-url]: https://codecov.io/gh/node-modules/common-bin
12
+ [snyk-image]: https://snyk.io/test/npm/common-bin/badge.svg?style=flat-square
13
+ [snyk-url]: https://snyk.io/test/npm/common-bin
14
+ [download-image]: https://img.shields.io/npm/dm/common-bin.svg?style=flat-square
15
+ [download-url]: https://npmjs.org/package/common-bin
16
+
17
+ Abstraction bin tool wrap [yargs](http://yargs.js.org/), to provide more convenient usage, support async / generator.
18
+
19
+ ---
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ $ npm i common-bin
25
+ ```
26
+
27
+ ## Build a bin tool for your team
28
+
29
+ You maybe need a custom xxx-bin to implement more custom features.
30
+
31
+ Now you can implement a [Command](lib/command.js) sub class to do that.
32
+
33
+ ### Example: Write your own `git` command
34
+
35
+ This example will show you how to create a new `my-git` tool.
36
+
37
+ - Full demo: [my-git](test/fixtures/my-git)
38
+
39
+ ```bash
40
+ test/fixtures/my-git
41
+ ├── bin
42
+ │ └── my-git.js
43
+ ├── command
44
+ │ ├── remote
45
+ │ │ ├── add.js
46
+ │ │ └── remove.js
47
+ │ ├── clone.js
48
+ │ └── remote.js
49
+ ├── index.js
50
+ └── package.json
51
+ ```
52
+
53
+ #### [my-git.js](test/fixtures/my-git/bin/my-git.js)
54
+
55
+ ```js
56
+ #!/usr/bin/env node
57
+
58
+ 'use strict';
59
+
60
+ const Command = require('..');
61
+ new Command().start();
62
+ ```
63
+
64
+ #### [Main Command](test/fixtures/my-git/index.js)
65
+
66
+ Just extend `Command`, and use as your bin start point.
67
+
68
+ You can use `this.yargs` to custom yargs config, see http://yargs.js.org/docs for more detail.
69
+
70
+ ```js
71
+ const Command = require('common-bin');
72
+ const pkg = require('./package.json');
73
+
74
+ class MainCommand extends Command {
75
+ constructor(rawArgv) {
76
+ super(rawArgv);
77
+ this.usage = 'Usage: my-git <command> [options]';
78
+
79
+ // load entire command directory
80
+ this.load(path.join(__dirname, 'command'));
81
+
82
+ // or load special command file
83
+ // this.add(path.join(__dirname, 'test_command.js'));
84
+
85
+ // more custom with `yargs` api, such as you can use `my-git -V`
86
+ this.yargs.alias('V', 'version');
87
+ }
88
+ }
89
+
90
+ module.exports = MainCommand;
91
+ ```
92
+
93
+ #### [CloneCommand](test/fixtures/my-git/command/clone.js)
94
+
95
+ ```js
96
+ const Command = require('common-bin');
97
+ class CloneCommand extends Command {
98
+ constructor(rawArgv) {
99
+ super(rawArgv);
100
+
101
+ this.options = {
102
+ depth: {
103
+ type: 'number',
104
+ description: 'Create a shallow clone with a history truncated to the specified number of commits',
105
+ },
106
+ };
107
+ }
108
+
109
+ * run({ argv }) {
110
+ console.log('git clone %s to %s with depth %d', argv._[0], argv._[1], argv.depth);
111
+ }
112
+
113
+ get description() {
114
+ return 'Clone a repository into a new directory';
115
+ }
116
+ }
117
+
118
+ module.exports = CloneCommand;
119
+ ```
120
+
121
+ #### Run result
122
+
123
+ ```bash
124
+ $ my-git clone gh://node-modules/common-bin dist --depth=1
125
+
126
+ git clone gh://node-modules/common-bin to dist with depth 1
127
+ ```
128
+
129
+ ## Concept
130
+
131
+ ### Command
132
+
133
+ Define the main logic of command
134
+
135
+ **Method:**
136
+
137
+ - `start()` - start your program, only use once in your bin file.
138
+ - `run(context)`
139
+ - should implement this to provide command handler, will exec when not found sub command.
140
+ - Support generator / async function / normal function which return promise.
141
+ - `context` is `{ cwd, env, argv, rawArgv }`
142
+ - `cwd` - `process.cwd()`
143
+ - `env` - clone env object from `process.env`
144
+ - `argv` - argv parse result by yargs, `{ _: [ 'start' ], '$0': '/usr/local/bin/common-bin', baseDir: 'simple'}`
145
+ - `rawArgv` - the raw argv, `[ "--baseDir=simple" ]`
146
+ - `load(fullPath)` - register the entire directory to commands
147
+ - `add(name, target)` - register special command with command name, `target` could be full path of file or Class.
148
+ - `alias(alias, name)` - register a command with an existing command
149
+ - `showHelp()` - print usage message to console.
150
+ - `options=` - a setter, shortcut for `yargs.options`
151
+ - `usage=` - a setter, shortcut for `yargs.usage`
152
+
153
+ **Properties:**
154
+
155
+ - `description` - {String} a getter, only show this description when it's a sub command in help console
156
+ - `helper` - {Object} helper instance
157
+ - `yargs` - {Object} yargs instance for advanced custom usage
158
+ - `options` - {Object} a setter, set yargs' options
159
+ - `version` - {String} customize version, can be defined as a getter to support lazy load.
160
+ - `parserOptions` - {Object} control `context` parse rule.
161
+ - `execArgv` - {Boolean} whether extract `execArgv` to `context.execArgv`
162
+ - `removeAlias` - {Boolean} whether remove alias key from `argv`
163
+ - `removeCamelCase` - {Boolean} whether remove camel case key from `argv`
164
+
165
+ You can define options by set `this.options`
166
+
167
+ ```js
168
+ this.options = {
169
+ baseDir: {
170
+ alias: 'b',
171
+ demandOption: true,
172
+ description: 'the target directory',
173
+ coerce: str => path.resolve(process.cwd(), str),
174
+ },
175
+ depth: {
176
+ description: 'level to clone',
177
+ type: 'number',
178
+ default: 1,
179
+ },
180
+ size: {
181
+ description: 'choose a size',
182
+ choices: ['xs', 's', 'm', 'l', 'xl']
183
+ },
184
+ };
185
+ ```
186
+
187
+ You can define version by define `this.version` getter:
188
+
189
+ ```js
190
+ get version() {
191
+ return 'v1.0.0';
192
+ }
193
+ ```
194
+
195
+ ### Helper
196
+
197
+ - `forkNode(modulePath, args, opt)` - fork child process, wrap with promise and gracefull exit
198
+ - `spawn(cmd, args, opt)` - spawn a new process, wrap with promise and gracefull exit
199
+ - `npmInstall(npmCli, name, cwd)` - install node modules, wrap with promise
200
+ - `* callFn(fn, args, thisArg)` - call fn, support gernerator / async / normal function return promise
201
+ - `unparseArgv(argv, opts)` - unparse argv and change it to array style
202
+
203
+ **Extend Helper**
204
+
205
+ ```js
206
+ // index.js
207
+ const Command = require('common-bin');
208
+ const helper = require('./helper');
209
+ class MainCommand extends Command {
210
+ constructor(rawArgv) {
211
+ super(rawArgv);
212
+
213
+ // load sub command
214
+ this.load(path.join(__dirname, 'command'));
215
+
216
+ // custom helper
217
+ Object.assign(this.helper, helper);
218
+ }
219
+ }
220
+ ```
221
+
222
+ ## Advanced Usage
223
+
224
+ ### Single Command
225
+
226
+ Just need to provide `options` and `run()`.
227
+
228
+ ```js
229
+ const Command = require('common-bin');
230
+ class MainCommand extends Command {
231
+ constructor(rawArgv) {
232
+ super(rawArgv);
233
+ this.options = {
234
+ baseDir: {
235
+ description: 'target directory',
236
+ },
237
+ };
238
+ }
239
+
240
+ * run(context) {
241
+ console.log('run default command at %s', context.argv.baseDir);
242
+ }
243
+ }
244
+ ```
245
+
246
+ ### Sub Command
247
+
248
+ Also support sub command such as `my-git remote add <name> <url> --tags`.
249
+
250
+ ```js
251
+ // test/fixtures/my-git/command/remote.js
252
+ class RemoteCommand extends Command {
253
+ constructor(rawArgv) {
254
+ // DO NOT forgot to pass params to super
255
+ super(rawArgv);
256
+ // load sub command for directory
257
+ this.load(path.join(__dirname, 'remote'));
258
+ }
259
+
260
+ * run({ argv }) {
261
+ console.log('run remote command with %j', argv._);
262
+ }
263
+
264
+ get description() {
265
+ return 'Manage set of tracked repositories';
266
+ }
267
+ }
268
+
269
+ // test/fixtures/my-git/command/remote/add.js
270
+ class AddCommand extends Command {
271
+ constructor(rawArgv) {
272
+ super(rawArgv);
273
+
274
+ this.options = {
275
+ tags: {
276
+ type: 'boolean',
277
+ default: false,
278
+ description: 'imports every tag from the remote repository',
279
+ },
280
+ };
281
+
282
+ }
283
+
284
+ * run({ argv }) {
285
+ console.log('git remote add %s to %s with tags=%s', argv.name, argv.url, argv.tags);
286
+ }
287
+
288
+ get description() {
289
+ return 'Adds a remote named <name> for the repository at <url>';
290
+ }
291
+ }
292
+ ```
293
+
294
+ see [remote.js](test/fixtures/my-git/command/remote.js) for more detail.
295
+
296
+
297
+ ### Async Support
298
+
299
+ ```js
300
+ class SleepCommand extends Command {
301
+
302
+ async run() {
303
+ await sleep('1s');
304
+ console.log('sleep 1s');
305
+ }
306
+
307
+ get description() {
308
+ return 'sleep showcase';
309
+ }
310
+ }
311
+
312
+ function sleep(ms) {
313
+ return new Promise(resolve => setTimeout(resolve, ms));
314
+ }
315
+ ```
316
+
317
+ see [async-bin](test/fixtures/async-bin) for more detail.
318
+
319
+
320
+ ### Bash-Completions
321
+
322
+ ```bash
323
+ $ # exec below will print usage for auto bash completion
324
+ $ my-git completion
325
+ $ # exec below will mount auto completion to your bash
326
+ $ my-git completion >> ~/.bashrc
327
+ ```
328
+
329
+ ![Bash-Completions](https://cloud.githubusercontent.com/assets/227713/23980327/0a00e1a0-0a3a-11e7-81be-23b4d54d91ad.gif)
330
+
331
+
332
+ ## Migrating from v1 to v2
333
+
334
+ ### bin
335
+
336
+ - `run` method is not longer exist.
337
+
338
+ ```js
339
+ // 1.x
340
+ const run = require('common-bin').run;
341
+ run(require('../lib/my_program'));
342
+
343
+ // 2.x
344
+ // require a main Command
345
+ const Command = require('..');
346
+ new Command().start();
347
+ ```
348
+
349
+ ### Program
350
+
351
+ - `Program` is just a `Command` sub class, you can call it `Main Command` now.
352
+ - `addCommand()` is replace with `add()`.
353
+ - Recommand to use `load()` to load the whole command directory.
354
+
355
+ ```js
356
+ // 1.x
357
+ this.addCommand('test', path.join(__dirname, 'test_command.js'));
358
+
359
+ // 2.x
360
+ const Command = require('common-bin');
361
+ const pkg = require('./package.json');
362
+
363
+ class MainCommand extends Command {
364
+ constructor() {
365
+ super();
366
+
367
+ this.add('test', path.join(__dirname, 'test_command.js'));
368
+ // or load the entire directory
369
+ this.load(path.join(__dirname, 'command'));
370
+ }
371
+ }
372
+ ```
373
+
374
+ ### Command
375
+
376
+ - `help()` is not use anymore.
377
+ - should provide `name`, `description`, `options`.
378
+ - `* run()` arguments had change to object, recommand to use destructuring style - `{ cwd, env, argv, rawArgv }`
379
+ - `argv` is an object parse by `yargs`, **not `args`.**
380
+ - `rawArgv` is equivalent to old `args`
381
+
382
+ ```js
383
+ // 1.x
384
+ class TestCommand extends Command {
385
+ * run(cwd, args) {
386
+ console.log('run mocha test at %s with %j', cwd, args);
387
+ }
388
+ }
389
+
390
+ // 2.x
391
+ class TestCommand extends Command {
392
+ constructor() {
393
+ super();
394
+ // my-bin test --require=co-mocha
395
+ this.options = {
396
+ require: {
397
+ description: 'require module name',
398
+ },
399
+ };
400
+ }
401
+
402
+ * run({ cwd, env, argv, rawArgv }) {
403
+ console.log('run mocha test at %s with %j', cwd, argv);
404
+ }
405
+
406
+ get description() {
407
+ return 'unit test';
408
+ }
409
+ }
410
+ ```
411
+
412
+ ### helper
413
+
414
+ - `getIronNodeBin` is remove.
415
+ - `child.kill` now support signal.
416
+
417
+ ## License
418
+
419
+ [MIT](LICENSE)
420
+ <!-- GITCONTRIBUTOR_START -->
421
+
422
+ ## Contributors
423
+
424
+ |[<img src="https://avatars.githubusercontent.com/u/227713?v=4" width="100px;"/><br/><sub><b>atian25</b></sub>](https://github.com/atian25)<br/>|[<img src="https://avatars.githubusercontent.com/u/156269?v=4" width="100px;"/><br/><sub><b>fengmk2</b></sub>](https://github.com/fengmk2)<br/>|[<img src="https://avatars.githubusercontent.com/u/360661?v=4" width="100px;"/><br/><sub><b>popomore</b></sub>](https://github.com/popomore)<br/>|[<img src="https://avatars.githubusercontent.com/u/985607?v=4" width="100px;"/><br/><sub><b>dead-horse</b></sub>](https://github.com/dead-horse)<br/>|[<img src="https://avatars.githubusercontent.com/u/5856440?v=4" width="100px;"/><br/><sub><b>whxaxes</b></sub>](https://github.com/whxaxes)<br/>|[<img src="https://avatars.githubusercontent.com/u/9692408?v=4" width="100px;"/><br/><sub><b>DiamondYuan</b></sub>](https://github.com/DiamondYuan)<br/>|
425
+ | :---: | :---: | :---: | :---: | :---: | :---: |
426
+ [<img src="https://avatars.githubusercontent.com/u/7477670?v=4" width="100px;"/><br/><sub><b>tenpend</b></sub>](https://github.com/tenpend)<br/>|[<img src="https://avatars.githubusercontent.com/u/6399899?v=4" width="100px;"/><br/><sub><b>hacke2</b></sub>](https://github.com/hacke2)<br/>|[<img src="https://avatars.githubusercontent.com/u/11896359?v=4" width="100px;"/><br/><sub><b>liuqipeng417</b></sub>](https://github.com/liuqipeng417)<br/>|[<img src="https://avatars.githubusercontent.com/u/36788851?v=4" width="100px;"/><br/><sub><b>Jarvis2018</b></sub>](https://github.com/Jarvis2018)<br/>
427
+
428
+ This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Wed Feb 09 2022 22:35:03 GMT+0800`.
429
+
430
+ <!-- GITCONTRIBUTOR_END -->
package/index.d.ts ADDED
@@ -0,0 +1,188 @@
1
+ import { Arguments, Argv, Options } from 'yargs';
2
+ import { ForkOptions, SpawnOptions } from 'child_process';
3
+ import * as dargs from 'dargs';
4
+
5
+ interface PlainObject {
6
+ [key: string]: any;
7
+ }
8
+
9
+ // migrating to common-bin later
10
+ declare class CommonBin {
11
+ usage: string;
12
+ version: string;
13
+
14
+ /**
15
+ * original argument
16
+ * @type {Array}
17
+ */
18
+ rawArgv: string[];
19
+
20
+ /**
21
+ * yargs
22
+ * @type {Object}
23
+ */
24
+ yargs: Argv;
25
+
26
+ /**
27
+ * helper function
28
+ * @type {Object}
29
+ */
30
+ helper: CommonBin.Helper;
31
+
32
+ /**
33
+ * parserOptions
34
+ * @type {Object}
35
+ * @property {Boolean} execArgv - whether extract `execArgv` to `context.execArgv`
36
+ * @property {Boolean} removeAlias - whether remove alias key from `argv`
37
+ * @property {Boolean} removeCamelCase - whether remove camel case key from `argv`
38
+ */
39
+ parserOptions: {
40
+ execArgv: boolean;
41
+ removeAlias: boolean;
42
+ removeCamelCase: boolean;
43
+ };
44
+
45
+ /**
46
+ * getter of context, default behavior is remove `help` / `h` / `version`
47
+ * @return {Object} context - { cwd, env, argv, rawArgv }
48
+ * @protected
49
+ */
50
+ protected context: CommonBin.Context;
51
+
52
+ constructor(rawArgv?: string[]);
53
+
54
+
55
+ /**
56
+ * shortcut for yargs.options
57
+ * @param {Object} opt - an object set to `yargs.options`
58
+ */
59
+ set options(opt: { [key: string]: Options });
60
+
61
+ /**
62
+ * command handler, could be generator / async function / normal function which return promise
63
+ * @param {Object} context - context object
64
+ * @param {String} context.cwd - process.cwd()
65
+ * @param {Object} context.argv - argv parse result by yargs, `{ _: [ 'start' ], '$0': '/usr/local/bin/common-bin', baseDir: 'simple'}`
66
+ * @param {Array} context.rawArgv - the raw argv, `[ "--baseDir=simple" ]`
67
+ * @protected
68
+ */
69
+ protected run(context?: CommonBin.Context): any;
70
+
71
+ /**
72
+ * load sub commands
73
+ * @param {String} fullPath - the command directory
74
+ * @example `load(path.join(__dirname, 'command'))`
75
+ */
76
+ load(fullPath: string): void;
77
+
78
+ /**
79
+ * add sub command
80
+ * @param {String} name - a command name
81
+ * @param {String|Class} target - special file path (must contains ext) or Command Class
82
+ * @example `add('test', path.join(__dirname, 'test_command.js'))`
83
+ */
84
+ add(name: string, target: string | CommonBin): void;
85
+
86
+ /**
87
+ * alias an existing command
88
+ * @param {String} alias - alias command
89
+ * @param {String} name - exist command
90
+ */
91
+ alias(alias: string, name: string): void;
92
+
93
+ /**
94
+ * start point of bin process
95
+ */
96
+ start(): void;
97
+
98
+ /**
99
+ * default error hander
100
+ * @param {Error} err - error object
101
+ * @protected
102
+ */
103
+ protected errorHandler(err: Error): void;
104
+
105
+ /**
106
+ * print help message to console
107
+ * @param {String} [level=log] - console level
108
+ */
109
+ showHelp(level?: string): void;
110
+ }
111
+
112
+ declare namespace CommonBin {
113
+ export interface Helper {
114
+ /**
115
+ * fork child process, wrap with promise and gracefull exit
116
+ * @method helper#forkNode
117
+ * @param {String} modulePath - bin path
118
+ * @param {Array} [args] - arguments
119
+ * @param {Object} [options] - options
120
+ * @return {Promise} err or undefined
121
+ * @see https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options
122
+ */
123
+ forkNode(modulePath: string, args?: string[], options?: ForkOptions): Promise<void>;
124
+
125
+ /**
126
+ * spawn a new process, wrap with promise and gracefull exit
127
+ * @method helper#forkNode
128
+ * @param {String} cmd - command
129
+ * @param {Array} [args] - arguments
130
+ * @param {Object} [options] - options
131
+ * @return {Promise} err or undefined
132
+ * @see https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options
133
+ */
134
+ spawn(cmd: string, args?: string[], options?: SpawnOptions): Promise<void>;
135
+
136
+ /**
137
+ * exec npm install
138
+ * @method helper#npmInstall
139
+ * @param {String} npmCli - npm cli, such as `npm` / `cnpm` / `npminstall`
140
+ * @param {String} name - node module name
141
+ * @param {String} cwd - target directory
142
+ * @return {Promise} err or undefined
143
+ */
144
+ npmInstall(npmCli: string, name: string, cwd?: string): Promise<void>;
145
+
146
+ /**
147
+ * call fn
148
+ * @method helper#callFn
149
+ * @param {Function} fn - support generator / async / normal function return promise
150
+ * @param {Array} [args] - fn args
151
+ * @param {Object} [thisArg] - this
152
+ * @return {Object} result
153
+ */
154
+ callFn<T = any, U extends any[] = any[]>(fn: (...args: U) => IterableIterator<T> | Promise<T> | T, args?: U, thisArg?: any): IterableIterator<T>;
155
+
156
+ /**
157
+ * unparse argv and change it to array style
158
+ * @method helper#unparseArgv
159
+ * @param {Object} argv - yargs style
160
+ * @param {Object} [options] - options, see more at https://github.com/sindresorhus/dargs
161
+ * @param {Array} [options.includes] - keys or regex of keys to include
162
+ * @param {Array} [options.excludes] - keys or regex of keys to exclude
163
+ * @return {Array} [ '--debug=7000', '--debug-brk' ]
164
+ */
165
+ unparseArgv(argv: object, options?: dargs.Options): string[];
166
+
167
+ /**
168
+ * extract execArgv from argv
169
+ * @method helper#extractExecArgv
170
+ * @param {Object} argv - yargs style
171
+ * @return {Object} { debugPort, debugOptions: {}, execArgvObj: {} }
172
+ */
173
+ extractExecArgv(argv: object): { debugPort?: number; debugOptions?: PlainObject; execArgvObj: PlainObject };
174
+ }
175
+
176
+ export interface Context extends PlainObject {
177
+ cwd: string;
178
+ rawArgv: string[];
179
+ env: PlainObject;
180
+ argv: Arguments<PlainObject>;
181
+ execArgvObj: PlainObject;
182
+ readonly execArgv: string[];
183
+ debugPort?: number;
184
+ debugOptions?: PlainObject;
185
+ }
186
+ }
187
+
188
+ export = CommonBin;
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('./lib/command');
package/lib/command.js ADDED
@@ -0,0 +1,370 @@
1
+ 'use strict';
2
+
3
+ const debug = require('debug')('common-bin');
4
+ const co = require('co');
5
+ const yargs = require('yargs');
6
+ const parser = require('yargs-parser');
7
+ const helper = require('./helper');
8
+ const assert = require('assert');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const semver = require('semver');
12
+ const changeCase = require('change-case');
13
+ const chalk = require('chalk');
14
+
15
+ const DISPATCH = Symbol.for('eb:Command#dispatch');
16
+ const PARSE = Symbol('Command#parse');
17
+ const COMMANDS = Symbol('Command#commands');
18
+ const VERSION = Symbol('Command#version');
19
+
20
+ class CommonBin {
21
+ constructor(rawArgv) {
22
+ /**
23
+ * original argument
24
+ * @type {Array}
25
+ */
26
+ this.rawArgv = rawArgv || process.argv.slice(2);
27
+ debug('[%s] origin argument `%s`', this.constructor.name, this.rawArgv.join(' '));
28
+
29
+ /**
30
+ * yargs
31
+ * @type {Object}
32
+ */
33
+ this.yargs = yargs(this.rawArgv);
34
+
35
+ /**
36
+ * helper function
37
+ * @type {Object}
38
+ */
39
+ this.helper = helper;
40
+
41
+ /**
42
+ * parserOptions
43
+ * @type {Object}
44
+ * @property {Boolean} execArgv - whether extract `execArgv` to `context.execArgv`
45
+ * @property {Boolean} removeAlias - whether remove alias key from `argv`
46
+ * @property {Boolean} removeCamelCase - whether remove camel case key from `argv`
47
+ */
48
+ this.parserOptions = {
49
+ execArgv: false,
50
+ removeAlias: false,
51
+ removeCamelCase: false,
52
+ };
53
+
54
+ // <commandName, Command>
55
+ this[COMMANDS] = new Map();
56
+ }
57
+
58
+ /**
59
+ * command handler, could be generator / async function / normal function which return promise
60
+ * @param {Object} context - context object
61
+ * @param {String} context.cwd - process.cwd()
62
+ * @param {Object} context.argv - argv parse result by yargs, `{ _: [ 'start' ], '$0': '/usr/local/bin/common-bin', baseDir: 'simple'}`
63
+ * @param {Array} context.rawArgv - the raw argv, `[ "--baseDir=simple" ]`
64
+ * @protected
65
+ */
66
+ run() {
67
+ this.showHelp();
68
+ }
69
+
70
+ /**
71
+ * load sub commands
72
+ * @param {String} fullPath - the command directory
73
+ * @example `load(path.join(__dirname, 'command'))`
74
+ */
75
+ load(fullPath) {
76
+ assert(fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory(),
77
+ `${fullPath} should exist and be a directory`);
78
+
79
+ // load entire directory
80
+ const files = fs.readdirSync(fullPath);
81
+ const names = [];
82
+ for (const file of files) {
83
+ if (path.extname(file) === '.js') {
84
+ const name = path.basename(file).replace(/\.js$/, '');
85
+ names.push(name);
86
+ this.add(name, path.join(fullPath, file));
87
+ }
88
+ }
89
+
90
+ debug('[%s] loaded command `%s` from directory `%s`',
91
+ this.constructor.name, names, fullPath);
92
+ }
93
+
94
+ /**
95
+ * add sub command
96
+ * @param {String} name - a command name
97
+ * @param {String|Class} target - special file path (must contains ext) or Command Class
98
+ * @example `add('test', path.join(__dirname, 'test_command.js'))`
99
+ */
100
+ add(name, target) {
101
+ assert(name, `${name} is required`);
102
+ if (!(target.prototype instanceof CommonBin)) {
103
+ assert(fs.existsSync(target) && fs.statSync(target).isFile(), `${target} is not a file.`);
104
+ debug('[%s] add command `%s` from `%s`', this.constructor.name, name, target);
105
+ target = require(target);
106
+ // try to require es module
107
+ if (target && target.__esModule && target.default) {
108
+ target = target.default;
109
+ }
110
+ assert(target.prototype instanceof CommonBin,
111
+ 'command class should be sub class of common-bin');
112
+ }
113
+ this[COMMANDS].set(name, target);
114
+ }
115
+
116
+ /**
117
+ * alias an existing command
118
+ * @param {String} alias - alias command
119
+ * @param {String} name - exist command
120
+ */
121
+ alias(alias, name) {
122
+ assert(alias, 'alias command name is required');
123
+ assert(this[COMMANDS].has(name), `${name} should be added first`);
124
+ debug('[%s] set `%s` as alias of `%s`', this.constructor.name, alias, name);
125
+ this[COMMANDS].set(alias, this[COMMANDS].get(name));
126
+ }
127
+
128
+ /**
129
+ * start point of bin process
130
+ */
131
+ start() {
132
+ co(function* () {
133
+ // replace `--get-yargs-completions` to our KEY, so yargs will not block our DISPATCH
134
+ const index = this.rawArgv.indexOf('--get-yargs-completions');
135
+ if (index !== -1) {
136
+ // bash will request as `--get-yargs-completions my-git remote add`, so need to remove 2
137
+ this.rawArgv.splice(index, 2, `--AUTO_COMPLETIONS=${this.rawArgv.join(',')}`);
138
+ }
139
+ yield this[DISPATCH]();
140
+ }.bind(this)).catch(this.errorHandler.bind(this));
141
+ }
142
+
143
+ /**
144
+ * default error hander
145
+ * @param {Error} err - error object
146
+ * @protected
147
+ */
148
+ errorHandler(err) {
149
+ console.error(chalk.red(`⚠️ ${err.name}: ${err.message}`));
150
+ console.error(chalk.red('⚠️ Command Error, enable `DEBUG=common-bin` for detail'));
151
+ debug('args %s', process.argv.slice(3));
152
+ debug(err.stack);
153
+ process.exit(1);
154
+ }
155
+
156
+ /**
157
+ * print help message to console
158
+ * @param {String} [level=log] - console level
159
+ */
160
+ showHelp(level = 'log') {
161
+ this.yargs.showHelp(level);
162
+ }
163
+
164
+ /**
165
+ * shortcut for yargs.options
166
+ * @param {Object} opt - an object set to `yargs.options`
167
+ */
168
+ set options(opt) {
169
+ this.yargs.options(opt);
170
+ }
171
+
172
+ /**
173
+ * shortcut for yargs.usage
174
+ * @param {String} usage - usage info
175
+ */
176
+ set usage(usage) {
177
+ this.yargs.usage(usage);
178
+ }
179
+
180
+ set version(ver) {
181
+ this[VERSION] = ver;
182
+ }
183
+
184
+ get version() {
185
+ return this[VERSION];
186
+ }
187
+
188
+ /**
189
+ * instantiaze sub command
190
+ * @param {CommonBin} Clz - sub command class
191
+ * @param {Array} args - args
192
+ * @return {CommonBin} sub command instance
193
+ */
194
+ getSubCommandInstance(Clz, ...args) {
195
+ return new Clz(...args);
196
+ }
197
+
198
+ /**
199
+ * dispatch command, either `subCommand.exec` or `this.run`
200
+ * @param {Object} context - context object
201
+ * @param {String} context.cwd - process.cwd()
202
+ * @param {Object} context.argv - argv parse result by yargs, `{ _: [ 'start' ], '$0': '/usr/local/bin/common-bin', baseDir: 'simple'}`
203
+ * @param {Array} context.rawArgv - the raw argv, `[ "--baseDir=simple" ]`
204
+ * @private
205
+ */
206
+ * [DISPATCH]() {
207
+ // define --help and --version by default
208
+ this.yargs
209
+ // .reset()
210
+ .completion()
211
+ .help()
212
+ .version()
213
+ .wrap(120)
214
+ .alias('h', 'help')
215
+ .alias('v', 'version')
216
+ .group([ 'help', 'version' ], 'Global Options:');
217
+
218
+ // get parsed argument without handling helper and version
219
+ const parsed = yield this[PARSE](this.rawArgv);
220
+ const commandName = parsed._[0];
221
+
222
+ if (parsed.version && this.version) {
223
+ console.log(this.version);
224
+ return;
225
+ }
226
+
227
+ // if sub command exist
228
+ if (this[COMMANDS].has(commandName)) {
229
+ const Command = this[COMMANDS].get(commandName);
230
+ const rawArgv = this.rawArgv.slice();
231
+ rawArgv.splice(rawArgv.indexOf(commandName), 1);
232
+
233
+ debug('[%s] dispatch to subcommand `%s` -> `%s` with %j', this.constructor.name, commandName, Command.name, rawArgv);
234
+ const command = this.getSubCommandInstance(Command, rawArgv);
235
+ yield command[DISPATCH]();
236
+ return;
237
+ }
238
+
239
+ // register command for printing
240
+ for (const [ name, Command ] of this[COMMANDS].entries()) {
241
+ this.yargs.command(name, Command.prototype.description || '');
242
+ }
243
+
244
+ debug('[%s] exec run command', this.constructor.name);
245
+ const context = this.context;
246
+
247
+ // print completion for bash
248
+ if (context.argv.AUTO_COMPLETIONS) {
249
+ // slice to remove `--AUTO_COMPLETIONS=` which we append
250
+ this.yargs.getCompletion(this.rawArgv.slice(1), completions => {
251
+ // console.log('%s', completions)
252
+ completions.forEach(x => console.log(x));
253
+ });
254
+ } else {
255
+ // handle by self
256
+ yield this.helper.callFn(this.run, [ context ], this);
257
+ }
258
+ }
259
+
260
+ /**
261
+ * getter of context, default behavior is remove `help` / `h` / `version`
262
+ * @return {Object} context - { cwd, env, argv, rawArgv }
263
+ * @protected
264
+ */
265
+ get context() {
266
+ const argv = this.yargs.argv;
267
+ const context = {
268
+ argv,
269
+ cwd: process.cwd(),
270
+ env: Object.assign({}, process.env),
271
+ rawArgv: this.rawArgv,
272
+ };
273
+
274
+ argv.help = undefined;
275
+ argv.h = undefined;
276
+ argv.version = undefined;
277
+ argv.v = undefined;
278
+
279
+ // remove alias result
280
+ if (this.parserOptions.removeAlias) {
281
+ const aliases = this.yargs.getOptions().alias;
282
+ for (const key of Object.keys(aliases)) {
283
+ aliases[key].forEach(item => {
284
+ argv[item] = undefined;
285
+ });
286
+ }
287
+ }
288
+
289
+ // remove camel case result
290
+ if (this.parserOptions.removeCamelCase) {
291
+ for (const key of Object.keys(argv)) {
292
+ if (key.includes('-')) {
293
+ argv[changeCase.camel(key)] = undefined;
294
+ }
295
+ }
296
+ }
297
+
298
+ // extract execArgv
299
+ if (this.parserOptions.execArgv) {
300
+ // extract from command argv
301
+ let { debugPort, debugOptions, execArgvObj } = this.helper.extractExecArgv(argv);
302
+
303
+ // extract from WebStorm env `$NODE_DEBUG_OPTION`
304
+ // Notice: WebStorm 2019 won't export the env, instead, use `env.NODE_OPTIONS="--require="`, but we can't extract it.
305
+ if (context.env.NODE_DEBUG_OPTION) {
306
+ console.log('Use $NODE_DEBUG_OPTION: %s', context.env.NODE_DEBUG_OPTION);
307
+ const argvFromEnv = parser(context.env.NODE_DEBUG_OPTION);
308
+ const obj = this.helper.extractExecArgv(argvFromEnv);
309
+ debugPort = obj.debugPort || debugPort;
310
+ Object.assign(debugOptions, obj.debugOptions);
311
+ Object.assign(execArgvObj, obj.execArgvObj);
312
+ }
313
+
314
+ // `--expose_debug_as` is not supported by 7.x+
315
+ if (execArgvObj.expose_debug_as && semver.gte(process.version, '7.0.0')) {
316
+ console.warn(chalk.yellow(`Node.js runtime is ${process.version}, and inspector protocol is not support --expose_debug_as`));
317
+ }
318
+
319
+ // remove from origin argv
320
+ for (const key of Object.keys(execArgvObj)) {
321
+ argv[key] = undefined;
322
+ argv[changeCase.camel(key)] = undefined;
323
+ }
324
+
325
+ // exports execArgv
326
+ const self = this;
327
+ context.execArgvObj = execArgvObj;
328
+
329
+ // convert execArgvObj to execArgv
330
+ // `--require` should be `--require abc --require 123`, not allow `=`
331
+ // `--debug` should be `--debug=9999`, only allow `=`
332
+ Object.defineProperty(context, 'execArgv', {
333
+ get() {
334
+ const lazyExecArgvObj = context.execArgvObj;
335
+ const execArgv = self.helper.unparseArgv(lazyExecArgvObj, { excludes: [ 'require' ] });
336
+ // convert require to execArgv
337
+ let requireArgv = lazyExecArgvObj.require;
338
+ if (requireArgv) {
339
+ if (!Array.isArray(requireArgv)) requireArgv = [ requireArgv ];
340
+ requireArgv.forEach(item => {
341
+ execArgv.push('--require');
342
+ execArgv.push(item.startsWith('./') || item.startsWith('.\\') ? path.resolve(context.cwd, item) : item);
343
+ });
344
+ }
345
+ return execArgv;
346
+ },
347
+ });
348
+
349
+ // only exports debugPort when any match
350
+ if (Object.keys(debugOptions).length) {
351
+ context.debugPort = debugPort;
352
+ context.debugOptions = debugOptions;
353
+ }
354
+ }
355
+
356
+ return context;
357
+ }
358
+
359
+ [PARSE](rawArgv) {
360
+ return new Promise((resolve, reject) => {
361
+ this.yargs.parse(rawArgv, (err, argv) => {
362
+ /* istanbul ignore next */
363
+ if (err) return reject(err);
364
+ resolve(argv);
365
+ });
366
+ });
367
+ }
368
+ }
369
+
370
+ module.exports = CommonBin;
package/lib/helper.js ADDED
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ const debug = require('debug')('common-bin');
4
+ const cp = require('child_process');
5
+ const is = require('is-type-of');
6
+ const unparse = require('dargs');
7
+
8
+ // only hook once and only when ever start any child.
9
+ const childs = new Set();
10
+ let hadHook = false;
11
+ function gracefull(proc) {
12
+ // save child ref
13
+ childs.add(proc);
14
+
15
+ // only hook once
16
+ /* istanbul ignore else */
17
+ if (!hadHook) {
18
+ hadHook = true;
19
+ let signal;
20
+ [ 'SIGINT', 'SIGQUIT', 'SIGTERM' ].forEach(event => {
21
+ process.once(event, () => {
22
+ signal = event;
23
+ process.exit(0);
24
+ });
25
+ });
26
+
27
+ process.once('exit', () => {
28
+ // had test at my-helper.test.js, but coffee can't collect coverage info.
29
+ for (const child of childs) {
30
+ debug('kill child %s with %s', child.pid, signal);
31
+ child.kill(signal);
32
+ }
33
+ });
34
+ }
35
+ }
36
+
37
+ /**
38
+ * fork child process, wrap with promise and gracefull exit
39
+ * @function helper#forkNode
40
+ * @param {String} modulePath - bin path
41
+ * @param {Array} [args] - arguments
42
+ * @param {Object} [options] - options
43
+ * @return {Promise} err or undefined
44
+ * @see https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options
45
+ */
46
+ exports.forkNode = (modulePath, args = [], options = {}) => {
47
+ options.stdio = options.stdio || 'inherit';
48
+ debug('Run fork `%s %s %s`', process.execPath, modulePath, args.join(' '));
49
+ const proc = cp.fork(modulePath, args, options);
50
+ gracefull(proc);
51
+
52
+ const promise = new Promise((resolve, reject) => {
53
+ proc.once('exit', code => {
54
+ childs.delete(proc);
55
+ if (code !== 0) {
56
+ const err = new Error(modulePath + ' ' + args + ' exit with code ' + code);
57
+ err.code = code;
58
+ reject(err);
59
+ } else {
60
+ resolve();
61
+ }
62
+ });
63
+ });
64
+
65
+ promise.proc = proc;
66
+
67
+ return promise;
68
+ };
69
+
70
+ /**
71
+ * spawn a new process, wrap with promise and gracefull exit
72
+ * @function helper#forkNode
73
+ * @param {String} cmd - command
74
+ * @param {Array} [args] - arguments
75
+ * @param {Object} [options] - options
76
+ * @return {Promise} err or undefined
77
+ * @see https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options
78
+ */
79
+ exports.spawn = (cmd, args = [], options = {}) => {
80
+ options.stdio = options.stdio || 'inherit';
81
+ debug('Run spawn `%s %s`', cmd, args.join(' '));
82
+
83
+ return new Promise((resolve, reject) => {
84
+ const proc = cp.spawn(cmd, args, options);
85
+ gracefull(proc);
86
+ proc.once('error', err => {
87
+ /* istanbul ignore next */
88
+ reject(err);
89
+ });
90
+ proc.once('exit', code => {
91
+ childs.delete(proc);
92
+
93
+ if (code !== 0) {
94
+ return reject(new Error(`spawn ${cmd} ${args.join(' ')} fail, exit code: ${code}`));
95
+ }
96
+ resolve();
97
+ });
98
+ });
99
+ };
100
+
101
+ /**
102
+ * exec npm install
103
+ * @function helper#npmInstall
104
+ * @param {String} npmCli - npm cli, such as `npm` / `cnpm` / `npminstall`
105
+ * @param {String} name - node module name
106
+ * @param {String} cwd - target directory
107
+ * @return {Promise} err or undefined
108
+ */
109
+ exports.npmInstall = (npmCli, name, cwd) => {
110
+ const options = {
111
+ stdio: 'inherit',
112
+ env: process.env,
113
+ cwd,
114
+ };
115
+
116
+ const args = [ 'i', name ];
117
+ console.log('[common-bin] `%s %s` to %s ...', npmCli, args.join(' '), options.cwd);
118
+
119
+ return exports.spawn(npmCli, args, options);
120
+ };
121
+
122
+ /**
123
+ * call fn
124
+ * @function helper#callFn
125
+ * @param {Function} fn - support generator / async / normal function return promise
126
+ * @param {Array} [args] - fn args
127
+ * @param {Object} [thisArg] - this
128
+ * @return {Object} result
129
+ */
130
+ exports.callFn = function* (fn, args = [], thisArg) {
131
+ if (!is.function(fn)) return;
132
+ if (is.generatorFunction(fn)) {
133
+ return yield fn.apply(thisArg, args);
134
+ }
135
+ const r = fn.apply(thisArg, args);
136
+ if (is.promise(r)) {
137
+ return yield r;
138
+ }
139
+ return r;
140
+ };
141
+
142
+ /**
143
+ * unparse argv and change it to array style
144
+ * @function helper#unparseArgv
145
+ * @param {Object} argv - yargs style
146
+ * @param {Object} [options] - options, see more at https://github.com/sindresorhus/dargs
147
+ * @param {Array} [options.includes] - keys or regex of keys to include
148
+ * @param {Array} [options.excludes] - keys or regex of keys to exclude
149
+ * @return {Array} [ '--debug=7000', '--debug-brk' ]
150
+ */
151
+ exports.unparseArgv = (argv, options = {}) => {
152
+ // revert argv object to array
153
+ // yargs will paser `debug-brk` to `debug-brk` and `debugBrk`, so we need to filter
154
+ return [ ...new Set(unparse(argv, options)) ];
155
+ };
156
+
157
+ /**
158
+ * extract execArgv from argv
159
+ * @function helper#extractExecArgv
160
+ * @param {Object} argv - yargs style
161
+ * @return {Object} { debugPort, debugOptions: {}, execArgvObj: {} }
162
+ */
163
+ exports.extractExecArgv = argv => {
164
+ const debugOptions = {};
165
+ const execArgvObj = {};
166
+ let debugPort;
167
+
168
+ for (const key of Object.keys(argv)) {
169
+ const value = argv[key];
170
+ // skip undefined set uppon (camel etc.)
171
+ if (value === undefined) continue;
172
+ // debug / debug-brk / debug-port / inspect / inspect-brk / inspect-port
173
+ if ([ 'debug', 'debug-brk', 'debug-port', 'inspect', 'inspect-brk', 'inspect-port' ].includes(key)) {
174
+ if (typeof value === 'number') debugPort = value;
175
+ debugOptions[key] = argv[key];
176
+ execArgvObj[key] = argv[key];
177
+ } else if (match(key, [ 'es_staging', 'expose_debug_as', /^harmony.*/ ])) {
178
+ execArgvObj[key] = argv[key];
179
+ } else if (key.startsWith('node-options--')) {
180
+ // support node options, like: commond --node-options--trace-warnings => execArgv.push('--trace-warnings')
181
+ execArgvObj[key.replace('node-options--', '')] = argv[key];
182
+ }
183
+ }
184
+ return { debugPort, debugOptions, execArgvObj };
185
+ };
186
+
187
+ function match(key, arr) {
188
+ return arr.some(x => x instanceof RegExp ? x.test(key) : x === key); // eslint-disable-line no-confusing-arrow
189
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@zhennann/common-bin",
3
+ "version": "2.9.2",
4
+ "description": "Abstraction bin tool",
5
+ "main": "index.js",
6
+ "dependencies": {
7
+ "@types/dargs": "^5.1.0",
8
+ "@types/node": "^10.12.18",
9
+ "@types/yargs": "^12.0.4",
10
+ "chalk": "^2.4.1",
11
+ "change-case": "^3.0.2",
12
+ "co": "^4.6.0",
13
+ "dargs": "^6.0.0",
14
+ "debug": "^4.1.0",
15
+ "is-type-of": "^1.2.1",
16
+ "semver": "^5.5.1",
17
+ "yargs": "^13.3.0",
18
+ "yargs-parser": "^13.1.2"
19
+ },
20
+ "devDependencies": {
21
+ "autod": "^3.0.1",
22
+ "coffee": "^5.1.0",
23
+ "egg-bin": "^4.17.0",
24
+ "egg-ci": "^1.19.0",
25
+ "eslint": "^5.6.1",
26
+ "eslint-config-egg": "^7.1.0",
27
+ "git-contributor": "^1.0.10",
28
+ "mm": "^2.4.1",
29
+ "rimraf": "^2.6.2",
30
+ "typescript": "^3.2.2"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/node-modules/common-bin.git"
35
+ },
36
+ "homepage": "https://github.com/node-modules/common-bin",
37
+ "author": "fengmk2 <fengmk2@gmail.com> (https://github.com/fengmk2)",
38
+ "license": "MIT",
39
+ "scripts": {
40
+ "contributor": "git-contributor",
41
+ "autod": "autod",
42
+ "clean": "rimraf coverage",
43
+ "lint": "eslint .",
44
+ "test": "npm run lint -- --fix && npm run test-local",
45
+ "test-local": "egg-bin test",
46
+ "cov": "egg-bin cov",
47
+ "ci": "npm run clean && npm run lint && egg-bin cov"
48
+ },
49
+ "engines": {
50
+ "node": ">= 6.0.0"
51
+ },
52
+ "files": [
53
+ "lib",
54
+ "index.d.ts",
55
+ "index.js"
56
+ ],
57
+ "types": "index.d.ts",
58
+ "ci": {
59
+ "version": "8, 10, 12, 14, 16",
60
+ "type": "github",
61
+ "license": {
62
+ "year": "2017",
63
+ "fullname": "node-modules and other contributors"
64
+ }
65
+ }
66
+ }