├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── build ├── at-rule.js ├── cookie-store-api.js ├── es.array.for-each.js ├── es.date.to-string.js ├── filterLimit.js ├── gyroscope.js ├── jpeg2000.js ├── lib │ ├── index.js │ └── string-utils.js ├── lua.json ├── meterRole.js ├── parseHeaders.js ├── passive-event-listener.js ├── pointer-events.js ├── register.lua ├── string-padding.js ├── sxg.js └── xhr2.js ├── index.mjs ├── lib ├── arg.js ├── cmd.js ├── coaobject.js ├── coaparam.js ├── completion.js ├── completion.sh ├── index.js ├── opt.js └── shell.js └── package.json /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [7.0.4](https://www.github.com/yargs/cliui/compare/v7.0.3...v7.0.4) (2020-11-08) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * **deno:** import UIOptions from definitions ([#97](https://www.github.com/yargs/cliui/issues/97)) ([f04f343](https://www.github.com/yargs/cliui/commit/f04f3439bc78114c7e90f82ff56f5acf16268ea8)) 11 | 12 | ### [7.0.3](https://www.github.com/yargs/cliui/compare/v7.0.2...v7.0.3) (2020-10-16) 13 | 14 | 15 | ### Bug Fixes 16 | 17 | * **exports:** node 13.0 and 13.1 require the dotted object form _with_ a string fallback ([#93](https://www.github.com/yargs/cliui/issues/93)) ([eca16fc](https://www.github.com/yargs/cliui/commit/eca16fc05d26255df3280906c36d7f0e5b05c6e9)) 18 | 19 | ### [7.0.2](https://www.github.com/yargs/cliui/compare/v7.0.1...v7.0.2) (2020-10-14) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * **exports:** node 13.0-13.6 require a string fallback ([#91](https://www.github.com/yargs/cliui/issues/91)) ([b529d7e](https://www.github.com/yargs/cliui/commit/b529d7e432901af1af7848b23ed6cf634497d961)) 25 | 26 | ### [7.0.1](https://www.github.com/yargs/cliui/compare/v7.0.0...v7.0.1) (2020-08-16) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * **build:** main should be build/index.cjs ([dc29a3c](https://www.github.com/yargs/cliui/commit/dc29a3cc617a410aa850e06337b5954b04f2cb4d)) 32 | 33 | ## [7.0.0](https://www.github.com/yargs/cliui/compare/v6.0.0...v7.0.0) (2020-08-16) 34 | 35 | 36 | ### ⚠ BREAKING CHANGES 37 | 38 | * tsc/ESM/Deno support (#82) 39 | * modernize deps and build (#80) 40 | 41 | ### Build System 42 | 43 | * modernize deps and build ([#80](https://www.github.com/yargs/cliui/issues/80)) ([339d08d](https://www.github.com/yargs/cliui/commit/339d08dc71b15a3928aeab09042af94db2f43743)) 44 | 45 | 46 | ### Code Refactoring 47 | 48 | * tsc/ESM/Deno support ([#82](https://www.github.com/yargs/cliui/issues/82)) ([4b777a5](https://www.github.com/yargs/cliui/commit/4b777a5fe01c5d8958c6708695d6aab7dbe5706c)) 49 | 50 | ## [6.0.0](https://www.github.com/yargs/cliui/compare/v5.0.0...v6.0.0) (2019-11-10) 51 | 52 | 53 | ### ⚠ BREAKING CHANGES 54 | 55 | * update deps, drop Node 6 56 | 57 | ### Code Refactoring 58 | 59 | * update deps, drop Node 6 ([62056df](https://www.github.com/yargs/cliui/commit/62056df)) 60 | 61 | ## [5.0.0](https://github.com/yargs/cliui/compare/v4.1.0...v5.0.0) (2019-04-10) 62 | 63 | 64 | ### Bug Fixes 65 | 66 | * Update wrap-ansi to fix compatibility with latest versions of chalk. ([#60](https://github.com/yargs/cliui/issues/60)) ([7bf79ae](https://github.com/yargs/cliui/commit/7bf79ae)) 67 | 68 | 69 | ### BREAKING CHANGES 70 | 71 | * Drop support for node < 6. 72 | 73 | 74 | 75 | 76 | ## [4.1.0](https://github.com/yargs/cliui/compare/v4.0.0...v4.1.0) (2018-04-23) 77 | 78 | 79 | ### Features 80 | 81 | * add resetOutput method ([#57](https://github.com/yargs/cliui/issues/57)) ([7246902](https://github.com/yargs/cliui/commit/7246902)) 82 | 83 | 84 | 85 | 86 | ## [4.0.0](https://github.com/yargs/cliui/compare/v3.2.0...v4.0.0) (2017-12-18) 87 | 88 | 89 | ### Bug Fixes 90 | 91 | * downgrades strip-ansi to version 3.0.1 ([#54](https://github.com/yargs/cliui/issues/54)) ([5764c46](https://github.com/yargs/cliui/commit/5764c46)) 92 | * set env variable FORCE_COLOR. ([#56](https://github.com/yargs/cliui/issues/56)) ([7350e36](https://github.com/yargs/cliui/commit/7350e36)) 93 | 94 | 95 | ### Chores 96 | 97 | * drop support for node < 4 ([#53](https://github.com/yargs/cliui/issues/53)) ([b105376](https://github.com/yargs/cliui/commit/b105376)) 98 | 99 | 100 | ### Features 101 | 102 | * add fallback for window width ([#45](https://github.com/yargs/cliui/issues/45)) ([d064922](https://github.com/yargs/cliui/commit/d064922)) 103 | 104 | 105 | ### BREAKING CHANGES 106 | 107 | * officially drop support for Node < 4 108 | 109 | 110 | 111 | 112 | ## [3.2.0](https://github.com/yargs/cliui/compare/v3.1.2...v3.2.0) (2016-04-11) 113 | 114 | 115 | ### Bug Fixes 116 | 117 | * reduces tarball size ([acc6c33](https://github.com/yargs/cliui/commit/acc6c33)) 118 | 119 | ### Features 120 | 121 | * adds standard-version for release management ([ff84e32](https://github.com/yargs/cliui/commit/ff84e32)) 122 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Contributors 2 | 3 | Permission to use, copy, modify, and/or distribute this software 4 | for any purpose with or without fee is hereby granted, provided 5 | that the above copyright notice and this permission notice 6 | appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES 10 | OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE 11 | LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES 12 | OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 13 | WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 14 | ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cliui 2 | 3 | ![ci](https://github.com/yargs/cliui/workflows/ci/badge.svg) 4 | [![NPM version](https://img.shields.io/npm/v/cliui.svg)](https://www.npmjs.com/package/cliui) 5 | [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org) 6 | ![nycrc config on GitHub](https://img.shields.io/nycrc/yargs/cliui) 7 | 8 | easily create complex multi-column command-line-interfaces. 9 | 10 | ## Example 11 | 12 | ```js 13 | const ui = require('cliui')() 14 | 15 | ui.div('Usage: $0 [command] [options]') 16 | 17 | ui.div({ 18 | text: 'Options:', 19 | padding: [2, 0, 1, 0] 20 | }) 21 | 22 | ui.div( 23 | { 24 | text: "-f, --file", 25 | width: 20, 26 | padding: [0, 4, 0, 4] 27 | }, 28 | { 29 | text: "the file to load." + 30 | chalk.green("(if this description is long it wraps).") 31 | , 32 | width: 20 33 | }, 34 | { 35 | text: chalk.red("[required]"), 36 | align: 'right' 37 | } 38 | ) 39 | 40 | console.log(ui.toString()) 41 | ``` 42 | 43 | ## Deno/ESM Support 44 | 45 | As of `v7` `cliui` supports [Deno](https://github.com/denoland/deno) and 46 | [ESM](https://nodejs.org/api/esm.html#esm_ecmascript_modules): 47 | 48 | ```typescript 49 | import cliui from "https://deno.land/x/cliui/deno.ts"; 50 | 51 | const ui = cliui({}) 52 | 53 | ui.div('Usage: $0 [command] [options]') 54 | 55 | ui.div({ 56 | text: 'Options:', 57 | padding: [2, 0, 1, 0] 58 | }) 59 | 60 | ui.div({ 61 | text: "-f, --file", 62 | width: 20, 63 | padding: [0, 4, 0, 4] 64 | }) 65 | 66 | console.log(ui.toString()) 67 | ``` 68 | 69 | 70 | 71 | ## Layout DSL 72 | 73 | cliui exposes a simple layout DSL: 74 | 75 | If you create a single `ui.div`, passing a string rather than an 76 | object: 77 | 78 | * `\n`: characters will be interpreted as new rows. 79 | * `\t`: characters will be interpreted as new columns. 80 | * `\s`: characters will be interpreted as padding. 81 | 82 | **as an example...** 83 | 84 | ```js 85 | var ui = require('./')({ 86 | width: 60 87 | }) 88 | 89 | ui.div( 90 | 'Usage: node ./bin/foo.js\n' + 91 | ' \t provide a regex\n' + 92 | ' \t provide a glob\t [required]' 93 | ) 94 | 95 | console.log(ui.toString()) 96 | ``` 97 | 98 | **will output:** 99 | 100 | ```shell 101 | Usage: node ./bin/foo.js 102 | provide a regex 103 | provide a glob [required] 104 | ``` 105 | 106 | ## Methods 107 | 108 | ```js 109 | cliui = require('cliui') 110 | ``` 111 | 112 | ### cliui({width: integer}) 113 | 114 | Specify the maximum width of the UI being generated. 115 | If no width is provided, cliui will try to get the current window's width and use it, and if that doesn't work, width will be set to `80`. 116 | 117 | ### cliui({wrap: boolean}) 118 | 119 | Enable or disable the wrapping of text in a column. 120 | 121 | ### cliui.div(column, column, column) 122 | 123 | Create a row with any number of columns, a column 124 | can either be a string, or an object with the following 125 | options: 126 | 127 | * **text:** some text to place in the column. 128 | * **width:** the width of a column. 129 | * **align:** alignment, `right` or `center`. 130 | * **padding:** `[top, right, bottom, left]`. 131 | * **border:** should a border be placed around the div? 132 | 133 | ### cliui.span(column, column, column) 134 | 135 | Similar to `div`, except the next row will be appended without 136 | a new line being created. 137 | 138 | ### cliui.resetOutput() 139 | 140 | Resets the UI elements of the current cliui instance, maintaining the values 141 | set for `width` and `wrap`. 142 | -------------------------------------------------------------------------------- /build/at-rule.js: -------------------------------------------------------------------------------- 1 | let Prefixer = require('./prefixer') 2 | 3 | class AtRule extends Prefixer { 4 | /** 5 | * Clone and add prefixes for at-rule 6 | */ 7 | add(rule, prefix) { 8 | let prefixed = prefix + rule.name 9 | 10 | let already = rule.parent.some( 11 | i => i.name === prefixed && i.params === rule.params 12 | ) 13 | if (already) { 14 | return undefined 15 | } 16 | 17 | let cloned = this.clone(rule, { name: prefixed }) 18 | return rule.parent.insertBefore(rule, cloned) 19 | } 20 | 21 | /** 22 | * Clone node with prefixes 23 | */ 24 | process(node) { 25 | let parent = this.parentPrefix(node) 26 | 27 | for (let prefix of this.prefixes) { 28 | if (!parent || parent === prefix) { 29 | this.add(node, prefix) 30 | } 31 | } 32 | } 33 | } 34 | 35 | module.exports = AtRule 36 | -------------------------------------------------------------------------------- /build/cookie-store-api.js: -------------------------------------------------------------------------------- 1 | module.exports={A:{A:{"2":"J D E F A B uB"},B:{"1":"W Z a b c d e f g h i j k X H","2":"C K L G M N O","194":"P Q R S T U V"},C:{"2":"0 1 2 3 4 5 6 7 8 9 vB jB I l J D E F A B C K L G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB kB QB lB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R mB S T U V W Z a b c d e f g h i j k X H nB wB xB"},D:{"1":"W Z a b c d e f g h i j k X H nB yB zB","2":"0 1 2 3 4 5 6 7 8 9 I l J D E F A B C K L G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB kB QB lB RB SB","194":"Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R S T U V"},E:{"2":"I l J D E F A B C K L G 0B oB 1B 2B 3B 4B pB hB iB 5B 6B 7B qB rB 8B"},F:{"1":"cB dB eB fB gB P Q R mB S T U V W","2":"0 1 2 3 4 5 6 7 8 9 F B C G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB 9B AC BC CC hB sB DC iB","194":"IB JB KB LB MB NB OB PB QB RB SB Y TB UB VB WB XB YB ZB aB bB"},G:{"2":"E oB EC tB FC GC HC IC JC KC LC MC NC OC PC QC RC SC TC UC VC WC XC qB rB"},H:{"2":"YC"},I:{"1":"H","2":"jB I ZC aC bC cC tB dC eC"},J:{"2":"D A"},K:{"1":"Y","2":"A B C hB sB iB"},L:{"1":"H"},M:{"2":"X"},N:{"2":"A B"},O:{"2":"fC"},P:{"1":"oC pC qC","2":"I gC hC iC jC kC pB lC mC nC"},Q:{"2":"rC"},R:{"2":"sC"},S:{"2":"tC"}},B:7,C:"Cookie Store API"}; 2 | -------------------------------------------------------------------------------- /build/es.array.for-each.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var $ = require('../internals/export'); 3 | var forEach = require('../internals/array-for-each'); 4 | 5 | // `Array.prototype.forEach` method 6 | // https://tc39.es/ecma262/#sec-array.prototype.foreach 7 | // eslint-disable-next-line es-x/no-array-prototype-foreach -- safe 8 | $({ target: 'Array', proto: true, forced: [].forEach != forEach }, { 9 | forEach: forEach 10 | }); 11 | -------------------------------------------------------------------------------- /build/es.date.to-string.js: -------------------------------------------------------------------------------- 1 | // empty 2 | -------------------------------------------------------------------------------- /build/filterLimit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _filter2 = require('./internal/filter.js'); 8 | 9 | var _filter3 = _interopRequireDefault(_filter2); 10 | 11 | var _eachOfLimit = require('./internal/eachOfLimit.js'); 12 | 13 | var _eachOfLimit2 = _interopRequireDefault(_eachOfLimit); 14 | 15 | var _awaitify = require('./internal/awaitify.js'); 16 | 17 | var _awaitify2 = _interopRequireDefault(_awaitify); 18 | 19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 20 | 21 | /** 22 | * The same as [`filter`]{@link module:Collections.filter} but runs a maximum of `limit` async operations at a 23 | * time. 24 | * 25 | * @name filterLimit 26 | * @static 27 | * @memberOf module:Collections 28 | * @method 29 | * @see [async.filter]{@link module:Collections.filter} 30 | * @alias selectLimit 31 | * @category Collection 32 | * @param {Array|Iterable|AsyncIterable|Object} coll - A collection to iterate over. 33 | * @param {number} limit - The maximum number of async operations at a time. 34 | * @param {Function} iteratee - A truth test to apply to each item in `coll`. 35 | * The `iteratee` is passed a `callback(err, truthValue)`, which must be called 36 | * with a boolean argument once it has completed. Invoked with (item, callback). 37 | * @param {Function} [callback] - A callback which is called after all the 38 | * `iteratee` functions have finished. Invoked with (err, results). 39 | * @returns {Promise} a promise, if no callback provided 40 | */ 41 | function filterLimit(coll, limit, iteratee, callback) { 42 | return (0, _filter3.default)((0, _eachOfLimit2.default)(limit), coll, iteratee, callback); 43 | } 44 | exports.default = (0, _awaitify2.default)(filterLimit, 4); 45 | module.exports = exports['default']; -------------------------------------------------------------------------------- /build/gyroscope.js: -------------------------------------------------------------------------------- 1 | module.exports={A:{A:{"2":"J D E F A B uB"},B:{"1":"P Q R S T U V W Z a b c d e f g h i j k X H","2":"C K L G M N O"},C:{"2":"0 1 2 3 4 5 6 7 8 9 vB jB I l J D E F A B C K L G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB kB QB lB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R mB S T U V W Z a b c d e f g h i j k X H nB wB xB"},D:{"1":"VB WB XB YB ZB aB bB cB dB eB fB gB P Q R S T U V W Z a b c d e f g h i j k X H nB yB zB","2":"0 1 2 3 4 5 6 7 8 9 I l J D E F A B C K L G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB","194":"PB kB QB lB RB SB Y TB UB"},E:{"2":"I l J D E F A B C K L G 0B oB 1B 2B 3B 4B pB hB iB 5B 6B 7B qB rB 8B"},F:{"1":"LB MB NB OB PB QB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R mB S T U V W","2":"0 1 2 3 4 5 6 7 8 9 F B C G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB 9B AC BC CC hB sB DC iB"},G:{"2":"E oB EC tB FC GC HC IC JC KC LC MC NC OC PC QC RC SC TC UC VC WC XC qB rB"},H:{"2":"YC"},I:{"1":"H","2":"jB I ZC aC bC cC tB dC eC"},J:{"2":"D A"},K:{"1":"Y","2":"A B C hB sB iB"},L:{"1":"H"},M:{"2":"X"},N:{"2":"A B"},O:{"2":"fC"},P:{"2":"I gC hC iC jC kC pB lC mC nC oC pC qC"},Q:{"2":"rC"},R:{"2":"sC"},S:{"2":"tC"}},B:4,C:"Gyroscope"}; 2 | -------------------------------------------------------------------------------- /build/jpeg2000.js: -------------------------------------------------------------------------------- 1 | module.exports={A:{A:{"2":"J D E F A B uB"},B:{"2":"C K L G M N O P Q R S T U V W Z a b c d e f g h i j k X H"},C:{"2":"0 1 2 3 4 5 6 7 8 9 vB jB I l J D E F A B C K L G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB kB QB lB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R mB S T U V W Z a b c d e f g h i j k X H nB wB xB"},D:{"2":"0 1 2 3 4 5 6 7 8 9 I l J D E F A B C K L G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB kB QB lB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R S T U V W Z a b c d e f g h i j k X H nB yB zB"},E:{"1":"J D E F A B C K L G 2B 3B 4B pB hB iB 5B 6B 7B qB rB 8B","2":"I 0B oB","129":"l 1B"},F:{"2":"0 1 2 3 4 5 6 7 8 9 F B C G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R mB S T U V W 9B AC BC CC hB sB DC iB"},G:{"1":"E FC GC HC IC JC KC LC MC NC OC PC QC RC SC TC UC VC WC XC qB rB","2":"oB EC tB"},H:{"2":"YC"},I:{"2":"jB I H ZC aC bC cC tB dC eC"},J:{"2":"D A"},K:{"2":"A B C Y hB sB iB"},L:{"2":"H"},M:{"2":"X"},N:{"2":"A B"},O:{"2":"fC"},P:{"2":"I gC hC iC jC kC pB lC mC nC oC pC qC"},Q:{"2":"rC"},R:{"2":"sC"},S:{"2":"tC"}},B:6,C:"JPEG 2000 image format"}; 2 | -------------------------------------------------------------------------------- /build/lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const align = { 3 | right: alignRight, 4 | center: alignCenter 5 | }; 6 | const top = 0; 7 | const right = 1; 8 | const bottom = 2; 9 | const left = 3; 10 | export class UI { 11 | constructor(opts) { 12 | var _a; 13 | this.width = opts.width; 14 | this.wrap = (_a = opts.wrap) !== null && _a !== void 0 ? _a : true; 15 | this.rows = []; 16 | } 17 | span(...args) { 18 | const cols = this.div(...args); 19 | cols.span = true; 20 | } 21 | resetOutput() { 22 | this.rows = []; 23 | } 24 | div(...args) { 25 | if (args.length === 0) { 26 | this.div(''); 27 | } 28 | if (this.wrap && this.shouldApplyLayoutDSL(...args) && typeof args[0] === 'string') { 29 | return this.applyLayoutDSL(args[0]); 30 | } 31 | const cols = args.map(arg => { 32 | if (typeof arg === 'string') { 33 | return this.colFromString(arg); 34 | } 35 | return arg; 36 | }); 37 | this.rows.push(cols); 38 | return cols; 39 | } 40 | shouldApplyLayoutDSL(...args) { 41 | return args.length === 1 && typeof args[0] === 'string' && 42 | /[\t\n]/.test(args[0]); 43 | } 44 | applyLayoutDSL(str) { 45 | const rows = str.split('\n').map(row => row.split('\t')); 46 | let leftColumnWidth = 0; 47 | // simple heuristic for layout, make sure the 48 | // second column lines up along the left-hand. 49 | // don't allow the first column to take up more 50 | // than 50% of the screen. 51 | rows.forEach(columns => { 52 | if (columns.length > 1 && mixin.stringWidth(columns[0]) > leftColumnWidth) { 53 | leftColumnWidth = Math.min(Math.floor(this.width * 0.5), mixin.stringWidth(columns[0])); 54 | } 55 | }); 56 | // generate a table: 57 | // replacing ' ' with padding calculations. 58 | // using the algorithmically generated width. 59 | rows.forEach(columns => { 60 | this.div(...columns.map((r, i) => { 61 | return { 62 | text: r.trim(), 63 | padding: this.measurePadding(r), 64 | width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined 65 | }; 66 | })); 67 | }); 68 | return this.rows[this.rows.length - 1]; 69 | } 70 | colFromString(text) { 71 | return { 72 | text, 73 | padding: this.measurePadding(text) 74 | }; 75 | } 76 | measurePadding(str) { 77 | // measure padding without ansi escape codes 78 | const noAnsi = mixin.stripAnsi(str); 79 | return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length]; 80 | } 81 | toString() { 82 | const lines = []; 83 | this.rows.forEach(row => { 84 | this.rowToString(row, lines); 85 | }); 86 | // don't display any lines with the 87 | // hidden flag set. 88 | return lines 89 | .filter(line => !line.hidden) 90 | .map(line => line.text) 91 | .join('\n'); 92 | } 93 | rowToString(row, lines) { 94 | this.rasterize(row).forEach((rrow, r) => { 95 | let str = ''; 96 | rrow.forEach((col, c) => { 97 | const { width } = row[c]; // the width with padding. 98 | const wrapWidth = this.negatePadding(row[c]); // the width without padding. 99 | let ts = col; // temporary string used during alignment/padding. 100 | if (wrapWidth > mixin.stringWidth(col)) { 101 | ts += ' '.repeat(wrapWidth - mixin.stringWidth(col)); 102 | } 103 | // align the string within its column. 104 | if (row[c].align && row[c].align !== 'left' && this.wrap) { 105 | const fn = align[row[c].align]; 106 | ts = fn(ts, wrapWidth); 107 | if (mixin.stringWidth(ts) < wrapWidth) { 108 | ts += ' '.repeat((width || 0) - mixin.stringWidth(ts) - 1); 109 | } 110 | } 111 | // apply border and padding to string. 112 | const padding = row[c].padding || [0, 0, 0, 0]; 113 | if (padding[left]) { 114 | str += ' '.repeat(padding[left]); 115 | } 116 | str += addBorder(row[c], ts, '| '); 117 | str += ts; 118 | str += addBorder(row[c], ts, ' |'); 119 | if (padding[right]) { 120 | str += ' '.repeat(padding[right]); 121 | } 122 | // if prior row is span, try to render the 123 | // current row on the prior line. 124 | if (r === 0 && lines.length > 0) { 125 | str = this.renderInline(str, lines[lines.length - 1]); 126 | } 127 | }); 128 | // remove trailing whitespace. 129 | lines.push({ 130 | text: str.replace(/ +$/, ''), 131 | span: row.span 132 | }); 133 | }); 134 | return lines; 135 | } 136 | // if the full 'source' can render in 137 | // the target line, do so. 138 | renderInline(source, previousLine) { 139 | const match = source.match(/^ */); 140 | const leadingWhitespace = match ? match[0].length : 0; 141 | const target = previousLine.text; 142 | const targetTextWidth = mixin.stringWidth(target.trimRight()); 143 | if (!previousLine.span) { 144 | return source; 145 | } 146 | // if we're not applying wrapping logic, 147 | // just always append to the span. 148 | if (!this.wrap) { 149 | previousLine.hidden = true; 150 | return target + source; 151 | } 152 | if (leadingWhitespace < targetTextWidth) { 153 | return source; 154 | } 155 | previousLine.hidden = true; 156 | return target.trimRight() + ' '.repeat(leadingWhitespace - targetTextWidth) + source.trimLeft(); 157 | } 158 | rasterize(row) { 159 | const rrows = []; 160 | const widths = this.columnWidths(row); 161 | let wrapped; 162 | // word wrap all columns, and create 163 | // a data-structure that is easy to rasterize. 164 | row.forEach((col, c) => { 165 | // leave room for left and right padding. 166 | col.width = widths[c]; 167 | if (this.wrap) { 168 | wrapped = mixin.wrap(col.text, this.negatePadding(col), { hard: true }).split('\n'); 169 | } 170 | else { 171 | wrapped = col.text.split('\n'); 172 | } 173 | if (col.border) { 174 | wrapped.unshift('.' + '-'.repeat(this.negatePadding(col) + 2) + '.'); 175 | wrapped.push("'" + '-'.repeat(this.negatePadding(col) + 2) + "'"); 176 | } 177 | // add top and bottom padding. 178 | if (col.padding) { 179 | wrapped.unshift(...new Array(col.padding[top] || 0).fill('')); 180 | wrapped.push(...new Array(col.padding[bottom] || 0).fill('')); 181 | } 182 | wrapped.forEach((str, r) => { 183 | if (!rrows[r]) { 184 | rrows.push([]); 185 | } 186 | const rrow = rrows[r]; 187 | for (let i = 0; i < c; i++) { 188 | if (rrow[i] === undefined) { 189 | rrow.push(''); 190 | } 191 | } 192 | rrow.push(str); 193 | }); 194 | }); 195 | return rrows; 196 | } 197 | negatePadding(col) { 198 | let wrapWidth = col.width || 0; 199 | if (col.padding) { 200 | wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0); 201 | } 202 | if (col.border) { 203 | wrapWidth -= 4; 204 | } 205 | return wrapWidth; 206 | } 207 | columnWidths(row) { 208 | if (!this.wrap) { 209 | return row.map(col => { 210 | return col.width || mixin.stringWidth(col.text); 211 | }); 212 | } 213 | let unset = row.length; 214 | let remainingWidth = this.width; 215 | // column widths can be set in config. 216 | const widths = row.map(col => { 217 | if (col.width) { 218 | unset--; 219 | remainingWidth -= col.width; 220 | return col.width; 221 | } 222 | return undefined; 223 | }); 224 | // any unset widths should be calculated. 225 | const unsetWidth = unset ? Math.floor(remainingWidth / unset) : 0; 226 | return widths.map((w, i) => { 227 | if (w === undefined) { 228 | return Math.max(unsetWidth, _minWidth(row[i])); 229 | } 230 | return w; 231 | }); 232 | } 233 | } 234 | function addBorder(col, ts, style) { 235 | if (col.border) { 236 | if (/[.']-+[.']/.test(ts)) { 237 | return ''; 238 | } 239 | if (ts.trim().length !== 0) { 240 | return style; 241 | } 242 | return ' '; 243 | } 244 | return ''; 245 | } 246 | // calculates the minimum width of 247 | // a column, based on padding preferences. 248 | function _minWidth(col) { 249 | const padding = col.padding || []; 250 | const minWidth = 1 + (padding[left] || 0) + (padding[right] || 0); 251 | if (col.border) { 252 | return minWidth + 4; 253 | } 254 | return minWidth; 255 | } 256 | function getWindowWidth() { 257 | /* istanbul ignore next: depends on terminal */ 258 | if (typeof process === 'object' && process.stdout && process.stdout.columns) { 259 | return process.stdout.columns; 260 | } 261 | return 80; 262 | } 263 | function alignRight(str, width) { 264 | str = str.trim(); 265 | const strWidth = mixin.stringWidth(str); 266 | if (strWidth < width) { 267 | return ' '.repeat(width - strWidth) + str; 268 | } 269 | return str; 270 | } 271 | function alignCenter(str, width) { 272 | str = str.trim(); 273 | const strWidth = mixin.stringWidth(str); 274 | /* istanbul ignore next */ 275 | if (strWidth >= width) { 276 | return str; 277 | } 278 | return ' '.repeat((width - strWidth) >> 1) + str; 279 | } 280 | let mixin; 281 | export function cliui(opts, _mixin) { 282 | mixin = _mixin; 283 | return new UI({ 284 | width: (opts === null || opts === void 0 ? void 0 : opts.width) || getWindowWidth(), 285 | wrap: opts === null || opts === void 0 ? void 0 : opts.wrap 286 | }); 287 | } 288 | -------------------------------------------------------------------------------- /build/lib/string-utils.js: -------------------------------------------------------------------------------- 1 | // Minimal replacement for ansi string helpers "wrap-ansi" and "strip-ansi". 2 | // to facilitate ESM and Deno modules. 3 | // TODO: look at porting https://www.npmjs.com/package/wrap-ansi to ESM. 4 | // The npm application 5 | // Copyright (c) npm, Inc. and Contributors 6 | // Licensed on the terms of The Artistic License 2.0 7 | // See: https://github.com/npm/cli/blob/4c65cd952bc8627811735bea76b9b110cc4fc80e/lib/utils/ansi-trim.js 8 | const ansi = new RegExp('\x1b(?:\\[(?:\\d+[ABCDEFGJKSTm]|\\d+;\\d+[Hfm]|' + 9 | '\\d+;\\d+;\\d+m|6n|s|u|\\?25[lh])|\\w)', 'g'); 10 | export function stripAnsi(str) { 11 | return str.replace(ansi, ''); 12 | } 13 | export function wrap(str, width) { 14 | const [start, end] = str.match(ansi) || ['', '']; 15 | str = stripAnsi(str); 16 | let wrapped = ''; 17 | for (let i = 0; i < str.length; i++) { 18 | if (i !== 0 && (i % width) === 0) { 19 | wrapped += '\n'; 20 | } 21 | wrapped += str.charAt(i); 22 | } 23 | if (start && end) { 24 | wrapped = `${start}${wrapped}${end}`; 25 | } 26 | return wrapped; 27 | } 28 | -------------------------------------------------------------------------------- /build/lua.json: -------------------------------------------------------------------------------- 1 | { 2 | "blacklist_client.lua": "local blacklist = ARGV[num_static_argv + 1]\n\nif redis.call('zscore', client_last_seen_key, blacklist) then\n redis.call('zadd', client_last_seen_key, 0, blacklist)\nend\n\n\nreturn {}\n", 3 | "check.lua": "local weight = tonumber(ARGV[num_static_argv + 1])\n\nlocal capacity = process_tick(now, false)['capacity']\nlocal nextRequest = tonumber(redis.call('hget', settings_key, 'nextRequest'))\n\nreturn conditions_check(capacity, weight) and nextRequest - now <= 0\n", 4 | "conditions_check.lua": "local conditions_check = function (capacity, weight)\n return capacity == nil or weight <= capacity\nend\n", 5 | "current_reservoir.lua": "return process_tick(now, false)['reservoir']\n", 6 | "done.lua": "process_tick(now, false)\n\nreturn tonumber(redis.call('hget', settings_key, 'done'))\n", 7 | "free.lua": "local index = ARGV[num_static_argv + 1]\n\nredis.call('zadd', job_expirations_key, 0, index)\n\nreturn process_tick(now, false)['running']\n", 8 | "get_time.lua": "redis.replicate_commands()\n\nlocal get_time = function ()\n local time = redis.call('time')\n\n return tonumber(time[1]..string.sub(time[2], 1, 3))\nend\n", 9 | "group_check.lua": "return not (redis.call('exists', settings_key) == 1)\n", 10 | "heartbeat.lua": "process_tick(now, true)\n", 11 | "increment_reservoir.lua": "local incr = tonumber(ARGV[num_static_argv + 1])\n\nredis.call('hincrby', settings_key, 'reservoir', incr)\n\nlocal reservoir = process_tick(now, true)['reservoir']\n\nlocal groupTimeout = tonumber(redis.call('hget', settings_key, 'groupTimeout'))\nrefresh_expiration(0, 0, groupTimeout)\n\nreturn reservoir\n", 12 | "init.lua": "local clear = tonumber(ARGV[num_static_argv + 1])\nlocal limiter_version = ARGV[num_static_argv + 2]\nlocal num_local_argv = num_static_argv + 2\n\nif clear == 1 then\n redis.call('del', unpack(KEYS))\nend\n\nif redis.call('exists', settings_key) == 0 then\n -- Create\n local args = {'hmset', settings_key}\n\n for i = num_local_argv + 1, #ARGV do\n table.insert(args, ARGV[i])\n end\n\n redis.call(unpack(args))\n redis.call('hmset', settings_key,\n 'nextRequest', now,\n 'lastReservoirRefresh', now,\n 'lastReservoirIncrease', now,\n 'running', 0,\n 'done', 0,\n 'unblockTime', 0,\n 'capacityPriorityCounter', 0\n )\n\nelse\n -- Apply migrations\n local settings = redis.call('hmget', settings_key,\n 'id',\n 'version'\n )\n local id = settings[1]\n local current_version = settings[2]\n\n if current_version ~= limiter_version then\n local version_digits = {}\n for k, v in string.gmatch(current_version, \"([^.]+)\") do\n table.insert(version_digits, tonumber(k))\n end\n\n -- 2.10.0\n if version_digits[2] < 10 then\n redis.call('hsetnx', settings_key, 'reservoirRefreshInterval', '')\n redis.call('hsetnx', settings_key, 'reservoirRefreshAmount', '')\n redis.call('hsetnx', settings_key, 'lastReservoirRefresh', '')\n redis.call('hsetnx', settings_key, 'done', 0)\n redis.call('hset', settings_key, 'version', '2.10.0')\n end\n\n -- 2.11.1\n if version_digits[2] < 11 or (version_digits[2] == 11 and version_digits[3] < 1) then\n if redis.call('hstrlen', settings_key, 'lastReservoirRefresh') == 0 then\n redis.call('hmset', settings_key,\n 'lastReservoirRefresh', now,\n 'version', '2.11.1'\n )\n end\n end\n\n -- 2.14.0\n if version_digits[2] < 14 then\n local old_running_key = 'b_'..id..'_running'\n local old_executing_key = 'b_'..id..'_executing'\n\n if redis.call('exists', old_running_key) == 1 then\n redis.call('rename', old_running_key, job_weights_key)\n end\n if redis.call('exists', old_executing_key) == 1 then\n redis.call('rename', old_executing_key, job_expirations_key)\n end\n redis.call('hset', settings_key, 'version', '2.14.0')\n end\n\n -- 2.15.2\n if version_digits[2] < 15 or (version_digits[2] == 15 and version_digits[3] < 2) then\n redis.call('hsetnx', settings_key, 'capacityPriorityCounter', 0)\n redis.call('hset', settings_key, 'version', '2.15.2')\n end\n\n -- 2.17.0\n if version_digits[2] < 17 then\n redis.call('hsetnx', settings_key, 'clientTimeout', 10000)\n redis.call('hset', settings_key, 'version', '2.17.0')\n end\n\n -- 2.18.0\n if version_digits[2] < 18 then\n redis.call('hsetnx', settings_key, 'reservoirIncreaseInterval', '')\n redis.call('hsetnx', settings_key, 'reservoirIncreaseAmount', '')\n redis.call('hsetnx', settings_key, 'reservoirIncreaseMaximum', '')\n redis.call('hsetnx', settings_key, 'lastReservoirIncrease', now)\n redis.call('hset', settings_key, 'version', '2.18.0')\n end\n\n end\n\n process_tick(now, false)\nend\n\nlocal groupTimeout = tonumber(redis.call('hget', settings_key, 'groupTimeout'))\nrefresh_expiration(0, 0, groupTimeout)\n\nreturn {}\n", 13 | "process_tick.lua": "local process_tick = function (now, always_publish)\n\n local compute_capacity = function (maxConcurrent, running, reservoir)\n if maxConcurrent ~= nil and reservoir ~= nil then\n return math.min((maxConcurrent - running), reservoir)\n elseif maxConcurrent ~= nil then\n return maxConcurrent - running\n elseif reservoir ~= nil then\n return reservoir\n else\n return nil\n end\n end\n\n local settings = redis.call('hmget', settings_key,\n 'id',\n 'maxConcurrent',\n 'running',\n 'reservoir',\n 'reservoirRefreshInterval',\n 'reservoirRefreshAmount',\n 'lastReservoirRefresh',\n 'reservoirIncreaseInterval',\n 'reservoirIncreaseAmount',\n 'reservoirIncreaseMaximum',\n 'lastReservoirIncrease',\n 'capacityPriorityCounter',\n 'clientTimeout'\n )\n local id = settings[1]\n local maxConcurrent = tonumber(settings[2])\n local running = tonumber(settings[3])\n local reservoir = tonumber(settings[4])\n local reservoirRefreshInterval = tonumber(settings[5])\n local reservoirRefreshAmount = tonumber(settings[6])\n local lastReservoirRefresh = tonumber(settings[7])\n local reservoirIncreaseInterval = tonumber(settings[8])\n local reservoirIncreaseAmount = tonumber(settings[9])\n local reservoirIncreaseMaximum = tonumber(settings[10])\n local lastReservoirIncrease = tonumber(settings[11])\n local capacityPriorityCounter = tonumber(settings[12])\n local clientTimeout = tonumber(settings[13])\n\n local initial_capacity = compute_capacity(maxConcurrent, running, reservoir)\n\n --\n -- Process 'running' changes\n --\n local expired = redis.call('zrangebyscore', job_expirations_key, '-inf', '('..now)\n\n if #expired > 0 then\n redis.call('zremrangebyscore', job_expirations_key, '-inf', '('..now)\n\n local flush_batch = function (batch, acc)\n local weights = redis.call('hmget', job_weights_key, unpack(batch))\n redis.call('hdel', job_weights_key, unpack(batch))\n local clients = redis.call('hmget', job_clients_key, unpack(batch))\n redis.call('hdel', job_clients_key, unpack(batch))\n\n -- Calculate sum of removed weights\n for i = 1, #weights do\n acc['total'] = acc['total'] + (tonumber(weights[i]) or 0)\n end\n\n -- Calculate sum of removed weights by client\n local client_weights = {}\n for i = 1, #clients do\n local removed = tonumber(weights[i]) or 0\n if removed > 0 then\n acc['client_weights'][clients[i]] = (acc['client_weights'][clients[i]] or 0) + removed\n end\n end\n end\n\n local acc = {\n ['total'] = 0,\n ['client_weights'] = {}\n }\n local batch_size = 1000\n\n -- Compute changes to Zsets and apply changes to Hashes\n for i = 1, #expired, batch_size do\n local batch = {}\n for j = i, math.min(i + batch_size - 1, #expired) do\n table.insert(batch, expired[j])\n end\n\n flush_batch(batch, acc)\n end\n\n -- Apply changes to Zsets\n if acc['total'] > 0 then\n redis.call('hincrby', settings_key, 'done', acc['total'])\n running = tonumber(redis.call('hincrby', settings_key, 'running', -acc['total']))\n end\n\n for client, weight in pairs(acc['client_weights']) do\n redis.call('zincrby', client_running_key, -weight, client)\n end\n end\n\n --\n -- Process 'reservoir' changes\n --\n local reservoirRefreshActive = reservoirRefreshInterval ~= nil and reservoirRefreshAmount ~= nil\n if reservoirRefreshActive and now >= lastReservoirRefresh + reservoirRefreshInterval then\n reservoir = reservoirRefreshAmount\n redis.call('hmset', settings_key,\n 'reservoir', reservoir,\n 'lastReservoirRefresh', now\n )\n end\n\n local reservoirIncreaseActive = reservoirIncreaseInterval ~= nil and reservoirIncreaseAmount ~= nil\n if reservoirIncreaseActive and now >= lastReservoirIncrease + reservoirIncreaseInterval then\n local num_intervals = math.floor((now - lastReservoirIncrease) / reservoirIncreaseInterval)\n local incr = reservoirIncreaseAmount * num_intervals\n if reservoirIncreaseMaximum ~= nil then\n incr = math.min(incr, reservoirIncreaseMaximum - (reservoir or 0))\n end\n if incr > 0 then\n reservoir = (reservoir or 0) + incr\n end\n redis.call('hmset', settings_key,\n 'reservoir', reservoir,\n 'lastReservoirIncrease', lastReservoirIncrease + (num_intervals * reservoirIncreaseInterval)\n )\n end\n\n --\n -- Clear unresponsive clients\n --\n local unresponsive = redis.call('zrangebyscore', client_last_seen_key, '-inf', (now - clientTimeout))\n local unresponsive_lookup = {}\n local terminated_clients = {}\n for i = 1, #unresponsive do\n unresponsive_lookup[unresponsive[i]] = true\n if tonumber(redis.call('zscore', client_running_key, unresponsive[i])) == 0 then\n table.insert(terminated_clients, unresponsive[i])\n end\n end\n if #terminated_clients > 0 then\n redis.call('zrem', client_running_key, unpack(terminated_clients))\n redis.call('hdel', client_num_queued_key, unpack(terminated_clients))\n redis.call('zrem', client_last_registered_key, unpack(terminated_clients))\n redis.call('zrem', client_last_seen_key, unpack(terminated_clients))\n end\n\n --\n -- Broadcast capacity changes\n --\n local final_capacity = compute_capacity(maxConcurrent, running, reservoir)\n\n if always_publish or (initial_capacity ~= nil and final_capacity == nil) then\n -- always_publish or was not unlimited, now unlimited\n redis.call('publish', 'b_'..id, 'capacity:'..(final_capacity or ''))\n\n elseif initial_capacity ~= nil and final_capacity ~= nil and final_capacity > initial_capacity then\n -- capacity was increased\n -- send the capacity message to the limiter having the lowest number of running jobs\n -- the tiebreaker is the limiter having not registered a job in the longest time\n\n local lowest_concurrency_value = nil\n local lowest_concurrency_clients = {}\n local lowest_concurrency_last_registered = {}\n local client_concurrencies = redis.call('zrange', client_running_key, 0, -1, 'withscores')\n\n for i = 1, #client_concurrencies, 2 do\n local client = client_concurrencies[i]\n local concurrency = tonumber(client_concurrencies[i+1])\n\n if (\n lowest_concurrency_value == nil or lowest_concurrency_value == concurrency\n ) and (\n not unresponsive_lookup[client]\n ) and (\n tonumber(redis.call('hget', client_num_queued_key, client)) > 0\n ) then\n lowest_concurrency_value = concurrency\n table.insert(lowest_concurrency_clients, client)\n local last_registered = tonumber(redis.call('zscore', client_last_registered_key, client))\n table.insert(lowest_concurrency_last_registered, last_registered)\n end\n end\n\n if #lowest_concurrency_clients > 0 then\n local position = 1\n local earliest = lowest_concurrency_last_registered[1]\n\n for i,v in ipairs(lowest_concurrency_last_registered) do\n if v < earliest then\n position = i\n earliest = v\n end\n end\n\n local next_client = lowest_concurrency_clients[position]\n redis.call('publish', 'b_'..id,\n 'capacity-priority:'..(final_capacity or '')..\n ':'..next_client..\n ':'..capacityPriorityCounter\n )\n redis.call('hincrby', settings_key, 'capacityPriorityCounter', '1')\n else\n redis.call('publish', 'b_'..id, 'capacity:'..(final_capacity or ''))\n end\n end\n\n return {\n ['capacity'] = final_capacity,\n ['running'] = running,\n ['reservoir'] = reservoir\n }\nend\n", 14 | "queued.lua": "local clientTimeout = tonumber(redis.call('hget', settings_key, 'clientTimeout'))\nlocal valid_clients = redis.call('zrangebyscore', client_last_seen_key, (now - clientTimeout), 'inf')\nlocal client_queued = redis.call('hmget', client_num_queued_key, unpack(valid_clients))\n\nlocal sum = 0\nfor i = 1, #client_queued do\n sum = sum + tonumber(client_queued[i])\nend\n\nreturn sum\n", 15 | "refresh_expiration.lua": "local refresh_expiration = function (now, nextRequest, groupTimeout)\n\n if groupTimeout ~= nil then\n local ttl = (nextRequest + groupTimeout) - now\n\n for i = 1, #KEYS do\n redis.call('pexpire', KEYS[i], ttl)\n end\n end\n\nend\n", 16 | "refs.lua": "local settings_key = KEYS[1]\nlocal job_weights_key = KEYS[2]\nlocal job_expirations_key = KEYS[3]\nlocal job_clients_key = KEYS[4]\nlocal client_running_key = KEYS[5]\nlocal client_num_queued_key = KEYS[6]\nlocal client_last_registered_key = KEYS[7]\nlocal client_last_seen_key = KEYS[8]\n\nlocal now = tonumber(ARGV[1])\nlocal client = ARGV[2]\n\nlocal num_static_argv = 2\n", 17 | "register.lua": "local index = ARGV[num_static_argv + 1]\nlocal weight = tonumber(ARGV[num_static_argv + 2])\nlocal expiration = tonumber(ARGV[num_static_argv + 3])\n\nlocal state = process_tick(now, false)\nlocal capacity = state['capacity']\nlocal reservoir = state['reservoir']\n\nlocal settings = redis.call('hmget', settings_key,\n 'nextRequest',\n 'minTime',\n 'groupTimeout'\n)\nlocal nextRequest = tonumber(settings[1])\nlocal minTime = tonumber(settings[2])\nlocal groupTimeout = tonumber(settings[3])\n\nif conditions_check(capacity, weight) then\n\n redis.call('hincrby', settings_key, 'running', weight)\n redis.call('hset', job_weights_key, index, weight)\n if expiration ~= nil then\n redis.call('zadd', job_expirations_key, now + expiration, index)\n end\n redis.call('hset', job_clients_key, index, client)\n redis.call('zincrby', client_running_key, weight, client)\n redis.call('hincrby', client_num_queued_key, client, -1)\n redis.call('zadd', client_last_registered_key, now, client)\n\n local wait = math.max(nextRequest - now, 0)\n local newNextRequest = now + wait + minTime\n\n if reservoir == nil then\n redis.call('hset', settings_key,\n 'nextRequest', newNextRequest\n )\n else\n reservoir = reservoir - weight\n redis.call('hmset', settings_key,\n 'reservoir', reservoir,\n 'nextRequest', newNextRequest\n )\n end\n\n refresh_expiration(now, newNextRequest, groupTimeout)\n\n return {true, wait, reservoir}\n\nelse\n return {false}\nend\n", 18 | "register_client.lua": "local queued = tonumber(ARGV[num_static_argv + 1])\n\n-- Could have been re-registered concurrently\nif not redis.call('zscore', client_last_seen_key, client) then\n redis.call('zadd', client_running_key, 0, client)\n redis.call('hset', client_num_queued_key, client, queued)\n redis.call('zadd', client_last_registered_key, 0, client)\nend\n\nredis.call('zadd', client_last_seen_key, now, client)\n\nreturn {}\n", 19 | "running.lua": "return process_tick(now, false)['running']\n", 20 | "submit.lua": "local queueLength = tonumber(ARGV[num_static_argv + 1])\nlocal weight = tonumber(ARGV[num_static_argv + 2])\n\nlocal capacity = process_tick(now, false)['capacity']\n\nlocal settings = redis.call('hmget', settings_key,\n 'id',\n 'maxConcurrent',\n 'highWater',\n 'nextRequest',\n 'strategy',\n 'unblockTime',\n 'penalty',\n 'minTime',\n 'groupTimeout'\n)\nlocal id = settings[1]\nlocal maxConcurrent = tonumber(settings[2])\nlocal highWater = tonumber(settings[3])\nlocal nextRequest = tonumber(settings[4])\nlocal strategy = tonumber(settings[5])\nlocal unblockTime = tonumber(settings[6])\nlocal penalty = tonumber(settings[7])\nlocal minTime = tonumber(settings[8])\nlocal groupTimeout = tonumber(settings[9])\n\nif maxConcurrent ~= nil and weight > maxConcurrent then\n return redis.error_reply('OVERWEIGHT:'..weight..':'..maxConcurrent)\nend\n\nlocal reachedHWM = (highWater ~= nil and queueLength == highWater\n and not (\n conditions_check(capacity, weight)\n and nextRequest - now <= 0\n )\n)\n\nlocal blocked = strategy == 3 and (reachedHWM or unblockTime >= now)\n\nif blocked then\n local computedPenalty = penalty\n if computedPenalty == nil then\n if minTime == 0 then\n computedPenalty = 5000\n else\n computedPenalty = 15 * minTime\n end\n end\n\n local newNextRequest = now + computedPenalty + minTime\n\n redis.call('hmset', settings_key,\n 'unblockTime', now + computedPenalty,\n 'nextRequest', newNextRequest\n )\n\n local clients_queued_reset = redis.call('hkeys', client_num_queued_key)\n local queued_reset = {}\n for i = 1, #clients_queued_reset do\n table.insert(queued_reset, clients_queued_reset[i])\n table.insert(queued_reset, 0)\n end\n redis.call('hmset', client_num_queued_key, unpack(queued_reset))\n\n redis.call('publish', 'b_'..id, 'blocked:')\n\n refresh_expiration(now, newNextRequest, groupTimeout)\nend\n\nif not blocked and not reachedHWM then\n redis.call('hincrby', client_num_queued_key, client, 1)\nend\n\nreturn {reachedHWM, blocked, strategy}\n", 21 | "update_settings.lua": "local args = {'hmset', settings_key}\n\nfor i = num_static_argv + 1, #ARGV do\n table.insert(args, ARGV[i])\nend\n\nredis.call(unpack(args))\n\nprocess_tick(now, true)\n\nlocal groupTimeout = tonumber(redis.call('hget', settings_key, 'groupTimeout'))\nrefresh_expiration(0, 0, groupTimeout)\n\nreturn {}\n", 22 | "validate_client.lua": "if not redis.call('zscore', client_last_seen_key, client) then\n return redis.error_reply('UNKNOWN_CLIENT')\nend\n\nredis.call('zadd', client_last_seen_key, now, client)\n", 23 | "validate_keys.lua": "if not (redis.call('exists', settings_key) == 1) then\n return redis.error_reply('SETTINGS_KEY_NOT_FOUND')\nend\n" 24 | } 25 | -------------------------------------------------------------------------------- /build/meterRole.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _Object$defineProperty = require("@babel/runtime-corejs3/core-js-stable/object/define-property"); 4 | 5 | _Object$defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | 9 | exports.default = void 0; 10 | var meterRole = { 11 | abstract: false, 12 | accessibleNameRequired: true, 13 | baseConcepts: [], 14 | childrenPresentational: true, 15 | nameFrom: ['author'], 16 | prohibitedProps: [], 17 | props: {}, 18 | relatedConcepts: [], 19 | requireContextRole: [], 20 | requiredContextRole: [], 21 | requiredOwnedElements: [], 22 | requiredProps: { 23 | 'aria-valuemax': null, 24 | 'aria-valuemin': null, 25 | 'aria-valuenow': null 26 | }, 27 | superClass: [['roletype', 'structure', 'range']] 28 | }; 29 | var _default = meterRole; 30 | exports.default = _default; -------------------------------------------------------------------------------- /build/parseHeaders.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import utils from './../utils.js'; 4 | 5 | // RawAxiosHeaders whose duplicates are ignored by node 6 | // c.f. https://nodejs.org/api/http.html#http_message_headers 7 | const ignoreDuplicateOf = utils.toObjectSet([ 8 | 'age', 'authorization', 'content-length', 'content-type', 'etag', 9 | 'expires', 'from', 'host', 'if-modified-since', 'if-unmodified-since', 10 | 'last-modified', 'location', 'max-forwards', 'proxy-authorization', 11 | 'referer', 'retry-after', 'user-agent' 12 | ]); 13 | 14 | /** 15 | * Parse headers into an object 16 | * 17 | * ``` 18 | * Date: Wed, 27 Aug 2014 08:58:49 GMT 19 | * Content-Type: application/json 20 | * Connection: keep-alive 21 | * Transfer-Encoding: chunked 22 | * ``` 23 | * 24 | * @param {String} rawHeaders Headers needing to be parsed 25 | * 26 | * @returns {Object} Headers parsed into an object 27 | */ 28 | export default rawHeaders => { 29 | const parsed = {}; 30 | let key; 31 | let val; 32 | let i; 33 | 34 | rawHeaders && rawHeaders.split('\n').forEach(function parser(line) { 35 | i = line.indexOf(':'); 36 | key = line.substring(0, i).trim().toLowerCase(); 37 | val = line.substring(i + 1).trim(); 38 | 39 | if (!key || (parsed[key] && ignoreDuplicateOf[key])) { 40 | return; 41 | } 42 | 43 | if (key === 'set-cookie') { 44 | if (parsed[key]) { 45 | parsed[key].push(val); 46 | } else { 47 | parsed[key] = [val]; 48 | } 49 | } else { 50 | parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; 51 | } 52 | }); 53 | 54 | return parsed; 55 | }; 56 | -------------------------------------------------------------------------------- /build/passive-event-listener.js: -------------------------------------------------------------------------------- 1 | module.exports={A:{A:{"2":"J D E F A B uB"},B:{"1":"M N O P Q R S T U V W Z a b c d e f g h i j k X H","2":"C K L G"},C:{"1":"GB HB IB JB KB LB MB NB OB PB kB QB lB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R mB S T U V W Z a b c d e f g h i j k X H nB","2":"0 1 2 3 4 5 6 7 8 9 vB jB I l J D E F A B C K L G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB wB xB"},D:{"1":"IB JB KB LB MB NB OB PB kB QB lB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R S T U V W Z a b c d e f g h i j k X H nB yB zB","2":"0 1 2 3 4 5 6 7 8 9 I l J D E F A B C K L G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB"},E:{"1":"A B C K L G pB hB iB 5B 6B 7B qB rB 8B","2":"I l J D E F 0B oB 1B 2B 3B 4B"},F:{"1":"5 6 7 8 9 AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R mB S T U V W","2":"0 1 2 3 4 F B C G M N O m n o p q r s t u v w x y z 9B AC BC CC hB sB DC iB"},G:{"1":"LC MC NC OC PC QC RC SC TC UC VC WC XC qB rB","2":"E oB EC tB FC GC HC IC JC KC"},H:{"2":"YC"},I:{"1":"H","2":"jB I ZC aC bC cC tB dC eC"},J:{"2":"D A"},K:{"1":"Y","2":"A B C hB sB iB"},L:{"1":"H"},M:{"1":"X"},N:{"2":"A B"},O:{"1":"fC"},P:{"1":"gC hC iC jC kC pB lC mC nC oC pC qC","2":"I"},Q:{"2":"rC"},R:{"2":"sC"},S:{"2":"tC"}},B:1,C:"Passive event listeners"}; 2 | -------------------------------------------------------------------------------- /build/pointer-events.js: -------------------------------------------------------------------------------- 1 | module.exports={A:{A:{"1":"B","2":"J D E F A uB"},B:{"1":"C K L G M N O P Q R S T U V W Z a b c d e f g h i j k X H"},C:{"1":"0 1 2 3 4 5 6 7 8 9 I l J D E F A B C K L G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB kB QB lB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R mB S T U V W Z a b c d e f g h i j k X H nB xB","2":"vB jB wB"},D:{"1":"0 1 2 3 4 5 6 7 8 9 I l J D E F A B C K L G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB kB QB lB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R S T U V W Z a b c d e f g h i j k X H nB yB zB"},E:{"1":"I l J D E F A B C K L G 1B 2B 3B 4B pB hB iB 5B 6B 7B qB rB 8B","2":"0B oB"},F:{"1":"0 1 2 3 4 5 6 7 8 9 G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R mB S T U V W","2":"F B C 9B AC BC CC hB sB DC iB"},G:{"1":"E oB EC tB FC GC HC IC JC KC LC MC NC OC PC QC RC SC TC UC VC WC XC qB rB"},H:{"2":"YC"},I:{"1":"jB I H ZC aC bC cC tB dC eC"},J:{"1":"D A"},K:{"1":"Y","2":"A B C hB sB iB"},L:{"1":"H"},M:{"1":"X"},N:{"1":"B","2":"A"},O:{"1":"fC"},P:{"1":"I gC hC iC jC kC pB lC mC nC oC pC qC"},Q:{"1":"rC"},R:{"1":"sC"},S:{"1":"tC"}},B:7,C:"CSS pointer-events (for HTML)"}; 2 | -------------------------------------------------------------------------------- /build/register.lua: -------------------------------------------------------------------------------- 1 | local index = ARGV[num_static_argv + 1] 2 | local weight = tonumber(ARGV[num_static_argv + 2]) 3 | local expiration = tonumber(ARGV[num_static_argv + 3]) 4 | 5 | local state = process_tick(now, false) 6 | local capacity = state['capacity'] 7 | local reservoir = state['reservoir'] 8 | 9 | local settings = redis.call('hmget', settings_key, 10 | 'nextRequest', 11 | 'minTime', 12 | 'groupTimeout' 13 | ) 14 | local nextRequest = tonumber(settings[1]) 15 | local minTime = tonumber(settings[2]) 16 | local groupTimeout = tonumber(settings[3]) 17 | 18 | if conditions_check(capacity, weight) then 19 | 20 | redis.call('hincrby', settings_key, 'running', weight) 21 | redis.call('hset', job_weights_key, index, weight) 22 | if expiration ~= nil then 23 | redis.call('zadd', job_expirations_key, now + expiration, index) 24 | end 25 | redis.call('hset', job_clients_key, index, client) 26 | redis.call('zincrby', client_running_key, weight, client) 27 | redis.call('hincrby', client_num_queued_key, client, -1) 28 | redis.call('zadd', client_last_registered_key, now, client) 29 | 30 | local wait = math.max(nextRequest - now, 0) 31 | local newNextRequest = now + wait + minTime 32 | 33 | if reservoir == nil then 34 | redis.call('hset', settings_key, 35 | 'nextRequest', newNextRequest 36 | ) 37 | else 38 | reservoir = reservoir - weight 39 | redis.call('hmset', settings_key, 40 | 'reservoir', reservoir, 41 | 'nextRequest', newNextRequest 42 | ) 43 | end 44 | 45 | refresh_expiration(now, newNextRequest, groupTimeout) 46 | 47 | return {true, wait, reservoir} 48 | 49 | else 50 | return {false} 51 | end 52 | -------------------------------------------------------------------------------- /build/string-padding.js: -------------------------------------------------------------------------------- 1 | // https://github.com/tc39/proposal-string-pad-start-end 2 | require('../modules/es.string.pad-end'); 3 | require('../modules/es.string.pad-start'); 4 | -------------------------------------------------------------------------------- /build/sxg.js: -------------------------------------------------------------------------------- 1 | module.exports={A:{A:{"2":"J D E F A B uB"},B:{"1":"P Q R S T U V W Z a b c d e f g h i j k X H","2":"C K L G M N O"},C:{"2":"0 1 2 3 4 5 6 7 8 9 vB jB I l J D E F A B C K L G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB kB QB lB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R mB S T U V W Z a b c d e f g h i j k X H nB wB xB"},D:{"1":"bB cB dB eB fB gB P Q R S T U V W Z a b c d e f g h i j k X H nB yB zB","2":"0 1 2 3 4 5 6 7 8 9 I l J D E F A B C K L G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB kB QB lB RB SB Y TB UB VB WB XB YB","132":"ZB aB"},E:{"2":"I l J D E F A B C K L G 0B oB 1B 2B 3B 4B pB hB iB 5B 6B 7B qB rB 8B"},F:{"1":"Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R mB S T U V W","2":"0 1 2 3 4 5 6 7 8 9 F B C G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB 9B AC BC CC hB sB DC iB"},G:{"2":"E oB EC tB FC GC HC IC JC KC LC MC NC OC PC QC RC SC TC UC VC WC XC qB rB"},H:{"2":"YC"},I:{"1":"H","2":"jB I ZC aC bC cC tB dC eC"},J:{"2":"D A"},K:{"1":"Y","2":"A B C hB sB iB"},L:{"1":"H"},M:{"2":"X"},N:{"2":"A B"},O:{"16":"fC"},P:{"1":"lC mC nC oC pC qC","2":"I gC hC iC jC kC pB"},Q:{"16":"rC"},R:{"16":"sC"},S:{"2":"tC"}},B:6,C:"Signed HTTP Exchanges (SXG)"}; 2 | -------------------------------------------------------------------------------- /build/xhr2.js: -------------------------------------------------------------------------------- 1 | module.exports={A:{A:{"2":"J D E F uB","132":"A B"},B:{"1":"C K L G M N O P Q R S T U V W Z a b c d e f g h i j k X H"},C:{"1":"0 1 2 3 4 5 6 7 8 9 C K L G M N O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB kB QB lB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R mB S T U V W Z a b c d e f g h i j k X H nB","2":"vB jB","260":"A B","388":"J D E F","900":"I l wB xB"},D:{"1":"0 1 2 3 4 5 6 7 8 9 y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB kB QB lB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R S T U V W Z a b c d e f g h i j k X H nB yB zB","16":"I l J","132":"w x","388":"D E F A B C K L G M N O m n o p q r s t u v"},E:{"1":"E F A B C K L G 3B 4B pB hB iB 5B 6B 7B qB rB 8B","2":"I 0B oB","132":"D 2B","388":"l J 1B"},F:{"1":"0 1 2 3 4 5 6 7 8 9 C O m n o p q r s t u v w x y z AB BB CB DB EB FB GB HB IB JB KB LB MB NB OB PB QB RB SB Y TB UB VB WB XB YB ZB aB bB cB dB eB fB gB P Q R mB S T U V W iB","2":"F B 9B AC BC CC hB sB DC","132":"G M N"},G:{"1":"E IC JC KC LC MC NC OC PC QC RC SC TC UC VC WC XC qB rB","2":"oB EC tB","132":"HC","388":"FC GC"},H:{"2":"YC"},I:{"1":"H eC","2":"ZC aC bC","388":"dC","900":"jB I cC tB"},J:{"132":"A","388":"D"},K:{"1":"C Y iB","2":"A B hB sB"},L:{"1":"H"},M:{"1":"X"},N:{"132":"A B"},O:{"1":"fC"},P:{"1":"I gC hC iC jC kC pB lC mC nC oC pC qC"},Q:{"1":"rC"},R:{"1":"sC"},S:{"1":"tC"}},B:1,C:"XMLHttpRequest advanced features"}; 2 | -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- 1 | // Bootstrap cliui with CommonJS dependencies: 2 | import { cliui } from './build/lib/index.js' 3 | import { wrap, stripAnsi } from './build/lib/string-utils.js' 4 | 5 | export default function ui (opts) { 6 | return cliui(opts, { 7 | stringWidth: (str) => { 8 | return [...str].length 9 | }, 10 | stripAnsi, 11 | wrap 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /lib/arg.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const 4 | CoaParam = require('./coaparam'), 5 | chalk = require('chalk'); 6 | 7 | /** 8 | * Argument 9 | * 10 | * Unnamed entity. From command line arguments passed as list of unnamed values. 11 | * 12 | * @class Arg 13 | * @extends CoaParam 14 | */ 15 | module.exports = class Arg extends CoaParam { 16 | /** 17 | * @constructs 18 | * @param {COA.Cmd} cmd - parent command 19 | */ 20 | constructor(cmd) { 21 | super(cmd); 22 | 23 | this._cmd._args.push(this); 24 | } 25 | 26 | _saveVal(args, val) { 27 | this._val && (val = this._val(val)); 28 | 29 | const name = this._name; 30 | this._arr 31 | ? (args[name] || (args[name] = [])).push(val) 32 | : (args[name] = val); 33 | 34 | return val; 35 | } 36 | 37 | _parse(arg, args) { 38 | return this._saveVal(args, arg); 39 | } 40 | 41 | _checkParsed(opts, args) { 42 | return !args.hasOwnProperty(this._name); 43 | } 44 | 45 | _usage() { 46 | const res = []; 47 | 48 | res.push(chalk.magentaBright(this._name.toUpperCase()), ' : ', this._title); 49 | 50 | this._req && res.push(' ', chalk.redBright('(required)')); 51 | 52 | return res.join(''); 53 | } 54 | 55 | _requiredText() { 56 | return `Missing required argument:\n ${this._usage()}`; 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /lib/cmd.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | 'use strict'; 3 | 4 | const 5 | UTIL = require('util'), 6 | PATH = require('path'), 7 | EOL = require('os').EOL, 8 | 9 | Q = require('q'), 10 | chalk = require('chalk'), 11 | 12 | CoaObject = require('./coaobject'), 13 | Opt = require('./opt'), 14 | Arg = require('./arg'), 15 | completion = require('./completion'); 16 | 17 | /** 18 | * Command 19 | * 20 | * Top level entity. Commands may have options and arguments. 21 | * 22 | * @namespace 23 | * @class Cmd 24 | * @extends CoaObject 25 | */ 26 | class Cmd extends CoaObject { 27 | /** 28 | * @constructs 29 | * @param {COA.Cmd} [cmd] parent command 30 | */ 31 | constructor(cmd) { 32 | super(cmd); 33 | 34 | this._parent(cmd); 35 | this._cmds = []; 36 | this._cmdsByName = {}; 37 | this._opts = []; 38 | this._optsByKey = {}; 39 | this._args = []; 40 | this._api = null; 41 | this._ext = false; 42 | } 43 | 44 | static create(cmd) { 45 | return new Cmd(cmd); 46 | } 47 | 48 | /** 49 | * Returns object containing all its subcommands as methods 50 | * to use from other programs. 51 | * 52 | * @returns {Object} 53 | */ 54 | get api() { 55 | // Need _this here because of passed arguments into _api 56 | const _this = this; 57 | this._api || (this._api = function () { 58 | return _this.invoke.apply(_this, arguments); 59 | }); 60 | 61 | const cmds = this._cmdsByName; 62 | Object.keys(cmds).forEach(cmd => { this._api[cmd] = cmds[cmd].api; }); 63 | 64 | return this._api; 65 | } 66 | 67 | _parent(cmd) { 68 | this._cmd = cmd || this; 69 | 70 | this.isRootCmd || 71 | cmd._cmds.push(this) && 72 | this._name && 73 | (this._cmd._cmdsByName[this._name] = this); 74 | 75 | return this; 76 | } 77 | 78 | get isRootCmd() { 79 | return this._cmd === this; 80 | } 81 | 82 | /** 83 | * Set a canonical command identifier to be used anywhere in the API. 84 | * 85 | * @param {String} name - command name 86 | * @returns {COA.Cmd} - this instance (for chainability) 87 | */ 88 | name(name) { 89 | super.name(name); 90 | 91 | this.isRootCmd || 92 | (this._cmd._cmdsByName[name] = this); 93 | 94 | return this; 95 | } 96 | 97 | /** 98 | * Create new or add existing subcommand for current command. 99 | * 100 | * @param {COA.Cmd} [cmd] existing command instance 101 | * @returns {COA.Cmd} new subcommand instance 102 | */ 103 | cmd(cmd) { 104 | return cmd? 105 | cmd._parent(this) 106 | : new Cmd(this); 107 | } 108 | 109 | /** 110 | * Create option for current command. 111 | * 112 | * @returns {COA.Opt} new option instance 113 | */ 114 | opt() { 115 | return new Opt(this); 116 | } 117 | 118 | /** 119 | * Create argument for current command. 120 | * 121 | * @returns {COA.Opt} new argument instance 122 | */ 123 | arg() { 124 | return new Arg(this); 125 | } 126 | 127 | /** 128 | * Add (or set) action for current command. 129 | * 130 | * @param {Function} act - action function, 131 | * invoked in the context of command instance 132 | * and has the parameters: 133 | * - {Object} opts - parsed options 134 | * - {String[]} args - parsed arguments 135 | * - {Object} res - actions result accumulator 136 | * It can return rejected promise by Cmd.reject (in case of error) 137 | * or any other value treated as result. 138 | * @param {Boolean} [force=false] flag for set action instead add to existings 139 | * @returns {COA.Cmd} - this instance (for chainability) 140 | */ 141 | act(act, force) { 142 | if(!act) return this; 143 | 144 | (!this._act || force) && (this._act = []); 145 | this._act.push(act); 146 | 147 | return this; 148 | } 149 | 150 | /** 151 | * Make command "helpful", i.e. add -h --help flags for print usage. 152 | * 153 | * @returns {COA.Cmd} - this instance (for chainability) 154 | */ 155 | helpful() { 156 | return this.opt() 157 | .name('help') 158 | .title('Help') 159 | .short('h') 160 | .long('help') 161 | .flag() 162 | .only() 163 | .act(function() { 164 | return this.usage(); 165 | }) 166 | .end(); 167 | } 168 | 169 | /** 170 | * Adds shell completion to command, adds "completion" subcommand, 171 | * that makes all the magic. 172 | * Must be called only on root command. 173 | * 174 | * @returns {COA.Cmd} - this instance (for chainability) 175 | */ 176 | completable() { 177 | return this.cmd() 178 | .name('completion') 179 | .apply(completion) 180 | .end(); 181 | } 182 | 183 | /** 184 | * Allow command to be extendable by external node.js modules. 185 | * 186 | * @param {String} [pattern] Pattern of node.js module to find subcommands at. 187 | * @returns {COA.Cmd} - this instance (for chainability) 188 | */ 189 | extendable(pattern) { 190 | this._ext = pattern || true; 191 | return this; 192 | } 193 | 194 | _exit(msg, code) { 195 | return process.once('exit', function(exitCode) { 196 | msg && console[code === 0 ? 'log' : 'error'](msg); 197 | process.exit(code || exitCode || 0); 198 | }); 199 | } 200 | 201 | /** 202 | * Build full usage text for current command instance. 203 | * 204 | * @returns {String} usage text 205 | */ 206 | usage() { 207 | const res = []; 208 | 209 | this._title && res.push(this._fullTitle()); 210 | 211 | res.push('', 'Usage:'); 212 | 213 | this._cmds.length 214 | && res.push([ 215 | '', '', chalk.redBright(this._fullName()), chalk.blueBright('COMMAND'), 216 | chalk.greenBright('[OPTIONS]'), chalk.magentaBright('[ARGS]') 217 | ].join(' ')); 218 | 219 | (this._opts.length + this._args.length) 220 | && res.push([ 221 | '', '', chalk.redBright(this._fullName()), 222 | chalk.greenBright('[OPTIONS]'), chalk.magentaBright('[ARGS]') 223 | ].join(' ')); 224 | 225 | res.push( 226 | this._usages(this._cmds, 'Commands'), 227 | this._usages(this._opts, 'Options'), 228 | this._usages(this._args, 'Arguments') 229 | ); 230 | 231 | return res.join(EOL); 232 | } 233 | 234 | _usage() { 235 | return chalk.blueBright(this._name) + ' : ' + this._title; 236 | } 237 | 238 | _usages(os, title) { 239 | if(!os.length) return; 240 | 241 | return ['', title + ':'] 242 | .concat(os.map(o => ` ${o._usage()}`)) 243 | .join(EOL); 244 | } 245 | 246 | _fullTitle() { 247 | return `${this.isRootCmd? '' : this._cmd._fullTitle() + EOL}${this._title}`; 248 | } 249 | 250 | _fullName() { 251 | return `${this.isRootCmd? '' : this._cmd._fullName() + ' '}${PATH.basename(this._name)}`; 252 | } 253 | 254 | _ejectOpt(opts, opt) { 255 | const pos = opts.indexOf(opt); 256 | if(pos === -1) return; 257 | 258 | return opts[pos]._arr? 259 | opts[pos] : 260 | opts.splice(pos, 1)[0]; 261 | } 262 | 263 | _checkRequired(opts, args) { 264 | if(this._opts.some(opt => opt._only && opts.hasOwnProperty(opt._name))) return; 265 | 266 | const all = this._opts.concat(this._args); 267 | let i; 268 | while(i = all.shift()) 269 | if(i._req && i._checkParsed(opts, args)) 270 | return this.reject(i._requiredText()); 271 | } 272 | 273 | _parseCmd(argv, unparsed) { 274 | unparsed || (unparsed = []); 275 | 276 | let i, 277 | optSeen = false; 278 | while(i = argv.shift()) { 279 | i.indexOf('-') || (optSeen = true); 280 | 281 | if(optSeen || !/^\w[\w-_]*$/.test(i)) { 282 | unparsed.push(i); 283 | continue; 284 | } 285 | 286 | let pkg, cmd = this._cmdsByName[i]; 287 | if(!cmd && this._ext) { 288 | if(this._ext === true) { 289 | pkg = i; 290 | let c = this; 291 | while(true) { // eslint-disable-line 292 | pkg = c._name + '-' + pkg; 293 | if(c.isRootCmd) break; 294 | c = c._cmd; 295 | } 296 | } else if(typeof this._ext === 'string') 297 | pkg = ~this._ext.indexOf('%s')? 298 | UTIL.format(this._ext, i) : 299 | this._ext + i; 300 | 301 | let cmdDesc; 302 | try { 303 | cmdDesc = require(pkg); 304 | } catch(e) { 305 | // Dummy 306 | } 307 | 308 | if(cmdDesc) { 309 | if(typeof cmdDesc === 'function') { 310 | this.cmd().name(i).apply(cmdDesc).end(); 311 | } else if(typeof cmdDesc === 'object') { 312 | this.cmd(cmdDesc); 313 | cmdDesc.name(i); 314 | } else throw new Error('Error: Unsupported command declaration type, ' 315 | + 'should be a function or COA.Cmd() object'); 316 | 317 | cmd = this._cmdsByName[i]; 318 | } 319 | } 320 | 321 | if(cmd) return cmd._parseCmd(argv, unparsed); 322 | 323 | unparsed.push(i); 324 | } 325 | 326 | return { cmd : this, argv : unparsed }; 327 | } 328 | 329 | _parseOptsAndArgs(argv) { 330 | const opts = {}, 331 | args = {}, 332 | nonParsedOpts = this._opts.concat(), 333 | nonParsedArgs = this._args.concat(); 334 | 335 | let res, i; 336 | while(i = argv.shift()) { 337 | if(i !== '--' && i[0] === '-') { 338 | const m = i.match(/^(--\w[\w-_]*)=(.*)$/); 339 | if(m) { 340 | i = m[1]; 341 | this._optsByKey[i]._flag || argv.unshift(m[2]); 342 | } 343 | 344 | const opt = this._ejectOpt(nonParsedOpts, this._optsByKey[i]); 345 | if(!opt) return this.reject(`Unknown option: ${i}`); 346 | 347 | if(Q.isRejected(res = opt._parse(argv, opts))) return res; 348 | 349 | continue; 350 | } 351 | 352 | i === '--' && (i = argv.splice(0)); 353 | Array.isArray(i) || (i = [i]); 354 | 355 | let a; 356 | while(a = i.shift()) { 357 | let arg = nonParsedArgs.shift(); 358 | if(!arg) return this.reject(`Unknown argument: ${a}`); 359 | 360 | arg._arr && nonParsedArgs.unshift(arg); 361 | if(Q.isRejected(res = arg._parse(a, args))) return res; 362 | } 363 | } 364 | 365 | return { 366 | opts : this._setDefaults(opts, nonParsedOpts), 367 | args : this._setDefaults(args, nonParsedArgs) 368 | }; 369 | } 370 | 371 | _setDefaults(params, desc) { 372 | for(const item of desc) 373 | item._def !== undefined && 374 | !params.hasOwnProperty(item._name) && 375 | item._saveVal(params, item._def); 376 | 377 | return params; 378 | } 379 | 380 | _processParams(params, desc) { 381 | const notExists = []; 382 | 383 | for(const item of desc) { 384 | const n = item._name; 385 | 386 | if(!params.hasOwnProperty(n)) { 387 | notExists.push(item); 388 | continue; 389 | } 390 | 391 | const vals = Array.isArray(params[n])? params[n] : [params[n]]; 392 | delete params[n]; 393 | 394 | let res; 395 | for(const v of vals) 396 | if(Q.isRejected(res = item._saveVal(params, v))) 397 | return res; 398 | } 399 | 400 | return this._setDefaults(params, notExists); 401 | } 402 | 403 | _parseArr(argv) { 404 | return Q.when(this._parseCmd(argv), p => 405 | Q.when(p.cmd._parseOptsAndArgs(p.argv), r => ({ 406 | cmd : p.cmd, 407 | opts : r.opts, 408 | args : r.args 409 | }))); 410 | } 411 | 412 | _do(inputPromise) { 413 | return Q.when(inputPromise, input => { 414 | return [this._checkRequired] 415 | .concat(input.cmd._act || []) 416 | .reduce((res, act) => 417 | Q.when(res, prev => act.call(input.cmd, input.opts, input.args, prev)), 418 | undefined); 419 | }); 420 | } 421 | 422 | /** 423 | * Parse arguments from simple format like NodeJS process.argv 424 | * and run ahead current program, i.e. call process.exit when all actions done. 425 | * 426 | * @param {String[]} argv - arguments 427 | * @returns {COA.Cmd} - this instance (for chainability) 428 | */ 429 | run(argv) { 430 | argv || (argv = process.argv.slice(2)); 431 | 432 | const cb = code => 433 | res => res? 434 | this._exit(res.stack || res.toString(), (res.hasOwnProperty('exitCode')? res.exitCode : code) || 0) : 435 | this._exit(); 436 | 437 | Q.when(this.do(argv), cb(0), cb(1)).done(); 438 | 439 | return this; 440 | } 441 | 442 | /** 443 | * Invoke specified (or current) command using provided 444 | * options and arguments. 445 | * 446 | * @param {String|String[]} [cmds] - subcommand to invoke (optional) 447 | * @param {Object} [opts] - command options (optional) 448 | * @param {Object} [args] - command arguments (optional) 449 | * @returns {Q.Promise} 450 | */ 451 | invoke(cmds, opts, args) { 452 | cmds || (cmds = []); 453 | opts || (opts = {}); 454 | args || (args = {}); 455 | typeof cmds === 'string' && (cmds = cmds.split(' ')); 456 | 457 | if(arguments.length < 3 && !Array.isArray(cmds)) { 458 | args = opts; 459 | opts = cmds; 460 | cmds = []; 461 | } 462 | 463 | return Q.when(this._parseCmd(cmds), p => { 464 | if(p.argv.length) 465 | return this.reject(`Unknown command: ${cmds.join(' ')}`); 466 | 467 | return Q.all([ 468 | this._processParams(opts, this._opts), 469 | this._processParams(args, this._args) 470 | ]).spread((_opts, _args) => 471 | this._do({ 472 | cmd : p.cmd, 473 | opts : _opts, 474 | args : _args 475 | }) 476 | .fail(res => (res && res.exitCode === 0)? 477 | res.toString() : 478 | this.reject(res))); 479 | }); 480 | } 481 | } 482 | 483 | /** 484 | * Convenient function to run command from tests. 485 | * 486 | * @param {String[]} argv - arguments 487 | * @returns {Q.Promise} 488 | */ 489 | Cmd.prototype.do = function(argv) { 490 | return this._do(this._parseArr(argv || [])); 491 | }; 492 | 493 | module.exports = Cmd; 494 | -------------------------------------------------------------------------------- /lib/coaobject.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | 'use strict'; 3 | 4 | const Q = require('q'); 5 | 6 | /** 7 | * COA Object 8 | * 9 | * Base class for all COA-related objects 10 | * 11 | * --------|-----|-----|----- 12 | * | Cmd | Opt | Arg 13 | * --------|-----|-----|----- 14 | * name | ✓ | ✓ | ✓ 15 | * title | ✓ | ✓ | ✓ 16 | * comp | ✓ | ✓ | ✓ 17 | * reject | ✓ | ✓ | ✓ 18 | * end | ✓ | ✓ | ✓ 19 | * apply | ✓ | ✓ | ✓ 20 | * 21 | * @class CoaObject 22 | */ 23 | module.exports = class CoaObject { 24 | constructor(cmd) { 25 | this._cmd = cmd; 26 | this._name = null; 27 | this._title = null; 28 | this._comp = null; 29 | } 30 | 31 | /** 32 | * Set a canonical identifier to be used anywhere in the API. 33 | * 34 | * @param {String} name - command, option or argument name 35 | * @returns {COA.CoaObject} - this instance (for chainability) 36 | */ 37 | name(name) { 38 | this._name = name; 39 | return this; 40 | } 41 | 42 | /** 43 | * Set a long description to be used anywhere in text messages. 44 | * @param {String} title - human readable entity title 45 | * @returns {COA.CoaObject} - this instance (for chainability) 46 | */ 47 | title(title) { 48 | this._title = title; 49 | return this; 50 | } 51 | 52 | /** 53 | * Set custom additional completion for current object. 54 | * 55 | * @param {Function} comp - completion generation function, 56 | * invoked in the context of object instance. 57 | * Accepts parameters: 58 | * - {Object} opts - completion options 59 | * It can return promise or any other value threated as a result. 60 | * @returns {COA.CoaObject} - this instance (for chainability) 61 | */ 62 | comp(comp) { 63 | this._comp = comp; 64 | return this; 65 | } 66 | 67 | /** 68 | * Apply function with arguments in a context of object instance. 69 | * 70 | * @param {Function} fn - body 71 | * @param {Array.<*>} args... - arguments 72 | * @returns {COA.CoaObject} - this instance (for chainability) 73 | */ 74 | apply(fn) { 75 | arguments.length > 1? 76 | fn.apply(this, [].slice.call(arguments, 1)) 77 | : fn.call(this); 78 | 79 | return this; 80 | } 81 | 82 | /** 83 | * Return reject of actions results promise with error code. 84 | * Use in .act() for return with error. 85 | * @param {Object} reason - reject reason 86 | * You can customize toString() method and exitCode property 87 | * of reason object. 88 | * @returns {Q.promise} rejected promise 89 | */ 90 | reject(reason) { 91 | return Q.reject(reason); 92 | } 93 | 94 | /** 95 | * Finish chain for current subcommand and return parent command instance. 96 | * @returns {COA.Cmd} parent command 97 | */ 98 | end() { 99 | return this._cmd; 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /lib/coaparam.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | const CoaObject = require('./coaobject'); 6 | 7 | /** 8 | * COA Parameter 9 | * 10 | * Base class for options and arguments 11 | * 12 | * --------|-----|-----|----- 13 | * | Cmd | Opt | Arg 14 | * --------|-----|-----|----- 15 | * arr | | ✓ | ✓ 16 | * req | | ✓ | ✓ 17 | * val | | ✓ | ✓ 18 | * def | | ✓ | ✓ 19 | * input | | ✓ | ✓ 20 | * output | | ✓ | ✓ 21 | * 22 | * @class CoaParam 23 | * @extends CoaObject 24 | */ 25 | module.exports = class CoaParam extends CoaObject { 26 | constructor(cmd) { 27 | super(cmd); 28 | 29 | this._arr = false; 30 | this._req = false; 31 | this._val = undefined; 32 | this._def = undefined; 33 | } 34 | 35 | /** 36 | * Makes a param accepts multiple values. 37 | * Otherwise, the value will be used by the latter passed. 38 | * 39 | * @returns {COA.CoaParam} - this instance (for chainability) 40 | */ 41 | arr() { 42 | this._arr = true; 43 | return this; 44 | } 45 | 46 | /** 47 | * Makes a param required. 48 | * 49 | * @returns {COA.CoaParam} - this instance (for chainability) 50 | */ 51 | req() { 52 | this._req = true; 53 | return this; 54 | } 55 | 56 | /** 57 | * Set a validation (or value) function for param. 58 | * Value from command line passes through before becoming available from API. 59 | * Using for validation and convertion simple types to any values. 60 | * 61 | * @param {Function} val - validating function, 62 | * invoked in the context of option instance 63 | * and has one parameter with value from command line. 64 | * @returns {COA.CoaParam} - this instance (for chainability) 65 | */ 66 | val(val) { 67 | this._val = val; 68 | return this; 69 | } 70 | 71 | /** 72 | * Set a default value for param. 73 | * Default value passed through validation function as ordinary value. 74 | * 75 | * @param {*} def - default value of function generator 76 | * @returns {COA.CoaParam} - this instance (for chainability) 77 | */ 78 | def(def) { 79 | this._def = def; 80 | return this; 81 | } 82 | 83 | /** 84 | * Make option value inputting stream. 85 | * It's add useful validation and shortcut for STDIN. 86 | * 87 | * @returns {COA.CoaParam} - this instance (for chainability) 88 | */ 89 | input() { 90 | process.stdin.pause(); 91 | return this 92 | .def(process.stdin) 93 | .val(function(v) { 94 | if(typeof v !== 'string') 95 | return v; 96 | 97 | if(v === '-') 98 | return process.stdin; 99 | 100 | const s = fs.createReadStream(v, { encoding : 'utf8' }); 101 | s.pause(); 102 | return s; 103 | }); 104 | } 105 | 106 | /** 107 | * Make option value outputing stream. 108 | * It's add useful validation and shortcut for STDOUT. 109 | * 110 | * @returns {COA.CoaParam} - this instance (for chainability) 111 | */ 112 | output() { 113 | return this 114 | .def(process.stdout) 115 | .val(function(v) { 116 | if(typeof v !== 'string') 117 | return v; 118 | 119 | if(v === '-') 120 | return process.stdout; 121 | 122 | return fs.createWriteStream(v, { encoding : 'utf8' }); 123 | }); 124 | } 125 | }; 126 | -------------------------------------------------------------------------------- /lib/completion.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const constants = require('constants'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | const Q = require('q'); 8 | 9 | const shell = require('./shell'); 10 | const escape = shell.escape; 11 | const unescape = shell.unescape; 12 | 13 | /** 14 | * Most of the code adopted from the npm package shell completion code. 15 | * See https://github.com/isaacs/npm/blob/master/lib/completion.js 16 | * 17 | * @returns {COA.CoaObject} 18 | */ 19 | module.exports = function completion() { 20 | return this 21 | .title('Shell completion') 22 | .helpful() 23 | .arg() 24 | .name('raw') 25 | .title('Completion words') 26 | .arr() 27 | .end() 28 | .act((opts, args) => { 29 | if(process.platform === 'win32') { 30 | const e = new Error('shell completion not supported on windows'); 31 | e.code = 'ENOTSUP'; 32 | e.errno = constants.ENOTSUP; 33 | return this.reject(e); 34 | } 35 | 36 | // if the COMP_* isn't in the env, then just dump the script 37 | if((process.env.COMP_CWORD == null) 38 | || (process.env.COMP_LINE == null) 39 | || (process.env.COMP_POINT == null)) { 40 | return dumpScript(this._cmd._name); 41 | } 42 | 43 | console.error('COMP_LINE: %s', process.env.COMP_LINE); 44 | console.error('COMP_CWORD: %s', process.env.COMP_CWORD); 45 | console.error('COMP_POINT: %s', process.env.COMP_POINT); 46 | console.error('args: %j', args.raw); 47 | 48 | // completion opts 49 | opts = getOpts(args.raw); 50 | 51 | // cmd 52 | const parsed = this._cmd._parseCmd(opts.partialWords); 53 | return Q.when(complete(parsed.cmd, parsed.opts), compls => { 54 | console.error('filtered: %j', compls); 55 | return console.log(compls.map(escape).join('\n')); 56 | }); 57 | }); 58 | }; 59 | 60 | function dumpScript(name) { 61 | const defer = Q.defer(); 62 | 63 | fs.readFile(path.resolve(__dirname, 'completion.sh'), 'utf8', function(err, d) { 64 | if(err) return defer.reject(err); 65 | d = d.replace(/{{cmd}}/g, path.basename(name)).replace(/^#!.*?\n/, ''); 66 | 67 | process.stdout.on('error', onError); 68 | process.stdout.write(d, () => defer.resolve()); 69 | }); 70 | 71 | return defer.promise; 72 | 73 | function onError(err) { 74 | // Darwin is a real dick sometimes. 75 | // 76 | // This is necessary because the "source" or "." program in 77 | // bash on OS X closes its file argument before reading 78 | // from it, meaning that you get exactly 1 write, which will 79 | // work most of the time, and will always raise an EPIPE. 80 | // 81 | // Really, one should not be tossing away EPIPE errors, or any 82 | // errors, so casually. But, without this, `. <(cmd completion)` 83 | // can never ever work on OS X. 84 | if(err.errno !== constants.EPIPE) return defer.reject(err); 85 | process.stdout.removeListener('error', onError); 86 | return defer.resolve(); 87 | } 88 | } 89 | 90 | function getOpts(argv) { 91 | // get the partial line and partial word, if the point isn't at the end 92 | // ie, tabbing at: cmd foo b|ar 93 | const line = process.env.COMP_LINE; 94 | const w = +process.env.COMP_CWORD; 95 | const point = +process.env.COMP_POINT; 96 | const words = argv.map(unescape); 97 | const word = words[w]; 98 | const partialLine = line.substr(0, point); 99 | const partialWords = words.slice(0, w); 100 | 101 | // figure out where in that last word the point is 102 | let partialWord = argv[w] || ''; 103 | let i = partialWord.length; 104 | while(partialWord.substr(0, i) !== partialLine.substr(-1 * i) && i > 0) i--; 105 | 106 | partialWord = unescape(partialWord.substr(0, i)); 107 | partialWord && partialWords.push(partialWord); 108 | 109 | return { 110 | line, 111 | w, 112 | point, 113 | words, 114 | word, 115 | partialLine, 116 | partialWords, 117 | partialWord 118 | }; 119 | } 120 | 121 | function complete(cmd, opts) { 122 | let optWord, optPrefix, 123 | compls = []; 124 | 125 | // Complete on cmds 126 | if(opts.partialWord.indexOf('-')) 127 | compls = Object.keys(cmd._cmdsByName); 128 | // Complete on required opts without '-' in last partial word 129 | // (if required not already specified) 130 | // 131 | // Commented out because of uselessness: 132 | // -b, --block suggest results in '-' on cmd line; 133 | // next completion suggest all options, because of '-' 134 | //.concat Object.keys(cmd._optsByKey).filter (v) -> cmd._optsByKey[v]._req 135 | else { 136 | // complete on opt values: --opt=| case 137 | const m = opts.partialWord.match(/^(--\w[\w-_]*)=(.*)$/); 138 | if(m) { 139 | optWord = m[1]; 140 | optPrefix = optWord + '='; 141 | } else 142 | // complete on opts 143 | // don't complete on opts in case of --opt=val completion 144 | // TODO: don't complete on opts in case of unknown arg after commands 145 | // TODO: complete only on opts with arr() or not already used 146 | // TODO: complete only on full opts? 147 | compls = Object.keys(cmd._optsByKey); 148 | } 149 | 150 | // complete on opt values: next arg case 151 | opts.partialWords[opts.w - 1].indexOf('-') || (optWord = opts.partialWords[opts.w - 1]); 152 | 153 | // complete on opt values: completion 154 | let opt; 155 | optWord 156 | && (opt = cmd._optsByKey[optWord]) 157 | && !opt._flag 158 | && opt._comp 159 | && (compls = Q.join(compls, 160 | Q.when(opt._comp(opts), 161 | (c, o) => c.concat(o.map(v => (optPrefix || '') + v))))); 162 | 163 | // TODO: complete on args values (context aware, custom completion?) 164 | 165 | // custom completion on cmds 166 | cmd._comp && (compls = Q.join(compls, Q.when(cmd._comp(opts)), (c, o) => c.concat(o))); 167 | 168 | // TODO: context aware custom completion on cmds, opts and args 169 | // (can depend on already entered values, especially options) 170 | 171 | return Q.when(compls, complitions => { 172 | console.error('partialWord: %s', opts.partialWord); 173 | console.error('compls: %j', complitions); 174 | return compls.filter(c => c.indexOf(opts.partialWord) === 0); 175 | }); 176 | } 177 | -------------------------------------------------------------------------------- /lib/completion.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ###-begin-{{cmd}}-completion-### 3 | # 4 | # {{cmd}} command completion script 5 | # 6 | # Installation: {{cmd}} completion >> ~/.bashrc (or ~/.zshrc) 7 | # Or, maybe: {{cmd}} completion > /usr/local/etc/bash_completion.d/{{cmd}} 8 | # 9 | 10 | COMP_WORDBREAKS=${COMP_WORDBREAKS/=/} 11 | COMP_WORDBREAKS=${COMP_WORDBREAKS/@/} 12 | export COMP_WORDBREAKS 13 | 14 | if complete &>/dev/null; then 15 | _{{cmd}}_completion () { 16 | local si="$IFS" 17 | IFS=$'\n' COMPREPLY=($(COMP_CWORD="$COMP_CWORD" \ 18 | COMP_LINE="$COMP_LINE" \ 19 | COMP_POINT="$COMP_POINT" \ 20 | {{cmd}} completion -- "${COMP_WORDS[@]}" \ 21 | 2>/dev/null)) || return $? 22 | IFS="$si" 23 | } 24 | complete -F _{{cmd}}_completion {{cmd}} 25 | elif compctl &>/dev/null; then 26 | _{{cmd}}_completion () { 27 | local cword line point words si 28 | read -Ac words 29 | read -cn cword 30 | let cword-=1 31 | read -l line 32 | read -ln point 33 | si="$IFS" 34 | IFS=$'\n' reply=($(COMP_CWORD="$cword" \ 35 | COMP_LINE="$line" \ 36 | COMP_POINT="$point" \ 37 | {{cmd}} completion -- "${words[@]}" \ 38 | 2>/dev/null)) || return $? 39 | IFS="$si" 40 | } 41 | compctl -K _{{cmd}}_completion {{cmd}} 42 | fi 43 | ###-end-{{cmd}}-completion-### 44 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const 2 | Cmd = require('./cmd'), 3 | Opt = require('./opt'), 4 | Arg = require('./arg'), 5 | shell = require('./shell'); 6 | 7 | module.exports = { 8 | Cmd : Cmd.create, 9 | Opt : Opt.create, 10 | Arg : Arg.create, 11 | classes : { Cmd, Opt, Arg }, 12 | shell, 13 | require 14 | }; 15 | -------------------------------------------------------------------------------- /lib/opt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const 4 | Q = require('q'), 5 | 6 | CoaParam = require('./coaparam'), 7 | chalk = require('chalk'); 8 | 9 | /** 10 | * Option 11 | * 12 | * Named entity. Options may have short and long keys for use from command line. 13 | * 14 | * @namespace 15 | * @class Opt 16 | * @extends CoaParam 17 | */ 18 | module.exports = class Opt extends CoaParam { 19 | /** 20 | * @constructs 21 | * @param {COA.Cmd} cmd - parent command 22 | */ 23 | constructor(cmd) { 24 | super(cmd); 25 | 26 | this._short = null; 27 | this._long = null; 28 | this._flag = false; 29 | this._only = false; 30 | this._cmd._opts.push(this); 31 | } 32 | 33 | /** 34 | * Set a short key for option to be used with one hyphen from command line. 35 | * 36 | * @param {String} short - short name 37 | * @returns {COA.Opt} - this instance (for chainability) 38 | */ 39 | short(short) { 40 | this._short = short; 41 | this._cmd._optsByKey[`-${short}`] = this; 42 | return this; 43 | } 44 | 45 | /** 46 | * Set a short key for option to be used with double hyphens from command line. 47 | * 48 | * @param {String} long - long name 49 | * @returns {COA.Opt} - this instance (for chainability) 50 | */ 51 | long(long) { 52 | this._long = long; 53 | this._cmd._optsByKey[`--${long}`] = this; 54 | return this; 55 | } 56 | 57 | /** 58 | * Make an option boolean, i.e. option without value. 59 | * 60 | * @returns {COA.Opt} - this instance (for chainability) 61 | */ 62 | flag() { 63 | this._flag = true; 64 | return this; 65 | } 66 | 67 | /** 68 | * Makes an option to act as a command, 69 | * i.e. program will exit just after option action. 70 | * 71 | * @returns {COA.Opt} - this instance (for chainability) 72 | */ 73 | only() { 74 | this._only = true; 75 | return this; 76 | } 77 | 78 | /** 79 | * Add action for current option command. 80 | * This action is performed if the current option 81 | * is present in parsed options (with any value). 82 | * 83 | * @param {Function} act - action function, 84 | * invoked in the context of command instance 85 | * and has the parameters: 86 | * - {Object} opts - parsed options 87 | * - {Array} args - parsed arguments 88 | * - {Object} res - actions result accumulator 89 | * It can return rejected promise by Cmd.reject (in case of error) 90 | * or any other value treated as result. 91 | * @returns {COA.Opt} - this instance (for chainability) 92 | */ 93 | act(act) { 94 | // Need function here for arguments 95 | const opt = this; 96 | this._cmd.act(function(opts) { 97 | if(!opts.hasOwnProperty(opt._name)) return; 98 | 99 | const res = act.apply(this, arguments); 100 | if(!opt._only) return res; 101 | 102 | return Q.when(res, out => this.reject({ 103 | toString : () => out.toString(), 104 | exitCode : 0 105 | })); 106 | }); 107 | 108 | return this; 109 | } 110 | 111 | _saveVal(opts, val) { 112 | this._val && (val = this._val(val)); 113 | 114 | const name = this._name; 115 | this._arr 116 | ? (opts[name] || (opts[name] = [])).push(val) 117 | : (opts[name] = val); 118 | 119 | return val; 120 | } 121 | 122 | _parse(argv, opts) { 123 | return this._saveVal(opts, this._flag ? true : argv.shift()); 124 | } 125 | 126 | _checkParsed(opts) { 127 | return !opts.hasOwnProperty(this._name); 128 | } 129 | 130 | _usage() { 131 | const res = [], 132 | nameStr = this._name.toUpperCase(); 133 | 134 | if(this._short) { 135 | res.push('-', chalk.greenBright(this._short)); 136 | this._flag || res.push(' ' + nameStr); 137 | res.push(', '); 138 | } 139 | 140 | if(this._long) { 141 | res.push('--', chalk.green(this._long)); 142 | this._flag || res.push('=' + nameStr); 143 | } 144 | 145 | res.push(' : ', this._title); 146 | 147 | this._req && res.push(' ', chalk.redBright('(required)')); 148 | 149 | return res.join(''); 150 | } 151 | 152 | _requiredText() { 153 | return `Missing required option:\n ${this._usage()}`; 154 | } 155 | }; 156 | -------------------------------------------------------------------------------- /lib/shell.js: -------------------------------------------------------------------------------- 1 | module.exports = { escape, unescape }; 2 | 3 | function unescape(w) { 4 | w = w.charAt(0) === '"' 5 | ? w.replace(/^"|([^\\])"$/g, '$1') 6 | : w.replace(/\\ /g, ' '); 7 | 8 | return w.replace(/\\("|'|\$|`|\\)/g, '$1'); 9 | } 10 | 11 | function escape(w) { 12 | w = w.replace(/(["'$`\\])/g,'\\$1'); 13 | return w.match(/\s+/) ? `"${w}"` : w; 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cliui", 3 | "version": "7.0.4", 4 | "description": "easily create complex multi-column command-line-interfaces", 5 | "main": "build/index.cjs", 6 | "exports": { 7 | ".": [ 8 | { 9 | "import": "./index.mjs", 10 | "require": "./build/index.cjs" 11 | }, 12 | "./build/index.cjs" 13 | ] 14 | }, 15 | "type": "module", 16 | "module": "./index.mjs", 17 | "scripts": { 18 | "check": "standardx '**/*.ts' && standardx '**/*.js' && standardx '**/*.cjs'", 19 | "fix": "standardx --fix '**/*.ts' && standardx --fix '**/*.js' && standardx --fix '**/*.cjs'", 20 | "pretest": "rimraf build && tsc -p tsconfig.test.json && cross-env NODE_ENV=test npm run build:cjs", 21 | "test": "c8 mocha ./test/*.cjs", 22 | "test:esm": "c8 mocha ./test/esm/cliui-test.mjs", 23 | "postest": "check", 24 | "coverage": "c8 report --check-coverage", 25 | "precompile": "rimraf build", 26 | "compile": "tsc", 27 | "postcompile": "npm run build:cjs", 28 | "build:cjs": "rollup -c", 29 | "prepare": "npm run compile" 30 | }, 31 | "repository": "yargs/cliui", 32 | "standard": { 33 | "ignore": [ 34 | "**/example/**" 35 | ], 36 | "globals": [ 37 | "it" 38 | ] 39 | }, 40 | "keywords": [ 41 | "cli", 42 | "command-line", 43 | "layout", 44 | "design", 45 | "console", 46 | "wrap", 47 | "table" 48 | ], 49 | "author": "Ben Coe ", 50 | "license": "ISC", 51 | "dependencies": { 52 | "string-width": "^4.2.0", 53 | "strip-ansi": "^6.0.0", 54 | "wrap-ansi": "^7.0.0" 55 | }, 56 | "devDependencies": { 57 | "@types/node": "^14.0.27", 58 | "@typescript-eslint/eslint-plugin": "^4.0.0", 59 | "@typescript-eslint/parser": "^4.0.0", 60 | "@wessberg/rollup-plugin-ts": "^1.3.2", 61 | "c8": "^7.3.0", 62 | "chai": "^4.2.0", 63 | "chalk": "^4.1.0", 64 | "cross-env": "^7.0.2", 65 | "eslint": "^7.6.0", 66 | "eslint-plugin-import": "^2.22.0", 67 | "eslint-plugin-node": "^11.1.0", 68 | "gts": "^3.0.0", 69 | "mocha": "^8.1.1", 70 | "rimraf": "^3.0.2", 71 | "rollup": "^2.23.1", 72 | "standardx": "^7.0.0", 73 | "typescript": "^4.0.0" 74 | }, 75 | "files": [ 76 | "build", 77 | "index.mjs", 78 | "!*.d.ts" 79 | ], 80 | "engine": { 81 | "node": ">=10" 82 | } 83 | } 84 | --------------------------------------------------------------------------------