@xmorse/cac 6.0.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/deno/CAC.ts ADDED
@@ -0,0 +1,306 @@
1
+ import { EventEmitter } from "https://deno.land/std@0.114.0/node/events.ts";
2
+ import mri from "https://cdn.skypack.dev/mri";
3
+ import Command, { GlobalCommand, CommandConfig, HelpCallback, CommandExample } from "./Command.ts";
4
+ import { OptionConfig } from "./Option.ts";
5
+ import { getMriOptions, setDotProp, setByType, getFileName, camelcaseOptionName } from "./utils.ts";
6
+ import { processArgs } from "./deno.ts";
7
+ interface ParsedArgv {
8
+ args: ReadonlyArray<string>;
9
+ options: {
10
+ [k: string]: any;
11
+ };
12
+ }
13
+ class CAC extends EventEmitter {
14
+ /** The program name to display in help and version message */
15
+ name: string;
16
+ commands: Command[];
17
+ globalCommand: GlobalCommand;
18
+ matchedCommand?: Command;
19
+ matchedCommandName?: string;
20
+ /**
21
+ * Raw CLI arguments
22
+ */
23
+ rawArgs: string[];
24
+ /**
25
+ * Parsed CLI arguments
26
+ */
27
+ args: ParsedArgv['args'];
28
+ /**
29
+ * Parsed CLI options, camelCased
30
+ */
31
+ options: ParsedArgv['options'];
32
+ showHelpOnExit?: boolean;
33
+ showVersionOnExit?: boolean;
34
+
35
+ /**
36
+ * @param name The program name to display in help and version message
37
+ */
38
+ constructor(name = '') {
39
+ super();
40
+ this.name = name;
41
+ this.commands = [];
42
+ this.rawArgs = [];
43
+ this.args = [];
44
+ this.options = {};
45
+ this.globalCommand = new GlobalCommand(this);
46
+ this.globalCommand.usage('<command> [options]');
47
+ }
48
+
49
+ /**
50
+ * Add a global usage text.
51
+ *
52
+ * This is not used by sub-commands.
53
+ */
54
+ usage(text: string) {
55
+ this.globalCommand.usage(text);
56
+ return this;
57
+ }
58
+
59
+ /**
60
+ * Add a sub-command
61
+ */
62
+ command(rawName: string, description?: string, config?: CommandConfig) {
63
+ const command = new Command(rawName, description || '', config, this);
64
+ command.globalCommand = this.globalCommand;
65
+ this.commands.push(command);
66
+ return command;
67
+ }
68
+
69
+ /**
70
+ * Add a global CLI option.
71
+ *
72
+ * Which is also applied to sub-commands.
73
+ */
74
+ option(rawName: string, description: string, config?: OptionConfig) {
75
+ this.globalCommand.option(rawName, description, config);
76
+ return this;
77
+ }
78
+
79
+ /**
80
+ * Show help message when `-h, --help` flags appear.
81
+ *
82
+ */
83
+ help(callback?: HelpCallback) {
84
+ this.globalCommand.option('-h, --help', 'Display this message');
85
+ this.globalCommand.helpCallback = callback;
86
+ this.showHelpOnExit = true;
87
+ return this;
88
+ }
89
+
90
+ /**
91
+ * Show version number when `-v, --version` flags appear.
92
+ *
93
+ */
94
+ version(version: string, customFlags = '-v, --version') {
95
+ this.globalCommand.version(version, customFlags);
96
+ this.showVersionOnExit = true;
97
+ return this;
98
+ }
99
+
100
+ /**
101
+ * Add a global example.
102
+ *
103
+ * This example added here will not be used by sub-commands.
104
+ */
105
+ example(example: CommandExample) {
106
+ this.globalCommand.example(example);
107
+ return this;
108
+ }
109
+
110
+ /**
111
+ * Output the corresponding help message
112
+ * When a sub-command is matched, output the help message for the command
113
+ * Otherwise output the global one.
114
+ *
115
+ */
116
+ outputHelp() {
117
+ if (this.matchedCommand) {
118
+ this.matchedCommand.outputHelp();
119
+ } else {
120
+ this.globalCommand.outputHelp();
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Output the version number.
126
+ *
127
+ */
128
+ outputVersion() {
129
+ this.globalCommand.outputVersion();
130
+ }
131
+ private setParsedInfo({
132
+ args,
133
+ options
134
+ }: ParsedArgv, matchedCommand?: Command, matchedCommandName?: string) {
135
+ this.args = args;
136
+ this.options = options;
137
+ if (matchedCommand) {
138
+ this.matchedCommand = matchedCommand;
139
+ }
140
+ if (matchedCommandName) {
141
+ this.matchedCommandName = matchedCommandName;
142
+ }
143
+ return this;
144
+ }
145
+ unsetMatchedCommand() {
146
+ this.matchedCommand = undefined;
147
+ this.matchedCommandName = undefined;
148
+ }
149
+
150
+ /**
151
+ * Parse argv
152
+ */
153
+ parse(argv = processArgs, {
154
+ /** Whether to run the action for matched command */
155
+ run = true
156
+ } = {}): ParsedArgv {
157
+ this.rawArgs = argv;
158
+ if (!this.name) {
159
+ this.name = argv[1] ? getFileName(argv[1]) : 'cli';
160
+ }
161
+ let shouldParse = true;
162
+
163
+ // Sort by name length (longest first) so "mcp login" matches before "mcp"
164
+ const sortedCommands = [...this.commands].sort((a, b) => {
165
+ const aLength = a.name.split(' ').filter(Boolean).length;
166
+ const bLength = b.name.split(' ').filter(Boolean).length;
167
+ return bLength - aLength;
168
+ });
169
+
170
+ // Search sub-commands
171
+ for (const command of sortedCommands) {
172
+ const parsed = this.mri(argv.slice(2), command);
173
+ const result = command.isMatched((parsed.args as string[]));
174
+ if (result.matched) {
175
+ shouldParse = false;
176
+ const matchedCommandName = parsed.args.slice(0, result.consumedArgs).join(' ');
177
+ const parsedInfo = {
178
+ ...parsed,
179
+ args: parsed.args.slice(result.consumedArgs)
180
+ };
181
+ this.setParsedInfo(parsedInfo, command, matchedCommandName);
182
+ this.emit(`command:${matchedCommandName}`, command);
183
+ break; // Stop after first match (greedy matching)
184
+ }
185
+ }
186
+ if (shouldParse) {
187
+ // Search the default command
188
+ for (const command of this.commands) {
189
+ if (command.name === '') {
190
+ shouldParse = false;
191
+ const parsed = this.mri(argv.slice(2), command);
192
+ this.setParsedInfo(parsed, command);
193
+ this.emit(`command:!`, command);
194
+ }
195
+ }
196
+ }
197
+ if (shouldParse) {
198
+ const parsed = this.mri(argv.slice(2));
199
+ this.setParsedInfo(parsed);
200
+ }
201
+ if (this.options.help && this.showHelpOnExit) {
202
+ this.outputHelp();
203
+ run = false;
204
+ this.unsetMatchedCommand();
205
+ }
206
+ if (this.options.version && this.showVersionOnExit && this.matchedCommandName == null) {
207
+ this.outputVersion();
208
+ run = false;
209
+ this.unsetMatchedCommand();
210
+ }
211
+ const parsedArgv = {
212
+ args: this.args,
213
+ options: this.options
214
+ };
215
+ if (run) {
216
+ this.runMatchedCommand();
217
+ }
218
+ if (!this.matchedCommand && this.args[0]) {
219
+ this.emit('command:*');
220
+ }
221
+ return parsedArgv;
222
+ }
223
+ private mri(argv: string[], /** Matched command */command?: Command): ParsedArgv {
224
+ // All added options
225
+ const cliOptions = [...this.globalCommand.options, ...(command ? command.options : [])];
226
+ const mriOptions = getMriOptions(cliOptions);
227
+
228
+ // Extract everything after `--` since mri doesn't support it
229
+ let argsAfterDoubleDashes: string[] = [];
230
+ const doubleDashesIndex = argv.indexOf('--');
231
+ if (doubleDashesIndex > -1) {
232
+ argsAfterDoubleDashes = argv.slice(doubleDashesIndex + 1);
233
+ argv = argv.slice(0, doubleDashesIndex);
234
+ }
235
+ let parsed = mri(argv, mriOptions);
236
+ parsed = Object.keys(parsed).reduce((res, name) => {
237
+ return {
238
+ ...res,
239
+ [camelcaseOptionName(name)]: parsed[name]
240
+ };
241
+ }, {
242
+ _: []
243
+ });
244
+ const args = parsed._;
245
+ const options: {
246
+ [k: string]: any;
247
+ } = {
248
+ '--': argsAfterDoubleDashes
249
+ };
250
+
251
+ // Set option default value
252
+ const ignoreDefault = command && command.config.ignoreOptionDefaultValue ? command.config.ignoreOptionDefaultValue : this.globalCommand.config.ignoreOptionDefaultValue;
253
+ let transforms = Object.create(null);
254
+ for (const cliOption of cliOptions) {
255
+ if (!ignoreDefault && cliOption.config.default !== undefined) {
256
+ for (const name of cliOption.names) {
257
+ options[name] = cliOption.config.default;
258
+ }
259
+ }
260
+
261
+ // If options type is defined
262
+ if (Array.isArray(cliOption.config.type)) {
263
+ if (transforms[cliOption.name] === undefined) {
264
+ transforms[cliOption.name] = Object.create(null);
265
+ transforms[cliOption.name]['shouldTransform'] = true;
266
+ transforms[cliOption.name]['transformFunction'] = cliOption.config.type[0];
267
+ }
268
+ }
269
+ }
270
+
271
+ // Set option values (support dot-nested property name)
272
+ for (const key of Object.keys(parsed)) {
273
+ if (key !== '_') {
274
+ const keys = key.split('.');
275
+ setDotProp(options, keys, parsed[key]);
276
+ setByType(options, transforms);
277
+ }
278
+ }
279
+ return {
280
+ args,
281
+ options
282
+ };
283
+ }
284
+ runMatchedCommand() {
285
+ const {
286
+ args,
287
+ options,
288
+ matchedCommand: command
289
+ } = this;
290
+ if (!command || !command.commandAction) return;
291
+ command.checkUnknownOptions();
292
+ command.checkOptionValue();
293
+ command.checkRequiredArgs();
294
+ const actionArgs: any[] = [];
295
+ command.args.forEach((arg, index) => {
296
+ if (arg.variadic) {
297
+ actionArgs.push(args.slice(index));
298
+ } else {
299
+ actionArgs.push(args[index]);
300
+ }
301
+ });
302
+ actionArgs.push(options);
303
+ return command.commandAction.apply(this, actionArgs);
304
+ }
305
+ }
306
+ export default CAC;
@@ -0,0 +1,257 @@
1
+ import CAC from "./CAC.ts";
2
+ import Option, { OptionConfig } from "./Option.ts";
3
+ import { removeBrackets, findAllBrackets, findLongest, padRight, CACError } from "./utils.ts";
4
+ import { platformInfo } from "./deno.ts";
5
+ interface CommandArg {
6
+ required: boolean;
7
+ value: string;
8
+ variadic: boolean;
9
+ }
10
+ interface HelpSection {
11
+ title?: string;
12
+ body: string;
13
+ }
14
+ interface CommandConfig {
15
+ allowUnknownOptions?: boolean;
16
+ ignoreOptionDefaultValue?: boolean;
17
+ }
18
+ type HelpCallback = (sections: HelpSection[]) => void | HelpSection[];
19
+ type CommandExample = ((bin: string) => string) | string;
20
+ class Command {
21
+ options: Option[];
22
+ aliasNames: string[];
23
+ /* Parsed command name */
24
+ name: string;
25
+ args: CommandArg[];
26
+ commandAction?: (...args: any[]) => any;
27
+ usageText?: string;
28
+ versionNumber?: string;
29
+ examples: CommandExample[];
30
+ helpCallback?: HelpCallback;
31
+ globalCommand?: GlobalCommand;
32
+ constructor(public rawName: string, public description: string, public config: CommandConfig = {}, public cli: CAC) {
33
+ this.options = [];
34
+ this.aliasNames = [];
35
+ this.name = removeBrackets(rawName);
36
+ this.args = findAllBrackets(rawName);
37
+ this.examples = [];
38
+ }
39
+ usage(text: string) {
40
+ this.usageText = text;
41
+ return this;
42
+ }
43
+ allowUnknownOptions() {
44
+ this.config.allowUnknownOptions = true;
45
+ return this;
46
+ }
47
+ ignoreOptionDefaultValue() {
48
+ this.config.ignoreOptionDefaultValue = true;
49
+ return this;
50
+ }
51
+ version(version: string, customFlags = '-v, --version') {
52
+ this.versionNumber = version;
53
+ this.option(customFlags, 'Display version number');
54
+ return this;
55
+ }
56
+ example(example: CommandExample) {
57
+ this.examples.push(example);
58
+ return this;
59
+ }
60
+
61
+ /**
62
+ * Add a option for this command
63
+ * @param rawName Raw option name(s)
64
+ * @param description Option description
65
+ * @param config Option config
66
+ */
67
+ option(rawName: string, description: string, config?: OptionConfig) {
68
+ const option = new Option(rawName, description, config);
69
+ this.options.push(option);
70
+ return this;
71
+ }
72
+ alias(name: string) {
73
+ this.aliasNames.push(name);
74
+ return this;
75
+ }
76
+ action(callback: (...args: any[]) => any) {
77
+ this.commandAction = callback;
78
+ return this;
79
+ }
80
+ isMatched(args: string[]): {
81
+ matched: boolean;
82
+ consumedArgs: number;
83
+ } {
84
+ const nameParts = this.name.split(' ').filter(Boolean);
85
+ if (nameParts.length === 0) {
86
+ return {
87
+ matched: false,
88
+ consumedArgs: 0
89
+ };
90
+ }
91
+ if (args.length < nameParts.length) {
92
+ return {
93
+ matched: false,
94
+ consumedArgs: 0
95
+ };
96
+ }
97
+ for (let i = 0; i < nameParts.length; i++) {
98
+ if (nameParts[i] !== args[i]) {
99
+ if (i === 0 && this.aliasNames.includes(args[i])) {
100
+ continue;
101
+ }
102
+ return {
103
+ matched: false,
104
+ consumedArgs: 0
105
+ };
106
+ }
107
+ }
108
+ return {
109
+ matched: true,
110
+ consumedArgs: nameParts.length
111
+ };
112
+ }
113
+ get isDefaultCommand() {
114
+ return this.name === '' || this.aliasNames.includes('!');
115
+ }
116
+ get isGlobalCommand(): boolean {
117
+ return this instanceof GlobalCommand;
118
+ }
119
+
120
+ /**
121
+ * Check if an option is registered in this command
122
+ * @param name Option name
123
+ */
124
+ hasOption(name: string) {
125
+ name = name.split('.')[0];
126
+ return this.options.find(option => {
127
+ return option.names.includes(name);
128
+ });
129
+ }
130
+ outputHelp() {
131
+ const {
132
+ name,
133
+ commands
134
+ } = this.cli;
135
+ const {
136
+ versionNumber,
137
+ options: globalOptions,
138
+ helpCallback
139
+ } = this.cli.globalCommand;
140
+ let sections: HelpSection[] = [{
141
+ body: `${name}${versionNumber ? `/${versionNumber}` : ''}`
142
+ }];
143
+ sections.push({
144
+ title: 'Usage',
145
+ body: ` $ ${name} ${this.usageText || this.rawName}`
146
+ });
147
+ const showCommands = (this.isGlobalCommand || this.isDefaultCommand) && commands.length > 0;
148
+ if (showCommands) {
149
+ const longestCommandName = findLongest(commands.map(command => command.rawName));
150
+ sections.push({
151
+ title: 'Commands',
152
+ body: commands.map(command => {
153
+ return ` ${padRight(command.rawName, longestCommandName.length)} ${command.description}`;
154
+ }).join('\n')
155
+ });
156
+ sections.push({
157
+ title: `For more info, run any command with the \`--help\` flag`,
158
+ body: commands.map(command => ` $ ${name}${command.name === '' ? '' : ` ${command.name}`} --help`).join('\n')
159
+ });
160
+ }
161
+ let options = this.isGlobalCommand ? globalOptions : [...this.options, ...(globalOptions || [])];
162
+ if (!this.isGlobalCommand && !this.isDefaultCommand) {
163
+ options = options.filter(option => option.name !== 'version');
164
+ }
165
+ if (options.length > 0) {
166
+ const longestOptionName = findLongest(options.map(option => option.rawName));
167
+ sections.push({
168
+ title: 'Options',
169
+ body: options.map(option => {
170
+ return ` ${padRight(option.rawName, longestOptionName.length)} ${option.description} ${option.config.default === undefined ? '' : `(default: ${option.config.default})`}`;
171
+ }).join('\n')
172
+ });
173
+ }
174
+ if (this.examples.length > 0) {
175
+ sections.push({
176
+ title: 'Examples',
177
+ body: this.examples.map(example => {
178
+ if (typeof example === 'function') {
179
+ return example(name);
180
+ }
181
+ return example;
182
+ }).join('\n')
183
+ });
184
+ }
185
+ if (helpCallback) {
186
+ sections = helpCallback(sections) || sections;
187
+ }
188
+ console.log(sections.map(section => {
189
+ return section.title ? `${section.title}:\n${section.body}` : section.body;
190
+ }).join('\n\n'));
191
+ }
192
+ outputVersion() {
193
+ const {
194
+ name
195
+ } = this.cli;
196
+ const {
197
+ versionNumber
198
+ } = this.cli.globalCommand;
199
+ if (versionNumber) {
200
+ console.log(`${name}/${versionNumber} ${platformInfo}`);
201
+ }
202
+ }
203
+ checkRequiredArgs() {
204
+ const minimalArgsCount = this.args.filter(arg => arg.required).length;
205
+ if (this.cli.args.length < minimalArgsCount) {
206
+ throw new CACError(`missing required args for command \`${this.rawName}\``);
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Check if the parsed options contain any unknown options
212
+ *
213
+ * Exit and output error when true
214
+ */
215
+ checkUnknownOptions() {
216
+ const {
217
+ options,
218
+ globalCommand
219
+ } = this.cli;
220
+ if (!this.config.allowUnknownOptions) {
221
+ for (const name of Object.keys(options)) {
222
+ if (name !== '--' && !this.hasOption(name) && !globalCommand.hasOption(name)) {
223
+ throw new CACError(`Unknown option \`${name.length > 1 ? `--${name}` : `-${name}`}\``);
224
+ }
225
+ }
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Check if the required string-type options exist
231
+ */
232
+ checkOptionValue() {
233
+ const {
234
+ options: parsedOptions,
235
+ globalCommand
236
+ } = this.cli;
237
+ const options = [...globalCommand.options, ...this.options];
238
+ for (const option of options) {
239
+ const value = parsedOptions[option.name.split('.')[0]];
240
+ // Check required option value
241
+ if (option.required) {
242
+ const hasNegated = options.some(o => o.negated && o.names.includes(option.name));
243
+ if (value === true || value === false && !hasNegated) {
244
+ throw new CACError(`option \`${option.rawName}\` value is missing`);
245
+ }
246
+ }
247
+ }
248
+ }
249
+ }
250
+ class GlobalCommand extends Command {
251
+ constructor(cli: CAC) {
252
+ super('@@global@@', '', {}, cli);
253
+ }
254
+ }
255
+ export type { HelpCallback, CommandExample, CommandConfig };
256
+ export { GlobalCommand };
257
+ export default Command;
package/deno/Option.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { removeBrackets, camelcaseOptionName } from "./utils.ts";
2
+ interface OptionConfig {
3
+ default?: any;
4
+ type?: any[];
5
+ }
6
+ export default class Option {
7
+ /** Option name */
8
+ name: string;
9
+ /** Option name and aliases */
10
+ names: string[];
11
+ isBoolean?: boolean;
12
+ // `required` will be a boolean for options with brackets
13
+ required?: boolean;
14
+ config: OptionConfig;
15
+ negated: boolean;
16
+ constructor(public rawName: string, public description: string, config?: OptionConfig) {
17
+ this.config = Object.assign({}, config);
18
+
19
+ // You may use cli.option('--env.* [value]', 'desc') to denote a dot-nested option
20
+ rawName = rawName.replace(/\.\*/g, '');
21
+ this.negated = false;
22
+ this.names = removeBrackets(rawName).split(',').map((v: string) => {
23
+ let name = v.trim().replace(/^-{1,2}/, '');
24
+ if (name.startsWith('no-')) {
25
+ this.negated = true;
26
+ name = name.replace(/^no-/, '');
27
+ }
28
+ return camelcaseOptionName(name);
29
+ }).sort((a, b) => a.length > b.length ? 1 : -1); // Sort names
30
+
31
+ // Use the longest name (last one) as actual option name
32
+ this.name = this.names[this.names.length - 1];
33
+ if (this.negated && this.config.default == null) {
34
+ this.config.default = true;
35
+ }
36
+ if (rawName.includes('<')) {
37
+ this.required = true;
38
+ } else if (rawName.includes('[')) {
39
+ this.required = false;
40
+ } else {
41
+ // No arg needed, it's boolean flag
42
+ this.isBoolean = true;
43
+ }
44
+ }
45
+ }
46
+ export type { OptionConfig };
package/deno/deno.ts ADDED
@@ -0,0 +1,5 @@
1
+ // Ignore the TypeScript errors
2
+ // Since this file will only be used in Deno runtime
3
+
4
+ export const processArgs = ['deno', 'cli'].concat(Deno.args);
5
+ export const platformInfo = `${Deno.build.os}-${Deno.build.arch} deno-${Deno.version.deno}`;
package/deno/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ import CAC from "./CAC.ts";
2
+ import Command from "./Command.ts";
3
+
4
+ /**
5
+ * @param name The program name to display in help and version message
6
+ */
7
+ const cac = (name = '') => new CAC(name);
8
+ export default cac;
9
+ export { cac, CAC, Command };