cli-api 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/app-help.ts DELETED
@@ -1,34 +0,0 @@
1
- import {App} from './interfaces'
2
- import {getProcName, print, printLn, space} from './utils'
3
- import Chalk from 'chalk'
4
- import stringWidth from 'string-width'
5
-
6
- export function printHelp(app: App) {
7
- print(Chalk.green(app.name))
8
- if (app.version) {
9
- print(` version ${Chalk.yellow(app.version)}`)
10
- }
11
- print('\n\n')
12
- printLn(Chalk.yellow("Usage:"))
13
- printLn(` ${Chalk.cyan(getProcName(app))} ${Chalk.gray('<')}command${Chalk.gray('>')} ${Chalk.gray(`[options] [arguments]`)}\n`)
14
-
15
- if (app.globalOptions) {
16
- printLn("TODO")
17
- }
18
-
19
- printAvailableCommands(app)
20
- }
21
-
22
- export function printAvailableCommands(app: App) {
23
- printLn(Chalk.yellow("Available commands:"))
24
- const width = Math.max(...app.commands.map(c => stringWidth(c.name))) + 2
25
- for (const cmd of app.commands) {
26
- print(` ${Chalk.green(cmd.name)}`)
27
- if (cmd.description) {
28
- print(`${space(width, cmd.name)}${cmd.description}`)
29
- }
30
- printLn()
31
- }
32
-
33
- // printLn()
34
- }
@@ -1,28 +0,0 @@
1
- import {App, Command} from '../interfaces'
2
- import {getCommand} from '../options'
3
- import {printCommandHelp} from '../print-command-help'
4
- import {printAvailableCommands} from '../app-help'
5
- import {printLn} from '../utils'
6
-
7
- export const helpCommand: Command = {
8
- name: 'help',
9
- // alias: '--help',
10
- description: "Displays help for a command",
11
- arguments: [
12
- {
13
- name: "command",
14
- description: "The command name.",
15
- required: false,
16
- }
17
- ],
18
- async execute(options: Record<string, string>, [commandName]: string[], app: App) {
19
- if(commandName) {
20
- printCommandHelp(app, getCommand(commandName, app))
21
- } else {
22
- printCommandHelp(app, getCommand('help', app))
23
- printLn()
24
- printAvailableCommands(app)
25
- }
26
- }
27
- }
28
-
@@ -1,11 +0,0 @@
1
- import {Command} from '../interfaces'
2
- import {printLn} from '../utils'
3
-
4
- export const versionCommand: Command = {
5
- name: 'version',
6
- // alias: '--version',
7
- description: "Displays current version",
8
- async execute(opts, args, app) {
9
- printLn(app.version)
10
- }
11
- }
package/src/constants.ts DELETED
@@ -1,4 +0,0 @@
1
- export const EMPTY_ARRAY: ReadonlyArray<any> = Object.freeze([])
2
- export const EMPTY_OBJECT: Record<string,any> = Object.freeze(Object.create({__proto__:null}))
3
- export const TRUE_VALUES = new Set(['y', 'yes', 't', 'true', '1', 'on'])
4
- export const FALSE_VALUES = new Set(['n', 'no', 'f', 'false', '0', 'off'])
package/src/index.ts DELETED
@@ -1,3 +0,0 @@
1
- export type {App, Command, Option} from "./interfaces"
2
- export {OptType} from './interfaces'
3
- export {default as default} from './run'
package/src/interfaces.ts DELETED
@@ -1,89 +0,0 @@
1
- export interface Command {
2
- name: string
3
- alias?: string|string[]
4
- description?: string
5
- longDescription?: string
6
- flags?: Flag[]
7
- options?: Option[]
8
- arguments?: Argument[]
9
- /**
10
- * Executed when the command matches.
11
- *
12
- * @param options Named arguments, options, and flags.
13
- * @param args Positional arguments.
14
- * @param app Entire app config.
15
- */
16
- execute(options: Record<string,any>, args: string[], app: App): Promise<number|void>
17
- }
18
-
19
- export enum OptType {
20
- STRING,
21
- BOOL,
22
- INT,
23
- FLOAT,
24
- /** A string, truncated and converted to lowercase. */
25
- ENUM,
26
- /** File must be readable. Single dash will be converted to STDIN. */
27
- INPUT_FILE,
28
- /** Directory must be readable. */
29
- INPUT_DIRECTORY,
30
- /** File's directory must exist and be writeable. Single dash will be converted to STDOUT. */
31
- OUTPUT_FILE,
32
- OUTPUT_DIRECTORY,
33
- /** An empty or non-existent directory. */
34
- EMPTY_DIRECTORY,
35
- }
36
-
37
- interface ArgumentOrOptionOrFlag {
38
- /** Name of the option to display in help. */
39
- name: string
40
- /** Alternative name for this option. */
41
- alias?: string|string[]
42
- /** Description of the option. */
43
- description?: string
44
- /** Default value if not provided. */
45
- defaultValue?: any|((value:string)=>any)
46
- /** Default value to display in help. */
47
- defaultValueText?: string
48
- /** Property name to use in `execute()` options. */
49
- key?: string
50
- }
51
-
52
- export type AnyOptType = OptType | string[] // | ((value:string)=>any)
53
-
54
- export interface ArgumentOrOption extends ArgumentOrOptionOrFlag {
55
- /** Type to coerce the option value to. */
56
- type?: AnyOptType
57
- /** Option is repeatable by specifying the flag again. Value will be an array. */
58
- repeatable?: boolean
59
- /** Option is required. */
60
- required?: boolean
61
- }
62
-
63
- /** Boolean flag. */
64
- export interface Flag extends ArgumentOrOptionOrFlag,OptionOrFlag {
65
-
66
- }
67
-
68
- /** Positional argument. */
69
- export interface Argument extends ArgumentOrOption {
70
-
71
- }
72
-
73
- interface OptionOrFlag {
74
- valueNotRequired?: boolean
75
- }
76
-
77
- /** Option with value. */
78
- export interface Option extends ArgumentOrOption,OptionOrFlag {
79
- /** Placeholder value to use in help. */
80
- valuePlaceholder?: string
81
- }
82
-
83
- export interface App {
84
- name: string
85
- argv0?: string
86
- version?: string
87
- commands: Command[]
88
- globalOptions?: Option[]
89
- }
package/src/options.ts DELETED
@@ -1,266 +0,0 @@
1
- import {AnyOptType, App, Command, Option, OptType} from './interfaces'
2
- import {abort, includes, resolve, statSync, toArray, toBool} from './utils'
3
- import Chalk from 'chalk'
4
- import Path from 'path'
5
- import FileSys from 'fs'
6
-
7
- export function formatOption(opt: Option): [string, string] {
8
- const aliases: string[] = []
9
- if (opt.alias) {
10
- if (Array.isArray(opt.alias)) {
11
- aliases.push(...opt.alias)
12
- } else {
13
- aliases.push(opt.alias)
14
- }
15
- }
16
- aliases.push(opt.name)
17
- let flags = aliases.map(a => Chalk.green(a.length === 1 ? `-${a}` : `--${a}`)).join(', ')
18
- let valuePlaceholder = getValuePlaceholder(opt)
19
- if (opt.type !== OptType.BOOL) {
20
- flags += `=${valuePlaceholder}`
21
- }
22
- let desc = opt.description ?? ''
23
- let defaultValueText = opt.defaultValueText
24
- if (defaultValueText === undefined && opt.defaultValue !== undefined) {
25
- defaultValueText = JSON.stringify(resolve(opt.defaultValue))
26
- }
27
- if (defaultValueText !== undefined) {
28
- desc += Chalk.yellow(` [default: ${defaultValueText}]`)
29
- }
30
- return [flags, desc]
31
- }
32
-
33
- export function getValuePlaceholder(opt: Option): string {
34
- if (opt.valuePlaceholder !== undefined) {
35
- return opt.valuePlaceholder
36
- }
37
- if (Array.isArray(opt.type)) {
38
- return opt.type.join('|')
39
- } else if (opt.type == OptType.BOOL) {
40
- return JSON.stringify(!resolve(opt.defaultValue))
41
- } else if (opt.type === OptType.INT || opt.type === OptType.FLOAT) {
42
- return '#'
43
- } else if (opt.type === OptType.INPUT_FILE || opt.type === OptType.OUTPUT_FILE) {
44
- return 'FILE'
45
- } else if (opt.type === OptType.INPUT_DIRECTORY || opt.type === OptType.OUTPUT_DIRECTORY || opt.type === OptType.EMPTY_DIRECTORY) {
46
- return 'DIR'
47
- } else {
48
- return opt.name
49
- }
50
- }
51
-
52
- export function getOptions(cmd: Command): Option[] {
53
- return [
54
- ...toArray(cmd.options),
55
- ...toArray(cmd.flags).map(f => ({
56
- ...f,
57
- valueNotRequired: true,
58
- type: OptType.BOOL,
59
-
60
- })),
61
- // {
62
- // name: 'help',
63
- // description: "Print help for this command",
64
- // valueNotRequired: true,
65
- // type: OptType.BOOL,
66
- // }
67
- ] as Option[]
68
- }
69
-
70
- export function parseArgs(cmd: Command, argv: string[]): [any[], Record<string, any>] {
71
- const args: any[] = []
72
- const opts: Record<string, any> = Object.create(null)
73
- let parseFlags = true
74
- // TODO: initialize all repeatables to empty arrays
75
-
76
- const allOptions = getOptions(cmd)
77
-
78
- let argIdx = 0
79
- for (let i = 0; i < argv.length; ++i) {
80
- let arg = argv[i]
81
-
82
- if (parseFlags && arg === '--') {
83
- parseFlags = false
84
- continue
85
- }
86
-
87
- if (parseFlags && arg.length >= 2 && arg.startsWith('-')) {
88
- let name: string
89
- let value: any
90
- if (arg.includes('=')) {
91
- [arg, value] = arg.split('=', 2)
92
- } /*else if(i < argv.length - 2 && argv[i+1] === '=') {
93
- value = argv[i+2]
94
- i += 2
95
- }*/
96
- if (arg.startsWith('--')) {
97
- name = arg.slice(2)
98
- } else {
99
- if (arg.length > 2) {
100
- if (value !== undefined) {
101
- abort(`Malformed option "${arg}"`)
102
- }
103
- value = arg.slice(2)
104
- arg = arg.slice(0, 2)
105
- }
106
- name = arg.slice(1)
107
- // TODO: parse multiple single-char flags
108
- }
109
-
110
- const opt = allOptions.find(opt => opt.name === name || includes(name, opt.alias))
111
- if (!opt) {
112
- abort(`"${cmd.name}" command does not have option "${name}".`)
113
- }
114
- if (value === undefined) {
115
- if (opt.valueNotRequired) {
116
- value = !resolve(opt.defaultValue)
117
- } else if (i < argv.length - 1) {
118
- value = argv[++i]
119
- } else {
120
- abort(`Missing required value for option "${arg}"`)
121
- }
122
- }
123
- if (opt.type != null) {
124
- value = coerceType(value, opt.type)
125
- }
126
- opts[opt.key ?? opt.name] = value
127
- } else {
128
- // TODO: examine cmd.arguments
129
- let value: any = arg
130
-
131
- if (cmd.arguments && cmd.arguments.length > argIdx) {
132
- const cmdArg = cmd.arguments[argIdx]
133
- if (cmdArg.type != null) {
134
- value = coerceType(value, cmdArg.type)
135
- }
136
- if (cmdArg.key) {
137
- opts[cmdArg.key] = value
138
- }
139
- }
140
- args.push(value)
141
- ++argIdx
142
- }
143
- }
144
-
145
- if (allOptions.length) {
146
- for (const opt of allOptions) {
147
- const k = opt.key ?? opt.name
148
- if (opts[k] === undefined) {
149
- if (opt.defaultValue !== undefined) {
150
- opts[k] = resolve(opt.defaultValue)
151
- } else if (opt.required) {
152
- throw new Error(`"${getOptName(opt)}" option is required`)
153
- } else {
154
- // TODO: should we fill in undefined options? with `null` or `undefined`?
155
- }
156
- }
157
- }
158
- }
159
-
160
- if (cmd.arguments?.length) {
161
- for (let i = 0; i < cmd.arguments.length; ++i) {
162
- if (cmd.arguments[i].required && argIdx <= i) {
163
- throw new Error(`"${cmd.arguments[i].name}" argument is required`)
164
- }
165
- }
166
- }
167
-
168
- // TODO: fill global options into opts
169
- // TODO: copy named arguments into opts too
170
- return [args, opts]
171
- }
172
-
173
- function coerceType(value: string, type: AnyOptType) {
174
- if (Array.isArray(type)) {
175
- // TODO: search for closest match of value in type or throw error
176
- return String(value).trim().toLowerCase()
177
- }
178
- switch (type) {
179
- case OptType.BOOL:
180
- return toBool(value)
181
- case OptType.INT:
182
- return Math.trunc(Number(value))
183
- case OptType.FLOAT:
184
- return Number(value)
185
- case OptType.ENUM:
186
- return String(value).trim().toLowerCase()
187
- case OptType.STRING:
188
- return String(value)
189
- case OptType.INPUT_FILE: {
190
- if (value === '-') return '/dev/stdin' // TODO: support windows
191
- const file = Path.normalize(value)
192
- const fullPath = Path.resolve(file)
193
- const stat = statSync(file)
194
- if (!stat) {
195
- throw new Error(`File ${Chalk.underline(fullPath)} does not exist`)
196
- }
197
- if (!stat.isFile()) {
198
- throw new Error(`${Chalk.underline(fullPath)} is not a file`)
199
- }
200
- try {
201
- FileSys.accessSync(file, FileSys.constants.R_OK)
202
- } catch (err) {
203
- throw new Error(`${Chalk.underline(fullPath)} is not readable`)
204
- }
205
- return file
206
- }
207
- case OptType.INPUT_DIRECTORY:
208
- const dir = Path.normalize(value)
209
- FileSys.accessSync(dir, FileSys.constants.X_OK)
210
- return dir
211
- case OptType.OUTPUT_FILE: {
212
- if (value === '-') return '/dev/stdout' // TODO: support windows
213
- const file = Path.normalize(value)
214
- const stat = statSync(file)
215
- if (stat) {
216
- if (!stat.isFile()) {
217
- throw new Error(`'${file}' is not a file`)
218
- }
219
- // if((stat.mode & 0x222) === 0) { // TODO: does this work?
220
- // throw new Error(`'${value}' is not writeable`);
221
- // }
222
- FileSys.accessSync(file, FileSys.constants.W_OK)
223
- } else {
224
- FileSys.accessSync(Path.dirname(file), FileSys.constants.W_OK)
225
- }
226
- return file
227
- }
228
- case OptType.OUTPUT_DIRECTORY: {
229
- FileSys.accessSync(value, FileSys.constants.W_OK)
230
- return Path.normalize(value)
231
- }
232
- case OptType.EMPTY_DIRECTORY: {
233
- const dir = Path.normalize(value)
234
- let files = []
235
- try {
236
- files = FileSys.readdirSync(dir)
237
- } catch (err) {
238
- if (err.code === 'ENOENT') {
239
- FileSys.accessSync(Path.dirname(dir), FileSys.constants.W_OK)
240
- } else {
241
- throw err
242
- }
243
- }
244
- if (files.length) {
245
- throw new Error(`${Chalk.underline(dir)} is not empty`)
246
- }
247
- return dir
248
- }
249
- }
250
- return value
251
- }
252
-
253
-
254
- export function getOptName(opt: Option) {
255
- return (opt.name.length > 1 ? '--' : '-') + opt.name
256
- }
257
-
258
- export function getCommand(name: string, app: App): Command {
259
- const cmdName = String(name).trim().replace(/^-{1,2}/,'').toLowerCase()
260
- const cmd = app.commands.find(c => c.name === cmdName || includes(cmdName, c.alias))
261
- if (cmd === undefined) {
262
- // TODO: levenshtein search for closest match? "Did you mean...?"
263
- throw new Error(`Command "${name}" does not exist.`)
264
- }
265
- return cmd
266
- }
@@ -1,78 +0,0 @@
1
- import {App, Command, OptType} from './interfaces'
2
- import {getProcName, print, printLn, space, toArray} from './utils'
3
- import Chalk from 'chalk'
4
- import {formatOption, getOptions, getOptName, getValuePlaceholder} from './options'
5
- import stringWidth from 'string-width'
6
-
7
-
8
- export function printCommandHelp(app: App, cmd: Command) {
9
- if (cmd.description) {
10
- printLn(cmd.description)
11
- printLn()
12
- }
13
-
14
- printLn(Chalk.yellow("Usage:"))
15
- print(` ${Chalk.cyan(getProcName(app))} ${cmd.name}`)
16
-
17
- const allOptions = getOptions(cmd)
18
-
19
- if (allOptions.length) {
20
- let otherOptions = 0
21
- for (let opt of allOptions) {
22
- if (opt.required) {
23
- print(` ${getOptName(opt)}`)
24
- if (opt.type !== OptType.BOOL) {
25
- print(`=${getValuePlaceholder(opt)}`)
26
- }
27
- } else {
28
- ++otherOptions
29
- }
30
- }
31
- if (otherOptions) {
32
- print(` ${Chalk.gray('[')}options${Chalk.gray(']')}`)
33
- }
34
- }
35
- if (cmd.arguments?.length) {
36
- print(` ${Chalk.grey('[')}--${Chalk.grey(']')}`)
37
- for (const arg of cmd.arguments) {
38
- print(' ')
39
- print(Chalk.grey(arg.required ? '<' : '['))
40
- if (arg.repeatable) {
41
- print(Chalk.grey('...'))
42
- }
43
- print(arg.name)
44
- print(Chalk.grey(arg.required ? '>' : ']'))
45
- }
46
- }
47
- printLn()
48
-
49
- if (allOptions.length) {
50
- printLn(Chalk.yellow("\nOptions:"))
51
- const lines = allOptions.map(formatOption)
52
- const width = Math.max(...lines.map(l => stringWidth(l[0])))
53
- for (const line of lines) {
54
- printLn(' ' + line[0] + space(width + 2, line[0]) + line[1])
55
- }
56
- }
57
-
58
- if (cmd.arguments?.length) {
59
- printLn(Chalk.yellow("\nArguments:"))
60
- const width = Math.max(...cmd.arguments.map(a => stringWidth(a.name)))
61
- for (const arg of cmd.arguments) {
62
- print(' ' + Chalk.green(arg.name))
63
- if (arg.description) {
64
- print(space(width + 2, arg.name) + arg.description)
65
- }
66
- printLn()
67
- }
68
- }
69
-
70
- if (cmd.alias) {
71
- const alaises = toArray(cmd.alias)
72
- printLn(Chalk.yellow(`\nAlias${alaises.length !== 1 ? 'es' : ''}: `) + toArray(cmd.alias).join(Chalk.gray(', ')))
73
- }
74
- if (cmd.longDescription) {
75
- printLn(Chalk.yellow("\nDescription:"))
76
- printLn(' ' + cmd.longDescription)
77
- }
78
- }
package/src/run.ts DELETED
@@ -1,45 +0,0 @@
1
- import {App} from './interfaces'
2
- import {helpCommand} from './commands/command-help'
3
- import {versionCommand} from './commands/version'
4
- import {printHelp} from './app-help'
5
- import {getCommand, parseArgs} from './options'
6
- import {abort, sortBy} from './utils'
7
- import {printCommandHelp} from './print-command-help'
8
-
9
- export default function run(app: App) {
10
- app = {
11
- ...app,
12
- commands: [...sortBy(app.commands, c => c.name), versionCommand,helpCommand],
13
- // commands: sortBy([...app.commands,versionCommand,helpCommand], c => c.name),
14
- }
15
- if (process.argv.length <= 2) {
16
- printHelp(app)
17
- process.exit(0)
18
- }
19
-
20
- const cmd = getCommand(process.argv[2], app)
21
- const rawArgs = process.argv.slice(3)
22
- if (rawArgs.includes('--help')) {
23
- printCommandHelp(app, cmd)
24
- process.exit(0)
25
- }
26
- let args, opts
27
- try {
28
- [args, opts] = parseArgs(cmd, rawArgs)
29
- } catch (err) {
30
- abort(String(err.message))
31
- process.exit(2)
32
- }
33
- Promise.resolve(cmd.execute(opts, args, app))
34
- .then(code => {
35
- if (code != null) {
36
- process.exit(code)
37
- }
38
- process.exit(0)
39
- }, err => {
40
- // console.error(err)
41
- abort(String(err.stack))
42
- process.exit(1)
43
- })
44
-
45
- }
package/src/utils.ts DELETED
@@ -1,86 +0,0 @@
1
- import {App} from './interfaces'
2
- import Path from 'path'
3
- import stringWidth from 'string-width'
4
- import Chalk from 'chalk'
5
- import {EMPTY_ARRAY, FALSE_VALUES, TRUE_VALUES} from './constants'
6
- import FileSys from 'fs'
7
-
8
- export const print = process.stdout.write.bind(process.stdout)
9
- export const printLn = console.log.bind(console)
10
-
11
- function blockError(str: string) {
12
- const lines = str.split('\n')
13
- const width = Math.max(...lines.map(l => stringWidth(l))) + 4
14
- printLn(Chalk.bgRed(space(width)))
15
- for (const line of lines) {
16
- const txt = ` ${line}`
17
- printLn(Chalk.bgRed(txt + space(width, txt)))
18
- }
19
- printLn(Chalk.bgRed(space(width)))
20
- }
21
-
22
- export function abort(message: string, code: number = 1): never {
23
- blockError(message)
24
- process.exit(code)
25
- }
26
-
27
- export function toArray<T>(x: T | T[]): readonly T[] {
28
- if (!x) return EMPTY_ARRAY
29
- return Array.isArray(x) ? x : [x]
30
- }
31
-
32
- export function resolve<T>(x: any): T {
33
- return typeof x === 'function' ? x() : x
34
- }
35
-
36
- export function toBool(str: string | boolean): boolean {
37
- if (typeof str === 'boolean') return str
38
- str = String(str).trim().toLowerCase()
39
- if (TRUE_VALUES.has(str)) {
40
- return true
41
- }
42
- if (FALSE_VALUES.has(str)) {
43
- return false
44
- }
45
- throw new Error(`Could not cast "${str}" to boolean`)
46
- }
47
-
48
- export function space(len: number, str?: string) {
49
- if (str) {
50
- len -= stringWidth(str)
51
- }
52
-
53
- return len > 0 ? ' '.repeat(len) : ''
54
- }
55
-
56
- export function getProcName(app: App) {
57
- if (app.argv0 != null) {
58
- return app.argv0
59
- }
60
- const relPath = Path.relative(process.cwd(), process.argv[1])
61
- // console.log(relPath, process.argv[1])
62
- // console.log(process.argv0,process.argv[0])
63
- return `${Path.basename(process.argv[0])} ${relPath.length < process.argv[1].length ? relPath : process.argv[1]}`
64
- }
65
-
66
- export function includes(needle: string, haystack: string | string[] | undefined) {
67
- if (!haystack) return false
68
- if (Array.isArray(haystack)) return haystack.includes(needle)
69
- return needle === haystack
70
- }
71
-
72
- export function statSync(path: string): FileSys.Stats | null {
73
- try {
74
- return FileSys.lstatSync(path)
75
- } catch {
76
- return null
77
- }
78
- }
79
-
80
- export function sortBy<T>(arr: T[], cmp: (x: T) => string): T[] {
81
- const collator = new Intl.Collator() // 'en',{usage: 'sort',sensitivity:'base'}
82
- const values = arr.map(cmp)
83
- const keys = Array.from(arr.keys())
84
- keys.sort((a, b) => collator.compare(values[a], values[b]))
85
- return keys.map(i => arr[i])
86
- }