cli-nano 1.1.0 → 1.1.2

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/src/index.ts DELETED
@@ -1,279 +0,0 @@
1
- import type { ArgsResult, ArgumentOptions, Config } from './interfaces.js';
2
-
3
- export type * from './interfaces.js';
4
-
5
- const defaultOptions: Record<string, ArgumentOptions> = {
6
- help: { alias: 'h', description: 'Show help', type: 'boolean' },
7
- version: { alias: 'v', description: 'Show version number', type: 'boolean' },
8
- };
9
-
10
- export function parseArgs<C extends Config>(config: C): ArgsResult<C> {
11
- const { command, options, version } = config;
12
-
13
- // Normalize args to support --option=value and -o=value
14
- const args = process.argv.slice(2).flatMap(arg => {
15
- if (/^--?\w[\w-]*=/.test(arg)) {
16
- const [flag, ...rest] = arg.split('=');
17
- return [flag, rest.join('=')];
18
- }
19
- return arg;
20
- });
21
- const result: Record<string, any> = {};
22
-
23
- // Check for duplicate aliases
24
- const aliasMap = new Map<string, string>();
25
- for (const [key, opt] of Object.entries(options)) {
26
- if (opt.alias) {
27
- if (aliasMap.has(opt.alias)) {
28
- throw new Error(`Duplicate alias detected: "${opt.alias}" used for both "${aliasMap.get(opt.alias)}" and "${key}"`);
29
- }
30
- aliasMap.set(opt.alias, key);
31
- }
32
- }
33
-
34
- // Handle --help and --version before anything else
35
- if (args.includes('--help') || args.includes('-h')) {
36
- printHelp(config);
37
- process.exit(0);
38
- }
39
- if ((version && args.includes('--version')) || args.includes('-v')) {
40
- console.log(version || 'No version specified');
41
- process.exit(0);
42
- }
43
-
44
- // Validate: required positionals must come before optional ones
45
- const positionals = command.positionals ?? [];
46
- let foundOptional = false;
47
- for (const pos of positionals) {
48
- if (!pos.required) {
49
- foundOptional = true;
50
- }
51
- if (foundOptional && pos.required) {
52
- throw new Error(`Invalid positional argument configuration: required positional "${pos.name}" cannot follow optional positional(s).`);
53
- }
54
- }
55
-
56
- // Handle positional arguments
57
- let argIndex = 0;
58
- const nonOptionArgs: string[] = [];
59
- while (argIndex < args.length && !args[argIndex].startsWith('-')) {
60
- nonOptionArgs.push(args[argIndex]);
61
- argIndex++;
62
- }
63
-
64
- let nonOptionIndex = 0;
65
- for (let i = 0; i < positionals.length; i++) {
66
- const pos = positionals[i];
67
- if (pos.variadic) {
68
- const remaining = positionals.length - (i + 1);
69
- const values = nonOptionArgs.slice(nonOptionIndex, nonOptionArgs.length - remaining);
70
- if (pos.required && values.length === 0) {
71
- const usagePositionals = buildUsagePositionals(positionals);
72
- throw new Error(`Missing required positional argument, i.e.: "${command.name} ${usagePositionals}"`);
73
- }
74
- result[pos.name] = !pos.required && values.length === 0 && pos.default !== undefined ? pos.default : values;
75
- nonOptionIndex += values.length;
76
- } else {
77
- const value = nonOptionArgs[nonOptionIndex];
78
- // Check if there are enough args left for required positionals
79
- const requiredLeft = positionals.slice(i).filter(p => p.required).length;
80
- const argsLeft = nonOptionArgs.length - nonOptionIndex;
81
- if (value !== undefined && (argsLeft > requiredLeft - (pos.required ? 1 : 0) || pos.required)) {
82
- result[pos.name] = value;
83
- nonOptionIndex++;
84
- } else if (!pos.required && pos.default !== undefined) {
85
- result[pos.name] = pos.default;
86
- } else if (pos.required) {
87
- const usagePositionals = buildUsagePositionals(positionals);
88
- throw new Error(`Missing required positional argument, i.e.: "${command.name} ${usagePositionals}"`);
89
- }
90
- }
91
- }
92
-
93
- // Handle options
94
- argIndex = 0;
95
- const consumedArgs = new Set<number>();
96
- // Mark all nonOptionArgs indices as consumed for positionals
97
- let tempNonOptionIndex = 0;
98
- for (let i = 0; i < positionals.length; i++) {
99
- const pos = positionals[i];
100
- if (pos.variadic) {
101
- const remaining = positionals.length - (i + 1);
102
- const values = nonOptionArgs.slice(tempNonOptionIndex, nonOptionArgs.length - remaining);
103
- for (let j = tempNonOptionIndex; j < tempNonOptionIndex + values.length; j++) {
104
- consumedArgs.add(args.findIndex((a, idx) => !a.startsWith('-') && !consumedArgs.has(idx) && a === nonOptionArgs[j]));
105
- }
106
- tempNonOptionIndex += values.length;
107
- } else {
108
- const value = nonOptionArgs[tempNonOptionIndex++];
109
- consumedArgs.add(args.findIndex((a, idx) => !a.startsWith('-') && !consumedArgs.has(idx) && a === value));
110
- }
111
- }
112
-
113
- while (argIndex < args.length) {
114
- if (consumedArgs.has(argIndex)) {
115
- argIndex++;
116
- continue;
117
- }
118
- const argOrg = args[argIndex] || '';
119
- let arg = argOrg;
120
- let option: ArgumentOptions | undefined;
121
- let configKey: string | undefined;
122
-
123
- if (argOrg.startsWith('-')) {
124
- if (argOrg.startsWith('--')) {
125
- arg = argOrg.slice(2);
126
- [option, configKey] = findOption(options, arg);
127
- } else if (argOrg.startsWith('-')) {
128
- arg = argOrg.slice(1);
129
- [option, configKey] = findOption(options, arg);
130
- }
131
-
132
- // Handle negated boolean in both forms
133
- if (!option) {
134
- const isNegated = arg.startsWith('no-');
135
- const optionName = isNegated ? arg.slice(3) : arg;
136
- const camelOptionName = kebabToCamel(optionName);
137
- option = options[optionName] || options[camelOptionName];
138
- configKey = camelOptionName in options ? camelOptionName : optionName;
139
- if (option?.type === 'boolean') {
140
- if (result[optionName] !== undefined || result[camelOptionName] !== undefined) {
141
- throw new Error('Providing same negated and truthy argument are not allowed');
142
- }
143
- result[configKey] = !isNegated;
144
- argIndex++;
145
- continue;
146
- }
147
- }
148
-
149
- if (!option || !configKey) {
150
- throw new Error(`Unknown CLI option: ${arg}`);
151
- }
152
-
153
- switch (option.type) {
154
- case 'boolean':
155
- if (result[configKey] !== undefined) {
156
- throw new Error('Providing same negated and truthy argument are not allowed');
157
- }
158
- result[configKey] = !argOrg.startsWith('--no-') && !argOrg.startsWith('-no-');
159
- break;
160
- case 'number':
161
- if (args[argIndex + 1] === undefined || args[argIndex + 1].startsWith('-')) {
162
- throw new Error(`Missing value for option: ${configKey}`);
163
- }
164
- result[configKey] = Number(args[++argIndex]);
165
- break;
166
- case 'array': {
167
- if (!result[configKey]) result[configKey] = [];
168
- const arrayValue = args[++argIndex];
169
- if (arrayValue === undefined || arrayValue.startsWith('-')) {
170
- throw new Error(`Missing value for array option: ${configKey}`);
171
- }
172
- result[configKey].push(arrayValue);
173
- break;
174
- }
175
- case 'string':
176
- default:
177
- if (args[argIndex + 1] === undefined || args[argIndex + 1].startsWith('-')) {
178
- throw new Error(`Missing value for option: ${configKey}`);
179
- }
180
- result[configKey] = args[++argIndex];
181
- break;
182
- }
183
- } else {
184
- throw new Error(`Unknown argument: ${arg}`);
185
- }
186
- argIndex++;
187
- }
188
-
189
- // After all parsing, assign any `default` CLI options when undefined
190
- // and check for any missing `required` CLI options
191
- Object.entries(options).forEach(([key, opt]) => {
192
- if (result[key] === undefined && opt.default !== undefined) {
193
- result[key] = opt.default;
194
- }
195
- if (opt.required && result[key] === undefined) {
196
- const aliasStr = opt.alias ? `-${opt.alias}, ` : '';
197
- throw new Error(`Missing required option: ${aliasStr}--${key}`);
198
- }
199
- });
200
-
201
- return result as ArgsResult<C>;
202
- }
203
-
204
- /** Format a text to a fixed length, truncating and padding as needed. */
205
- function formatHelpText(text: string, max: number) {
206
- const truncated = text.length > max ? `${text.slice(0, max - 3)}...` : text;
207
- return truncated.padEnd(max);
208
- }
209
-
210
- /** Build the usage string for positionals, e.g. "<input..> [output]" */
211
- function buildUsagePositionals(positionals: readonly any[] = []) {
212
- return positionals
213
- .map(p => {
214
- const variadic = p.variadic ? '..' : '';
215
- return p.required ? `<${p.name}${variadic}>` : `[${p.name}${variadic}]`;
216
- })
217
- .join(' ');
218
- }
219
-
220
- /** Format the option/argument type for help output */
221
- function formatOptionType(type: string | undefined, variadic?: boolean, required?: boolean) {
222
- const t = type || 'string';
223
- const variadicStr = variadic ? '..' : '';
224
- return required ? `<${t}${variadicStr}>` : `[${t}${variadicStr}]`;
225
- }
226
-
227
- /** Helper to find an option and its config key by argument name or alias. */
228
- function findOption(options: Record<string, ArgumentOptions>, arg: string): [ArgumentOptions | undefined, string | undefined] {
229
- // Try all forms: as-is, kebab-to-camel, camel-to-kebab
230
- const option = options[arg] || options[kebabToCamel(arg)] || options[camelToKebab(arg).replace(/-/g, '')];
231
- if (option) {
232
- const configKey = Object.keys(options).find(key => options[key] === option);
233
- return [option, configKey];
234
- }
235
- // Try matching alias in all forms
236
- for (const key of Object.keys(options)) {
237
- const opt = options[key];
238
- if (opt.alias && (opt.alias === arg || opt.alias === kebabToCamel(arg) || opt.alias === camelToKebab(arg))) {
239
- return [opt, key];
240
- }
241
- }
242
- return [undefined, undefined];
243
- }
244
-
245
- /** Print CLI help documentation to the screen */
246
- function printHelp(config: Config) {
247
- const { command, options, version, helpOptLength = 20, helpDescLength = 65 } = config;
248
- const usagePositionals = buildUsagePositionals(command.positionals);
249
-
250
- console.log('Usage:');
251
- console.log(` ${command.name} ${usagePositionals} [options] ${command.description}`);
252
- console.log('\nArguments:');
253
- command.positionals?.forEach(arg => {
254
- console.log(
255
- ` ${formatHelpText(arg.name, helpOptLength)}${formatHelpText(arg.description, helpDescLength)} ${formatOptionType(arg.type, arg.variadic, arg.required)}`,
256
- );
257
- });
258
-
259
- console.log('\nOptions:');
260
- for (const [key, option] of Object.entries({ ...options, ...defaultOptions })) {
261
- const aliasStr = option.alias ? `-${option.alias}, ` : '';
262
- if (!version && key === 'version') {
263
- continue;
264
- }
265
- console.log(
266
- ` ${aliasStr.padEnd(4)}--${formatHelpText(key, helpOptLength - 6)}${formatHelpText(option.description || '', helpDescLength)} ${formatOptionType(option.type, false, option.required)}`,
267
- );
268
- }
269
- }
270
-
271
- /** Utility to convert kebab-case to camelCase */
272
- function kebabToCamel(str: string) {
273
- return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
274
- }
275
-
276
- /** Utility to convert camelCase to kebab-case */
277
- function camelToKebab(str: string) {
278
- return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
279
- }
package/src/interfaces.ts DELETED
@@ -1,116 +0,0 @@
1
- export interface ArgumentOptions {
2
- /** option type */
3
- type?: 'string' | 'boolean' | 'number' | 'array';
4
-
5
- /** description of the flag option */
6
- description: string;
7
-
8
- /** defaults to undefined, provide shorter alias as command options */
9
- alias?: string;
10
-
11
- /** default value for the option if not provided */
12
- default?: any;
13
-
14
- /** defaults to false, is the option required? */
15
- required?: boolean;
16
- }
17
-
18
- export interface PositionalArgument {
19
- /** positional argument name (it will be displayed in the help docs) */
20
- name: string;
21
-
22
- /** positional argument description */
23
- description: string;
24
-
25
- /** postional argument type */
26
- type?: 'string' | 'boolean' | 'number' | 'array';
27
-
28
- /** defaults to false, allows multiple values for this positional argument */
29
- variadic?: boolean;
30
-
31
- /** default value for the option if not provided */
32
- default?: any;
33
-
34
- /** defaults to false, is the positional argument required? */
35
- required?: boolean;
36
- }
37
-
38
- export interface CommandOptions {
39
- /** command name, used in the help docs */
40
- name: string;
41
-
42
- /** command description */
43
- description: string;
44
-
45
- /** list of positional arguments */
46
- positionals?: readonly PositionalArgument[];
47
- }
48
-
49
- /** CLI options */
50
- export interface Config {
51
- /** CLI definition */
52
- command: CommandOptions;
53
-
54
- /** option name length (w/wo alias) shown in the help (defaults to 20) */
55
- helpOptLength?: number;
56
-
57
- /** description length shown in the help (defaults to 65) */
58
- helpDescLength?: number;
59
-
60
- /** CLI list of flag options */
61
- options: Record<string, ArgumentOptions>;
62
-
63
- /** CLI or package version */
64
- version?: string;
65
- }
66
-
67
- /** Utility type to map ArgumentOptions/PositionalArgument to their value type */
68
- export type ArgValueType<T extends { type?: string; default?: any; variadic?: boolean; required?: boolean }> = T['type'] extends 'boolean'
69
- ? boolean
70
- : T['type'] extends 'number'
71
- ? number
72
- : T['type'] extends 'array'
73
- ? T extends { variadic: true }
74
- ? T extends { required: true }
75
- ? [string, ...string[]]
76
- : string[]
77
- : string | string[]
78
- : T['type'] extends 'string'
79
- ? T extends { variadic: true }
80
- ? T extends { required: true }
81
- ? [string, ...string[]]
82
- : string[]
83
- : string
84
- : T['type'] extends undefined
85
- ? T extends { variadic: true }
86
- ? T extends { required: true }
87
- ? [string, ...string[]]
88
- : string[]
89
- : string
90
- : T['default'] extends undefined
91
- ? string
92
- : T['default'];
93
-
94
- /** Helper to get required keys */
95
- type RequiredKeys<T> = {
96
- [K in keyof T]: T[K] extends { required: true } ? K : never;
97
- }[keyof T];
98
-
99
- /** Helper to get optional keys */
100
- type OptionalKeys<T> = Exclude<keyof T, RequiredKeys<T>>;
101
-
102
- /** Map options record to an object type with required/optional properties */
103
- export type OptionsToObject<T extends Record<string, any>> = { [K in RequiredKeys<T>]: ArgValueType<T[K]> } & {
104
- [K in OptionalKeys<T>]?: ArgValueType<T[K]>;
105
- };
106
-
107
- /** Map positionals array to an object type with required/optional properties */
108
- export type PositionalsToObject<T extends readonly PositionalArgument[] | undefined> = T extends readonly [infer P, ...infer Rest]
109
- ? P extends PositionalArgument
110
- ? (P['required'] extends true ? { [K in P['name']]: ArgValueType<P> } : { [K in P['name']]?: ArgValueType<P> }) &
111
- PositionalsToObject<Rest extends readonly PositionalArgument[] ? Rest : []>
112
- : PositionalsToObject<Rest extends readonly PositionalArgument[] ? Rest : []>
113
- : { [key: string]: never };
114
-
115
- /** The full result type for parseArgs */
116
- export type ArgsResult<C extends Config> = PositionalsToObject<C['command']['positionals']> & OptionsToObject<C['options']>;