cli-nano 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2024-present, Ghislain B.
2
+ https://github.com/ghiscoding/gc-utils/frameworks/release
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
2
+ [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/)
3
+ [![Vitest](https://img.shields.io/badge/tested%20with-vitest-fcc72b.svg?logo=vitest)](https://vitest.dev/)
4
+ [![codecov](https://codecov.io/gh/ghiscoding/cli-nano/branch/main/graph/badge.svg)](https://codecov.io/gh/ghiscoding/cli-nano)
5
+ [![npm](https://img.shields.io/npm/v/cli-nano.svg)](https://www.npmjs.com/package/cli-nano)
6
+ [![npm](https://img.shields.io/npm/dy/cli-nano)](https://www.npmjs.com/package/cli-nano)
7
+ [![npm bundle size](https://img.shields.io/bundlephobia/minzip/cli-nano?color=success&label=gzip)](https://bundlephobia.com/result?p=cli-nano)
8
+
9
+ ## cli-nano
10
+
11
+ Super small custom CLI similar to `Yargs` but much smaller, it uses a similar approach to NodeJS `parseArgs()` to create command-line tool (aka CLI). It is much more complete than NodeJS `parseArgs()` since it supports Positional Arguments, negated flags and also accepts both syntax `--camelCase` and/or `--kebab-case`.
12
+
13
+ ### Features
14
+ - Parses arguments
15
+ - Converts flags to camelCase
16
+ - Negates flags when using the `--no-` prefix
17
+ - Outputs version when `--version`
18
+ - Outputs description and supplied help text when `--help`
19
+ - No dependencies!
20
+
21
+ ### Install
22
+ ```sh
23
+ npm install cli-nano
24
+ ```
25
+
26
+ ### Usage
27
+
28
+ ```ts
29
+ #!/usr/bin/env node
30
+
31
+ import { type Config, parseArgs } from 'cli-nano';
32
+
33
+ const config: Config = {
34
+ command: {
35
+ name: 'unicorns',
36
+ description: 'Show a list of unicorns',
37
+ positional: [
38
+ {
39
+ name: 'inputs',
40
+ description: 'unicorn inputs',
41
+ type: 'string',
42
+ variadic: true, // one or more inputs could be provided
43
+ required: true,
44
+ },
45
+ {
46
+ name: 'output',
47
+ description: 'output directory',
48
+ type: 'string',
49
+ required: false,
50
+ },
51
+ ],
52
+ },
53
+ options: {
54
+ dryRun: {
55
+ alias: 'd',
56
+ type: 'boolean',
57
+ description: 'Show what would be copied, but do not actually copy any files',
58
+ },
59
+ exclude: {
60
+ alias: 'e',
61
+ type: 'array',
62
+ description: 'pattern or glob to exclude (may be passed multiple times)',
63
+ },
64
+ rainbow: {
65
+ type: 'boolean',
66
+ alias: 'r',
67
+ description: 'Enable rainbow mode',
68
+ },
69
+ verbose: {
70
+ alias: 'V',
71
+ type: 'boolean',
72
+ description: 'print more information to console',
73
+ },
74
+ up: {
75
+ type: 'number',
76
+ description: 'slice a path off the bottom of the paths',
77
+ },
78
+ bar: {
79
+ alias: 'b',
80
+ required: true,
81
+ description: 'a required bar option',
82
+ }
83
+ },
84
+ version: '0.1.6',
85
+ };
86
+
87
+ const argv = parseArgs(config);
88
+ console.log(argv);
89
+ ```
90
+
91
+ #### Example CLI Calls
92
+
93
+ ```sh
94
+ # Show help
95
+ unicorns --help
96
+
97
+ # Show version
98
+ unicorns --version
99
+
100
+ # With required and optional positionals
101
+ unicorns file1.txt file2.txt output/ -b value
102
+
103
+ # With boolean and array options
104
+ unicorns file1.txt output/ --dryRun --exclude pattern1 --exclude pattern2 -b value
105
+
106
+ # With negated boolean
107
+ unicorns file1.txt output/ --no-dryRun -b value
108
+
109
+ # With short aliases
110
+ unicorns file1.txt output/ -d -e pattern1 -e pattern2 -b value
111
+
112
+ # With number option
113
+ unicorns file1.txt output/ --up 2 -b value
114
+ ```
115
+
116
+ #### Notes
117
+
118
+ - **Variadic positionals**: Use `variadic: true` for arguments that accept multiple values.
119
+ - **Required options**: Add `required: true` to enforce presence of an option.
120
+ - **Negated booleans**: Use `--no-flag` to set a boolean option to `false`.
121
+ - **Array options**: Repeat the flag to collect multiple values (e.g., `--exclude a --exclude b`).
122
+ - **Aliases**: Use `alias` for short flags (e.g., `-d` for `--dryRun`).
123
+
124
+ See [examples/](examples/) for more usage patterns.
@@ -0,0 +1,3 @@
1
+ import type { Config } from './interfaces.js';
2
+ export declare function parseArgs(config: Config): Record<string, any>;
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAG/D,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAqM7D"}
package/dist/index.js ADDED
@@ -0,0 +1,225 @@
1
+ import { camelToKebab, kebabToCamel, padString } from './utils.js';
2
+ export function parseArgs(config) {
3
+ const { command, options, version } = config;
4
+ const args = process.argv.slice(2);
5
+ const result = {};
6
+ // Check for duplicate aliases
7
+ const aliasMap = new Map();
8
+ for (const [key, opt] of Object.entries(options)) {
9
+ if (opt.alias) {
10
+ const optAlias = opt.alias;
11
+ if (aliasMap.has(optAlias)) {
12
+ throw new Error(`Duplicate alias detected: "${opt.alias}" used for both "${aliasMap.get(optAlias)}" and "${key}"`);
13
+ }
14
+ aliasMap.set(optAlias, key);
15
+ }
16
+ }
17
+ // Handle --help and --version before anything else
18
+ if (args.includes('--help') || args.includes('-h')) {
19
+ printHelp(config);
20
+ process.exit(0);
21
+ }
22
+ if (args.includes('--version') || args.includes('-v')) {
23
+ console.log(version);
24
+ process.exit(0);
25
+ }
26
+ // Handle positional arguments
27
+ let argIndex = 0;
28
+ const positionals = command.positional ?? [];
29
+ const nonOptionArgs = [];
30
+ while (argIndex < args.length && !args[argIndex].startsWith('-')) {
31
+ nonOptionArgs.push(args[argIndex]);
32
+ argIndex++;
33
+ }
34
+ let nonOptionIndex = 0;
35
+ for (let i = 0; i < positionals.length; i++) {
36
+ const pos = positionals[i];
37
+ if (pos.variadic) {
38
+ const remaining = positionals.length - (i + 1);
39
+ const values = nonOptionArgs.slice(nonOptionIndex, nonOptionArgs.length - remaining);
40
+ if (pos.required && values.length === 0) {
41
+ const usagePositionals = positionals.map(posArg => `<${posArg.name}>`).join(' ');
42
+ throw new Error(`Missing required positional argument, i.e.: "${command.name} ${usagePositionals}"`);
43
+ }
44
+ result[pos.name] = values;
45
+ nonOptionIndex += values.length;
46
+ }
47
+ else {
48
+ const value = nonOptionArgs[nonOptionIndex++];
49
+ if (!value) {
50
+ const usagePositionals = positionals.map(posArg => `<${posArg.name}>`).join(' ');
51
+ throw new Error(`Missing required positional argument, i.e.: "${command.name} ${usagePositionals}"`);
52
+ }
53
+ result[pos.name] = value;
54
+ }
55
+ }
56
+ // Handle options
57
+ // Start parsing options after all non-option args used for positionals
58
+ argIndex = 0;
59
+ const consumedArgs = new Set();
60
+ // Mark all nonOptionArgs indices as consumed for positionals
61
+ let tempNonOptionIndex = 0;
62
+ for (let i = 0; i < positionals.length; i++) {
63
+ const pos = positionals[i];
64
+ if (pos.variadic) {
65
+ const remaining = positionals.length - (i + 1);
66
+ const values = nonOptionArgs.slice(tempNonOptionIndex, nonOptionArgs.length - remaining);
67
+ for (let j = tempNonOptionIndex; j < tempNonOptionIndex + values.length; j++) {
68
+ consumedArgs.add(args.findIndex((a, idx) => !a.startsWith('-') && !consumedArgs.has(idx) && a === nonOptionArgs[j]));
69
+ }
70
+ tempNonOptionIndex += values.length;
71
+ }
72
+ else {
73
+ const value = nonOptionArgs[tempNonOptionIndex++];
74
+ consumedArgs.add(args.findIndex((a, idx) => !a.startsWith('-') && !consumedArgs.has(idx) && a === value));
75
+ }
76
+ }
77
+ while (argIndex < args.length) {
78
+ if (consumedArgs.has(argIndex)) {
79
+ argIndex++;
80
+ continue;
81
+ }
82
+ const argOrg = args[argIndex] || '';
83
+ let arg = args[argIndex];
84
+ let option;
85
+ let configKey;
86
+ if (argOrg.startsWith('-')) {
87
+ if (argOrg.startsWith('--')) {
88
+ arg = argOrg.slice(2);
89
+ // Try all forms: as-is, kebab-to-camel, camel-to-kebab
90
+ option = options[arg] || options[kebabToCamel(arg)] || options[camelToKebab(arg).replace(/-/g, '')];
91
+ if (option) {
92
+ // Find the actual config key
93
+ for (const key of Object.keys(options)) {
94
+ if (options[key] === option) {
95
+ configKey = key;
96
+ break;
97
+ }
98
+ }
99
+ }
100
+ if (!option) {
101
+ // Try matching aliases in all forms
102
+ for (const key of Object.keys(options)) {
103
+ const opt = options[key];
104
+ if (!opt.alias)
105
+ continue;
106
+ if (opt.alias.includes(arg) || opt.alias.includes(kebabToCamel(arg)) || opt.alias.includes(camelToKebab(arg))) {
107
+ option = opt;
108
+ configKey = key;
109
+ break;
110
+ }
111
+ }
112
+ }
113
+ }
114
+ else if (argOrg.startsWith('-')) {
115
+ // alias
116
+ arg = argOrg.slice(1);
117
+ if (arg) {
118
+ const optionKeys = Object.keys(options);
119
+ for (let j = 0; j < optionKeys.length; j++) {
120
+ const opt = options[optionKeys[j]];
121
+ if (!opt.alias)
122
+ continue;
123
+ if (opt.alias === arg || opt.alias === kebabToCamel(arg) || opt.alias === camelToKebab(arg)) {
124
+ option = opt;
125
+ configKey = optionKeys[j];
126
+ break;
127
+ }
128
+ }
129
+ }
130
+ }
131
+ if (!option) {
132
+ // Handle negated boolean in both forms
133
+ const isNegated = arg.startsWith('no-');
134
+ const optionName = isNegated ? arg.slice(3) : arg;
135
+ const camelOptionName = kebabToCamel(optionName);
136
+ option = options[optionName] || options[camelOptionName];
137
+ configKey = camelOptionName in options ? camelOptionName : optionName;
138
+ if (option?.type === 'boolean') {
139
+ if (result[optionName] !== undefined || result[camelOptionName] !== undefined) {
140
+ throw new Error('Providing same negated and truthy argument are not allowed');
141
+ }
142
+ result[configKey] = !isNegated;
143
+ argIndex++;
144
+ continue;
145
+ }
146
+ }
147
+ if (!option || !configKey) {
148
+ throw new Error(`Unknown option: ${arg}`);
149
+ }
150
+ switch (option.type || 'string') {
151
+ case 'boolean':
152
+ if (result[configKey] !== undefined) {
153
+ throw new Error('Providing same negated and truthy argument are not allowed');
154
+ }
155
+ result[configKey] = !argOrg.startsWith('--no-') && !argOrg.startsWith('-no-');
156
+ break;
157
+ case 'string':
158
+ if (args[argIndex + 1] === undefined || args[argIndex + 1].startsWith('-')) {
159
+ throw new Error(`Missing value for option: ${configKey}`);
160
+ }
161
+ result[configKey] = args[++argIndex];
162
+ break;
163
+ case 'number':
164
+ if (args[argIndex + 1] === undefined || args[argIndex + 1].startsWith('-')) {
165
+ throw new Error(`Missing value for option: ${configKey}`);
166
+ }
167
+ result[configKey] = Number(args[++argIndex]);
168
+ break;
169
+ case 'array': {
170
+ if (!result[configKey])
171
+ result[configKey] = [];
172
+ const arrayValue = args[++argIndex];
173
+ if (arrayValue === undefined || arrayValue.startsWith('-')) {
174
+ throw new Error(`Missing value for array option: ${configKey}`);
175
+ }
176
+ result[configKey].push(arrayValue);
177
+ break;
178
+ }
179
+ }
180
+ }
181
+ else {
182
+ throw new Error(`Unknown argument: ${arg}`);
183
+ }
184
+ argIndex++;
185
+ }
186
+ // After all parsing, check for required options
187
+ Object.entries(options).forEach(([key, opt]) => {
188
+ if (opt.required && result[key] === undefined) {
189
+ const aliasStr = opt.alias ? `-${opt.alias}, ` : '';
190
+ throw new Error(`Missing required option: ${aliasStr}--${key}`);
191
+ }
192
+ });
193
+ return result;
194
+ }
195
+ function printHelp(config) {
196
+ const { command, options } = config;
197
+ // Build usage string for positionals
198
+ const usagePositionals = (command.positional ?? [])
199
+ .map(p => {
200
+ const variadic = p.variadic ? '..' : '';
201
+ if (p.required) {
202
+ return `<${p.name}${variadic}>`;
203
+ }
204
+ return `[${p.name}${variadic}]`;
205
+ })
206
+ .join(' ');
207
+ console.log('Usage:');
208
+ console.log(` ${command.name} ${usagePositionals} [options] ${command.description}`);
209
+ console.log('\nPositionals:');
210
+ command.positional?.forEach(arg => {
211
+ console.log(` ${arg.name.padEnd(20)}${arg.description.slice(0, 65).padEnd(65)}[${arg.type || 'string'}]`);
212
+ });
213
+ console.log('\nOptions:');
214
+ Object.keys(options).forEach(key => {
215
+ const option = options[key];
216
+ const requiredStr = option.required ? '[required]' : '';
217
+ const aliasStr = option.alias ? `-${option.alias}, ` : '';
218
+ console.log(` ${aliasStr.padEnd(4)}--${key.padEnd(14)}${(option.description || '').slice(0, 65).padEnd(65)}[${option.type || 'string'}]${requiredStr}`);
219
+ });
220
+ console.log('\nDefault options:');
221
+ console.log(`${padString(' -h, --help', 21)} ${padString('Show help', 64)} [boolean]`);
222
+ console.log(`${padString(' -v, --version', 21)} ${padString('Show version number', 64)} [boolean]`);
223
+ console.log('\n');
224
+ }
225
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ export interface ArgumentOptions {
2
+ /** command option type */
3
+ type?: 'string' | 'boolean' | 'number' | 'array';
4
+ /** description of the command option */
5
+ description: string;
6
+ /** defaults to undefined, provide shorter aliases as command options */
7
+ alias?: string | string[];
8
+ /** defaults to false, is the option required? */
9
+ required?: boolean;
10
+ }
11
+ export interface CommandOptions {
12
+ /** CLI command name, used in the help docs */
13
+ name: string;
14
+ description: string;
15
+ positional?: {
16
+ /** positional argument name (it will be displayed in the help docs) */
17
+ name: string;
18
+ /** positional argument description */
19
+ description: string;
20
+ /** postional argument type */
21
+ type?: 'string';
22
+ /** defaults to false, allows multiple values for this positional argument */
23
+ variadic?: boolean;
24
+ /** defaults to false, is the positional argument required? */
25
+ required?: boolean;
26
+ }[];
27
+ }
28
+ export interface Config {
29
+ command: CommandOptions;
30
+ options: Record<string, ArgumentOptions>;
31
+ version: string;
32
+ }
33
+ //# sourceMappingURL=interfaces.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interfaces.d.ts","sourceRoot":"","sources":["../src/interfaces.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,0BAA0B;IAC1B,IAAI,CAAC,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;IACjD,wCAAwC;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC1B,iDAAiD;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE;QACX,uEAAuE;QACvE,IAAI,EAAE,MAAM,CAAC;QACb,sCAAsC;QACtC,WAAW,EAAE,MAAM,CAAC;QACpB,8BAA8B;QAC9B,IAAI,CAAC,EAAE,QAAQ,CAAC;QAChB,6EAA6E;QAC7E,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,8DAA8D;QAC9D,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB,EAAE,CAAC;CACL;AAED,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE,cAAc,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACzC,OAAO,EAAE,MAAM,CAAC;CACjB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=interfaces.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interfaces.js","sourceRoot":"","sources":["../src/interfaces.ts"],"names":[],"mappings":""}
@@ -0,0 +1,7 @@
1
+ /** Utility to convert kebab-case to camelCase */
2
+ export declare function kebabToCamel(str: string): string;
3
+ /** Utility to convert camelCase to kebab-case */
4
+ export declare function camelToKebab(str: string): string;
5
+ /** add whitespace padding to any input string */
6
+ export declare function padString(input: string, padding: number): string;
7
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,UAEvC;AAED,iDAAiD;AACjD,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,UAEvC;AAED,iDAAiD;AACjD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,UAEvD"}
package/dist/utils.js ADDED
@@ -0,0 +1,13 @@
1
+ /** Utility to convert kebab-case to camelCase */
2
+ export function kebabToCamel(str) {
3
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
4
+ }
5
+ /** Utility to convert camelCase to kebab-case */
6
+ export function camelToKebab(str) {
7
+ return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
8
+ }
9
+ /** add whitespace padding to any input string */
10
+ export function padString(input, padding) {
11
+ return input.padEnd(padding);
12
+ }
13
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,OAAO,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;AAC7D,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,OAAO,GAAG,CAAC,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;AAClE,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,OAAe;IACtD,OAAO,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AAC/B,CAAC"}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "cli-nano",
3
+ "version": "0.2.0",
4
+ "description": "Mini command-line tool similar to `yargs` or `parseArgs` from Node.js that accepts positional arguments, flags and options.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "types": "./dist/index.d.ts",
14
+ "module": "./dist/index.js",
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "files": [
19
+ "/dist",
20
+ "/src"
21
+ ],
22
+ "license": "MIT",
23
+ "author": "Ghislain B.",
24
+ "homepage": "https://github.com/ghiscoding/cli-nano",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/ghiscoding/cli-nano.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/ghiscoding/cli-nano/issues"
31
+ },
32
+ "scripts": {
33
+ "clean": "premove dist",
34
+ "build": "npm run clean && npm run biome:write && tsc",
35
+ "biome:check": "npm run biome:lint:check && npm run biome:format:check",
36
+ "biome:write": "npm run biome:lint:write && npm run biome:format:write",
37
+ "biome:lint:check": "biome lint ./src",
38
+ "biome:lint:write": "biome lint --write ./src",
39
+ "biome:format:check": "biome format ./src",
40
+ "biome:format:write": "biome format --write ./src",
41
+ "preview:release": "release-it --only-version --dry-run",
42
+ "release": "release-it --only-version",
43
+ "test": "vitest --watch --config ./vitest.config.mts",
44
+ "test:coverage": "vitest --coverage --config ./vitest.config.mts"
45
+ },
46
+ "devDependencies": {
47
+ "@biomejs/biome": "^2.0.5",
48
+ "@release-it/conventional-changelog": "^10.0.1",
49
+ "@types/node": "^22.15.33",
50
+ "@vitest/coverage-v8": "^3.2.4",
51
+ "premove": "^4.0.0",
52
+ "release-it": "^19.0.3",
53
+ "typescript": "^5.8.3",
54
+ "vitest": "^3.2.4"
55
+ },
56
+ "engines": {
57
+ "node": ">=20.0.0"
58
+ }
59
+ }
@@ -0,0 +1,502 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { parseArgs } from '../index.js';
4
+ import type { Config } from '../interfaces.js';
5
+
6
+ const config: Config = {
7
+ command: {
8
+ name: 'copyfiles',
9
+ description: 'Copy files from a source to a destination directory',
10
+ positional: [
11
+ {
12
+ name: 'inFile',
13
+ description: 'source files',
14
+ type: 'string',
15
+ required: true,
16
+ },
17
+ {
18
+ name: 'outDirectory',
19
+ description: 'destination directory',
20
+ required: true,
21
+ },
22
+ ],
23
+ },
24
+ options: {
25
+ all: {
26
+ alias: 'a',
27
+ type: 'boolean',
28
+ description: 'include files & directories begining with a dot (.)',
29
+ },
30
+ dryRun: {
31
+ alias: 'd',
32
+ type: 'boolean',
33
+ description: 'Show what would be copied, but do not actually copy any files',
34
+ },
35
+ exclude: {
36
+ alias: 'e',
37
+ type: 'array',
38
+ description: 'pattern or glob to exclude (may be passed multiple times)',
39
+ },
40
+ up: {
41
+ type: 'number',
42
+ description: 'slice a path off the bottom of the paths',
43
+ },
44
+ bar: {
45
+ alias: 'b',
46
+ required: true,
47
+ description: 'a required bar option',
48
+ },
49
+ },
50
+ version: '0.1.6',
51
+ };
52
+
53
+ describe('parseArgs', () => {
54
+ beforeEach(() => {
55
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...[]]);
56
+ });
57
+
58
+ afterEach(() => {
59
+ vi.restoreAllMocks();
60
+ });
61
+
62
+ it('should parse positional arguments correctly', () => {
63
+ const args = ['file1.txt', 'output/', '--bar', 'value'];
64
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
65
+
66
+ const result = parseArgs(config);
67
+
68
+ expect(result.inFile).toBe('file1.txt');
69
+ expect(result.outDirectory).toBe('output/');
70
+ });
71
+
72
+ it('should parse camelCase boolean options correctly', () => {
73
+ const args = ['file1.txt', 'output/', '--all', '--no-dryRun', '--bar', 'value'];
74
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
75
+
76
+ const result = parseArgs(config);
77
+
78
+ expect(result.inFile).toBe('file1.txt');
79
+ expect(result.outDirectory).toBe('output/');
80
+ expect(result.all).toBe(true);
81
+ expect(result.dryRun).toBe(false);
82
+ });
83
+
84
+ it('should parse kebab-case boolean options correctly', () => {
85
+ const args = ['file1.txt', 'output/', '--all', '--no-dry-run', '--bar', 'value'];
86
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
87
+
88
+ const result = parseArgs(config);
89
+
90
+ expect(result.inFile).toBe('file1.txt');
91
+ expect(result.outDirectory).toBe('output/');
92
+ expect(result.all).toBe(true);
93
+ expect(result.dryRun).toBe(false);
94
+ });
95
+
96
+ it('should parse string options correctly', () => {
97
+ const args = ['file1.txt', 'output/', '--up', '2', '--bar', 'value'];
98
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
99
+ const result = parseArgs(config);
100
+ expect(result.up).toBe(2);
101
+ });
102
+
103
+ it('should parse string options correctly with an alias', () => {
104
+ const args = ['file1.txt', 'output/', '--up', '2', '-b', 'value'];
105
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
106
+ const result = parseArgs(config);
107
+ expect(result.up).toBe(2);
108
+ expect(result.bar).toBe('value');
109
+ });
110
+
111
+ it('should parse array options correctly when defined at the end of the command options', () => {
112
+ const args = ['file1.txt', 'output/', '-b', 'value', '--exclude', 'pattern1', '--exclude', 'pattern2'];
113
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
114
+ const result = parseArgs(config);
115
+ expect(result.exclude).toEqual(['pattern1', 'pattern2']);
116
+ expect(result.bar).toBe('value');
117
+ });
118
+
119
+ it('should parse array options correctly when defined in the middle of the command options with camelCase arguments', () => {
120
+ const args = ['file1.txt', 'output/', '--exclude', 'pattern1', '--exclude', 'pattern2', '-b', 'value', '--dryRun'];
121
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
122
+ const result = parseArgs(config);
123
+ expect(result.exclude).toEqual(['pattern1', 'pattern2']);
124
+ expect(result.dryRun).toBe(true);
125
+ expect(result.bar).toBe('value');
126
+ });
127
+
128
+ it('should parse array options correctly when defined in the middle of the command options with kebab-case arguments', () => {
129
+ const args = ['file1.txt', 'output/', '--exclude', 'pattern1', '--exclude', 'pattern2', '-b', 'value', '--dry-run'];
130
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
131
+ const result = parseArgs(config);
132
+ expect(result.exclude).toEqual(['pattern1', 'pattern2']);
133
+ expect(result.dryRun).toBe(true);
134
+ expect(result.bar).toBe('value');
135
+ });
136
+
137
+ it('should throw an error for unknown options', () => {
138
+ const args = ['file1.txt', 'output/', '-b', 'value', '--unknown'];
139
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
140
+ expect(() => parseArgs(config)).toThrowError('Unknown option: unknown');
141
+ });
142
+
143
+ it('should throw an error for unknown kebab-case options', () => {
144
+ const args = ['file1.txt', 'output/', '-b', 'value', '--unknown-kebab'];
145
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
146
+ expect(() => parseArgs(config)).toThrowError('Unknown option: unknown-kebab');
147
+ });
148
+
149
+ it('should throw an error for unknown camelCase options', () => {
150
+ const args = ['file1.txt', 'output/', '-b', 'value', '--unknownCamel'];
151
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
152
+ expect(() => parseArgs(config)).toThrowError('Unknown option: unknownCamel');
153
+ });
154
+
155
+ it('should throw when truthy and --no camelCase prefix arguments are both provided', () => {
156
+ const args = ['file1.txt', 'output/', '--all', '--dryRun', '--no-dryRun', '-b', 'value'];
157
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
158
+ expect(() => parseArgs(config)).toThrowError('Providing same negated and truthy argument are not allowed');
159
+ });
160
+
161
+ it('should throw when truthy and --no kebab-case prefix arguments are both provided', () => {
162
+ const args = ['file1.txt', 'output/', '--all', '--dryRun', '--no-dry-run', '-b', 'value'];
163
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
164
+ expect(() => parseArgs(config)).toThrowError('Providing same negated and truthy argument are not allowed');
165
+ });
166
+
167
+ it('should throw when --no prefix and truthy arguments are both provided', () => {
168
+ const args = ['file1.txt', 'output/', '--all', '--no-dryRun', '--dryRun', '-b', 'value'];
169
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
170
+ expect(() => parseArgs(config)).toThrowError('Providing same negated and truthy argument are not allowed');
171
+ });
172
+
173
+ it('should throw when positional arguments are missing', () => {
174
+ const args = ['file1.txt', '--all', '--dryRun'];
175
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
176
+
177
+ expect(() => parseArgs(config)).toThrow('Missing required positional argument, i.e.: "copyfiles <inFile> <outDirectory>');
178
+ });
179
+
180
+ it('should throw when positional arguments are missing and it will not try to read "value" as positional argument either', () => {
181
+ const args = ['file1.txt', '--all', '--dryRun', '-b', 'value'];
182
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
183
+
184
+ expect(() => parseArgs(config)).toThrow('Missing required positional argument, i.e.: "copyfiles <inFile> <outDirectory>');
185
+ });
186
+
187
+ it('should throw when required options are missing', () => {
188
+ const args = ['file1.txt', 'output', '--all', '--dryRun'];
189
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
190
+
191
+ expect(() => parseArgs(config)).toThrow('Missing required option: -b, --bar');
192
+ });
193
+
194
+ it('should throw if the same alias is defined for multiple options', () => {
195
+ const configWithDupAlias: Config = {
196
+ ...config,
197
+ options: {
198
+ foo: { alias: 'x', type: 'boolean', description: '' },
199
+ bar: { alias: 'x', type: 'boolean', description: '' },
200
+ },
201
+ };
202
+ expect(() => parseArgs(configWithDupAlias)).toThrow('Duplicate alias detected: "x" used for both "foo" and "bar"');
203
+ });
204
+
205
+ it('should handle help command', () =>
206
+ new Promise((done: any) => {
207
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
208
+ const args = ['--help'];
209
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
210
+ try {
211
+ parseArgs(config);
212
+ } catch (error: any) {
213
+ expect(error.message).toBe('process.exit unexpectedly called with "0"');
214
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Usage:'));
215
+ expect(consoleLogSpy).toHaveBeenCalledWith(
216
+ expect.stringContaining('copyfiles <inFile> <outDirectory> [options] Copy files from a source to a destination directory'),
217
+ );
218
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('\nPositionals:'));
219
+ expect(consoleLogSpy).toHaveBeenCalledWith(
220
+ expect.stringContaining(' inFile source files [string]'),
221
+ );
222
+ expect(consoleLogSpy).toHaveBeenCalledWith(
223
+ expect.stringContaining(' -d, --dryRun Show what would be copied, but do not actually copy any files [boolean]'),
224
+ );
225
+ expect(consoleLogSpy).toHaveBeenCalledWith(
226
+ expect.stringContaining(
227
+ ' -b, --bar a required bar option [string][required]',
228
+ ),
229
+ );
230
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('\nDefault options:'));
231
+ expect(consoleLogSpy).toHaveBeenCalledWith(
232
+ expect.stringContaining(' -h, --help Show help [boolean]'),
233
+ );
234
+ expect(consoleLogSpy).toHaveBeenCalledWith(
235
+ expect.stringContaining(' -v, --version Show version number [boolean]'),
236
+ );
237
+ done();
238
+ }
239
+ }));
240
+
241
+ it('should handle version command', () =>
242
+ new Promise((done: any) => {
243
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
244
+ const args = ['--version'];
245
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
246
+ try {
247
+ parseArgs(config);
248
+ } catch (error: any) {
249
+ expect(error.message).toBe('process.exit unexpectedly called with "0"');
250
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('0.1.6'));
251
+ done();
252
+ }
253
+ }));
254
+
255
+ it('should parse optional variadic positional arguments (zero or more)', () => {
256
+ const config: Config = {
257
+ command: {
258
+ name: 'test',
259
+ description: 'Test optional variadic',
260
+ positional: [
261
+ {
262
+ name: 'inputs',
263
+ description: 'input files',
264
+ type: 'string',
265
+ variadic: true,
266
+ required: false,
267
+ },
268
+ {
269
+ name: 'outDir',
270
+ description: 'output directory',
271
+ required: true,
272
+ },
273
+ ],
274
+ },
275
+ options: {},
276
+ version: '1.0.0',
277
+ };
278
+ // No inputs
279
+ let args = ['dist'];
280
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
281
+ let result = parseArgs(config);
282
+ expect(result.inputs).toEqual([]);
283
+ expect(result.outDir).toBe('dist');
284
+
285
+ // Multiple inputs
286
+ args = ['file1', 'file2', 'dist'];
287
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
288
+ result = parseArgs(config);
289
+ expect(result.inputs).toEqual(['file1', 'file2']);
290
+ expect(result.outDir).toBe('dist');
291
+ });
292
+
293
+ it('should parse a single optional variadic positional arguments (zero or more)', () => {
294
+ const config: Config = {
295
+ command: {
296
+ name: 'test',
297
+ description: 'Test optional variadic',
298
+ positional: [
299
+ {
300
+ name: 'inputs',
301
+ description: 'input files',
302
+ type: 'string',
303
+ variadic: true,
304
+ required: true,
305
+ },
306
+ ],
307
+ },
308
+ options: {},
309
+ version: '1.0.0',
310
+ };
311
+ // No inputs
312
+ let args: string[] = [];
313
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
314
+ expect(() => parseArgs(config)).toThrow('Missing required positional argument, i.e.: "test <inputs>');
315
+
316
+ // Multiple inputs
317
+ args = ['file1', 'file2'];
318
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
319
+ const result = parseArgs(config);
320
+ expect(result.inputs).toEqual(['file1', 'file2']);
321
+ });
322
+
323
+ it('should print usage with required variadic positional argument using <inFile..>', () => {
324
+ const configWithVariadic: Config = {
325
+ ...config,
326
+ command: {
327
+ ...config.command,
328
+ positional: [
329
+ {
330
+ name: 'inFile',
331
+ description: 'source files',
332
+ type: 'string',
333
+ variadic: true,
334
+ required: true,
335
+ },
336
+ {
337
+ name: 'outDirectory',
338
+ description: 'destination directory',
339
+ required: true,
340
+ },
341
+ ],
342
+ },
343
+ };
344
+ const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
345
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', '--help']);
346
+ try {
347
+ parseArgs(configWithVariadic);
348
+ } catch {}
349
+ expect(spy).toHaveBeenCalledWith(expect.stringContaining('<inFile..>'));
350
+ spy.mockRestore();
351
+ });
352
+
353
+ it('should print usage without any positional argument defined', () => {
354
+ const configWithoutPositional: Config = {
355
+ ...config,
356
+ command: {
357
+ ...config.command,
358
+ },
359
+ options: {
360
+ file: {
361
+ description: 'source files',
362
+ type: 'string',
363
+ },
364
+ },
365
+ };
366
+ const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
367
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', '--help']);
368
+ try {
369
+ parseArgs(configWithoutPositional);
370
+ } catch {}
371
+ expect(spy).toHaveBeenCalledWith(expect.stringContaining('--file'));
372
+ expect(spy).toHaveBeenCalledWith(expect.stringContaining('source files'));
373
+ expect(spy).toHaveBeenCalledWith(expect.stringContaining('[string]'));
374
+ spy.mockRestore();
375
+ });
376
+
377
+ it('should throw if required boolean flag is missing', () => {
378
+ const config: Config = {
379
+ command: {
380
+ name: 'test',
381
+ description: 'Test required flag',
382
+ },
383
+ options: {
384
+ force: {
385
+ alias: 'f',
386
+ type: 'boolean',
387
+ required: true,
388
+ description: 'Force operation',
389
+ },
390
+ },
391
+ version: '1.0.0',
392
+ };
393
+ const args: string[] = [];
394
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
395
+ expect(() => parseArgs(config)).toThrow('Missing required option: -f, --force');
396
+ });
397
+
398
+ it('should parse kebab-case long option when alias is camelCase', () => {
399
+ const configWithCamelAlias: Config = {
400
+ ...config,
401
+ options: {
402
+ ...config.options,
403
+ testOption: {
404
+ alias: 'testAliasCamel',
405
+ type: 'boolean',
406
+ description: 'A test option with camelCase alias',
407
+ },
408
+ },
409
+ };
410
+ const args = ['file1.txt', 'output/', '--test-alias-camel', '-b', 'value'];
411
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
412
+ const result = parseArgs(configWithCamelAlias);
413
+ expect(result.testOption).toBe(true);
414
+ });
415
+
416
+ it('should parse camelCase long option when alias is kebab-case', () => {
417
+ const configWithKebabAlias: Config = {
418
+ ...config,
419
+ options: {
420
+ ...config.options,
421
+ testOption: {
422
+ alias: 'test-alias-kebab',
423
+ type: 'boolean',
424
+ description: 'A test option with kebab-case alias',
425
+ },
426
+ },
427
+ };
428
+ const args = ['file1.txt', 'output/', '--testAliasKebab', '-b', 'value'];
429
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
430
+ const result = parseArgs(configWithKebabAlias);
431
+ expect(result.testOption).toBe(true);
432
+ });
433
+
434
+ it('should throw if too many positional arguments are provided', () => {
435
+ const args = ['file1.txt', 'output/', 'extra.txt', '-b', 'value'];
436
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
437
+ expect(() => parseArgs(config)).toThrow('Unknown argument: extra.txt');
438
+ });
439
+
440
+ it('should throw if array option is missing a value', () => {
441
+ const args = ['file1.txt', 'output/', '--exclude', '-b', 'value'];
442
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
443
+ expect(() => parseArgs(config)).toThrow('Missing value for array option: exclude');
444
+ });
445
+
446
+ it('should throw if string option is missing a value', () => {
447
+ const args = ['file1.txt', 'output/', '--bar'];
448
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
449
+ expect(() => parseArgs(config)).toThrow('Missing value for option: bar');
450
+ });
451
+
452
+ it('should throw if number option is missing a value', () => {
453
+ const args = ['file1.txt', 'output/', '--up'];
454
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
455
+ expect(() => parseArgs(config)).toThrow('Missing value for option: up');
456
+ });
457
+
458
+ it('should throw unknown option when short flag does not match any alias', () => {
459
+ const configWithNoAlias: Config = {
460
+ ...config,
461
+ options: {
462
+ ...config.options,
463
+ noAliasOpt: {
464
+ type: 'boolean',
465
+ description: 'Option with no alias',
466
+ },
467
+ },
468
+ };
469
+ const args = ['file1.txt', 'output/', '-x', '-b', 'value'];
470
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', ...args]);
471
+ expect(() => parseArgs(configWithNoAlias)).toThrow('Unknown option: x');
472
+ });
473
+
474
+ it('should print usage with optional positional argument', () => {
475
+ const configWithOptional: Config = {
476
+ ...config,
477
+ command: {
478
+ ...config.command,
479
+ positional: [
480
+ {
481
+ name: 'inFile',
482
+ description: 'source files',
483
+ type: 'string',
484
+ required: false, // <-- optional positional
485
+ },
486
+ {
487
+ name: 'outDirectory',
488
+ description: 'destination directory',
489
+ required: true,
490
+ },
491
+ ],
492
+ },
493
+ };
494
+ const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
495
+ vi.spyOn(process, 'argv', 'get').mockReturnValue(['node', 'cli.js', '--help']);
496
+ try {
497
+ parseArgs(configWithOptional);
498
+ } catch {}
499
+ expect(spy).toHaveBeenCalledWith(expect.stringContaining('[inFile]'));
500
+ spy.mockRestore();
501
+ });
502
+ });
package/src/index.ts ADDED
@@ -0,0 +1,235 @@
1
+ import type { ArgumentOptions, Config } from './interfaces.js';
2
+ import { camelToKebab, kebabToCamel, padString } from './utils.js';
3
+
4
+ export function parseArgs(config: Config): Record<string, any> {
5
+ const { command, options, version } = config;
6
+ const args = process.argv.slice(2);
7
+ const result: Record<string, any> = {};
8
+
9
+ // Check for duplicate aliases
10
+ const aliasMap = new Map<string, string>();
11
+ for (const [key, opt] of Object.entries(options)) {
12
+ if (opt.alias) {
13
+ const optAlias = opt.alias as string;
14
+ if (aliasMap.has(optAlias)) {
15
+ throw new Error(`Duplicate alias detected: "${opt.alias}" used for both "${aliasMap.get(optAlias)}" and "${key}"`);
16
+ }
17
+ aliasMap.set(optAlias, key);
18
+ }
19
+ }
20
+
21
+ // Handle --help and --version before anything else
22
+ if (args.includes('--help') || args.includes('-h')) {
23
+ printHelp(config);
24
+ process.exit(0);
25
+ }
26
+ if (args.includes('--version') || args.includes('-v')) {
27
+ console.log(version);
28
+ process.exit(0);
29
+ }
30
+
31
+ // Handle positional arguments
32
+ let argIndex = 0;
33
+ const positionals = command.positional ?? [];
34
+ const nonOptionArgs: string[] = [];
35
+ while (argIndex < args.length && !args[argIndex].startsWith('-')) {
36
+ nonOptionArgs.push(args[argIndex]);
37
+ argIndex++;
38
+ }
39
+ let nonOptionIndex = 0;
40
+
41
+ for (let i = 0; i < positionals.length; i++) {
42
+ const pos = positionals[i];
43
+ if (pos.variadic) {
44
+ const remaining = positionals.length - (i + 1);
45
+ const values = nonOptionArgs.slice(nonOptionIndex, nonOptionArgs.length - remaining);
46
+ if (pos.required && values.length === 0) {
47
+ const usagePositionals = positionals.map(posArg => `<${posArg.name}>`).join(' ');
48
+ throw new Error(`Missing required positional argument, i.e.: "${command.name} ${usagePositionals}"`);
49
+ }
50
+ result[pos.name] = values;
51
+ nonOptionIndex += values.length;
52
+ } else {
53
+ const value = nonOptionArgs[nonOptionIndex++];
54
+ if (!value) {
55
+ const usagePositionals = positionals.map(posArg => `<${posArg.name}>`).join(' ');
56
+ throw new Error(`Missing required positional argument, i.e.: "${command.name} ${usagePositionals}"`);
57
+ }
58
+ result[pos.name] = value;
59
+ }
60
+ }
61
+
62
+ // Handle options
63
+ // Start parsing options after all non-option args used for positionals
64
+ argIndex = 0;
65
+ const consumedArgs = new Set<number>();
66
+ // Mark all nonOptionArgs indices as consumed for positionals
67
+ let tempNonOptionIndex = 0;
68
+ for (let i = 0; i < positionals.length; i++) {
69
+ const pos = positionals[i];
70
+ if (pos.variadic) {
71
+ const remaining = positionals.length - (i + 1);
72
+ const values = nonOptionArgs.slice(tempNonOptionIndex, nonOptionArgs.length - remaining);
73
+ for (let j = tempNonOptionIndex; j < tempNonOptionIndex + values.length; j++) {
74
+ consumedArgs.add(args.findIndex((a, idx) => !a.startsWith('-') && !consumedArgs.has(idx) && a === nonOptionArgs[j]));
75
+ }
76
+ tempNonOptionIndex += values.length;
77
+ } else {
78
+ const value = nonOptionArgs[tempNonOptionIndex++];
79
+ consumedArgs.add(args.findIndex((a, idx) => !a.startsWith('-') && !consumedArgs.has(idx) && a === value));
80
+ }
81
+ }
82
+
83
+ while (argIndex < args.length) {
84
+ if (consumedArgs.has(argIndex)) {
85
+ argIndex++;
86
+ continue;
87
+ }
88
+ const argOrg = args[argIndex] || '';
89
+ let arg = args[argIndex];
90
+ let option: ArgumentOptions | undefined;
91
+ let configKey: string | undefined;
92
+
93
+ if (argOrg.startsWith('-')) {
94
+ if (argOrg.startsWith('--')) {
95
+ arg = argOrg.slice(2);
96
+ // Try all forms: as-is, kebab-to-camel, camel-to-kebab
97
+ option = options[arg] || options[kebabToCamel(arg)] || options[camelToKebab(arg).replace(/-/g, '')];
98
+ if (option) {
99
+ // Find the actual config key
100
+ for (const key of Object.keys(options)) {
101
+ if (options[key] === option) {
102
+ configKey = key;
103
+ break;
104
+ }
105
+ }
106
+ }
107
+ if (!option) {
108
+ // Try matching aliases in all forms
109
+ for (const key of Object.keys(options)) {
110
+ const opt = options[key];
111
+ if (!opt.alias) continue;
112
+ if (opt.alias.includes(arg) || opt.alias.includes(kebabToCamel(arg)) || opt.alias.includes(camelToKebab(arg))) {
113
+ option = opt;
114
+ configKey = key;
115
+ break;
116
+ }
117
+ }
118
+ }
119
+ } else if (argOrg.startsWith('-')) {
120
+ // alias
121
+ arg = argOrg.slice(1);
122
+ if (arg) {
123
+ const optionKeys = Object.keys(options);
124
+ for (let j = 0; j < optionKeys.length; j++) {
125
+ const opt = options[optionKeys[j]];
126
+ if (!opt.alias) continue;
127
+ if (opt.alias === arg || opt.alias === kebabToCamel(arg) || opt.alias === camelToKebab(arg)) {
128
+ option = opt;
129
+ configKey = optionKeys[j];
130
+ break;
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ if (!option) {
137
+ // Handle negated boolean in both forms
138
+ const isNegated = arg.startsWith('no-');
139
+ const optionName = isNegated ? arg.slice(3) : arg;
140
+ const camelOptionName = kebabToCamel(optionName);
141
+ option = options[optionName] || options[camelOptionName];
142
+ configKey = camelOptionName in options ? camelOptionName : optionName;
143
+ if (option?.type === 'boolean') {
144
+ if (result[optionName] !== undefined || result[camelOptionName] !== undefined) {
145
+ throw new Error('Providing same negated and truthy argument are not allowed');
146
+ }
147
+ result[configKey] = !isNegated;
148
+ argIndex++;
149
+ continue;
150
+ }
151
+ }
152
+
153
+ if (!option || !configKey) {
154
+ throw new Error(`Unknown option: ${arg}`);
155
+ }
156
+
157
+ switch (option.type || 'string') {
158
+ case 'boolean':
159
+ if (result[configKey] !== undefined) {
160
+ throw new Error('Providing same negated and truthy argument are not allowed');
161
+ }
162
+ result[configKey] = !argOrg.startsWith('--no-') && !argOrg.startsWith('-no-');
163
+ break;
164
+ case 'string':
165
+ if (args[argIndex + 1] === undefined || args[argIndex + 1].startsWith('-')) {
166
+ throw new Error(`Missing value for option: ${configKey}`);
167
+ }
168
+ result[configKey] = args[++argIndex];
169
+ break;
170
+ case 'number':
171
+ if (args[argIndex + 1] === undefined || args[argIndex + 1].startsWith('-')) {
172
+ throw new Error(`Missing value for option: ${configKey}`);
173
+ }
174
+ result[configKey] = Number(args[++argIndex]);
175
+ break;
176
+ case 'array': {
177
+ if (!result[configKey]) result[configKey] = [];
178
+ const arrayValue = args[++argIndex];
179
+ if (arrayValue === undefined || arrayValue.startsWith('-')) {
180
+ throw new Error(`Missing value for array option: ${configKey}`);
181
+ }
182
+ result[configKey].push(arrayValue);
183
+ break;
184
+ }
185
+ }
186
+ } else {
187
+ throw new Error(`Unknown argument: ${arg}`);
188
+ }
189
+ argIndex++;
190
+ }
191
+
192
+ // After all parsing, check for required options
193
+ Object.entries(options).forEach(([key, opt]) => {
194
+ if (opt.required && result[key] === undefined) {
195
+ const aliasStr = opt.alias ? `-${opt.alias}, ` : '';
196
+ throw new Error(`Missing required option: ${aliasStr}--${key}`);
197
+ }
198
+ });
199
+
200
+ return result;
201
+ }
202
+
203
+ function printHelp(config: Config) {
204
+ const { command, options } = config;
205
+
206
+ // Build usage string for positionals
207
+ const usagePositionals = (command.positional ?? [])
208
+ .map(p => {
209
+ const variadic = p.variadic ? '..' : '';
210
+ if (p.required) {
211
+ return `<${p.name}${variadic}>`;
212
+ }
213
+ return `[${p.name}${variadic}]`;
214
+ })
215
+ .join(' ');
216
+ console.log('Usage:');
217
+ console.log(` ${command.name} ${usagePositionals} [options] ${command.description}`);
218
+ console.log('\nPositionals:');
219
+ command.positional?.forEach(arg => {
220
+ console.log(` ${arg.name.padEnd(20)}${arg.description.slice(0, 65).padEnd(65)}[${arg.type || 'string'}]`);
221
+ });
222
+ console.log('\nOptions:');
223
+ Object.keys(options).forEach(key => {
224
+ const option = options[key];
225
+ const requiredStr = option.required ? '[required]' : '';
226
+ const aliasStr = option.alias ? `-${option.alias}, ` : '';
227
+ console.log(
228
+ ` ${aliasStr.padEnd(4)}--${key.padEnd(14)}${(option.description || '').slice(0, 65).padEnd(65)}[${option.type || 'string'}]${requiredStr}`,
229
+ );
230
+ });
231
+ console.log('\nDefault options:');
232
+ console.log(`${padString(' -h, --help', 21)} ${padString('Show help', 64)} [boolean]`);
233
+ console.log(`${padString(' -v, --version', 21)} ${padString('Show version number', 64)} [boolean]`);
234
+ console.log('\n');
235
+ }
@@ -0,0 +1,34 @@
1
+ export interface ArgumentOptions {
2
+ /** command option type */
3
+ type?: 'string' | 'boolean' | 'number' | 'array';
4
+ /** description of the command option */
5
+ description: string;
6
+ /** defaults to undefined, provide shorter aliases as command options */
7
+ alias?: string | string[];
8
+ /** defaults to false, is the option required? */
9
+ required?: boolean;
10
+ }
11
+
12
+ export interface CommandOptions {
13
+ /** CLI command name, used in the help docs */
14
+ name: string;
15
+ description: string;
16
+ positional?: {
17
+ /** positional argument name (it will be displayed in the help docs) */
18
+ name: string;
19
+ /** positional argument description */
20
+ description: string;
21
+ /** postional argument type */
22
+ type?: 'string';
23
+ /** defaults to false, allows multiple values for this positional argument */
24
+ variadic?: boolean;
25
+ /** defaults to false, is the positional argument required? */
26
+ required?: boolean;
27
+ }[];
28
+ }
29
+
30
+ export interface Config {
31
+ command: CommandOptions;
32
+ options: Record<string, ArgumentOptions>;
33
+ version: string;
34
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,14 @@
1
+ /** Utility to convert kebab-case to camelCase */
2
+ export function kebabToCamel(str: string) {
3
+ return str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
4
+ }
5
+
6
+ /** Utility to convert camelCase to kebab-case */
7
+ export function camelToKebab(str: string) {
8
+ return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
9
+ }
10
+
11
+ /** add whitespace padding to any input string */
12
+ export function padString(input: string, padding: number) {
13
+ return input.padEnd(padding);
14
+ }