@bemoje/cli 1.1.1 → 2.0.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 DELETED
@@ -1,246 +0,0 @@
1
- # @bemoje/cli
2
-
3
- A type-safe CLI composer that can parse argv and generate help without execution coupling.
4
-
5
- ## Key Features
6
-
7
- - **🎯 Composition-Focused**: Build command structures without execution logic.
8
- - **🔒 Type-Safe**: Full TypeScript support with type inference for arguments and options
9
- - **🎨 Flexible Help**: Fork of commander.js Help class with enhanced API and adapter support
10
- - **✅ Validation**: Built-in CLI argument ordering validation and name conflict detection
11
-
12
- ## Quick Start
13
-
14
- ```ts
15
- import { Command } from '@bemoje/cli'
16
-
17
- const cmd = new Command('myapp')
18
- .setVersion('1.0.0')
19
- .setDescription('My awesome CLI application')
20
- .addArgument('<input>', 'Input file path')
21
- .addArgument('[output]', 'Output file path', { defaultValue: 'out.txt' })
22
- .addOption('-v, --verbose', 'Enable verbose output')
23
- .addOption('-f, --format <type>', 'Output format', { choices: ['json', 'xml', 'yaml'] })
24
-
25
- console.log(cmd.parseArgv(['input.txt', '-v', '-f', 'json']))
26
- // {
27
- // command: [Getter],
28
- // arguments: [ 'input.txt', 'out.txt' ],
29
- // options: { verbose: true, format: 'json' }
30
- // }
31
- ```
32
-
33
- ## Command Definition
34
-
35
- ### Basic Setup
36
-
37
- ```ts
38
- const cmd = new Command('myapp')
39
- .setVersion('1.0.0')
40
- .setDescription('Application description')
41
- .setSummary('Short summary for help')
42
- .setAliases(['app', 'my-app'])
43
- ```
44
-
45
- ### Arguments (Positional)
46
-
47
- Arguments follow strict ordering rules: required → optional → variadic
48
-
49
- ```ts
50
- // Required argument
51
- cmd.addArgument('<input>', 'Input file path')
52
-
53
- // Optional argument with default
54
- cmd.addArgument('[output]', 'Output file path', { defaultValue: 'dist/output.txt' })
55
-
56
- // Required variadic (multiple values)
57
- cmd.addArgument('<files...>', 'Multiple input files')
58
-
59
- // Optional variadic with defaults
60
- cmd.addArgument('[patterns...]', 'Glob patterns', { defaultValue: ['**/*.js'] })
61
- ```
62
-
63
- ### Options (Named Parameters)
64
-
65
- ```ts
66
- // Boolean flag
67
- cmd.addOption('-v, --verbose', 'Enable verbose output')
68
-
69
- // Required string option
70
- cmd.addOption('-f, --format <type>', 'Output format')
71
-
72
- // Optional string option with default
73
- cmd.addOption('-o, --output [path]', 'Output directory', { defaultValue: 'dist' })
74
-
75
- // Required variadic option
76
- cmd.addOption('-i, --include <patterns...>', 'Include patterns')
77
-
78
- // Optional variadic option with defaults
79
- cmd.addOption('-e, --exclude [patterns...]', 'Exclude patterns', {
80
- defaultValue: ['node_modules', '.git'],
81
- })
82
-
83
- // Option with choices and environment variable
84
- cmd.addOption('-l, --log-level [level]', 'Log level', {
85
- choices: ['error', 'warn', 'info', 'debug'],
86
- defaultValue: 'info',
87
- env: 'LOG_LEVEL',
88
- })
89
- ```
90
-
91
- ### Global Options
92
-
93
- Options defined on parent commands are available to subcommands:
94
-
95
- ```ts
96
- const app = new Command('myapp').addOption('-c, --config <file>', 'Config file')
97
-
98
- app.addSubcommand('build').addOption('-w, --watch', 'Watch mode')
99
-
100
- // Both --config and --watch are available to 'build' subcommand
101
- const result = app.parseArgv(['build', '--config', 'myconfig.json', '--watch'])
102
- ```
103
-
104
- ### Subcommands
105
-
106
- ```ts
107
- const cmd = new Command('git')
108
-
109
- // Create subcommand
110
- const add = cmd
111
- .addSubcommand('add')
112
- .setDescription('Add files to staging area')
113
- .addArgument('<files...>', 'Files to add')
114
- .addOption('-A, --all', 'Add all files')
115
-
116
- const commit = cmd
117
- .addSubcommand('commit')
118
- .setDescription('Create a commit')
119
- .addArgument('[message]', 'Commit message')
120
- .addOption('-m, --message <msg>', 'Commit message')
121
- .addOption('-a, --all', 'Commit all changes')
122
-
123
- // Parsing automatically routes to subcommands
124
- const result = cmd.parseArgv(['add', 'file1.js', 'file2.js', '-A'])
125
- // result.command === add subcommand instance
126
- ```
127
-
128
- ## Help System
129
-
130
- ### Rendering Help
131
-
132
- ```ts
133
- import { Help } from '@bemoje/cli'
134
-
135
- // Use help formatting (requires Help instance)
136
- const help = new Help()
137
- console.log(cmd.renderHelp(help))
138
-
139
- // Customize help configuration
140
- const customHelp = new Help()
141
- customHelp.helpWidth = 100
142
- customHelp.sortOptions = true
143
- customHelp.showGlobalOptions = true
144
- console.log(cmd.renderHelp(customHelp))
145
- ```
146
-
147
- ### Help Configuration
148
-
149
- Configure help behavior per command:
150
-
151
- ```ts
152
- cmd.setHelpConfiguration({
153
- sortOptions: true,
154
- sortSubcommands: true,
155
- showGlobalOptions: false,
156
- helpWidth: 80,
157
- })
158
- ```
159
-
160
- ### Custom Help Styling
161
-
162
- It may be more convenient to extend the Help class for more extensive customization.
163
-
164
- ```ts
165
- import { Help } from '@bemoje/cli'
166
-
167
- class ColoredHelp extends Help {
168
- styleTitle(str: string): string {
169
- return `\x1b[1m${str}\x1b[0m` // Bold
170
- }
171
-
172
- styleOptionText(str: string): string {
173
- return `\x1b[36m${str}\x1b[0m` // Cyan
174
- }
175
-
176
- styleArgumentText(str: string): string {
177
- return `\x1b[33m${str}\x1b[0m` // Yellow
178
- }
179
- }
180
-
181
- console.log(cmd.renderHelp(new ColoredHelp()))
182
- ```
183
-
184
- ## Validation
185
-
186
- Commands automatically validate:
187
-
188
- - Argument ordering (required before optional before variadic)
189
-
190
- ```ts
191
- cmd.addArgument('[optional]', 'Optional arg').addArgument('<required>', 'Required arg')
192
- //=> ❌ Error!
193
- ```
194
-
195
- - Unique option names and short flags, including globals across parent/child commands
196
-
197
- ```ts
198
- cmd.addOption('-v, --verbose', 'Verbose output').addOption('-v, --video', 'Video mode')
199
- //=> ❌ Error!
200
- ```
201
-
202
- - Single variadic argument per command
203
-
204
- ```ts
205
- cmd.addArgument('<files...>', 'First variadic').addArgument('<more...>', 'Second variadic')
206
- //=> ❌ Error!
207
- ```
208
-
209
- ## Command Class
210
-
211
- **Constructor**:
212
-
213
- ```ts
214
- - new (name: string, parent?: Command): Command
215
- ```
216
-
217
- **Structure Methods**:
218
-
219
- ```ts
220
- - addArgument(usage, description, options?): this
221
- - addOption(usage, description, options?): this
222
- - addSubcommand(name: string): Command
223
- ```
224
-
225
- **Configuration Methods**:
226
-
227
- ```ts
228
- - setVersion(version?: string): this
229
- - setName(name: string): this
230
- - setDescription(...lines: string[]): this
231
- - setSummary(summary?: string): this
232
- - setHidden(hidden?: boolean): this
233
- - setGroup(group?: string): this
234
- - setHelpConfiguration(config?: Partial<IHelp>): this
235
- - extendHelpConfiguration(config: Partial<IHelp>): this
236
- - setAliases(...aliases: (string | string[])[]): this
237
- - addAliases(...aliases: (string | string[])[]): this
238
- - setParent(parent: Command | null): this
239
- ```
240
-
241
- **Parsing & Help**:
242
-
243
- ```ts
244
- - parseArgv(argv?: string[], globalOptions?: OptionDescriptor[]): ParseResult
245
- - renderHelp(help: IHelp): string
246
- ```
package/index.js DELETED
@@ -1,3 +0,0 @@
1
- export * from "./lib/Command.js";
2
- export * from "./lib/Help.js";
3
- //# sourceMappingURL=index.js.map
package/lib/Command.js DELETED
@@ -1,421 +0,0 @@
1
- var __defProp = Object.defineProperty;
2
- var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
- import { parseArgs } from "node:util";
4
- import { Help } from "./Help.js";
5
- class Command {
6
- static {
7
- __name(this, "Command");
8
- }
9
- /** Command name used for invocation */
10
- name;
11
- /** Optional version string */
12
- version;
13
- /** Alternative names for this command */
14
- aliases;
15
- /** Brief single-line description */
16
- summary;
17
- /** Full command description */
18
- description;
19
- /** Whether command should be hidden from help */
20
- hidden;
21
- /** Group name for organizing commands in help */
22
- group;
23
- /** Parent command if this is a subcommand */
24
- parent;
25
- /** Child subcommands */
26
- commands;
27
- /** Positional arguments */
28
- arguments;
29
- /** Named options/flags */
30
- options;
31
- /** Help system configuration */
32
- helpConfiguration;
33
- constructor(name = "", parent = null) {
34
- this.name = name;
35
- this.parent = parent;
36
- this.aliases = [];
37
- this.description = "";
38
- this.commands = [];
39
- this.arguments = [];
40
- this.options = [];
41
- this.helpConfiguration = { showGlobalOptions: true, sortOptions: true, sortSubcommands: true };
42
- Object.defineProperty(this, "parent", { enumerable: false });
43
- }
44
- /** Sets the command name */
45
- setName(name) {
46
- this.name = name;
47
- }
48
- /** Sets command aliases, flattening nested arrays */
49
- setAliases(...aliases) {
50
- this.aliases = aliases.flat();
51
- return this;
52
- }
53
- /** Adds aliases to existing ones */
54
- addAliases(...aliases) {
55
- this.aliases.push(...aliases.flat());
56
- return this;
57
- }
58
- /** Sets the command version */
59
- setVersion(version) {
60
- this.version = version;
61
- return this;
62
- }
63
- /** Sets the command summary */
64
- setSummary(summary) {
65
- this.summary = summary;
66
- return this;
67
- }
68
- /** Sets command description, joining variadic lines */
69
- setDescription(...lines) {
70
- this.description = lines.join("\n");
71
- return this;
72
- }
73
- /** Sets whether command is hidden from help */
74
- setHidden(hidden = true) {
75
- this.hidden = hidden;
76
- return this;
77
- }
78
- /** Sets the command group for help organization */
79
- setGroup(group) {
80
- this.group = group;
81
- return this;
82
- }
83
- /** Sets the parent command */
84
- setParent(parent) {
85
- this.parent = parent;
86
- return this;
87
- }
88
- /** Extends existing help configuration with new settings */
89
- extendHelpConfiguration(config) {
90
- this.helpConfiguration = { ...this.helpConfiguration, ...config };
91
- return this;
92
- }
93
- /** Sets help configuration, using defaults if not provided */
94
- setHelpConfiguration(config) {
95
- this.helpConfiguration = config ? { ...config } : { showGlobalOptions: true, sortOptions: true, sortSubcommands: true };
96
- return this;
97
- }
98
- /** Creates and adds a subcommand */
99
- addSubcommand(name) {
100
- const sub = this.createCommand(name, this);
101
- this.commands.push(sub);
102
- return sub;
103
- }
104
- /**
105
- * Adds positional argument with type inference and CLI ordering validation.
106
- */
107
- addArgument(usage, description, options = {}) {
108
- const match = usage.match(/^<(.*?)>$|^\[(.*?)\]$/);
109
- if (!match) throw new Error(`Invalid argument format: ${usage}`);
110
- const nameMatch = match[1] || match[2];
111
- const name = nameMatch.replace(/\.\.\.$/, "");
112
- this.assertArgumentNameNotInUse(name);
113
- const props = { name, description };
114
- if (usage.startsWith("<")) {
115
- if (nameMatch.endsWith("...")) {
116
- this.assertNoMultipleVariadicArguments();
117
- this.arguments.push({
118
- ...options,
119
- ...props,
120
- required: true,
121
- variadic: true
122
- });
123
- } else {
124
- this.assertNoOptionalOrVariadicArguments();
125
- this.arguments.push({
126
- ...options,
127
- ...props,
128
- required: true,
129
- variadic: false
130
- });
131
- }
132
- } else if (usage.startsWith("[")) {
133
- if (nameMatch.endsWith("...")) {
134
- this.assertNoMultipleVariadicArguments();
135
- this.arguments.push({
136
- ...options,
137
- ...props,
138
- required: false,
139
- variadic: true,
140
- defaultValue: options.defaultValue ?? []
141
- });
142
- } else {
143
- this.assertNoVariadicArgument();
144
- this.arguments.push({
145
- ...options,
146
- ...props,
147
- required: false,
148
- variadic: false
149
- });
150
- }
151
- }
152
- return this;
153
- }
154
- /**
155
- * Adds command-line option with type inference. Parses format: `-s, --long [<value>|[value]|<value...>|[value...]]`
156
- */
157
- addOption(flags, description, opts = {}) {
158
- const match = flags.match(/^-(.+?), --([a-zA-Z][\w-]*)(?:\s*(<(.+?)>|\[(.+?)\]))?$/);
159
- if (!match) throw new Error(`Invalid option format: ${flags}`);
160
- const short = match[1];
161
- this.assertOptionShortNameIsValid(short);
162
- this.assertOptionShortNameNotInUse(short);
163
- const name = match[2];
164
- const argName = (match[4] || match[5])?.replace(/\.\.\.$/, "");
165
- this.assertOptionNameNotInUse(name);
166
- const props = {
167
- flags,
168
- short,
169
- long: name,
170
- name,
171
- description
172
- };
173
- if (!argName) {
174
- this._addOption({
175
- type: "boolean",
176
- ...opts,
177
- ...props,
178
- negate: false,
179
- optional: true,
180
- variadic: false,
181
- get multiple() {
182
- return this.variadic;
183
- }
184
- });
185
- } else if (flags.endsWith(">")) {
186
- if (flags.endsWith("...>")) {
187
- this._addOption({
188
- type: "string",
189
- ...opts,
190
- ...props,
191
- argName,
192
- required: true,
193
- optional: false,
194
- variadic: true,
195
- get multiple() {
196
- return this.variadic;
197
- }
198
- });
199
- } else {
200
- this._addOption({
201
- type: "string",
202
- ...opts,
203
- ...props,
204
- argName,
205
- required: true,
206
- optional: false,
207
- variadic: false,
208
- get multiple() {
209
- return this.variadic;
210
- }
211
- });
212
- }
213
- } else if (flags.endsWith("]")) {
214
- if (flags.endsWith("...]")) {
215
- this._addOption({
216
- type: "string",
217
- ...opts,
218
- ...props,
219
- argName,
220
- required: false,
221
- optional: true,
222
- variadic: true,
223
- get multiple() {
224
- return this.variadic;
225
- },
226
- defaultValue: opts.defaultValue ?? []
227
- });
228
- } else {
229
- this._addOption({
230
- type: "string",
231
- ...opts,
232
- ...props,
233
- argName,
234
- required: false,
235
- optional: true,
236
- variadic: false,
237
- get multiple() {
238
- return this.variadic;
239
- }
240
- });
241
- }
242
- }
243
- return this;
244
- }
245
- _addOption(opt) {
246
- this.options.push(opt);
247
- }
248
- /**
249
- * Parses command-line arguments with subcommand support and type-safe validation.
250
- *
251
- * @example
252
- * ```typescript
253
- * const result = cmd.parse(['input.txt', '-v', '--format', 'json'])
254
- * // { arguments: ['input.txt'], options: { verbose: true, format: 'json' } }
255
- * ```
256
- */
257
- parseArgv(argv = process.argv.slice(2), globalOptions = []) {
258
- const maybeSubArg = parseArgs({
259
- args: argv,
260
- allowPositionals: true,
261
- tokens: false,
262
- strict: false,
263
- allowNegative: true
264
- }).positionals[0];
265
- const sub = this.findCommand(maybeSubArg);
266
- if (sub) {
267
- return sub.parseArgv(
268
- argv?.filter((a) => a !== maybeSubArg),
269
- [...globalOptions, ...this.options]
270
- );
271
- }
272
- const parsed = parseArgs({
273
- args: argv,
274
- options: Object.fromEntries(
275
- [...globalOptions, ...this.options].map((o) => {
276
- return [o.name, o];
277
- })
278
- ),
279
- allowPositionals: true,
280
- tokens: true,
281
- strict: true,
282
- allowNegative: true
283
- });
284
- for (let i = 0; i < parsed.tokens.length; i++) {
285
- const token = parsed.tokens[i];
286
- if (token.kind === "option") {
287
- const optionDescriptor = this.options.find((o) => o.name === token.name);
288
- if (optionDescriptor && optionDescriptor.variadic && optionDescriptor.type === "string") {
289
- const values = [token.value];
290
- let j = i + 1;
291
- while (j < parsed.tokens.length && parsed.tokens[j].kind === "positional") {
292
- const positionalToken = parsed.tokens[j];
293
- if (positionalToken.kind === "positional") {
294
- values.push(positionalToken.value);
295
- const posIndex = parsed.positionals.indexOf(positionalToken.value);
296
- if (posIndex !== -1) {
297
- parsed.positionals.splice(posIndex, 1);
298
- }
299
- }
300
- j++;
301
- }
302
- Reflect.set(
303
- parsed.values,
304
- token.name,
305
- values.filter((v) => v !== void 0)
306
- );
307
- }
308
- }
309
- }
310
- const parsedArguments = this.arguments.map((arg, index) => {
311
- if (arg.variadic) {
312
- const remainingArgs = parsed.positionals.slice(index);
313
- return remainingArgs.length > 0 ? remainingArgs : arg.defaultValue ?? [];
314
- } else {
315
- return parsed.positionals[index] ?? arg.defaultValue;
316
- }
317
- }).filter((arg) => arg !== void 0);
318
- for (const option of this.options) {
319
- if (!(option.name in parsed.values) && "defaultValue" in option) {
320
- Reflect.set(parsed.values, option.name, option.defaultValue);
321
- }
322
- }
323
- const self = this;
324
- return {
325
- get command() {
326
- return self;
327
- },
328
- arguments: parsedArguments,
329
- options: { ...parsed.values }
330
- };
331
- }
332
- /** Renders formatted help text using provided help definition */
333
- renderHelp(help = new Help()) {
334
- const helper = Object.assign(help, this.helpConfiguration);
335
- return helper.formatHelp(this, helper);
336
- }
337
- /** Validates CLI argument ordering */
338
- assertNoOptionalOrVariadicArguments() {
339
- if (this.arguments.some((arg) => !arg.required)) {
340
- throw new Error("Cannot add required argument after optional or variadic arguments");
341
- }
342
- }
343
- /** Validates optional args don't follow variadic args */
344
- assertNoVariadicArgument() {
345
- if (this.arguments.some((arg) => arg.variadic)) {
346
- throw new Error("Cannot add optional argument after variadic argument");
347
- }
348
- }
349
- /** Ensures only one variadic argument per command */
350
- assertNoMultipleVariadicArguments() {
351
- if (this.arguments.some((arg) => arg.variadic)) {
352
- throw new Error("Cannot add more than one variadic argument");
353
- }
354
- }
355
- /** Ensures unique argument names across arguments and options */
356
- assertArgumentNameNotInUse(name) {
357
- if (this.arguments.some((arg) => arg.name === name)) {
358
- throw new Error(`Argument name already in use: ${name}`);
359
- }
360
- if (this.options.some((opt) => opt.name === name)) {
361
- throw new Error(`Argument name already in use: ${name}`);
362
- }
363
- }
364
- /** Validates option short names are single alphanumeric characters */
365
- assertOptionShortNameIsValid(short) {
366
- const isSingleAlphaNumericChar = /^[a-zA-Z0-9]$/.test(short);
367
- if (!isSingleAlphaNumericChar) {
368
- throw new Error(`Expected short name to be a single alpha-numeric character. Got: ${short}`);
369
- }
370
- }
371
- /** Validates option short names are unique across command hierarchy */
372
- assertOptionShortNameNotInUse(short) {
373
- for (const opt of this.options) {
374
- if (opt.short === short) {
375
- throw new Error(`Option short name already in use: -${short}`);
376
- }
377
- }
378
- this.parent?.assertOptionShortNameNotInUse(short);
379
- }
380
- /** Validates option names are unique across command hierarchy */
381
- assertOptionNameNotInUse(name) {
382
- if (this.options.some((opt) => opt.name === name)) {
383
- throw new Error(`Option name already in use: --${name}`);
384
- }
385
- this.parent?.assertOptionNameNotInUse(name);
386
- }
387
- /** Returns command and all ancestor commands in hierarchy */
388
- getCommandAndAncestors() {
389
- const result = [];
390
- let command = this;
391
- for (; command; command = command.parent) {
392
- result.push(command);
393
- }
394
- return result;
395
- }
396
- /** Returns all ancestor commands excluding this command */
397
- getAncestors() {
398
- return this.getCommandAndAncestors().slice(1);
399
- }
400
- /** Returns all options from this command and ancestors */
401
- getOptionsInclAncestors() {
402
- return this.getCommandAndAncestors().flatMap((cmd) => cmd.options);
403
- }
404
- /** Finds subcommand by name or alias */
405
- findCommand(name) {
406
- if (!name) return void 0;
407
- return this.commands.find((cmd) => cmd.name === name || cmd.aliases.includes(name));
408
- }
409
- /** Finds option by short or long name */
410
- findOption(arg) {
411
- return this.options.find((option) => option.short === arg || option.name === arg);
412
- }
413
- /** Returns a new Command instance. Override this method in subclasses. */
414
- createCommand(name = "", parent = null) {
415
- return new Command(name, parent);
416
- }
417
- }
418
- export {
419
- Command
420
- };
421
- //# sourceMappingURL=Command.js.map