breadc 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Breadc
2
2
 
3
- [![CI](https://github.com/yjl9903/Breadc/actions/workflows/ci.yml/badge.svg)](https://github.com/yjl9903/Breadc/actions/workflows/ci.yml)
3
+ [![version](https://img.shields.io/npm/v/breadc?color=rgb%2850%2C203%2C86%29&label=Breadc)](https://www.npmjs.com/package/breadc) [![CI](https://github.com/yjl9903/Breadc/actions/workflows/ci.yml/badge.svg)](https://github.com/yjl9903/Breadc/actions/workflows/ci.yml)
4
4
 
5
- Yet another Command Line Application Framework powered by [minimist](https://www.npmjs.com/package/minimist).
5
+ Yet another Command Line Application Framework powered by [minimist](https://www.npmjs.com/package/minimist), but with fully [TypeScript](https://www.typescriptlang.org/) support.
6
6
 
7
7
  ## Installation
8
8
 
@@ -12,14 +12,57 @@ npm i breadc
12
12
 
13
13
  ## Usage
14
14
 
15
+ Try [./examples/echo.ts](./examples/echo.ts).
16
+
15
17
  ```ts
16
18
  import Breadc from 'breadc'
17
19
 
18
- const cli = Breadc('cli', { version: '1.0.0' })
20
+ const cli = Breadc('echo', { version: '1.0.0' })
21
+ .option('--host <host>')
22
+ .option('--port <port>')
23
+
24
+ cli
25
+ .command('[message]')
26
+ .action((message, option) => {
27
+ const host = option.host;
28
+ const port = option.port;
29
+ console.log(`Host: ${host}`);
30
+ console.log(`Port: ${port}`);
31
+ })
32
+
33
+ cli.run(process.argv.slice(2))
34
+ .catch(err => cli.logger.error(err.message))
35
+ ```
36
+
37
+ If you are using IDEs that support TypeScript (like [Visual Studio Code](https://code.visualstudio.com/)), move your cursor to the parameter `option` in the default command, and then you will find the `option` is automatically typed with `{ host: string | boolean, port: string | boolean }`.
38
+
39
+ ![vscode](./images/vscode.png)
40
+
41
+ ### Limitation
19
42
 
20
- cli.parse(process.argv.slice(2))
43
+ For the limitation of TypeScript, in the command format string, you can only write up to **5** pieces. That is to say, you can only write format string like `<p1> <p2> <p3> <p4> [p5]`, but `<p1> <p2> <p3> <p4> <p5> [p6]` does not work.
44
+
45
+ You should always use method chaining when registering options and commands. The example below will fail to infer the option `--host`.
46
+
47
+ ```ts
48
+ const cli = Breadc('cli')
49
+
50
+ cli
51
+ .option('--host')
52
+
53
+ cli
54
+ .option('--port')
55
+ .command('')
56
+ .action((option) => {
57
+ // The type of option is Record<'port', string>
58
+ })
21
59
  ```
22
60
 
61
+ ## Inspiration
62
+
63
+ + [cac](https://github.com/cacjs/cac): Simple yet powerful framework for building command-line apps.
64
+ + [TypeScript: Documentation - Template Literal Types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html)
65
+
23
66
  ## License
24
67
 
25
68
  MIT License © 2021 [XLor](https://github.com/yjl9903)
package/dist/index.cjs CHANGED
@@ -1,62 +1,404 @@
1
1
  'use strict';
2
2
 
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ const kolorist = require('kolorist');
3
6
  const minimist = require('minimist');
7
+ const createDebug = require('debug');
4
8
 
5
9
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; }
6
10
 
11
+ const kolorist__default = /*#__PURE__*/_interopDefaultLegacy(kolorist);
7
12
  const minimist__default = /*#__PURE__*/_interopDefaultLegacy(minimist);
13
+ const createDebug__default = /*#__PURE__*/_interopDefaultLegacy(createDebug);
14
+
15
+ function createDefaultLogger(name, logger) {
16
+ if (!!logger && typeof logger === "object") {
17
+ return logger;
18
+ }
19
+ const debug = createDebug__default(name + ":breadc");
20
+ const println = !!logger && typeof logger === "function" ? logger : (message, ...args) => {
21
+ console.log(message, ...args);
22
+ };
23
+ return {
24
+ println,
25
+ info(message, ...args) {
26
+ println(`${kolorist.blue("INFO")} ${message}`, ...args);
27
+ },
28
+ warn(message, ...args) {
29
+ println(`${kolorist.yellow("WARN")} ${message}`, ...args);
30
+ },
31
+ error(message, ...args) {
32
+ println(`${kolorist.red("ERROR")} ${message}`, ...args);
33
+ },
34
+ debug(message, ...args) {
35
+ debug(message, ...args);
36
+ }
37
+ };
38
+ }
39
+
40
+ const _Option = class {
41
+ constructor(format, config = {}) {
42
+ this.format = format;
43
+ const match = _Option.OptionRE.exec(format);
44
+ if (match) {
45
+ if (match[3]) {
46
+ this.type = "string";
47
+ } else {
48
+ this.type = "boolean";
49
+ }
50
+ this.name = match[2];
51
+ if (match[1]) {
52
+ this.shortcut = match[1][1];
53
+ }
54
+ } else {
55
+ throw new Error(`Can not parse option format from "${format}"`);
56
+ }
57
+ this.description = config.description ?? "";
58
+ this.required = format.indexOf("<") !== -1;
59
+ this.default = config.default;
60
+ this.construct = config.construct ?? ((text) => text ?? config.default ?? void 0);
61
+ }
62
+ };
63
+ let Option = _Option;
64
+ Option.OptionRE = /^(-[a-zA-Z], )?--([a-zA-Z.]+)( \[[a-zA-Z]+\]| <[a-zA-Z]+>)?$/;
65
+
66
+ const _Command = class {
67
+ constructor(format, config) {
68
+ this.options = [];
69
+ this.format = config.condition ? [format] : format.split(" ").map((t) => t.trim()).filter(Boolean);
70
+ this.default = this.format.length === 0 || this.format[0][0] === "[" || this.format[0][0] === "<";
71
+ this.description = config.description ?? "";
72
+ this.conditionFn = config.condition;
73
+ this.logger = config.logger;
74
+ if (this.format.length > _Command.MaxDep) {
75
+ this.logger.warn(`Command format string "${format}" is too long`);
76
+ }
77
+ }
78
+ option(format, configOrDescription = "", otherConfig = {}) {
79
+ const config = typeof configOrDescription === "object" ? configOrDescription : { ...otherConfig, description: configOrDescription };
80
+ try {
81
+ const option = new Option(format, config);
82
+ this.options.push(option);
83
+ } catch (error) {
84
+ this.logger.warn(error.message);
85
+ }
86
+ return this;
87
+ }
88
+ get hasConditionFn() {
89
+ return !!this.conditionFn;
90
+ }
91
+ shouldRun(args) {
92
+ if (this.conditionFn) {
93
+ return this.conditionFn(args);
94
+ } else {
95
+ if (this.default)
96
+ return true;
97
+ const isCmd = (t) => t[0] !== "[" && t[0] !== "<";
98
+ for (let i = 0; i < this.format.length; i++) {
99
+ if (!isCmd(this.format[i])) {
100
+ return true;
101
+ }
102
+ if (i >= args["_"].length || this.format[i] !== args["_"][i]) {
103
+ return false;
104
+ }
105
+ }
106
+ return true;
107
+ }
108
+ }
109
+ parseArgs(args, globalOptions) {
110
+ if (this.conditionFn) {
111
+ const argumentss2 = args["_"];
112
+ const options2 = args;
113
+ delete options2["_"];
114
+ return {
115
+ command: this,
116
+ arguments: argumentss2,
117
+ options: args
118
+ };
119
+ }
120
+ const isCmd = (t) => t[0] !== "[" && t[0] !== "<";
121
+ const argumentss = [];
122
+ for (let i = 0; i < this.format.length; i++) {
123
+ if (isCmd(this.format[i]))
124
+ continue;
125
+ if (i < args["_"].length) {
126
+ if (this.format[i].startsWith("[...")) {
127
+ argumentss.push(args["_"].slice(i).map(String));
128
+ } else {
129
+ argumentss.push(String(args["_"][i]));
130
+ }
131
+ } else {
132
+ if (this.format[i].startsWith("<")) {
133
+ this.logger.warn(`You should provide the argument "${this.format[i]}"`);
134
+ argumentss.push(void 0);
135
+ } else if (this.format[i].startsWith("[...")) {
136
+ argumentss.push([]);
137
+ } else if (this.format[i].startsWith("[")) {
138
+ argumentss.push(void 0);
139
+ } else {
140
+ this.logger.warn(`unknown format string ("${this.format[i]}")`);
141
+ }
142
+ }
143
+ }
144
+ const fullOptions = globalOptions.concat(this.options).reduce((map, o) => {
145
+ map.set(o.name, o);
146
+ return map;
147
+ }, /* @__PURE__ */ new Map());
148
+ const options = args;
149
+ delete options["_"];
150
+ for (const [name, rawOption] of fullOptions) {
151
+ if (rawOption.required) {
152
+ if (options[name] === void 0) {
153
+ options[name] = false;
154
+ } else if (options[name] === "") {
155
+ options[name] = true;
156
+ }
157
+ } else {
158
+ if (options[name] === false) {
159
+ options[name] = void 0;
160
+ } else if (!(name in options)) {
161
+ options[name] = void 0;
162
+ }
163
+ }
164
+ }
165
+ return {
166
+ command: this,
167
+ arguments: argumentss,
168
+ options
169
+ };
170
+ }
171
+ action(fn) {
172
+ this.actionFn = fn;
173
+ return this;
174
+ }
175
+ async run(...args) {
176
+ if (this.actionFn) {
177
+ this.actionFn(...args);
178
+ } else {
179
+ this.logger.warn(`You may miss action function in "${this.format}"`);
180
+ }
181
+ }
182
+ };
183
+ let Command = _Command;
184
+ Command.MaxDep = 5;
185
+ function createHelpCommand(breadc) {
186
+ let helpCommand = void 0;
187
+ return new Command("-h, --help", {
188
+ condition(args) {
189
+ const isEmpty = !args["--"]?.length;
190
+ if ((args.help || args.h) && isEmpty) {
191
+ if (args["_"].length > 0) {
192
+ for (const cmd of breadc.commands) {
193
+ if (!cmd.hasConditionFn && !cmd.default && cmd.shouldRun(args)) {
194
+ helpCommand = cmd;
195
+ return true;
196
+ }
197
+ }
198
+ }
199
+ return true;
200
+ } else {
201
+ return false;
202
+ }
203
+ },
204
+ logger: breadc.logger
205
+ }).action(() => {
206
+ for (const line of breadc.help(helpCommand)) {
207
+ breadc.logger.println(line);
208
+ }
209
+ });
210
+ }
211
+ function createVersionCommand(breadc) {
212
+ return new Command("-v, --version", {
213
+ condition(args) {
214
+ const isEmpty = !args["_"].length && !args["--"]?.length;
215
+ if (args.version && isEmpty) {
216
+ return true;
217
+ } else if (args.v && isEmpty) {
218
+ return true;
219
+ } else {
220
+ return false;
221
+ }
222
+ },
223
+ logger: breadc.logger
224
+ }).action(() => {
225
+ breadc.logger.println(breadc.version());
226
+ });
227
+ }
8
228
 
9
229
  class Breadc {
10
230
  constructor(name, option) {
11
231
  this.options = [];
232
+ this.commands = [];
12
233
  this.name = name;
13
- this.version = option.version;
234
+ this._version = option.version ?? "unknown";
235
+ this.description = option.description;
236
+ this.logger = createDefaultLogger(name, option.logger);
237
+ const breadc = {
238
+ name: this.name,
239
+ version: () => this.version.call(this),
240
+ help: (command) => this.help.call(this, command),
241
+ logger: this.logger,
242
+ options: this.options,
243
+ commands: this.commands
244
+ };
245
+ this.commands.push(createVersionCommand(breadc), createHelpCommand(breadc));
246
+ }
247
+ version() {
248
+ return `${this.name}/${this._version}`;
249
+ }
250
+ help(command) {
251
+ const output = [];
252
+ const println = (msg) => output.push(msg);
253
+ println(this.version());
254
+ if (!command) {
255
+ if (this.description) {
256
+ println("");
257
+ if (Array.isArray(this.description)) {
258
+ for (const line of this.description) {
259
+ println(line);
260
+ }
261
+ } else {
262
+ println(this.description);
263
+ }
264
+ }
265
+ } else {
266
+ if (command.description) {
267
+ println("");
268
+ println(command.description);
269
+ }
270
+ }
271
+ if (!command) {
272
+ if (this.defaultCommand) {
273
+ println(``);
274
+ println(`Usage:`);
275
+ println(` $ ${this.name} ${this.defaultCommand.format.join(" ")}`);
276
+ }
277
+ } else {
278
+ println(``);
279
+ println(`Usage:`);
280
+ println(` $ ${this.name} ${command.format.join(" ")}`);
281
+ }
282
+ if (!command && this.commands.length > 2) {
283
+ println(``);
284
+ println(`Commands:`);
285
+ const commandHelps = this.commands.filter((c) => !c.hasConditionFn).map((c) => [` $ ${this.name} ${c.format.join(" ")}`, c.description]);
286
+ for (const line of twoColumn(commandHelps)) {
287
+ println(line);
288
+ }
289
+ }
290
+ println(``);
291
+ println(`Options:`);
292
+ const optionHelps = [].concat([
293
+ ...command ? command.options.map((o) => [` ${o.format}`, o.description]) : [],
294
+ ...this.options.map((o) => [` ${o.format}`, o.description]),
295
+ [` -h, --help`, `Display this message`],
296
+ [` -v, --version`, `Display version number`]
297
+ ]);
298
+ for (const line of twoColumn(optionHelps)) {
299
+ println(line);
300
+ }
301
+ println(``);
302
+ return output;
14
303
  }
15
- option(text) {
304
+ option(format, configOrDescription = "", otherConfig = {}) {
305
+ const config = typeof configOrDescription === "object" ? configOrDescription : { ...otherConfig, description: configOrDescription };
16
306
  try {
17
- const option = new BreadcOption(text);
307
+ const option = new Option(format, config);
18
308
  this.options.push(option);
19
309
  } catch (error) {
310
+ this.logger.warn(error.message);
20
311
  }
21
312
  return this;
22
313
  }
23
- command(text) {
24
- return new Breadcommand(this, text);
314
+ command(format, configOrDescription = "", otherConfig = {}) {
315
+ const config = typeof configOrDescription === "object" ? configOrDescription : { ...otherConfig, description: configOrDescription };
316
+ const command = new Command(format, { ...config, logger: this.logger });
317
+ if (command.default) {
318
+ if (this.defaultCommand) {
319
+ this.logger.warn("You can not have two default commands.");
320
+ }
321
+ this.defaultCommand = command;
322
+ }
323
+ this.commands.push(command);
324
+ return command;
25
325
  }
26
326
  parse(args) {
327
+ const allowOptions = [
328
+ ...this.options,
329
+ ...this.commands.flatMap((c) => c.options)
330
+ ];
331
+ const alias = allowOptions.reduce((map, o) => {
332
+ if (o.shortcut) {
333
+ map[o.shortcut] = o.name;
334
+ }
335
+ return map;
336
+ }, {});
337
+ const defaults = allowOptions.reduce((map, o) => {
338
+ if (o.default) {
339
+ map[o.name] = o.default;
340
+ }
341
+ return map;
342
+ }, {});
27
343
  const argv = minimist__default(args, {
28
- string: this.options.filter((o) => o.type === "string").map((o) => o.name),
29
- boolean: this.options.filter((o) => o.type === "boolean").map((o) => o.name)
344
+ string: allowOptions.filter((o) => o.type === "string").map((o) => o.name),
345
+ boolean: allowOptions.filter((o) => o.type === "boolean").map((o) => o.name),
346
+ alias,
347
+ default: defaults,
348
+ unknown: (t) => {
349
+ if (t[0] !== "-")
350
+ return true;
351
+ else {
352
+ if (["--help", "-h", "--version", "-v"].includes(t)) {
353
+ return true;
354
+ } else {
355
+ this.logger.warn(`Find unknown flag "${t}"`);
356
+ return false;
357
+ }
358
+ }
359
+ }
30
360
  });
31
- return argv;
32
- }
33
- }
34
- class Breadcommand {
35
- constructor(breadc, text) {
36
- this.breadc = breadc;
37
- }
38
- }
39
- const _BreadcOption = class {
40
- constructor(text) {
41
- if (_BreadcOption.BooleanRE.test(text)) {
42
- this.type = "boolean";
43
- } else {
44
- this.type = "string";
361
+ for (const shortcut of Object.keys(alias)) {
362
+ delete argv[shortcut];
45
363
  }
46
- const match = _BreadcOption.NameRE.exec(text);
47
- if (match) {
48
- this.name = match[1];
49
- } else {
50
- throw new Error(`Can not extract option name from "${text}"`);
364
+ for (const command of this.commands) {
365
+ if (!command.default && command.shouldRun(argv)) {
366
+ return command.parseArgs(argv, this.options);
367
+ }
51
368
  }
369
+ if (this.defaultCommand) {
370
+ return this.defaultCommand.parseArgs(argv, this.options);
371
+ }
372
+ const argumentss = argv["_"];
373
+ const options = argv;
374
+ delete options["_"];
375
+ return {
376
+ command: void 0,
377
+ arguments: argumentss,
378
+ options
379
+ };
52
380
  }
53
- };
54
- let BreadcOption = _BreadcOption;
55
- BreadcOption.BooleanRE = /^--[a-zA-Z.]+$/;
56
- BreadcOption.NameRE = /--([a-zA-Z.]+)/;
381
+ async run(args) {
382
+ const parsed = this.parse(args);
383
+ if (parsed.command) {
384
+ parsed.command.run(...parsed.arguments, parsed.options);
385
+ }
386
+ }
387
+ }
388
+ function twoColumn(texts, split = " ") {
389
+ const left = padRight(texts.map((t) => t[0]));
390
+ return left.map((l, idx) => l + split + texts[idx][1]);
391
+ }
392
+ function padRight(texts, fill = " ") {
393
+ const length = texts.map((t) => t.length).reduce((max, l) => Math.max(max, l), 0);
394
+ return texts.map((t) => t + fill.repeat(length - t.length));
395
+ }
57
396
 
58
397
  function breadc(name, option = {}) {
59
- return new Breadc(name, { version: option.version ?? "unknown" });
398
+ return new Breadc(name, option);
60
399
  }
61
400
 
62
- module.exports = breadc;
401
+ exports.kolorist = kolorist__default;
402
+ exports.minimist = minimist__default;
403
+ exports.createDebug = createDebug__default;
404
+ exports["default"] = breadc;
package/dist/index.d.ts CHANGED
@@ -1,24 +1,123 @@
1
- import minimist from 'minimist';
1
+ import { ParsedArgs } from 'minimist';
2
+ export { default as minimist } from 'minimist';
3
+ export { default as kolorist } from 'kolorist';
4
+ export { default as createDebug } from 'debug';
2
5
 
3
- declare class Breadc {
4
- private readonly name;
5
- private readonly version;
6
- private readonly options;
7
- constructor(name: string, option: {
8
- version: string;
9
- });
10
- option(text: string): this;
11
- command(text: string): Breadcommand;
12
- parse(args: string[]): minimist.ParsedArgs;
6
+ interface OptionConfig<T = string> {
7
+ description?: string;
8
+ default?: T;
9
+ construct?: (rawText?: string) => T;
10
+ }
11
+ /**
12
+ * Option
13
+ *
14
+ * Option format must follow:
15
+ * + --option
16
+ * + -o, --option
17
+ * + --option <arg>
18
+ * + --option [arg]
19
+ */
20
+ declare class Option<T extends string = string, F = string> {
21
+ private static OptionRE;
22
+ readonly name: string;
23
+ readonly shortcut?: string;
24
+ readonly default?: F;
25
+ readonly format: string;
26
+ readonly description: string;
27
+ readonly type: 'string' | 'boolean';
28
+ readonly required: boolean;
29
+ readonly construct: (rawText: string | undefined) => any;
30
+ constructor(format: T, config?: OptionConfig<F>);
13
31
  }
14
- declare class Breadcommand {
15
- private readonly breadc;
16
- constructor(breadc: Breadc, text: string);
32
+
33
+ declare type ConditionFn = (args: ParsedArgs) => boolean;
34
+ interface CommandConfig {
35
+ description?: string;
36
+ }
37
+ declare class Command<F extends string = string, CommandOption extends object = {}> {
38
+ private static MaxDep;
39
+ private readonly conditionFn?;
40
+ private readonly logger;
41
+ readonly format: string[];
42
+ readonly default: boolean;
43
+ readonly description: string;
44
+ readonly options: Option[];
45
+ private actionFn?;
46
+ constructor(format: F, config: CommandConfig & {
47
+ condition?: ConditionFn;
48
+ logger: Logger;
49
+ });
50
+ option<OF extends string, T = string>(format: OF, description: string, config?: Omit<OptionConfig<T>, 'description'>): Command<F, CommandOption & ExtractOption<OF>>;
51
+ option<OF extends string, T = string>(format: OF, config?: OptionConfig<T>): Command<F, CommandOption & ExtractOption<OF>>;
52
+ get hasConditionFn(): boolean;
53
+ shouldRun(args: ParsedArgs): boolean;
54
+ parseArgs(args: ParsedArgs, globalOptions: Option[]): ParseResult;
55
+ action(fn: ActionFn<ExtractCommand<F>, CommandOption>): this;
56
+ run(...args: any[]): Promise<void>;
17
57
  }
18
58
 
19
- interface Option {
59
+ interface AppOption {
20
60
  version?: string;
61
+ description?: string | string[];
62
+ help?: string | string[] | (() => string | string[]);
63
+ logger?: Logger | LoggerFn;
21
64
  }
22
- declare function breadc(name: string, option?: Option): Breadc;
65
+ declare type LoggerFn = (message: string, ...args: any[]) => void;
66
+ interface Logger {
67
+ println: LoggerFn;
68
+ info: LoggerFn;
69
+ warn: LoggerFn;
70
+ error: LoggerFn;
71
+ debug: LoggerFn;
72
+ }
73
+ interface ParseResult {
74
+ command: Command | undefined;
75
+ arguments: any[];
76
+ options: Record<string, string>;
77
+ }
78
+ declare type ExtractOption<T extends string> = {
79
+ [k in ExtractOptionName<T>]: ExtractOptionType<T>;
80
+ };
81
+ /**
82
+ * Extract option name type
83
+ *
84
+ * Examples:
85
+ * + const t1: ExtractOption<'--option' | '--hello'> = 'hello'
86
+ * + const t2: ExtractOption<'-r, --root'> = 'root'
87
+ */
88
+ declare type ExtractOptionName<T extends string> = T extends `-${Letter}, --${infer R} [${infer U}]` ? R : T extends `-${Letter}, --${infer R} <${infer U}>` ? R : T extends `-${Letter}, --${infer R}` ? R : T extends `--${infer R} [${infer U}]` ? R : T extends `--${infer R} <${infer U}>` ? R : T extends `--${infer R}` ? R : never;
89
+ declare type ExtractOptionType<T extends string> = T extends `-${Letter}, --${infer R} [${infer U}]` ? string | undefined : T extends `-${Letter}, --${infer R} <${infer U}>` ? string | boolean : T extends `-${Letter}, --${infer R}` ? boolean : T extends `--${infer R} [${infer U}]` ? string | undefined : T extends `--${infer R} <${infer U}>` ? string | boolean : T extends `--${infer R}` ? boolean : never;
90
+ declare type Lowercase = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z';
91
+ declare type Uppercase = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z';
92
+ declare type Letter = Lowercase | Uppercase;
93
+ declare type Push<T extends any[], U> = [...T, U];
94
+ declare type ActionFn<T extends any[], Option extends object = {}> = (...arg: Push<T, Option>) => void;
95
+ /**
96
+ * Max Dep: 5
97
+ *
98
+ * Generated by: npx tsx scripts/genType.ts 5
99
+ */
100
+ declare type ExtractCommand<T extends string> = T extends `<${infer P1}> <${infer P2}> <${infer P3}> <${infer P4}> [...${infer P5}]` ? [string, string, string, string, string[]] : T extends `<${infer P1}> <${infer P2}> <${infer P3}> <${infer P4}> [${infer P5}]` ? [string, string, string, string, string | undefined] : T extends `<${infer P1}> <${infer P2}> <${infer P3}> <${infer P4}> <${infer P5}>` ? [string, string, string, string, string] : T extends `${infer P1} <${infer P2}> <${infer P3}> <${infer P4}> [...${infer P5}]` ? [string, string, string, string[]] : T extends `${infer P1} <${infer P2}> <${infer P3}> <${infer P4}> [${infer P5}]` ? [string, string, string, string | undefined] : T extends `${infer P1} <${infer P2}> <${infer P3}> <${infer P4}> <${infer P5}>` ? [string, string, string, string] : T extends `${infer P1} ${infer P2} <${infer P3}> <${infer P4}> [...${infer P5}]` ? [string, string, string[]] : T extends `${infer P1} ${infer P2} <${infer P3}> <${infer P4}> [${infer P5}]` ? [string, string, string | undefined] : T extends `${infer P1} ${infer P2} <${infer P3}> <${infer P4}> <${infer P5}>` ? [string, string, string] : T extends `${infer P1} ${infer P2} ${infer P3} <${infer P4}> [...${infer P5}]` ? [string, string[]] : T extends `${infer P1} ${infer P2} ${infer P3} <${infer P4}> [${infer P5}]` ? [string, string | undefined] : T extends `${infer P1} ${infer P2} ${infer P3} <${infer P4}> <${infer P5}>` ? [string, string] : T extends `<${infer P1}> <${infer P2}> <${infer P3}> [...${infer P4}]` ? [string, string, string, string[]] : T extends `<${infer P1}> <${infer P2}> <${infer P3}> [${infer P4}]` ? [string, string, string, string | undefined] : T extends `<${infer P1}> <${infer P2}> <${infer P3}> <${infer P4}>` ? [string, string, string, string] : T extends `${infer P1} <${infer P2}> <${infer P3}> [...${infer P4}]` ? [string, string, string[]] : T extends `${infer P1} <${infer P2}> <${infer P3}> [${infer P4}]` ? [string, string, string | undefined] : T extends `${infer P1} <${infer P2}> <${infer P3}> <${infer P4}>` ? [string, string, string] : T extends `${infer P1} ${infer P2} <${infer P3}> [...${infer P4}]` ? [string, string[]] : T extends `${infer P1} ${infer P2} <${infer P3}> [${infer P4}]` ? [string, string | undefined] : T extends `${infer P1} ${infer P2} <${infer P3}> <${infer P4}>` ? [string, string] : T extends `${infer P1} ${infer P2} ${infer P3} [...${infer P4}]` ? [string[]] : T extends `${infer P1} ${infer P2} ${infer P3} [${infer P4}]` ? [string | undefined] : T extends `${infer P1} ${infer P2} ${infer P3} <${infer P4}>` ? [string] : T extends `<${infer P1}> <${infer P2}> [...${infer P3}]` ? [string, string, string[]] : T extends `<${infer P1}> <${infer P2}> [${infer P3}]` ? [string, string, string | undefined] : T extends `<${infer P1}> <${infer P2}> <${infer P3}>` ? [string, string, string] : T extends `${infer P1} <${infer P2}> [...${infer P3}]` ? [string, string[]] : T extends `${infer P1} <${infer P2}> [${infer P3}]` ? [string, string | undefined] : T extends `${infer P1} <${infer P2}> <${infer P3}>` ? [string, string] : T extends `${infer P1} ${infer P2} [...${infer P3}]` ? [string[]] : T extends `${infer P1} ${infer P2} [${infer P3}]` ? [string | undefined] : T extends `${infer P1} ${infer P2} <${infer P3}>` ? [string] : T extends `${infer P1} ${infer P2} ${infer P3}` ? [] : T extends `<${infer P1}> [...${infer P2}]` ? [string, string[]] : T extends `<${infer P1}> [${infer P2}]` ? [string, string | undefined] : T extends `<${infer P1}> <${infer P2}>` ? [string, string] : T extends `${infer P1} [...${infer P2}]` ? [string[]] : T extends `${infer P1} [${infer P2}]` ? [string | undefined] : T extends `${infer P1} <${infer P2}>` ? [string] : T extends `${infer P1} ${infer P2}` ? [] : T extends `[...${infer P1}]` ? [string[]] : T extends `[${infer P1}]` ? [string | undefined] : T extends `<${infer P1}>` ? [string] : T extends `${infer P1}` ? [] : T extends `` ? [] : never;
101
+
102
+ declare class Breadc<GlobalOption extends object = {}> {
103
+ private readonly name;
104
+ private readonly _version;
105
+ private readonly description?;
106
+ private readonly options;
107
+ private readonly commands;
108
+ private defaultCommand?;
109
+ readonly logger: Logger;
110
+ constructor(name: string, option: AppOption);
111
+ version(): string;
112
+ help(command?: Command): string[];
113
+ option<F extends string, T = string>(format: F, description: string, config?: Omit<OptionConfig<T>, 'description'>): Breadc<GlobalOption & ExtractOption<F>>;
114
+ option<F extends string, T = string>(format: F, config?: OptionConfig<T>): Breadc<GlobalOption & ExtractOption<F>>;
115
+ command<F extends string>(format: F, description: string, config?: Omit<CommandConfig, 'description'>): Command<F, GlobalOption>;
116
+ command<F extends string>(format: F, config?: CommandConfig): Command<F, GlobalOption>;
117
+ parse(args: string[]): ParseResult;
118
+ run(args: string[]): Promise<void>;
119
+ }
120
+
121
+ declare function breadc(name: string, option?: AppOption): Breadc<{}>;
23
122
 
24
123
  export { breadc as default };
package/dist/index.mjs CHANGED
@@ -1,56 +1,394 @@
1
+ import { blue, yellow, red } from 'kolorist';
2
+ export { default as kolorist } from 'kolorist';
1
3
  import minimist from 'minimist';
4
+ export { default as minimist } from 'minimist';
5
+ import createDebug from 'debug';
6
+ export { default as createDebug } from 'debug';
7
+
8
+ function createDefaultLogger(name, logger) {
9
+ if (!!logger && typeof logger === "object") {
10
+ return logger;
11
+ }
12
+ const debug = createDebug(name + ":breadc");
13
+ const println = !!logger && typeof logger === "function" ? logger : (message, ...args) => {
14
+ console.log(message, ...args);
15
+ };
16
+ return {
17
+ println,
18
+ info(message, ...args) {
19
+ println(`${blue("INFO")} ${message}`, ...args);
20
+ },
21
+ warn(message, ...args) {
22
+ println(`${yellow("WARN")} ${message}`, ...args);
23
+ },
24
+ error(message, ...args) {
25
+ println(`${red("ERROR")} ${message}`, ...args);
26
+ },
27
+ debug(message, ...args) {
28
+ debug(message, ...args);
29
+ }
30
+ };
31
+ }
32
+
33
+ const _Option = class {
34
+ constructor(format, config = {}) {
35
+ this.format = format;
36
+ const match = _Option.OptionRE.exec(format);
37
+ if (match) {
38
+ if (match[3]) {
39
+ this.type = "string";
40
+ } else {
41
+ this.type = "boolean";
42
+ }
43
+ this.name = match[2];
44
+ if (match[1]) {
45
+ this.shortcut = match[1][1];
46
+ }
47
+ } else {
48
+ throw new Error(`Can not parse option format from "${format}"`);
49
+ }
50
+ this.description = config.description ?? "";
51
+ this.required = format.indexOf("<") !== -1;
52
+ this.default = config.default;
53
+ this.construct = config.construct ?? ((text) => text ?? config.default ?? void 0);
54
+ }
55
+ };
56
+ let Option = _Option;
57
+ Option.OptionRE = /^(-[a-zA-Z], )?--([a-zA-Z.]+)( \[[a-zA-Z]+\]| <[a-zA-Z]+>)?$/;
58
+
59
+ const _Command = class {
60
+ constructor(format, config) {
61
+ this.options = [];
62
+ this.format = config.condition ? [format] : format.split(" ").map((t) => t.trim()).filter(Boolean);
63
+ this.default = this.format.length === 0 || this.format[0][0] === "[" || this.format[0][0] === "<";
64
+ this.description = config.description ?? "";
65
+ this.conditionFn = config.condition;
66
+ this.logger = config.logger;
67
+ if (this.format.length > _Command.MaxDep) {
68
+ this.logger.warn(`Command format string "${format}" is too long`);
69
+ }
70
+ }
71
+ option(format, configOrDescription = "", otherConfig = {}) {
72
+ const config = typeof configOrDescription === "object" ? configOrDescription : { ...otherConfig, description: configOrDescription };
73
+ try {
74
+ const option = new Option(format, config);
75
+ this.options.push(option);
76
+ } catch (error) {
77
+ this.logger.warn(error.message);
78
+ }
79
+ return this;
80
+ }
81
+ get hasConditionFn() {
82
+ return !!this.conditionFn;
83
+ }
84
+ shouldRun(args) {
85
+ if (this.conditionFn) {
86
+ return this.conditionFn(args);
87
+ } else {
88
+ if (this.default)
89
+ return true;
90
+ const isCmd = (t) => t[0] !== "[" && t[0] !== "<";
91
+ for (let i = 0; i < this.format.length; i++) {
92
+ if (!isCmd(this.format[i])) {
93
+ return true;
94
+ }
95
+ if (i >= args["_"].length || this.format[i] !== args["_"][i]) {
96
+ return false;
97
+ }
98
+ }
99
+ return true;
100
+ }
101
+ }
102
+ parseArgs(args, globalOptions) {
103
+ if (this.conditionFn) {
104
+ const argumentss2 = args["_"];
105
+ const options2 = args;
106
+ delete options2["_"];
107
+ return {
108
+ command: this,
109
+ arguments: argumentss2,
110
+ options: args
111
+ };
112
+ }
113
+ const isCmd = (t) => t[0] !== "[" && t[0] !== "<";
114
+ const argumentss = [];
115
+ for (let i = 0; i < this.format.length; i++) {
116
+ if (isCmd(this.format[i]))
117
+ continue;
118
+ if (i < args["_"].length) {
119
+ if (this.format[i].startsWith("[...")) {
120
+ argumentss.push(args["_"].slice(i).map(String));
121
+ } else {
122
+ argumentss.push(String(args["_"][i]));
123
+ }
124
+ } else {
125
+ if (this.format[i].startsWith("<")) {
126
+ this.logger.warn(`You should provide the argument "${this.format[i]}"`);
127
+ argumentss.push(void 0);
128
+ } else if (this.format[i].startsWith("[...")) {
129
+ argumentss.push([]);
130
+ } else if (this.format[i].startsWith("[")) {
131
+ argumentss.push(void 0);
132
+ } else {
133
+ this.logger.warn(`unknown format string ("${this.format[i]}")`);
134
+ }
135
+ }
136
+ }
137
+ const fullOptions = globalOptions.concat(this.options).reduce((map, o) => {
138
+ map.set(o.name, o);
139
+ return map;
140
+ }, /* @__PURE__ */ new Map());
141
+ const options = args;
142
+ delete options["_"];
143
+ for (const [name, rawOption] of fullOptions) {
144
+ if (rawOption.required) {
145
+ if (options[name] === void 0) {
146
+ options[name] = false;
147
+ } else if (options[name] === "") {
148
+ options[name] = true;
149
+ }
150
+ } else {
151
+ if (options[name] === false) {
152
+ options[name] = void 0;
153
+ } else if (!(name in options)) {
154
+ options[name] = void 0;
155
+ }
156
+ }
157
+ }
158
+ return {
159
+ command: this,
160
+ arguments: argumentss,
161
+ options
162
+ };
163
+ }
164
+ action(fn) {
165
+ this.actionFn = fn;
166
+ return this;
167
+ }
168
+ async run(...args) {
169
+ if (this.actionFn) {
170
+ this.actionFn(...args);
171
+ } else {
172
+ this.logger.warn(`You may miss action function in "${this.format}"`);
173
+ }
174
+ }
175
+ };
176
+ let Command = _Command;
177
+ Command.MaxDep = 5;
178
+ function createHelpCommand(breadc) {
179
+ let helpCommand = void 0;
180
+ return new Command("-h, --help", {
181
+ condition(args) {
182
+ const isEmpty = !args["--"]?.length;
183
+ if ((args.help || args.h) && isEmpty) {
184
+ if (args["_"].length > 0) {
185
+ for (const cmd of breadc.commands) {
186
+ if (!cmd.hasConditionFn && !cmd.default && cmd.shouldRun(args)) {
187
+ helpCommand = cmd;
188
+ return true;
189
+ }
190
+ }
191
+ }
192
+ return true;
193
+ } else {
194
+ return false;
195
+ }
196
+ },
197
+ logger: breadc.logger
198
+ }).action(() => {
199
+ for (const line of breadc.help(helpCommand)) {
200
+ breadc.logger.println(line);
201
+ }
202
+ });
203
+ }
204
+ function createVersionCommand(breadc) {
205
+ return new Command("-v, --version", {
206
+ condition(args) {
207
+ const isEmpty = !args["_"].length && !args["--"]?.length;
208
+ if (args.version && isEmpty) {
209
+ return true;
210
+ } else if (args.v && isEmpty) {
211
+ return true;
212
+ } else {
213
+ return false;
214
+ }
215
+ },
216
+ logger: breadc.logger
217
+ }).action(() => {
218
+ breadc.logger.println(breadc.version());
219
+ });
220
+ }
2
221
 
3
222
  class Breadc {
4
223
  constructor(name, option) {
5
224
  this.options = [];
225
+ this.commands = [];
6
226
  this.name = name;
7
- this.version = option.version;
227
+ this._version = option.version ?? "unknown";
228
+ this.description = option.description;
229
+ this.logger = createDefaultLogger(name, option.logger);
230
+ const breadc = {
231
+ name: this.name,
232
+ version: () => this.version.call(this),
233
+ help: (command) => this.help.call(this, command),
234
+ logger: this.logger,
235
+ options: this.options,
236
+ commands: this.commands
237
+ };
238
+ this.commands.push(createVersionCommand(breadc), createHelpCommand(breadc));
239
+ }
240
+ version() {
241
+ return `${this.name}/${this._version}`;
242
+ }
243
+ help(command) {
244
+ const output = [];
245
+ const println = (msg) => output.push(msg);
246
+ println(this.version());
247
+ if (!command) {
248
+ if (this.description) {
249
+ println("");
250
+ if (Array.isArray(this.description)) {
251
+ for (const line of this.description) {
252
+ println(line);
253
+ }
254
+ } else {
255
+ println(this.description);
256
+ }
257
+ }
258
+ } else {
259
+ if (command.description) {
260
+ println("");
261
+ println(command.description);
262
+ }
263
+ }
264
+ if (!command) {
265
+ if (this.defaultCommand) {
266
+ println(``);
267
+ println(`Usage:`);
268
+ println(` $ ${this.name} ${this.defaultCommand.format.join(" ")}`);
269
+ }
270
+ } else {
271
+ println(``);
272
+ println(`Usage:`);
273
+ println(` $ ${this.name} ${command.format.join(" ")}`);
274
+ }
275
+ if (!command && this.commands.length > 2) {
276
+ println(``);
277
+ println(`Commands:`);
278
+ const commandHelps = this.commands.filter((c) => !c.hasConditionFn).map((c) => [` $ ${this.name} ${c.format.join(" ")}`, c.description]);
279
+ for (const line of twoColumn(commandHelps)) {
280
+ println(line);
281
+ }
282
+ }
283
+ println(``);
284
+ println(`Options:`);
285
+ const optionHelps = [].concat([
286
+ ...command ? command.options.map((o) => [` ${o.format}`, o.description]) : [],
287
+ ...this.options.map((o) => [` ${o.format}`, o.description]),
288
+ [` -h, --help`, `Display this message`],
289
+ [` -v, --version`, `Display version number`]
290
+ ]);
291
+ for (const line of twoColumn(optionHelps)) {
292
+ println(line);
293
+ }
294
+ println(``);
295
+ return output;
8
296
  }
9
- option(text) {
297
+ option(format, configOrDescription = "", otherConfig = {}) {
298
+ const config = typeof configOrDescription === "object" ? configOrDescription : { ...otherConfig, description: configOrDescription };
10
299
  try {
11
- const option = new BreadcOption(text);
300
+ const option = new Option(format, config);
12
301
  this.options.push(option);
13
302
  } catch (error) {
303
+ this.logger.warn(error.message);
14
304
  }
15
305
  return this;
16
306
  }
17
- command(text) {
18
- return new Breadcommand(this, text);
307
+ command(format, configOrDescription = "", otherConfig = {}) {
308
+ const config = typeof configOrDescription === "object" ? configOrDescription : { ...otherConfig, description: configOrDescription };
309
+ const command = new Command(format, { ...config, logger: this.logger });
310
+ if (command.default) {
311
+ if (this.defaultCommand) {
312
+ this.logger.warn("You can not have two default commands.");
313
+ }
314
+ this.defaultCommand = command;
315
+ }
316
+ this.commands.push(command);
317
+ return command;
19
318
  }
20
319
  parse(args) {
320
+ const allowOptions = [
321
+ ...this.options,
322
+ ...this.commands.flatMap((c) => c.options)
323
+ ];
324
+ const alias = allowOptions.reduce((map, o) => {
325
+ if (o.shortcut) {
326
+ map[o.shortcut] = o.name;
327
+ }
328
+ return map;
329
+ }, {});
330
+ const defaults = allowOptions.reduce((map, o) => {
331
+ if (o.default) {
332
+ map[o.name] = o.default;
333
+ }
334
+ return map;
335
+ }, {});
21
336
  const argv = minimist(args, {
22
- string: this.options.filter((o) => o.type === "string").map((o) => o.name),
23
- boolean: this.options.filter((o) => o.type === "boolean").map((o) => o.name)
337
+ string: allowOptions.filter((o) => o.type === "string").map((o) => o.name),
338
+ boolean: allowOptions.filter((o) => o.type === "boolean").map((o) => o.name),
339
+ alias,
340
+ default: defaults,
341
+ unknown: (t) => {
342
+ if (t[0] !== "-")
343
+ return true;
344
+ else {
345
+ if (["--help", "-h", "--version", "-v"].includes(t)) {
346
+ return true;
347
+ } else {
348
+ this.logger.warn(`Find unknown flag "${t}"`);
349
+ return false;
350
+ }
351
+ }
352
+ }
24
353
  });
25
- return argv;
26
- }
27
- }
28
- class Breadcommand {
29
- constructor(breadc, text) {
30
- this.breadc = breadc;
31
- }
32
- }
33
- const _BreadcOption = class {
34
- constructor(text) {
35
- if (_BreadcOption.BooleanRE.test(text)) {
36
- this.type = "boolean";
37
- } else {
38
- this.type = "string";
354
+ for (const shortcut of Object.keys(alias)) {
355
+ delete argv[shortcut];
39
356
  }
40
- const match = _BreadcOption.NameRE.exec(text);
41
- if (match) {
42
- this.name = match[1];
43
- } else {
44
- throw new Error(`Can not extract option name from "${text}"`);
357
+ for (const command of this.commands) {
358
+ if (!command.default && command.shouldRun(argv)) {
359
+ return command.parseArgs(argv, this.options);
360
+ }
45
361
  }
362
+ if (this.defaultCommand) {
363
+ return this.defaultCommand.parseArgs(argv, this.options);
364
+ }
365
+ const argumentss = argv["_"];
366
+ const options = argv;
367
+ delete options["_"];
368
+ return {
369
+ command: void 0,
370
+ arguments: argumentss,
371
+ options
372
+ };
46
373
  }
47
- };
48
- let BreadcOption = _BreadcOption;
49
- BreadcOption.BooleanRE = /^--[a-zA-Z.]+$/;
50
- BreadcOption.NameRE = /--([a-zA-Z.]+)/;
374
+ async run(args) {
375
+ const parsed = this.parse(args);
376
+ if (parsed.command) {
377
+ parsed.command.run(...parsed.arguments, parsed.options);
378
+ }
379
+ }
380
+ }
381
+ function twoColumn(texts, split = " ") {
382
+ const left = padRight(texts.map((t) => t[0]));
383
+ return left.map((l, idx) => l + split + texts[idx][1]);
384
+ }
385
+ function padRight(texts, fill = " ") {
386
+ const length = texts.map((t) => t.length).reduce((max, l) => Math.max(max, l), 0);
387
+ return texts.map((t) => t + fill.repeat(length - t.length));
388
+ }
51
389
 
52
390
  function breadc(name, option = {}) {
53
- return new Breadc(name, { version: option.version ?? "unknown" });
391
+ return new Breadc(name, option);
54
392
  }
55
393
 
56
394
  export { breadc as default };
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "breadc",
3
- "version": "0.0.0",
4
- "description": "",
3
+ "version": "0.2.0",
4
+ "description": "Yet another Command Line Application Framework",
5
5
  "keywords": [
6
- "cli"
6
+ "cli",
7
+ "framework",
8
+ "command-line",
9
+ "minimist"
7
10
  ],
8
11
  "homepage": "https://github.com/yjl9903/Breadc#readme",
9
12
  "bugs": {
@@ -30,9 +33,12 @@
30
33
  "dist"
31
34
  ],
32
35
  "dependencies": {
36
+ "debug": "^4.3.4",
37
+ "kolorist": "^1.5.1",
33
38
  "minimist": "^1.2.6"
34
39
  },
35
40
  "devDependencies": {
41
+ "@types/debug": "^4.1.7",
36
42
  "@types/minimist": "^1.2.2",
37
43
  "@types/node": "^17.0.43",
38
44
  "bumpp": "^8.2.1",
@@ -45,7 +51,7 @@
45
51
  "packageManager": "pnpm@7.3.0",
46
52
  "scripts": {
47
53
  "build": "unbuild",
48
- "format": "prettier --write src/**/*.ts",
54
+ "format": "prettier --write src/**/*.ts test/*.ts examples/*.ts scripts/*.ts",
49
55
  "release": "bumpp --commit --push --tag && pnpm publish",
50
56
  "test": "vitest",
51
57
  "typecheck": "tsc --noEmit",