breadc 0.4.4 → 0.5.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
@@ -6,7 +6,7 @@ Yet another Command Line Application Framework powered by [minimist](https://www
6
6
 
7
7
  ## Features
8
8
 
9
- + ⚡️ **Light-weight**: Only 61 kB.
9
+ + ⚡️ **Light-weight**: Only 40 kB (Unpacked).
10
10
  + 📖 **East to Learn**: Breadc is basically compatible with [cac](https://github.com/cacjs/cac) and there are only 4 APIs for building a CLI application: `command`, `option`, `action`, `run`.
11
11
  + 💻 **TypeScript Infer**: IDE will automatically infer the type of your command action function.
12
12
 
package/dist/index.cjs CHANGED
@@ -1,14 +1,21 @@
1
1
  'use strict';
2
2
 
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
5
- const kolorist = require('kolorist');
6
3
  const minimist = require('minimist');
4
+ const kolorist = require('kolorist');
7
5
 
8
6
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e["default"] : e; }
9
7
 
10
- const kolorist__default = /*#__PURE__*/_interopDefaultLegacy(kolorist);
11
8
  const minimist__default = /*#__PURE__*/_interopDefaultLegacy(minimist);
9
+ const kolorist__default = /*#__PURE__*/_interopDefaultLegacy(kolorist);
10
+
11
+ function twoColumn(texts, split = " ") {
12
+ const left = padRight(texts.map((t) => t[0]));
13
+ return left.map((l, idx) => l + split + texts[idx][1]);
14
+ }
15
+ function padRight(texts, fill = " ") {
16
+ const length = texts.map((t) => t.length).reduce((max, l) => Math.max(max, l), 0);
17
+ return texts.map((t) => t + fill.repeat(length - t.length));
18
+ }
12
19
 
13
20
  function createDefaultLogger(name, logger) {
14
21
  const println = !!logger && typeof logger === "function" ? logger : logger?.println ?? ((message, ...args) => {
@@ -59,20 +66,31 @@ const _Option = class {
59
66
  }
60
67
  };
61
68
  let Option = _Option;
62
- Option.OptionRE = /^(-[a-zA-Z], )?--([a-zA-Z]+)( \[[a-zA-Z]+\]| <[a-zA-Z]+>)?$/;
69
+ Option.OptionRE = /^(-[a-zA-Z0-9], )?--([a-zA-Z0-9\-]+)( \[[a-zA-Z0-9]+\]| <[a-zA-Z0-9]+>)?$/;
63
70
 
64
71
  const _Command = class {
65
72
  constructor(format, config) {
66
73
  this.options = [];
67
- this.format = config.condition ? [format] : format.split(" ").map((t) => t.trim()).filter(Boolean);
68
- this.default = this.format.length === 0 || this.format[0][0] === "[" || this.format[0][0] === "<";
74
+ this.format = format;
75
+ const pieces = format.split(" ").map((t) => t.trim()).filter(Boolean);
76
+ const prefix = pieces.filter((p) => !isArg(p));
77
+ this.default = prefix.length === 0;
78
+ this.prefix = this.default ? [] : [prefix];
79
+ this.arguments = pieces.filter(isArg);
69
80
  this.description = config.description ?? "";
70
- this.conditionFn = config.condition;
71
81
  this.logger = config.logger;
72
- if (this.format.length > _Command.MaxDep) {
82
+ if (pieces.length > _Command.MaxDep) {
73
83
  this.logger.warn(`Command format string "${format}" is too long`);
74
84
  }
75
85
  }
86
+ get isInternal() {
87
+ return this instanceof InternalCommand;
88
+ }
89
+ alias(command) {
90
+ const pieces = command.split(" ").map((t) => t.trim()).filter(Boolean);
91
+ this.prefix.push(pieces);
92
+ return this;
93
+ }
76
94
  option(format, configOrDescription = "", otherConfig = {}) {
77
95
  const config = typeof configOrDescription === "object" ? configOrDescription : { ...otherConfig, description: configOrDescription };
78
96
  try {
@@ -83,59 +101,56 @@ const _Command = class {
83
101
  }
84
102
  return this;
85
103
  }
86
- get hasConditionFn() {
87
- return !!this.conditionFn;
88
- }
89
- shouldRun(args) {
90
- if (this.conditionFn) {
91
- return this.conditionFn(args);
104
+ hasPrefix(parsedArgs) {
105
+ const argv = parsedArgs["_"];
106
+ if (argv.length === 0) {
107
+ return this.default;
92
108
  } else {
93
- if (this.default)
94
- return true;
95
- const isCmd = (t) => t[0] !== "[" && t[0] !== "<";
96
- for (let i = 0; i < this.format.length; i++) {
97
- if (!isCmd(this.format[i])) {
109
+ for (const prefix of this.prefix) {
110
+ if (prefix.length > 0 && prefix[0] === argv[0]) {
98
111
  return true;
99
112
  }
100
- if (i >= args["_"].length || this.format[i] !== args["_"][i]) {
101
- return false;
113
+ }
114
+ return false;
115
+ }
116
+ }
117
+ shouldRun(parsedArgs) {
118
+ const args = parsedArgs["_"];
119
+ for (const prefix of this.prefix) {
120
+ let match = true;
121
+ for (let i = 0; match && i < prefix.length; i++) {
122
+ if (args[i] !== prefix[i]) {
123
+ match = false;
102
124
  }
103
125
  }
104
- return true;
126
+ if (match) {
127
+ args.splice(0, prefix.length);
128
+ return true;
129
+ }
105
130
  }
131
+ if (this.default)
132
+ return true;
133
+ return false;
106
134
  }
107
135
  parseArgs(args, globalOptions) {
108
- if (this.conditionFn) {
109
- const argumentss2 = args["_"];
110
- const options2 = args;
111
- delete options2["_"];
112
- return {
113
- command: this,
114
- arguments: argumentss2,
115
- options: args
116
- };
117
- }
118
- const isCmd = (t) => t[0] !== "[" && t[0] !== "<";
119
136
  const argumentss = [];
120
- for (let i = 0; i < this.format.length; i++) {
121
- if (isCmd(this.format[i]))
122
- continue;
137
+ for (let i = 0; i < this.arguments.length; i++) {
123
138
  if (i < args["_"].length) {
124
- if (this.format[i].startsWith("[...")) {
139
+ if (this.arguments[i].startsWith("[...")) {
125
140
  argumentss.push(args["_"].slice(i).map(String));
126
141
  } else {
127
142
  argumentss.push(String(args["_"][i]));
128
143
  }
129
144
  } else {
130
- if (this.format[i].startsWith("<")) {
131
- this.logger.warn(`You should provide the argument "${this.format[i]}"`);
132
- argumentss.push(void 0);
133
- } else if (this.format[i].startsWith("[...")) {
145
+ if (this.arguments[i].startsWith("<")) {
146
+ this.logger.warn(`You should provide the argument "${this.arguments[i]}"`);
147
+ argumentss.push("");
148
+ } else if (this.arguments[i].startsWith("[...")) {
134
149
  argumentss.push([]);
135
- } else if (this.format[i].startsWith("[")) {
150
+ } else if (this.arguments[i].startsWith("[")) {
136
151
  argumentss.push(void 0);
137
152
  } else {
138
- this.logger.warn(`unknown format string ("${this.format[i]}")`);
153
+ this.logger.warn(`unknown format string ("${this.arguments[i]}")`);
139
154
  }
140
155
  }
141
156
  }
@@ -159,13 +174,14 @@ const _Command = class {
159
174
  options[name] = void 0;
160
175
  }
161
176
  }
162
- if (rawOption.construct) {
163
- options[name] = rawOption.construct(options[name]);
164
- } else if (rawOption.default) {
165
- if (!options[name]) {
177
+ if (rawOption.default !== void 0) {
178
+ if (options[name] === void 0 || options[name] === false) {
166
179
  options[name] = rawOption.default;
167
180
  }
168
181
  }
182
+ if (rawOption.construct !== void 0) {
183
+ options[name] = rawOption.construct(options[name]);
184
+ }
169
185
  }
170
186
  for (const key of Object.keys(options)) {
171
187
  if (!fullOptions.has(key)) {
@@ -180,88 +196,115 @@ const _Command = class {
180
196
  }
181
197
  action(fn) {
182
198
  this.actionFn = fn;
183
- return this;
184
199
  }
185
200
  async run(...args) {
186
201
  if (this.actionFn) {
187
- return await this.actionFn(...args, { logger: this.logger });
202
+ return await this.actionFn(...args, { logger: this.logger, color: kolorist__default });
188
203
  } else {
189
- this.logger.warn(`You may miss action function in "${this.format}"`);
204
+ this.logger.warn(`You may miss action function in ${this.format ? `"${this.format}"` : "<default command>"}`);
190
205
  }
191
206
  }
192
207
  };
193
208
  let Command = _Command;
194
209
  Command.MaxDep = 5;
195
- function createHelpCommand(breadc) {
196
- let helpCommand = void 0;
197
- return new Command("-h, --help", {
198
- condition(args) {
199
- const isEmpty = !args["--"]?.length;
200
- if ((args.help || args.h) && isEmpty) {
201
- if (args["_"].length > 0) {
202
- for (const cmd of breadc.commands) {
203
- if (!cmd.hasConditionFn && !cmd.default && cmd.shouldRun(args)) {
204
- helpCommand = cmd;
205
- return true;
210
+ class InternalCommand extends Command {
211
+ hasPrefix(_args) {
212
+ return false;
213
+ }
214
+ parseArgs(args, _globalOptions) {
215
+ const argumentss = args["_"];
216
+ const options = args;
217
+ delete options["_"];
218
+ delete options["help"];
219
+ delete options["version"];
220
+ return {
221
+ command: this,
222
+ arguments: argumentss,
223
+ options: args
224
+ };
225
+ }
226
+ }
227
+ class HelpCommand extends InternalCommand {
228
+ constructor(commands, help, logger) {
229
+ super("-h, --help", { description: "Display this message", logger });
230
+ this.runCommands = [];
231
+ this.helpCommands = [];
232
+ this.commands = commands;
233
+ this.help = help;
234
+ }
235
+ shouldRun(args) {
236
+ const isRestEmpty = !args["--"]?.length;
237
+ if ((args.help || args.h) && isRestEmpty) {
238
+ if (args["_"].length > 0) {
239
+ for (const cmd of this.commands) {
240
+ if (!cmd.default && !cmd.isInternal) {
241
+ if (cmd.shouldRun(args)) {
242
+ this.runCommands.push(cmd);
243
+ } else if (cmd.hasPrefix(args)) {
244
+ this.helpCommands.push(cmd);
206
245
  }
207
246
  }
208
247
  }
209
- return true;
210
- } else {
211
- return false;
212
248
  }
213
- },
214
- logger: breadc.logger
215
- }).action(() => {
216
- for (const line of breadc.help(helpCommand)) {
217
- breadc.logger.println(line);
249
+ return true;
250
+ } else {
251
+ return false;
218
252
  }
219
- });
253
+ }
254
+ async run() {
255
+ const shouldHelp = this.runCommands.length > 0 ? this.runCommands : this.helpCommands;
256
+ for (const line of this.help(shouldHelp)) {
257
+ this.logger.println(line);
258
+ }
259
+ this.runCommands.splice(0);
260
+ this.helpCommands.splice(0);
261
+ }
220
262
  }
221
- function createVersionCommand(breadc) {
222
- return new Command("-v, --version", {
223
- condition(args) {
224
- const isEmpty = !args["_"].length && !args["--"]?.length;
225
- if (args.version && isEmpty) {
226
- return true;
227
- } else if (args.v && isEmpty) {
228
- return true;
229
- } else {
230
- return false;
231
- }
232
- },
233
- logger: breadc.logger
234
- }).action(() => {
235
- breadc.logger.println(breadc.version());
236
- });
263
+ class VersionCommand extends InternalCommand {
264
+ constructor(version, logger) {
265
+ super("-v, --version", { description: "Display version number", logger });
266
+ this.version = version;
267
+ }
268
+ shouldRun(args) {
269
+ const isEmpty = !args["_"].length && !args["--"]?.length;
270
+ if (args.version && isEmpty) {
271
+ return true;
272
+ } else if (args.v && isEmpty) {
273
+ return true;
274
+ } else {
275
+ return false;
276
+ }
277
+ }
278
+ async run() {
279
+ this.logger.println(this.version);
280
+ }
281
+ }
282
+ function isArg(arg) {
283
+ return arg[0] === "[" && arg[arg.length - 1] === "]" || arg[0] === "<" && arg[arg.length - 1] === ">";
237
284
  }
238
285
 
239
286
  class Breadc {
240
287
  constructor(name, option) {
241
288
  this.options = [];
242
289
  this.commands = [];
290
+ this.callbacks = {
291
+ pre: [],
292
+ post: []
293
+ };
243
294
  this.name = name;
244
295
  this._version = option.version ?? "unknown";
245
296
  this.description = option.description;
246
297
  this.logger = createDefaultLogger(name, option.logger);
247
- const breadc = {
248
- name: this.name,
249
- version: () => this.version.call(this),
250
- help: (command) => this.help.call(this, command),
251
- logger: this.logger,
252
- options: this.options,
253
- commands: this.commands
254
- };
255
- this.commands.push(createVersionCommand(breadc), createHelpCommand(breadc));
298
+ this.commands.push(new VersionCommand(this.version(), this.logger), new HelpCommand(this.commands, this.help.bind(this), this.logger));
256
299
  }
257
300
  version() {
258
301
  return `${this.name}/${this._version}`;
259
302
  }
260
- help(command) {
303
+ help(commands = []) {
261
304
  const output = [];
262
305
  const println = (msg) => output.push(msg);
263
306
  println(this.version());
264
- if (!command) {
307
+ if (commands.length === 0) {
265
308
  if (this.description) {
266
309
  println("");
267
310
  if (Array.isArray(this.description)) {
@@ -272,27 +315,26 @@ class Breadc {
272
315
  println(this.description);
273
316
  }
274
317
  }
275
- } else {
276
- if (command.description) {
277
- println("");
278
- println(command.description);
279
- }
280
- }
281
- if (!command) {
282
318
  if (this.defaultCommand) {
283
319
  println(``);
284
320
  println(`Usage:`);
285
- println(` $ ${this.name} ${this.defaultCommand.format.join(" ")}`);
321
+ println(` $ ${this.name} ${this.defaultCommand.format}`);
322
+ }
323
+ } else if (commands.length === 1) {
324
+ const command = commands[0];
325
+ if (command.description) {
326
+ println("");
327
+ println(command.description);
286
328
  }
287
- } else {
288
329
  println(``);
289
330
  println(`Usage:`);
290
- println(` $ ${this.name} ${command.format.join(" ")}`);
331
+ println(` $ ${this.name} ${command.format}`);
291
332
  }
292
- if (!command && this.commands.length > 2) {
333
+ if (commands.length !== 1) {
334
+ const cmdList = (commands.length === 0 ? this.commands : commands).filter((c) => !c.isInternal);
293
335
  println(``);
294
336
  println(`Commands:`);
295
- const commandHelps = this.commands.filter((c) => !c.hasConditionFn).map((c) => [` $ ${this.name} ${c.format.join(" ")}`, c.description]);
337
+ const commandHelps = cmdList.map((c) => [` $ ${this.name} ${c.format}`, c.description]);
296
338
  for (const line of twoColumn(commandHelps)) {
297
339
  println(line);
298
340
  }
@@ -300,7 +342,7 @@ class Breadc {
300
342
  println(``);
301
343
  println(`Options:`);
302
344
  const optionHelps = [].concat([
303
- ...command ? command.options.map((o) => [` ${o.format}`, o.description]) : [],
345
+ ...commands.length > 0 ? commands.flatMap((cmd) => cmd.options.map((o) => [` ${o.format}`, o.description])) : [],
304
346
  ...this.options.map((o) => [` ${o.format}`, o.description]),
305
347
  [` -h, --help`, `Display this message`],
306
348
  [` -v, --version`, `Display version number`]
@@ -338,15 +380,28 @@ class Breadc {
338
380
  ...this.options,
339
381
  ...this.commands.flatMap((c) => c.options)
340
382
  ];
383
+ {
384
+ const names = /* @__PURE__ */ new Map();
385
+ for (const option of allowOptions) {
386
+ if (names.has(option.name)) {
387
+ const otherOption = names.get(option.name);
388
+ if (otherOption.type !== option.type) {
389
+ this.logger.warn(`Option "${option.name}" encounters conflict`);
390
+ }
391
+ } else {
392
+ names.set(option.name, option);
393
+ }
394
+ }
395
+ }
341
396
  const alias = allowOptions.reduce((map, o) => {
342
397
  if (o.shortcut) {
343
398
  map[o.shortcut] = o.name;
344
399
  }
345
400
  return map;
346
- }, {});
401
+ }, { h: "help", v: "version" });
347
402
  const argv = minimist__default(args, {
348
403
  string: allowOptions.filter((o) => o.type === "string").map((o) => o.name),
349
- boolean: allowOptions.filter((o) => o.type === "boolean").map((o) => o.name),
404
+ boolean: allowOptions.filter((o) => o.type === "boolean").map((o) => o.name).concat(["help", "version"]),
350
405
  alias,
351
406
  unknown: (t) => {
352
407
  if (t[0] !== "-")
@@ -370,37 +425,36 @@ class Breadc {
370
425
  }
371
426
  }
372
427
  if (this.defaultCommand) {
428
+ this.defaultCommand.shouldRun(argv);
373
429
  return this.defaultCommand.parseArgs(argv, this.options);
374
430
  }
375
431
  const argumentss = argv["_"];
376
432
  const options = argv;
377
433
  delete options["_"];
434
+ delete options["help"];
435
+ delete options["version"];
378
436
  return {
379
437
  command: void 0,
380
438
  arguments: argumentss,
381
439
  options
382
440
  };
383
441
  }
442
+ on(event, fn) {
443
+ this.callbacks[event].push(fn);
444
+ }
384
445
  async run(args) {
385
446
  const parsed = this.parse(args);
386
447
  if (parsed.command) {
387
- return await parsed.command.run(...parsed.arguments, parsed.options);
448
+ await Promise.all(this.callbacks.pre.map((fn) => fn(parsed.options)));
449
+ const returnValue = await parsed.command.run(...parsed.arguments, parsed.options);
450
+ await Promise.all(this.callbacks.post.map((fn) => fn(parsed.options)));
451
+ return returnValue;
388
452
  }
389
453
  }
390
454
  }
391
- function twoColumn(texts, split = " ") {
392
- const left = padRight(texts.map((t) => t[0]));
393
- return left.map((l, idx) => l + split + texts[idx][1]);
394
- }
395
- function padRight(texts, fill = " ") {
396
- const length = texts.map((t) => t.length).reduce((max, l) => Math.max(max, l), 0);
397
- return texts.map((t) => t + fill.repeat(length - t.length));
398
- }
399
455
 
400
456
  function breadc(name, option = {}) {
401
457
  return new Breadc(name, option);
402
458
  }
403
459
 
404
- exports.kolorist = kolorist__default;
405
- exports.minimist = minimist__default;
406
- exports["default"] = breadc;
460
+ module.exports = breadc;
package/dist/index.d.ts CHANGED
@@ -1,10 +1,18 @@
1
+ import kolorist from 'kolorist';
1
2
  import { ParsedArgs } from 'minimist';
2
- export { default as minimist } from 'minimist';
3
- export { default as kolorist } from 'kolorist';
4
3
 
5
4
  interface OptionConfig<F extends string, T = never> {
5
+ /**
6
+ * Option description
7
+ */
6
8
  description?: string;
7
- default?: T;
9
+ /**
10
+ * Option string default value
11
+ */
12
+ default?: string;
13
+ /**
14
+ * Transform option text
15
+ */
8
16
  construct?: (rawText: ExtractOptionType<F>) => T;
9
17
  }
10
18
  /**
@@ -20,7 +28,7 @@ declare class Option<T extends string = string, F = string> {
20
28
  private static OptionRE;
21
29
  readonly name: string;
22
30
  readonly shortcut?: string;
23
- readonly default?: F;
31
+ readonly default?: string;
24
32
  readonly format: string;
25
33
  readonly description: string;
26
34
  readonly type: 'string' | 'boolean';
@@ -29,29 +37,30 @@ declare class Option<T extends string = string, F = string> {
29
37
  constructor(format: T, config?: OptionConfig<T, F>);
30
38
  }
31
39
 
32
- declare type ConditionFn = (args: ParsedArgs) => boolean;
33
40
  interface CommandConfig {
34
41
  description?: string;
35
42
  }
36
43
  declare class Command<F extends string = string, CommandOption extends object = {}> {
37
- private static MaxDep;
38
- private readonly conditionFn?;
39
- private readonly logger;
40
- readonly format: string[];
41
- readonly default: boolean;
44
+ protected static MaxDep: number;
45
+ protected readonly logger: Logger;
46
+ readonly format: string;
42
47
  readonly description: string;
48
+ readonly prefix: string[][];
49
+ readonly arguments: string[];
50
+ readonly default: boolean;
43
51
  readonly options: Option[];
44
52
  private actionFn?;
45
53
  constructor(format: F, config: CommandConfig & {
46
- condition?: ConditionFn;
47
54
  logger: Logger;
48
55
  });
56
+ get isInternal(): boolean;
57
+ alias(command: string): this;
49
58
  option<OF extends string, T = undefined>(format: OF, description: string, config?: Omit<OptionConfig<OF, T>, 'description'>): Command<F, CommandOption & ExtractOption<OF, T>>;
50
59
  option<OF extends string, T = undefined>(format: OF, config?: OptionConfig<OF, T>): Command<F, CommandOption & ExtractOption<OF, T>>;
51
- get hasConditionFn(): boolean;
52
- shouldRun(args: ParsedArgs): boolean;
60
+ hasPrefix(parsedArgs: ParsedArgs): boolean;
61
+ shouldRun(parsedArgs: ParsedArgs): boolean;
53
62
  parseArgs(args: ParsedArgs, globalOptions: Option[]): ParseResult;
54
- action(fn: ActionFn<ExtractCommand<F>, CommandOption>): this;
63
+ action(fn: ActionFn<ExtractCommand<F>, CommandOption>): void;
55
64
  run(...args: any[]): Promise<any>;
56
65
  }
57
66
 
@@ -92,6 +101,7 @@ declare type Letter = Lowercase | Uppercase;
92
101
  declare type Push<T extends any[], U, R> = [...T, U, R];
93
102
  declare type Context = {
94
103
  logger: Logger;
104
+ color: typeof kolorist;
95
105
  };
96
106
  declare type ActionFn<T extends any[], Option extends object = {}, R = any> = (...arg: Push<T, Option, Context>) => R | Promise<R>;
97
107
  /**
@@ -105,18 +115,20 @@ declare class Breadc<GlobalOption extends object = {}> {
105
115
  private readonly name;
106
116
  private readonly _version;
107
117
  private readonly description?;
118
+ readonly logger: Logger;
108
119
  private readonly options;
109
120
  private readonly commands;
110
121
  private defaultCommand?;
111
- readonly logger: Logger;
112
122
  constructor(name: string, option: AppOption);
113
123
  version(): string;
114
- help(command?: Command): string[];
124
+ help(commands?: Command[]): string[];
115
125
  option<F extends string, T = undefined>(format: F, description: string, config?: Omit<OptionConfig<F, T>, 'description'>): Breadc<GlobalOption & ExtractOption<F, T>>;
116
126
  option<F extends string, T = undefined>(format: F, config?: OptionConfig<F, T>): Breadc<GlobalOption & ExtractOption<F, T>>;
117
127
  command<F extends string>(format: F, description: string, config?: Omit<CommandConfig, 'description'>): Command<F, GlobalOption>;
118
128
  command<F extends string>(format: F, config?: CommandConfig): Command<F, GlobalOption>;
119
129
  parse(args: string[]): ParseResult;
130
+ private readonly callbacks;
131
+ on(event: 'pre' | 'post', fn: (option: GlobalOption) => void | Promise<void>): void;
120
132
  run(args: string[]): Promise<any>;
121
133
  }
122
134
 
package/dist/index.mjs CHANGED
@@ -1,7 +1,14 @@
1
- import { blue, yellow, red, gray } from 'kolorist';
2
- export { default as kolorist } from 'kolorist';
3
1
  import minimist from 'minimist';
4
- export { default as minimist } from 'minimist';
2
+ import kolorist, { blue, yellow, red, gray } from 'kolorist';
3
+
4
+ function twoColumn(texts, split = " ") {
5
+ const left = padRight(texts.map((t) => t[0]));
6
+ return left.map((l, idx) => l + split + texts[idx][1]);
7
+ }
8
+ function padRight(texts, fill = " ") {
9
+ const length = texts.map((t) => t.length).reduce((max, l) => Math.max(max, l), 0);
10
+ return texts.map((t) => t + fill.repeat(length - t.length));
11
+ }
5
12
 
6
13
  function createDefaultLogger(name, logger) {
7
14
  const println = !!logger && typeof logger === "function" ? logger : logger?.println ?? ((message, ...args) => {
@@ -52,20 +59,31 @@ const _Option = class {
52
59
  }
53
60
  };
54
61
  let Option = _Option;
55
- Option.OptionRE = /^(-[a-zA-Z], )?--([a-zA-Z]+)( \[[a-zA-Z]+\]| <[a-zA-Z]+>)?$/;
62
+ Option.OptionRE = /^(-[a-zA-Z0-9], )?--([a-zA-Z0-9\-]+)( \[[a-zA-Z0-9]+\]| <[a-zA-Z0-9]+>)?$/;
56
63
 
57
64
  const _Command = class {
58
65
  constructor(format, config) {
59
66
  this.options = [];
60
- this.format = config.condition ? [format] : format.split(" ").map((t) => t.trim()).filter(Boolean);
61
- this.default = this.format.length === 0 || this.format[0][0] === "[" || this.format[0][0] === "<";
67
+ this.format = format;
68
+ const pieces = format.split(" ").map((t) => t.trim()).filter(Boolean);
69
+ const prefix = pieces.filter((p) => !isArg(p));
70
+ this.default = prefix.length === 0;
71
+ this.prefix = this.default ? [] : [prefix];
72
+ this.arguments = pieces.filter(isArg);
62
73
  this.description = config.description ?? "";
63
- this.conditionFn = config.condition;
64
74
  this.logger = config.logger;
65
- if (this.format.length > _Command.MaxDep) {
75
+ if (pieces.length > _Command.MaxDep) {
66
76
  this.logger.warn(`Command format string "${format}" is too long`);
67
77
  }
68
78
  }
79
+ get isInternal() {
80
+ return this instanceof InternalCommand;
81
+ }
82
+ alias(command) {
83
+ const pieces = command.split(" ").map((t) => t.trim()).filter(Boolean);
84
+ this.prefix.push(pieces);
85
+ return this;
86
+ }
69
87
  option(format, configOrDescription = "", otherConfig = {}) {
70
88
  const config = typeof configOrDescription === "object" ? configOrDescription : { ...otherConfig, description: configOrDescription };
71
89
  try {
@@ -76,59 +94,56 @@ const _Command = class {
76
94
  }
77
95
  return this;
78
96
  }
79
- get hasConditionFn() {
80
- return !!this.conditionFn;
81
- }
82
- shouldRun(args) {
83
- if (this.conditionFn) {
84
- return this.conditionFn(args);
97
+ hasPrefix(parsedArgs) {
98
+ const argv = parsedArgs["_"];
99
+ if (argv.length === 0) {
100
+ return this.default;
85
101
  } else {
86
- if (this.default)
87
- return true;
88
- const isCmd = (t) => t[0] !== "[" && t[0] !== "<";
89
- for (let i = 0; i < this.format.length; i++) {
90
- if (!isCmd(this.format[i])) {
102
+ for (const prefix of this.prefix) {
103
+ if (prefix.length > 0 && prefix[0] === argv[0]) {
91
104
  return true;
92
105
  }
93
- if (i >= args["_"].length || this.format[i] !== args["_"][i]) {
94
- return false;
106
+ }
107
+ return false;
108
+ }
109
+ }
110
+ shouldRun(parsedArgs) {
111
+ const args = parsedArgs["_"];
112
+ for (const prefix of this.prefix) {
113
+ let match = true;
114
+ for (let i = 0; match && i < prefix.length; i++) {
115
+ if (args[i] !== prefix[i]) {
116
+ match = false;
95
117
  }
96
118
  }
97
- return true;
119
+ if (match) {
120
+ args.splice(0, prefix.length);
121
+ return true;
122
+ }
98
123
  }
124
+ if (this.default)
125
+ return true;
126
+ return false;
99
127
  }
100
128
  parseArgs(args, globalOptions) {
101
- if (this.conditionFn) {
102
- const argumentss2 = args["_"];
103
- const options2 = args;
104
- delete options2["_"];
105
- return {
106
- command: this,
107
- arguments: argumentss2,
108
- options: args
109
- };
110
- }
111
- const isCmd = (t) => t[0] !== "[" && t[0] !== "<";
112
129
  const argumentss = [];
113
- for (let i = 0; i < this.format.length; i++) {
114
- if (isCmd(this.format[i]))
115
- continue;
130
+ for (let i = 0; i < this.arguments.length; i++) {
116
131
  if (i < args["_"].length) {
117
- if (this.format[i].startsWith("[...")) {
132
+ if (this.arguments[i].startsWith("[...")) {
118
133
  argumentss.push(args["_"].slice(i).map(String));
119
134
  } else {
120
135
  argumentss.push(String(args["_"][i]));
121
136
  }
122
137
  } else {
123
- if (this.format[i].startsWith("<")) {
124
- this.logger.warn(`You should provide the argument "${this.format[i]}"`);
125
- argumentss.push(void 0);
126
- } else if (this.format[i].startsWith("[...")) {
138
+ if (this.arguments[i].startsWith("<")) {
139
+ this.logger.warn(`You should provide the argument "${this.arguments[i]}"`);
140
+ argumentss.push("");
141
+ } else if (this.arguments[i].startsWith("[...")) {
127
142
  argumentss.push([]);
128
- } else if (this.format[i].startsWith("[")) {
143
+ } else if (this.arguments[i].startsWith("[")) {
129
144
  argumentss.push(void 0);
130
145
  } else {
131
- this.logger.warn(`unknown format string ("${this.format[i]}")`);
146
+ this.logger.warn(`unknown format string ("${this.arguments[i]}")`);
132
147
  }
133
148
  }
134
149
  }
@@ -152,13 +167,14 @@ const _Command = class {
152
167
  options[name] = void 0;
153
168
  }
154
169
  }
155
- if (rawOption.construct) {
156
- options[name] = rawOption.construct(options[name]);
157
- } else if (rawOption.default) {
158
- if (!options[name]) {
170
+ if (rawOption.default !== void 0) {
171
+ if (options[name] === void 0 || options[name] === false) {
159
172
  options[name] = rawOption.default;
160
173
  }
161
174
  }
175
+ if (rawOption.construct !== void 0) {
176
+ options[name] = rawOption.construct(options[name]);
177
+ }
162
178
  }
163
179
  for (const key of Object.keys(options)) {
164
180
  if (!fullOptions.has(key)) {
@@ -173,88 +189,115 @@ const _Command = class {
173
189
  }
174
190
  action(fn) {
175
191
  this.actionFn = fn;
176
- return this;
177
192
  }
178
193
  async run(...args) {
179
194
  if (this.actionFn) {
180
- return await this.actionFn(...args, { logger: this.logger });
195
+ return await this.actionFn(...args, { logger: this.logger, color: kolorist });
181
196
  } else {
182
- this.logger.warn(`You may miss action function in "${this.format}"`);
197
+ this.logger.warn(`You may miss action function in ${this.format ? `"${this.format}"` : "<default command>"}`);
183
198
  }
184
199
  }
185
200
  };
186
201
  let Command = _Command;
187
202
  Command.MaxDep = 5;
188
- function createHelpCommand(breadc) {
189
- let helpCommand = void 0;
190
- return new Command("-h, --help", {
191
- condition(args) {
192
- const isEmpty = !args["--"]?.length;
193
- if ((args.help || args.h) && isEmpty) {
194
- if (args["_"].length > 0) {
195
- for (const cmd of breadc.commands) {
196
- if (!cmd.hasConditionFn && !cmd.default && cmd.shouldRun(args)) {
197
- helpCommand = cmd;
198
- return true;
203
+ class InternalCommand extends Command {
204
+ hasPrefix(_args) {
205
+ return false;
206
+ }
207
+ parseArgs(args, _globalOptions) {
208
+ const argumentss = args["_"];
209
+ const options = args;
210
+ delete options["_"];
211
+ delete options["help"];
212
+ delete options["version"];
213
+ return {
214
+ command: this,
215
+ arguments: argumentss,
216
+ options: args
217
+ };
218
+ }
219
+ }
220
+ class HelpCommand extends InternalCommand {
221
+ constructor(commands, help, logger) {
222
+ super("-h, --help", { description: "Display this message", logger });
223
+ this.runCommands = [];
224
+ this.helpCommands = [];
225
+ this.commands = commands;
226
+ this.help = help;
227
+ }
228
+ shouldRun(args) {
229
+ const isRestEmpty = !args["--"]?.length;
230
+ if ((args.help || args.h) && isRestEmpty) {
231
+ if (args["_"].length > 0) {
232
+ for (const cmd of this.commands) {
233
+ if (!cmd.default && !cmd.isInternal) {
234
+ if (cmd.shouldRun(args)) {
235
+ this.runCommands.push(cmd);
236
+ } else if (cmd.hasPrefix(args)) {
237
+ this.helpCommands.push(cmd);
199
238
  }
200
239
  }
201
240
  }
202
- return true;
203
- } else {
204
- return false;
205
241
  }
206
- },
207
- logger: breadc.logger
208
- }).action(() => {
209
- for (const line of breadc.help(helpCommand)) {
210
- breadc.logger.println(line);
242
+ return true;
243
+ } else {
244
+ return false;
211
245
  }
212
- });
246
+ }
247
+ async run() {
248
+ const shouldHelp = this.runCommands.length > 0 ? this.runCommands : this.helpCommands;
249
+ for (const line of this.help(shouldHelp)) {
250
+ this.logger.println(line);
251
+ }
252
+ this.runCommands.splice(0);
253
+ this.helpCommands.splice(0);
254
+ }
213
255
  }
214
- function createVersionCommand(breadc) {
215
- return new Command("-v, --version", {
216
- condition(args) {
217
- const isEmpty = !args["_"].length && !args["--"]?.length;
218
- if (args.version && isEmpty) {
219
- return true;
220
- } else if (args.v && isEmpty) {
221
- return true;
222
- } else {
223
- return false;
224
- }
225
- },
226
- logger: breadc.logger
227
- }).action(() => {
228
- breadc.logger.println(breadc.version());
229
- });
256
+ class VersionCommand extends InternalCommand {
257
+ constructor(version, logger) {
258
+ super("-v, --version", { description: "Display version number", logger });
259
+ this.version = version;
260
+ }
261
+ shouldRun(args) {
262
+ const isEmpty = !args["_"].length && !args["--"]?.length;
263
+ if (args.version && isEmpty) {
264
+ return true;
265
+ } else if (args.v && isEmpty) {
266
+ return true;
267
+ } else {
268
+ return false;
269
+ }
270
+ }
271
+ async run() {
272
+ this.logger.println(this.version);
273
+ }
274
+ }
275
+ function isArg(arg) {
276
+ return arg[0] === "[" && arg[arg.length - 1] === "]" || arg[0] === "<" && arg[arg.length - 1] === ">";
230
277
  }
231
278
 
232
279
  class Breadc {
233
280
  constructor(name, option) {
234
281
  this.options = [];
235
282
  this.commands = [];
283
+ this.callbacks = {
284
+ pre: [],
285
+ post: []
286
+ };
236
287
  this.name = name;
237
288
  this._version = option.version ?? "unknown";
238
289
  this.description = option.description;
239
290
  this.logger = createDefaultLogger(name, option.logger);
240
- const breadc = {
241
- name: this.name,
242
- version: () => this.version.call(this),
243
- help: (command) => this.help.call(this, command),
244
- logger: this.logger,
245
- options: this.options,
246
- commands: this.commands
247
- };
248
- this.commands.push(createVersionCommand(breadc), createHelpCommand(breadc));
291
+ this.commands.push(new VersionCommand(this.version(), this.logger), new HelpCommand(this.commands, this.help.bind(this), this.logger));
249
292
  }
250
293
  version() {
251
294
  return `${this.name}/${this._version}`;
252
295
  }
253
- help(command) {
296
+ help(commands = []) {
254
297
  const output = [];
255
298
  const println = (msg) => output.push(msg);
256
299
  println(this.version());
257
- if (!command) {
300
+ if (commands.length === 0) {
258
301
  if (this.description) {
259
302
  println("");
260
303
  if (Array.isArray(this.description)) {
@@ -265,27 +308,26 @@ class Breadc {
265
308
  println(this.description);
266
309
  }
267
310
  }
268
- } else {
269
- if (command.description) {
270
- println("");
271
- println(command.description);
272
- }
273
- }
274
- if (!command) {
275
311
  if (this.defaultCommand) {
276
312
  println(``);
277
313
  println(`Usage:`);
278
- println(` $ ${this.name} ${this.defaultCommand.format.join(" ")}`);
314
+ println(` $ ${this.name} ${this.defaultCommand.format}`);
315
+ }
316
+ } else if (commands.length === 1) {
317
+ const command = commands[0];
318
+ if (command.description) {
319
+ println("");
320
+ println(command.description);
279
321
  }
280
- } else {
281
322
  println(``);
282
323
  println(`Usage:`);
283
- println(` $ ${this.name} ${command.format.join(" ")}`);
324
+ println(` $ ${this.name} ${command.format}`);
284
325
  }
285
- if (!command && this.commands.length > 2) {
326
+ if (commands.length !== 1) {
327
+ const cmdList = (commands.length === 0 ? this.commands : commands).filter((c) => !c.isInternal);
286
328
  println(``);
287
329
  println(`Commands:`);
288
- const commandHelps = this.commands.filter((c) => !c.hasConditionFn).map((c) => [` $ ${this.name} ${c.format.join(" ")}`, c.description]);
330
+ const commandHelps = cmdList.map((c) => [` $ ${this.name} ${c.format}`, c.description]);
289
331
  for (const line of twoColumn(commandHelps)) {
290
332
  println(line);
291
333
  }
@@ -293,7 +335,7 @@ class Breadc {
293
335
  println(``);
294
336
  println(`Options:`);
295
337
  const optionHelps = [].concat([
296
- ...command ? command.options.map((o) => [` ${o.format}`, o.description]) : [],
338
+ ...commands.length > 0 ? commands.flatMap((cmd) => cmd.options.map((o) => [` ${o.format}`, o.description])) : [],
297
339
  ...this.options.map((o) => [` ${o.format}`, o.description]),
298
340
  [` -h, --help`, `Display this message`],
299
341
  [` -v, --version`, `Display version number`]
@@ -331,15 +373,28 @@ class Breadc {
331
373
  ...this.options,
332
374
  ...this.commands.flatMap((c) => c.options)
333
375
  ];
376
+ {
377
+ const names = /* @__PURE__ */ new Map();
378
+ for (const option of allowOptions) {
379
+ if (names.has(option.name)) {
380
+ const otherOption = names.get(option.name);
381
+ if (otherOption.type !== option.type) {
382
+ this.logger.warn(`Option "${option.name}" encounters conflict`);
383
+ }
384
+ } else {
385
+ names.set(option.name, option);
386
+ }
387
+ }
388
+ }
334
389
  const alias = allowOptions.reduce((map, o) => {
335
390
  if (o.shortcut) {
336
391
  map[o.shortcut] = o.name;
337
392
  }
338
393
  return map;
339
- }, {});
394
+ }, { h: "help", v: "version" });
340
395
  const argv = minimist(args, {
341
396
  string: allowOptions.filter((o) => o.type === "string").map((o) => o.name),
342
- boolean: allowOptions.filter((o) => o.type === "boolean").map((o) => o.name),
397
+ boolean: allowOptions.filter((o) => o.type === "boolean").map((o) => o.name).concat(["help", "version"]),
343
398
  alias,
344
399
  unknown: (t) => {
345
400
  if (t[0] !== "-")
@@ -363,32 +418,33 @@ class Breadc {
363
418
  }
364
419
  }
365
420
  if (this.defaultCommand) {
421
+ this.defaultCommand.shouldRun(argv);
366
422
  return this.defaultCommand.parseArgs(argv, this.options);
367
423
  }
368
424
  const argumentss = argv["_"];
369
425
  const options = argv;
370
426
  delete options["_"];
427
+ delete options["help"];
428
+ delete options["version"];
371
429
  return {
372
430
  command: void 0,
373
431
  arguments: argumentss,
374
432
  options
375
433
  };
376
434
  }
435
+ on(event, fn) {
436
+ this.callbacks[event].push(fn);
437
+ }
377
438
  async run(args) {
378
439
  const parsed = this.parse(args);
379
440
  if (parsed.command) {
380
- return await parsed.command.run(...parsed.arguments, parsed.options);
441
+ await Promise.all(this.callbacks.pre.map((fn) => fn(parsed.options)));
442
+ const returnValue = await parsed.command.run(...parsed.arguments, parsed.options);
443
+ await Promise.all(this.callbacks.post.map((fn) => fn(parsed.options)));
444
+ return returnValue;
381
445
  }
382
446
  }
383
447
  }
384
- function twoColumn(texts, split = " ") {
385
- const left = padRight(texts.map((t) => t[0]));
386
- return left.map((l, idx) => l + split + texts[idx][1]);
387
- }
388
- function padRight(texts, fill = " ") {
389
- const length = texts.map((t) => t.length).reduce((max, l) => Math.max(max, l), 0);
390
- return texts.map((t) => t + fill.repeat(length - t.length));
391
- }
392
448
 
393
449
  function breadc(name, option = {}) {
394
450
  return new Breadc(name, option);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "breadc",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "Yet another Command Line Application Framework with fully strong TypeScript support",
5
5
  "keywords": [
6
6
  "cli",
@@ -43,11 +43,11 @@
43
43
  "bumpp": "^8.2.1",
44
44
  "prettier": "^2.7.1",
45
45
  "typescript": "^4.7.4",
46
- "unbuild": "^0.7.4",
47
- "vite": "^2.9.12",
48
- "vitest": "^0.15.2"
46
+ "unbuild": "^0.7.6",
47
+ "vite": "^3.0.2",
48
+ "vitest": "^0.18.1"
49
49
  },
50
- "packageManager": "pnpm@7.3.0",
50
+ "packageManager": "pnpm@7.5.2",
51
51
  "scripts": {
52
52
  "build": "unbuild",
53
53
  "format": "prettier --write src/**/*.ts test/*.ts examples/*.ts",