bob-core 0.8.7 → 0.9.1

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,5 +1,275 @@
1
- # BOB - Bash Operation Buddy
1
+ # BOB Core - Bash Operation Buddy Core
2
2
 
3
3
  ## Introduction
4
4
 
5
- BOB (Bash Operation Buddy) est un le coeur pour crée facilement un CLI à votre sauce.
5
+ BOB (Bash Operation Buddy) Core is a library that provides a set of functions to create your own CLI in TypeScript.
6
+
7
+ ## Usage
8
+
9
+ ```bash
10
+ npm install bob-core
11
+ ```
12
+
13
+ Initialize the CLI:
14
+
15
+ ```typescript
16
+ import { CLI } from 'bob-core';
17
+
18
+ const cli = new CLI();
19
+
20
+ await cli.withCommands('./commands');
21
+
22
+ cli.run('commandName', 'arg1', 'arg2', 'arg3');
23
+ ```
24
+
25
+ Create a command in the `commands` folder:
26
+
27
+ ```typescript
28
+ import { Command } from 'bob-core';
29
+
30
+ export default class MyCommand extends Command {
31
+ public name = 'my-command {arg1} {--option1}';
32
+ public description = 'This is my command';
33
+
34
+ /**
35
+ * Define the arguments and options help
36
+ *
37
+ * Optional
38
+ */
39
+ helpDefinition = {
40
+ arg1: 'This is the first argument',
41
+ '--option1': 'This is the first option'
42
+ }
43
+
44
+ /**
45
+ * Provide examples of how to use the command
46
+ *
47
+ * Optional
48
+ */
49
+ commandsExamples = [
50
+ {
51
+ command: 'my-command value1 --option1',
52
+ description: 'This is an example'
53
+ }
54
+ ]
55
+
56
+ public async handle() {
57
+ console.log('Hello World');
58
+ console.log('Arguments:', this.argument('arg1'));
59
+ console.log('Options:', this.option('option1'));
60
+ }
61
+ }
62
+ ```
63
+
64
+ ## Cli Help
65
+
66
+ The CLI provides a help command that displays all available commands and their descriptions.
67
+
68
+ ```bash
69
+ node cli.js help
70
+ ```
71
+
72
+ ## Commands
73
+
74
+ ```bash
75
+ Bob CLI x.x.x 💪
76
+
77
+ Usage:
78
+ command [options] [arguments]
79
+
80
+ Available commands:
81
+
82
+ help Show help
83
+ test test description
84
+ sub:
85
+ sub sub command description
86
+ sub:sub sub:sub command description
87
+
88
+ ```
89
+
90
+ ## Command help
91
+
92
+ You can also display the help of a specific command.
93
+
94
+ ```bash
95
+ node cli.js test -h
96
+ ```
97
+
98
+ ```bash
99
+ Description:
100
+ test description
101
+
102
+ Usage:
103
+ test <user> [options]
104
+
105
+ Arguments:
106
+ user user description
107
+ test test description [default: null]
108
+ test2 [default: []] (variadic)
109
+
110
+ Options:
111
+ --option, -o, -b option description (boolean) [default: false]
112
+ --flag flag description (string) [default: null]
113
+ --arr arr description (array) [default: null]
114
+ --flag2 flag2 description (string) [default: 2]
115
+ --help, -h Display help for the given command. When no command is given display help for the list command (boolean)
116
+
117
+ Examples:
118
+ Example description 1
119
+
120
+ node cli.js test yayo --option
121
+
122
+ Example description 2
123
+
124
+ node cli.js test anothervalue --flag=2
125
+ ```
126
+
127
+ Depending on the command, the help will display the command signature, description, arguments, options and examples.
128
+
129
+
130
+ ## Commands signature
131
+
132
+ The command signature is a string that defines the command name, arguments and options.
133
+
134
+ Example:
135
+ ```typescript
136
+ signature = 'my-command {arg1} {arg2} {arg3} {--option1} {--option2}';
137
+ ```
138
+
139
+ ### Arguments
140
+
141
+ All user supplied arguments and options are wrapped in curly braces.
142
+ In the following example, the command defines three **required** arguments `arg1`, `arg2` and `arg3`.
143
+
144
+ ```typescript
145
+ signature = 'my-command {arg1} {arg2} {arg3}';
146
+ ```
147
+
148
+ You may want to make an argument optional by adding a `?` after the argument name or by providing a default value with `=`.
149
+
150
+ ```typescript
151
+ signature = 'my-command {arg1} {arg2?} {arg3=defaultValue}';
152
+
153
+ handle() {
154
+ this.argument('arg1'); // 'value' or throw an error if not provided
155
+ this.argument('arg2'); // 'value' or null if not provided
156
+ this.argument('arg3'); // 'value' or 'defaultValue' if not provided
157
+ }
158
+ ```
159
+
160
+ You can also define a variadic argument by adding `*` after the argument name.
161
+ Variadic arguments are stored in an array.
162
+
163
+ ```typescript
164
+ signature = 'my-command {arg1} {arg2*}';
165
+
166
+ handle() {
167
+ this.argument('arg1'); // 'value1'
168
+ this.argument('arg2'); // ['value2', 'value3']
169
+ }
170
+ ```
171
+
172
+ Variadic arguments can also be optional.
173
+
174
+ ```typescript
175
+ signature = 'my-command {arg1} {arg2*?}';
176
+ ```
177
+
178
+ ### Options
179
+
180
+ Options are defined by `{--optionName}`.
181
+
182
+ ```typescript
183
+ signature = 'my-command {--option1} {--option2} {--option3}';
184
+ ```
185
+
186
+ By default options are boolean with a default value of `false`.
187
+ You can also change the option type to string by adding `=` to the option definition.
188
+ You can also provide a default value by adding `=value`.
189
+
190
+ If the value is 'true' or 'false', the option will be converted to a boolean.
191
+
192
+ ```typescript
193
+ signature = 'my-command {--option1} {--option2=true} {--option3=} {--option4=defaultValue} {--option5=}';
194
+
195
+ handle() {
196
+ this.option('option1'); // by default `false` and can be set to `true` by the user
197
+ this.option('option2'); // by default `true` and can be set to `false` by the user
198
+ this.option('option3'); // by default `null` and can be set to "value" by the user
199
+ this.option('option4'); // by default "defaultValue" and can be set to "value" by the user
200
+ }
201
+ ```
202
+
203
+ You can also define a variadic option by adding `*` as option value. (e.g. `{--option2=*}`)
204
+ Variadic options are stored in an array.
205
+
206
+ ```typescript
207
+ signature = 'my-command {--option1} {--option2=*}';
208
+
209
+ handle() {
210
+ this.option('option1'); // 'value1'
211
+ this.option('option2'); // ['value2', 'value3'] // or [] if not provided
212
+ }
213
+ ```
214
+
215
+ ## Commands I/O
216
+
217
+ ### Arguments
218
+
219
+ ```typescript
220
+ this.argument('arg1');
221
+ ```
222
+
223
+ You can also provide a default value if the argument is optional.
224
+
225
+ ```typescript
226
+ this.argument('arg1', 'defaultValue');
227
+ ```
228
+
229
+ If you always need a boolean value, you can use the `argumentBoolean` method.
230
+
231
+ ```typescript
232
+ this.argumentBoolean('arg1');
233
+ ```
234
+
235
+ If you always need a number value, you can use the `argumentNumber` method.
236
+
237
+ ```typescript
238
+ this.argumentNumber('arg1');
239
+ ```
240
+
241
+ If you always need a array value, you can use the `argumentArray` method.
242
+
243
+ ```typescript
244
+ this.argumentArray('arg1');
245
+ ```
246
+
247
+ ### Options
248
+
249
+ ```typescript
250
+ this.option('option1');
251
+ ```
252
+
253
+ You can also provide a default value if the option is optional.
254
+
255
+ ```typescript
256
+ this.option('option1', 'defaultValue');
257
+ ```
258
+
259
+ If you always need a boolean value, you can use the `optionBoolean` method.
260
+
261
+ ```typescript
262
+ this.optionBoolean('option1');
263
+ ```
264
+
265
+ If you always need a number value, you can use the `optionNumber` method.
266
+
267
+ ```typescript
268
+ this.optionNumber('option1');
269
+ ```
270
+
271
+ If you always need a array value, you can use the `optionArray` method.
272
+
273
+ ```typescript
274
+ this.optionArray('option1');
275
+ ```
package/dist/Cli.d.ts CHANGED
@@ -6,12 +6,16 @@ export declare class Cli<C> {
6
6
  readonly commandRegistry: CommandRegistry;
7
7
  private readonly exceptionHandler;
8
8
  private readonly ctx?;
9
+ private readonly helpCommand;
9
10
  get CommandRegistryClass(): typeof CommandRegistry;
10
11
  get HelpCommandClass(): typeof HelpCommand;
11
12
  get ExceptionHandlerClass(): typeof ExceptionHandler;
12
13
  constructor(ctx?: C);
13
14
  setCommandResolver(resolver: (path: string) => Promise<Command>): void;
14
- loadCommandsPath(commandsPath: string): Promise<void>;
15
- registerCommand(command: Command): void;
15
+ withCommands(...commands: Array<Command | {
16
+ new (): Command;
17
+ } | string>): Promise<void>;
16
18
  runCommand(command: string, ...args: any[]): Promise<number>;
19
+ runHelpCommand(): Promise<number>;
20
+ protected registerCommand(command: Command): void;
17
21
  }
package/dist/Cli.js CHANGED
@@ -11,6 +11,7 @@ class Cli {
11
11
  commandRegistry;
12
12
  exceptionHandler;
13
13
  ctx;
14
+ helpCommand;
14
15
  get CommandRegistryClass() {
15
16
  return CommandRegistry_1.CommandRegistry;
16
17
  }
@@ -24,20 +25,36 @@ class Cli {
24
25
  this.ctx = ctx;
25
26
  this.commandRegistry = new this.CommandRegistryClass();
26
27
  this.exceptionHandler = new this.ExceptionHandlerClass();
27
- this.registerCommand(new this.HelpCommandClass(this.commandRegistry));
28
+ this.helpCommand = new this.HelpCommandClass(this.commandRegistry);
29
+ this.registerCommand(this.helpCommand);
28
30
  }
29
31
  setCommandResolver(resolver) {
30
32
  this.commandRegistry.setCommandResolver(resolver);
31
33
  }
32
- async loadCommandsPath(commandsPath) {
33
- await this.commandRegistry.loadCommandsPath(commandsPath);
34
- }
35
- registerCommand(command) {
36
- this.commandRegistry.registerCommand(command);
34
+ async withCommands(...commands) {
35
+ for (const command of commands) {
36
+ if (typeof command === 'string') {
37
+ await this.commandRegistry.loadCommandsPath(command);
38
+ }
39
+ else {
40
+ if (typeof command === 'function') {
41
+ this.registerCommand(new command());
42
+ }
43
+ else {
44
+ this.registerCommand(command);
45
+ }
46
+ }
47
+ }
37
48
  }
38
49
  async runCommand(command, ...args) {
39
50
  return await this.commandRegistry.runCommand(this.ctx, command, ...args)
40
51
  .catch(this.exceptionHandler.handle);
41
52
  }
53
+ async runHelpCommand() {
54
+ return await this.runCommand(this.helpCommand.command);
55
+ }
56
+ registerCommand(command) {
57
+ this.commandRegistry.registerCommand(command);
58
+ }
42
59
  }
43
60
  exports.Cli = Cli;
package/dist/Command.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CommandHelper } from "./CommandHelper";
2
- import { CommandParser } from "./CommandParser";
2
+ import { ArgSignature, CommandParser } from "./CommandParser";
3
3
  export type CommandExample = {
4
4
  description: string;
5
5
  command: string;
@@ -13,8 +13,10 @@ export declare abstract class Command<C = undefined> extends CommandHelper {
13
13
  };
14
14
  protected commandsExamples: CommandExample[];
15
15
  protected parser: CommandParser;
16
- get command(): string;
17
16
  protected abstract handle(): Promise<void | number>;
17
+ private get CommandParserClass();
18
+ protected get defaultOptions(): ArgSignature[];
19
+ get command(): string;
18
20
  run(ctx: C, ...args: any[]): Promise<number>;
19
21
  protected setOption(name: string, value: any): void;
20
22
  protected setArgument(name: string, value: any): void;
package/dist/Command.js CHANGED
@@ -1,13 +1,31 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.Command = void 0;
4
7
  const CommandHelper_1 = require("./CommandHelper");
5
8
  const CommandParser_1 = require("./CommandParser");
9
+ const chalk_1 = __importDefault(require("chalk"));
6
10
  class Command extends CommandHelper_1.CommandHelper {
7
11
  ctx;
8
12
  helperDefinitions = {};
9
13
  commandsExamples = [];
10
14
  parser;
15
+ get CommandParserClass() {
16
+ return CommandParser_1.CommandParser;
17
+ }
18
+ get defaultOptions() {
19
+ return [
20
+ {
21
+ name: 'help',
22
+ optional: true,
23
+ type: 'boolean',
24
+ help: (0, chalk_1.default) `Display help for the given command. When no command is given display help for the {green list} command`,
25
+ alias: ['h']
26
+ }
27
+ ];
28
+ }
11
29
  get command() {
12
30
  if (this.parser) {
13
31
  return this.parser.command;
@@ -16,7 +34,7 @@ class Command extends CommandHelper_1.CommandHelper {
16
34
  }
17
35
  async run(ctx, ...args) {
18
36
  this.ctx = ctx;
19
- this.parser = new CommandParser_1.CommandParser(this.signature, this.helperDefinitions, ...args);
37
+ this.parser = new this.CommandParserClass(this.signature, this.helperDefinitions, this.defaultOptions, ...args);
20
38
  if (args.includes('--help') || args.includes('-h')) {
21
39
  return this.help.call(this);
22
40
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const Command_1 = require("./Command");
4
+ const MissingRequiredArgumentValue_1 = require("./errors/MissingRequiredArgumentValue");
5
+ class MockCommand extends Command_1.Command {
6
+ signature = 'mockCommand {argument} {--option}';
7
+ description = 'This is a mock command for testing';
8
+ handle() {
9
+ const opts = this.option('option');
10
+ const arg = this.argument('argument');
11
+ if (opts) {
12
+ return Promise.resolve(11);
13
+ }
14
+ if (arg === 'value') {
15
+ return Promise.resolve(1);
16
+ }
17
+ else if (arg) {
18
+ return Promise.resolve(-1);
19
+ }
20
+ return Promise.resolve(0);
21
+ }
22
+ }
23
+ describe('Command', () => {
24
+ let command;
25
+ beforeEach(() => {
26
+ command = new MockCommand();
27
+ });
28
+ it('should have a command', () => {
29
+ expect(command.command).toBe('mockCommand');
30
+ });
31
+ it('should have a signature', () => {
32
+ expect(command.signature).toBe('mockCommand {argument} {--option}');
33
+ });
34
+ it('should have a description', () => {
35
+ expect(command.description).toBe('This is a mock command for testing');
36
+ });
37
+ it('should handle command with argument', async () => {
38
+ const result = await command.run(undefined, 'value');
39
+ expect(result).toBe(1);
40
+ });
41
+ it('should handle command with argument', async () => {
42
+ const result = await command.run(undefined, 'badValue');
43
+ expect(result).toBe(-1);
44
+ });
45
+ it('should throw error if argument is missing', async () => {
46
+ await expect(command.run(undefined)).rejects.toThrowError(MissingRequiredArgumentValue_1.MissingRequiredArgumentValue);
47
+ });
48
+ it('should handle command with option', async () => {
49
+ const result = await command.run(undefined, 'value', '--option');
50
+ expect(result).toBe(11);
51
+ });
52
+ });
@@ -1,6 +1,4 @@
1
1
  import { Command } from "./Command";
2
- import { ArgSignature } from "./CommandParser";
3
2
  export declare class CommandHelper {
4
- get defaultOptions(): ArgSignature[];
5
3
  help(this: Command<any>): number;
6
4
  }
@@ -8,21 +8,10 @@ const chalk_1 = __importDefault(require("chalk"));
8
8
  const lodash_1 = require("lodash");
9
9
  const string_1 = require("./lib/string");
10
10
  class CommandHelper {
11
- get defaultOptions() {
12
- return [
13
- {
14
- name: 'help',
15
- optional: true,
16
- type: 'boolean',
17
- help: (0, chalk_1.default) `Display help for the given command. When no command is given display help for the {green list} command`,
18
- alias: ['h']
19
- }
20
- ];
21
- }
22
11
  help() {
23
12
  const log = console.log;
24
- const availableArguments = Object.values(this.parser.argumentsSignatures());
25
- const availableOptions = [...Object.values(this.parser.optionsSignatures()), ...this.defaultOptions]
13
+ const availableArguments = Object.values(this.parser.getArgumentSignatures());
14
+ const availableOptions = Object.values(this.parser.getOptionSignatures())
26
15
  .map((signature) => ({
27
16
  ...signature,
28
17
  optionWithAlias: `--${signature.name}${signature.alias?.map(a => `, -${a}`).join('') ?? ''}`
@@ -13,24 +13,31 @@ export declare class CommandParser {
13
13
  protected readonly helperDefinitions: {
14
14
  [key: string]: string;
15
15
  };
16
+ protected readonly defaultOptions: ArgSignature[];
16
17
  command: string;
17
18
  private arguments;
18
19
  private options;
19
20
  private argumentsSignature;
20
- private optionsSignature;
21
+ private optionSignatures;
22
+ private optionAliases;
21
23
  option(name: string): any;
22
24
  setOption(name: string, value: any): void;
25
+ optionHelp(name: string): string | undefined;
23
26
  argument(name: string): any;
24
27
  setArgument(name: string, value: any): void;
25
- argumentsSignatures(): {
28
+ argumentHelp(name: string): string | undefined;
29
+ getArgumentSignatures(): {
26
30
  [argument: string]: ArgSignature;
27
31
  };
28
- optionsSignatures(): {
32
+ getOptionSignatures(): {
29
33
  [option: string]: ArgSignature;
30
34
  };
31
35
  constructor(signature: string, helperDefinitions: {
32
36
  [key: string]: string;
33
- }, ...args: any[]);
37
+ }, defaultOptions?: ArgSignature[], ...args: any[]);
38
+ private getParamValue;
39
+ private parseArguments;
40
+ private parseOptions;
34
41
  private parseSignature;
35
42
  private parseParamSignature;
36
43
  validate(): void;
@@ -6,105 +6,154 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.CommandParser = void 0;
7
7
  const minimist_1 = __importDefault(require("minimist"));
8
8
  const MissingRequiredArgumentValue_1 = require("./errors/MissingRequiredArgumentValue");
9
+ const MissingSignatureOption_1 = require("./errors/MissingSignatureOption");
10
+ const MissingSignatureArgument_1 = require("./errors/MissingSignatureArgument");
11
+ const InvalidOption_1 = require("./errors/InvalidOption");
9
12
  class CommandParser {
10
13
  signature;
11
14
  helperDefinitions;
15
+ defaultOptions;
12
16
  command;
13
17
  arguments = {};
14
18
  options = {};
15
19
  argumentsSignature = {};
16
- optionsSignature = {};
20
+ optionSignatures = {};
21
+ optionAliases = {};
17
22
  option(name) {
18
- if (!this.optionsSignature[name]) {
19
- throw new Error(`Option ${name} not found`);
20
- }
21
- const signature = this.optionsSignature[name];
22
- if (signature.type === 'boolean') {
23
- if (this.options[name] === 'true' || this.options[name] === '1') {
24
- return true;
25
- }
26
- else if (this.options[name] === 'false' || this.options[name] === '0') {
27
- return false;
28
- }
29
- return Boolean(this.options[name]);
30
- }
31
- if (signature.type === 'array') {
32
- if (!this.options[name]) {
33
- return [];
34
- }
35
- return Array.isArray(this.options[name]) ? this.options[name] : [this.options[name]];
23
+ if (!this.optionSignatures[name]) {
24
+ throw new MissingSignatureOption_1.MissingSignatureOption(name, Object.values(this.optionSignatures));
36
25
  }
37
26
  return this.options[name];
38
27
  }
39
28
  setOption(name, value) {
29
+ if (!this.optionSignatures[name]) {
30
+ throw new MissingSignatureOption_1.MissingSignatureOption(name, Object.values(this.optionSignatures));
31
+ }
40
32
  this.options[name] = value;
41
33
  }
34
+ optionHelp(name) {
35
+ const optionSignature = this.optionSignatures[name];
36
+ if (!optionSignature) {
37
+ throw new MissingSignatureOption_1.MissingSignatureOption(name, Object.values(this.optionSignatures));
38
+ }
39
+ return this.optionSignatures[name].help;
40
+ }
42
41
  argument(name) {
42
+ if (!this.argumentsSignature[name]) {
43
+ throw new MissingSignatureArgument_1.MissingSignatureArgument(name, Object.values(this.argumentsSignature));
44
+ }
43
45
  return this.arguments[name];
44
46
  }
45
47
  setArgument(name, value) {
48
+ if (!this.argumentsSignature[name]) {
49
+ throw new MissingSignatureArgument_1.MissingSignatureArgument(name, Object.values(this.argumentsSignature));
50
+ }
46
51
  this.arguments[name] = value;
47
52
  }
48
- argumentsSignatures() {
53
+ argumentHelp(name) {
54
+ const argumentSignature = this.argumentsSignature[name];
55
+ if (!argumentSignature) {
56
+ throw new MissingSignatureArgument_1.MissingSignatureArgument(name, Object.values(this.argumentsSignature));
57
+ }
58
+ return this.argumentsSignature[name].help;
59
+ }
60
+ getArgumentSignatures() {
49
61
  return this.argumentsSignature;
50
62
  }
51
- optionsSignatures() {
52
- return this.optionsSignature;
63
+ getOptionSignatures() {
64
+ return this.optionSignatures;
53
65
  }
54
- constructor(signature, helperDefinitions, ...args) {
66
+ constructor(signature, helperDefinitions, defaultOptions = [], ...args) {
55
67
  this.signature = signature;
56
68
  this.helperDefinitions = helperDefinitions;
57
- const [command, ...params] = signature.split(/\{(.*?)\}/g).map(param => param.trim()).filter(Boolean);
69
+ this.defaultOptions = defaultOptions;
70
+ const [command, ...signatureParams] = signature.split(/\{(.*?)\}/g).map(param => param.trim()).filter(Boolean);
58
71
  const { _: paramValues, ...optionValues } = (0, minimist_1.default)(args);
72
+ if (defaultOptions.length) {
73
+ for (const option of defaultOptions) {
74
+ this.optionSignatures[option.name] = option;
75
+ this.options[option.name] = option.defaultValue;
76
+ if (option.alias) {
77
+ for (const alias of option.alias) {
78
+ this.optionAliases[alias] = option.name;
79
+ }
80
+ }
81
+ }
82
+ }
59
83
  this.command = command;
60
- this.parseSignature(params, optionValues, paramValues);
84
+ this.parseSignature(signatureParams);
85
+ this.parseArguments(paramValues);
86
+ this.parseOptions(optionValues);
87
+ }
88
+ getParamValue(value, signature) {
89
+ if (signature.type === 'boolean') {
90
+ if (value === 'true' || value === '1') {
91
+ return true;
92
+ }
93
+ else if (value === 'false' || value === '0') {
94
+ return false;
95
+ }
96
+ return Boolean(value);
97
+ }
98
+ if (signature.type === 'array') {
99
+ if (!value) {
100
+ return [];
101
+ }
102
+ return Array.isArray(value) ? value : [value];
103
+ }
104
+ return value ?? signature.defaultValue;
105
+ }
106
+ parseArguments(paramValues) {
107
+ for (const [argument, value] of Object.entries(this.arguments)) {
108
+ const argSignature = this.argumentsSignature[argument];
109
+ if (argSignature.variadic) {
110
+ this.arguments[argument] = paramValues;
111
+ }
112
+ else {
113
+ const paramValue = paramValues.shift();
114
+ this.arguments[argument] = this.getParamValue(paramValue, argSignature);
115
+ }
116
+ }
61
117
  }
62
- parseSignature(params, optionValues, paramValues) {
118
+ parseOptions(optionValues) {
119
+ for (const [option, value] of Object.entries(optionValues)) {
120
+ const optionAlias = this.optionAliases[option];
121
+ const optionSignature = this.optionSignatures[option] ?? this.optionSignatures[optionAlias];
122
+ if (!optionSignature) {
123
+ throw new InvalidOption_1.InvalidOption(option, Object.values(this.optionSignatures));
124
+ }
125
+ this.options[option] = this.getParamValue(value, optionSignature);
126
+ for (const alias of optionSignature.alias ?? []) {
127
+ if (optionValues[alias]) {
128
+ this.options[optionSignature.name] = optionValues[alias];
129
+ }
130
+ }
131
+ }
132
+ }
133
+ parseSignature(params) {
63
134
  for (const paramSignature of params) {
64
135
  const param = this.parseParamSignature(paramSignature);
65
136
  if (param.isOption) {
66
- const optionValue = optionValues[param.name];
67
- this.options[param.name] = optionValue ?? param.defaultValue ?? null;
68
- this.optionsSignature[param.name] = param;
137
+ this.options[param.name] = param.defaultValue ?? null;
138
+ this.optionSignatures[param.name] = param;
69
139
  for (const alias of param.alias ?? []) {
70
- if (optionValues[alias]) {
71
- this.options[param.name] = optionValues[alias];
72
- this.optionsSignature[param.name] = param;
73
- }
140
+ this.optionAliases[alias] = param.name;
74
141
  }
75
142
  }
76
143
  else {
77
- if (param.variadic) {
78
- const paramValue = paramValues.splice(0, paramValues.length);
79
- this.arguments[param.name] = paramValue ?? [];
80
- }
81
- else {
82
- const paramValue = paramValues.shift();
83
- this.arguments[param.name] = paramValue ?? param.defaultValue ?? null;
84
- }
144
+ this.arguments[param.name] = param.defaultValue ?? null;
85
145
  this.argumentsSignature[param.name] = param;
86
146
  }
87
147
  }
88
148
  }
89
149
  parseParamSignature(argument) {
90
- let cleanedArgs = argument;
91
- let isOptional = false;
92
- let isVariadic = false;
93
- if (cleanedArgs.endsWith('?')) {
94
- cleanedArgs = cleanedArgs.slice(0, -1);
95
- isOptional = true;
96
- }
97
- if (cleanedArgs.endsWith('*')) {
98
- cleanedArgs = cleanedArgs.slice(0, -1);
99
- isVariadic = true;
100
- }
101
150
  const arg = {
102
- name: cleanedArgs,
103
- optional: isOptional,
104
- type: isVariadic ? 'array' : 'string',
151
+ name: argument,
152
+ optional: false,
153
+ type: 'string',
105
154
  help: undefined,
106
- defaultValue: isVariadic ? [] : null,
107
- variadic: isVariadic,
155
+ defaultValue: null,
156
+ variadic: false,
108
157
  isOption: false
109
158
  };
110
159
  if (arg.name.includes(':')) {
@@ -149,6 +198,16 @@ class CommandParser {
149
198
  arg.defaultValue = [];
150
199
  arg.type = 'array';
151
200
  }
201
+ if (arg.name.endsWith('?')) {
202
+ arg.optional = true;
203
+ arg.name = arg.name.slice(0, -1);
204
+ }
205
+ if (arg.name.endsWith('*')) {
206
+ arg.type = 'array';
207
+ arg.variadic = true;
208
+ arg.defaultValue = [];
209
+ arg.name = arg.name.slice(0, -1);
210
+ }
152
211
  arg.help = arg.help ?? this.helperDefinitions[arg.name] ?? this.helperDefinitions[`--${arg.name}`];
153
212
  return arg;
154
213
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const CommandParser_1 = require("./CommandParser");
4
+ const MissingRequiredArgumentValue_1 = require("./errors/MissingRequiredArgumentValue");
5
+ describe('CommandParser', () => {
6
+ let commandParser;
7
+ const parseCommand = (signature, args, helperDefinition = {}) => {
8
+ return new CommandParser_1.CommandParser(signature, helperDefinition, [], ...args);
9
+ };
10
+ it('should parse signature without arguments & options', () => {
11
+ commandParser = parseCommand('test', []);
12
+ expect(commandParser.command).toBe('test');
13
+ });
14
+ describe('Arguments', () => {
15
+ it('should parse signature with arguments', () => {
16
+ commandParser = parseCommand('test {arg1} {arg2}', ['value1', 'value2']);
17
+ expect(commandParser.argument('arg1')).toBe('value1');
18
+ expect(commandParser.argument('arg2')).toBe('value2');
19
+ });
20
+ it('should parse signature with optional arguments', () => {
21
+ commandParser = parseCommand('test {arg1?} {arg2?}', ['value1']);
22
+ expect(commandParser.argument('arg1')).toBe('value1');
23
+ expect(commandParser.argument('arg2')).toBeNull();
24
+ });
25
+ it('should parse signature with optional arguments with default value', () => {
26
+ commandParser = parseCommand('test {arg1?} {arg2=defaultValue1}', ['value1']);
27
+ expect(commandParser.argument('arg1')).toBe('value1');
28
+ expect(commandParser.argument('arg2')).toBe('defaultValue1');
29
+ });
30
+ it('should parse signature with variadic arguments', () => {
31
+ commandParser = parseCommand('test {arg1*}', ['value1', 'value2']);
32
+ expect(commandParser.argument('arg1')).toEqual(['value1', 'value2']);
33
+ });
34
+ it('should parse signature with optional variadic arguments', () => {
35
+ commandParser = parseCommand('test {arg1*?}', ['value1', 'value2']);
36
+ expect(commandParser.argument('arg1')).toEqual(['value1', 'value2']);
37
+ });
38
+ it('should parse signature with optional variadic arguments without value', () => {
39
+ commandParser = parseCommand('test {arg1*?}', []);
40
+ expect(commandParser.argument('arg1')).toEqual([]);
41
+ });
42
+ it('should set argument value', () => {
43
+ commandParser = parseCommand('test {arg1}', ['value1']);
44
+ commandParser.setArgument('arg1', 'newValue');
45
+ expect(commandParser.argument('arg1')).toBe('newValue');
46
+ });
47
+ it('should throw error when argument is missing with setArgument', () => {
48
+ commandParser = parseCommand('test {arg1}', []);
49
+ expect(() => commandParser.setArgument('arg2', 'newValue')).toThrowError(Error);
50
+ });
51
+ it('calling validate method should throw error when argument is missing', () => {
52
+ commandParser = parseCommand('test {arg1}', []);
53
+ expect(() => commandParser.validate()).toThrowError(MissingRequiredArgumentValue_1.MissingRequiredArgumentValue);
54
+ });
55
+ it('calling validate should throw with variadic argument is missing', () => {
56
+ commandParser = parseCommand('test {arg1*}', []);
57
+ expect(() => commandParser.validate()).toThrowError(MissingRequiredArgumentValue_1.MissingRequiredArgumentValue);
58
+ });
59
+ });
60
+ describe('Options', () => {
61
+ it('boolean option should be false when not provided', () => {
62
+ commandParser = parseCommand('test {--option}', []);
63
+ expect(commandParser.option('option')).toBeFalsy();
64
+ });
65
+ it('boolean option should be true when provided', () => {
66
+ commandParser = parseCommand('test {--option}', ['--option']);
67
+ expect(commandParser.option('option')).toBeTruthy();
68
+ });
69
+ it('boolean option should be true when provided with value', () => {
70
+ commandParser = parseCommand('test {--option}', ['--option=true']);
71
+ expect(commandParser.option('option')).toBeTruthy();
72
+ });
73
+ it('boolean option should be false when provided with value', () => {
74
+ commandParser = parseCommand('test {--option}', ['--option=false']);
75
+ expect(commandParser.option('option')).toBeFalsy();
76
+ });
77
+ it('string option should be null when not provided', () => {
78
+ commandParser = parseCommand('test {--option=}', []);
79
+ expect(commandParser.option('option')).toBeNull();
80
+ });
81
+ it('string option should be value when provided', () => {
82
+ commandParser = parseCommand('test {--option=}', ['--option=value']);
83
+ expect(commandParser.option('option')).toBe('value');
84
+ });
85
+ it('string option should take the default value when not provided', () => {
86
+ commandParser = parseCommand('test {--option=default}', []);
87
+ expect(commandParser.option('option')).toBe('default');
88
+ });
89
+ it('string option should take the provided value with default value', () => {
90
+ commandParser = parseCommand('test {--option=default}', ['--option=value']);
91
+ expect(commandParser.option('option')).toBe('value');
92
+ });
93
+ it('array option should be empty when not provided', () => {
94
+ commandParser = parseCommand('test {--option=*}', []);
95
+ expect(commandParser.option('option')).toEqual([]);
96
+ });
97
+ it('array option should be value when provided', () => {
98
+ commandParser = parseCommand('test {--option=*}', ['--option=value1', '--option=value2']);
99
+ expect(commandParser.option('option')).toEqual(['value1', 'value2']);
100
+ });
101
+ });
102
+ describe('Mixed', () => {
103
+ it('should parse signature with arguments and options', () => {
104
+ commandParser = parseCommand('test {arg1} {arg2} {--option}', ['value1', 'value2', '--option']);
105
+ expect(commandParser.argument('arg1')).toBe('value1');
106
+ expect(commandParser.argument('arg2')).toBe('value2');
107
+ expect(commandParser.option('option')).toBeTruthy();
108
+ });
109
+ it('should parse signature with optional arguments and options', () => {
110
+ commandParser = parseCommand('test {arg1?} {arg2?} {--option}', ['value1', '--option']);
111
+ expect(commandParser.argument('arg1')).toBe('value1');
112
+ expect(commandParser.argument('arg2')).toBeNull();
113
+ expect(commandParser.option('option')).toBeTruthy();
114
+ });
115
+ });
116
+ describe('Helper', () => {
117
+ it('should parse help signature', () => {
118
+ commandParser = parseCommand('test {arg1} {arg2:help 1} {--option : option help 2 }', ['value1', 'value2', '--option']);
119
+ expect(commandParser.argumentHelp('arg1')).toBeUndefined();
120
+ expect(commandParser.argumentHelp('arg2')).toBe('help 1');
121
+ expect(commandParser.optionHelp('option')).toBe('option help 2');
122
+ });
123
+ it('should define help with helperDefinition', () => {
124
+ commandParser = parseCommand('test {arg1} {arg2} {--option} {--option2}', ['value1', 'value2', '--option'], {
125
+ arg1: 'arg1 help',
126
+ arg2: 'arg2 help',
127
+ '--option': 'option help'
128
+ });
129
+ expect(commandParser.argumentHelp('arg1')).toBe('arg1 help');
130
+ expect(commandParser.argumentHelp('arg2')).toBe('arg2 help');
131
+ expect(commandParser.optionHelp('option')).toBe('option help');
132
+ expect(commandParser.optionHelp('option2')).toBeUndefined();
133
+ });
134
+ it('should define help with helperDefinition with default value', () => {
135
+ commandParser = parseCommand('test {arg1:arg1 help} {arg2} {--option=default}', ['value1', 'value2'], {
136
+ arg2: 'arg2 help',
137
+ '--option': 'option help'
138
+ });
139
+ expect(commandParser.argumentHelp('arg1')).toBe('arg1 help');
140
+ expect(commandParser.argumentHelp('arg2')).toBe('arg2 help');
141
+ expect(commandParser.optionHelp('option')).toBe('option help');
142
+ });
143
+ });
144
+ });
@@ -0,0 +1,8 @@
1
+ import { BobError } from "./BobError";
2
+ import { ArgSignature } from "../CommandParser";
3
+ export declare class InvalidOption extends BobError {
4
+ private option;
5
+ private optionsSignature;
6
+ constructor(option: string, optionsSignature: ArgSignature[]);
7
+ pretty(): void;
8
+ }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.InvalidOption = void 0;
7
+ const BobError_1 = require("./BobError");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ class InvalidOption extends BobError_1.BobError {
10
+ option;
11
+ optionsSignature;
12
+ constructor(option, optionsSignature) {
13
+ super(`Invalid option ${option} in not recognized`);
14
+ this.option = option;
15
+ this.optionsSignature = optionsSignature;
16
+ }
17
+ pretty() {
18
+ const log = console.log;
19
+ if (this.optionsSignature.length > 0) {
20
+ log((0, chalk_1.default) `\n{yellow Available options}:`);
21
+ for (const option of this.optionsSignature) {
22
+ const type = option.type ? (0, chalk_1.default) `{white (${option.type})}` : '';
23
+ const nameWithAlias = `--${option.name}${option.alias?.map(a => `, -${a}`).join('') ?? ''}`;
24
+ const spaces = ' '.repeat(30 - nameWithAlias.length);
25
+ log((0, chalk_1.default) ` {green ${nameWithAlias}} ${spaces} ${option.help ?? '\b'} ${type}`);
26
+ }
27
+ log('');
28
+ }
29
+ log((0, chalk_1.default) ` {white.bgRed ERROR } Option "{yellow ${this.option}}" is not recognized.`);
30
+ }
31
+ }
32
+ exports.InvalidOption = InvalidOption;
@@ -0,0 +1,8 @@
1
+ import { BobError } from "./BobError";
2
+ import { ArgSignature } from "../CommandParser";
3
+ export declare class MissingSignatureArgument extends BobError {
4
+ private argument;
5
+ private argumentSignatures;
6
+ constructor(argument: string, argumentSignatures: ArgSignature[]);
7
+ pretty(): void;
8
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MissingSignatureArgument = void 0;
7
+ const BobError_1 = require("./BobError");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ class MissingSignatureArgument extends BobError_1.BobError {
10
+ argument;
11
+ argumentSignatures;
12
+ constructor(argument, argumentSignatures) {
13
+ super(`Missing ${argument} in the command signature`);
14
+ this.argument = argument;
15
+ this.argumentSignatures = argumentSignatures;
16
+ }
17
+ pretty() {
18
+ const log = console.log;
19
+ if (this.argumentSignatures.length) {
20
+ log((0, chalk_1.default) `\n{yellow Available arguments}:`);
21
+ for (const argument of this.argumentSignatures) {
22
+ const type = argument.type ? (0, chalk_1.default) `{white (${argument.type})}` : '';
23
+ const spaces = ' '.repeat(20 - argument.name.length);
24
+ log((0, chalk_1.default) ` {green ${argument.name}} ${spaces} ${argument.help ?? '\b'} ${type}`);
25
+ }
26
+ log('');
27
+ }
28
+ log((0, chalk_1.default) ` {white.bgRed ERROR } Argument "{yellow ${this.argument}}" is missing in the signature.`);
29
+ }
30
+ }
31
+ exports.MissingSignatureArgument = MissingSignatureArgument;
@@ -0,0 +1,8 @@
1
+ import { BobError } from "./BobError";
2
+ import { ArgSignature } from "../CommandParser";
3
+ export declare class MissingSignatureOption extends BobError {
4
+ private option;
5
+ private optionsSignature;
6
+ constructor(option: string, optionsSignature: ArgSignature[]);
7
+ pretty(): void;
8
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.MissingSignatureOption = void 0;
7
+ const BobError_1 = require("./BobError");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ class MissingSignatureOption extends BobError_1.BobError {
10
+ option;
11
+ optionsSignature;
12
+ constructor(option, optionsSignature) {
13
+ super(`Missing ${option} in the command signature`);
14
+ this.option = option;
15
+ this.optionsSignature = optionsSignature;
16
+ }
17
+ pretty() {
18
+ const log = console.log;
19
+ if (this.optionsSignature.length) {
20
+ log((0, chalk_1.default) `{yellow Available options}:`);
21
+ for (const option of this.optionsSignature) {
22
+ const type = option.type ? (0, chalk_1.default) `{white (${option.type})}` : '';
23
+ const spaces = ' '.repeat(20 - option.name.length);
24
+ log((0, chalk_1.default) ` {green ${option.name}} ${spaces} ${option.help ?? '\b'} ${type}`);
25
+ }
26
+ log('');
27
+ }
28
+ log((0, chalk_1.default) ` {white.bgRed ERROR } Option "{yellow ${this.option}}" is missing in the signature.`);
29
+ }
30
+ }
31
+ exports.MissingSignatureOption = MissingSignatureOption;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bob-core",
3
- "version": "0.8.7",
3
+ "version": "0.9.1",
4
4
  "description": "BOB Core",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,18 +8,22 @@
8
8
  "/dist"
9
9
  ],
10
10
  "scripts": {
11
- "start": "node -r @swc-node/register ./tests/main.ts",
11
+ "start": "node -r @swc-node/register debug/main.ts",
12
12
  "build": "tsc",
13
- "prepare": "npm run build"
13
+ "prepare": "npm run build",
14
+ "test": "jest"
14
15
  },
15
16
  "author": "Léo Hubert",
16
17
  "license": "ISC",
17
18
  "devDependencies": {
18
19
  "@swc-node/register": "^1.9.2",
20
+ "@types/jest": "^29.5.12",
19
21
  "@types/lodash": "^4.17.5",
20
22
  "@types/minimist": "^1.2.5",
21
23
  "@types/node": "^20.14.5",
22
24
  "@types/string-similarity": "^4.0.2",
25
+ "jest": "^29.7.0",
26
+ "ts-jest": "^29.1.5",
23
27
  "typescript": "^5.4.5"
24
28
  },
25
29
  "dependencies": {