citty 0.2.1 → 0.2.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/README.md +157 -31
- package/dist/_chunks/libs/scule.mjs +6 -1
- package/dist/index.d.mts +12 -1
- package/dist/index.mjs +157 -29
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -10,32 +10,19 @@
|
|
|
10
10
|
|
|
11
11
|
Elegant CLI Builder
|
|
12
12
|
|
|
13
|
-
- Zero dependency
|
|
14
|
-
- Fast and lightweight argument parser (based on native [Node.js `util.parseArgs`](https://nodejs.org/api/util.html#utilparseargsconfig))
|
|
13
|
+
- Zero dependency, fast and lightweight (based on native [`util.parseArgs`](https://nodejs.org/api/util.html#utilparseargsconfig))
|
|
15
14
|
- Smart value parsing with typecast and boolean shortcuts
|
|
16
|
-
- Nested sub-commands
|
|
17
|
-
-
|
|
18
|
-
- Pluggable and composable API
|
|
19
|
-
- Auto generated usage and help
|
|
15
|
+
- Nested sub-commands with lazy and async loading
|
|
16
|
+
- Pluggable and composable API with auto generated usage
|
|
20
17
|
|
|
21
18
|
## Usage
|
|
22
19
|
|
|
23
|
-
Install package:
|
|
24
|
-
|
|
25
20
|
```sh
|
|
26
21
|
npx nypm add -D citty
|
|
27
22
|
```
|
|
28
23
|
|
|
29
|
-
Import:
|
|
30
|
-
|
|
31
24
|
```js
|
|
32
25
|
import { defineCommand, runMain } from "citty";
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Define main command to run:
|
|
36
|
-
|
|
37
|
-
```ts
|
|
38
|
-
import { defineCommand, runMain } from "citty";
|
|
39
26
|
|
|
40
27
|
const main = defineCommand({
|
|
41
28
|
meta: {
|
|
@@ -54,6 +41,12 @@ const main = defineCommand({
|
|
|
54
41
|
description: "Use friendly greeting",
|
|
55
42
|
},
|
|
56
43
|
},
|
|
44
|
+
setup({ args }) {
|
|
45
|
+
console.log(`now setup ${args.command}`);
|
|
46
|
+
},
|
|
47
|
+
cleanup({ args }) {
|
|
48
|
+
console.log(`now cleanup ${args.command}`);
|
|
49
|
+
},
|
|
57
50
|
run({ args }) {
|
|
58
51
|
console.log(`${args.friendly ? "Hi" : "Greetings"} ${args.name}!`);
|
|
59
52
|
},
|
|
@@ -62,35 +55,168 @@ const main = defineCommand({
|
|
|
62
55
|
runMain(main);
|
|
63
56
|
```
|
|
64
57
|
|
|
65
|
-
|
|
58
|
+
```sh
|
|
59
|
+
node index.mjs john
|
|
60
|
+
# Greetings john!
|
|
61
|
+
```
|
|
66
62
|
|
|
67
|
-
###
|
|
63
|
+
### Sub Commands
|
|
68
64
|
|
|
69
|
-
|
|
65
|
+
Sub commands can be nested recursively. Use lazy imports for large CLIs to avoid loading all commands at once.
|
|
70
66
|
|
|
71
|
-
|
|
67
|
+
```js
|
|
68
|
+
import { defineCommand, runMain } from "citty";
|
|
72
69
|
|
|
73
|
-
|
|
70
|
+
const sub = defineCommand({
|
|
71
|
+
meta: { name: "sub", description: "Sub command" },
|
|
72
|
+
args: {
|
|
73
|
+
name: { type: "positional", description: "Your name", required: true },
|
|
74
|
+
},
|
|
75
|
+
run({ args }) {
|
|
76
|
+
console.log(`Hello ${args.name}!`);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
74
79
|
|
|
75
|
-
|
|
80
|
+
const main = defineCommand({
|
|
81
|
+
meta: { name: "hello", version: "1.0.0", description: "My Awesome CLI App" },
|
|
82
|
+
subCommands: { sub },
|
|
83
|
+
});
|
|
76
84
|
|
|
77
|
-
|
|
85
|
+
runMain(main);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Subcommands support `meta.alias` (e.g., `["i", "add"]`) and `meta.hidden: true` to hide from help output.
|
|
89
|
+
|
|
90
|
+
### Lazy Commands
|
|
91
|
+
|
|
92
|
+
For large CLIs, lazy load sub commands so only the executed command is imported:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
const main = defineCommand({
|
|
96
|
+
meta: { name: "hello", version: "1.0.0", description: "My Awesome CLI App" },
|
|
97
|
+
subCommands: {
|
|
98
|
+
sub: () => import("./sub.mjs").then((m) => m.default),
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`meta`, `args`, and `subCommands` all accept `Resolvable<T>` values — a value, Promise, function, or async function — enabling lazy and dynamic resolution.
|
|
104
|
+
|
|
105
|
+
### Hooks
|
|
106
|
+
|
|
107
|
+
Commands support `setup` and `cleanup` functions called before and after `run()`. Only the executed command's hooks run. `cleanup` always runs, even if `run()` throws.
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
const main = defineCommand({
|
|
111
|
+
meta: { name: "hello", version: "1.0.0", description: "My Awesome CLI App" },
|
|
112
|
+
setup() {
|
|
113
|
+
console.log("Setting up...");
|
|
114
|
+
},
|
|
115
|
+
cleanup() {
|
|
116
|
+
console.log("Cleaning up...");
|
|
117
|
+
},
|
|
118
|
+
run() {
|
|
119
|
+
console.log("Hello World!");
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Plugins
|
|
125
|
+
|
|
126
|
+
Plugins extend commands with reusable `setup` and `cleanup` hooks:
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
import { defineCommand, defineCittyPlugin, runMain } from "citty";
|
|
130
|
+
|
|
131
|
+
const logger = defineCittyPlugin({
|
|
132
|
+
name: "logger",
|
|
133
|
+
setup({ args }) {
|
|
134
|
+
console.log("Logger setup, args:", args);
|
|
135
|
+
},
|
|
136
|
+
cleanup() {
|
|
137
|
+
console.log("Logger cleanup");
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const main = defineCommand({
|
|
142
|
+
meta: { name: "hello", description: "My CLI App" },
|
|
143
|
+
plugins: [logger],
|
|
144
|
+
run() {
|
|
145
|
+
console.log("Hello!");
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
runMain(main);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Plugin `setup` hooks run before the command's `setup` (in order), `cleanup` hooks run after (in reverse). Plugins can be async or factory functions.
|
|
153
|
+
|
|
154
|
+
## Arguments
|
|
155
|
+
|
|
156
|
+
### Argument Types
|
|
157
|
+
|
|
158
|
+
| Type | Description | Example |
|
|
159
|
+
| ------------ | ---------------------------------------- | --------------------------- |
|
|
160
|
+
| `positional` | Unnamed positional args | `cli <name>` |
|
|
161
|
+
| `string` | Named string options | `--name value` |
|
|
162
|
+
| `boolean` | Boolean flags, supports `--no-` negation | `--verbose` |
|
|
163
|
+
| `enum` | Constrained to `options` array | `--level=info\|warn\|error` |
|
|
164
|
+
|
|
165
|
+
### Common Options
|
|
166
|
+
|
|
167
|
+
| Option | Description |
|
|
168
|
+
| ------------- | ------------------------------------------------------------- |
|
|
169
|
+
| `description` | Help text shown in usage output |
|
|
170
|
+
| `required` | Whether the argument is required |
|
|
171
|
+
| `default` | Default value when not provided |
|
|
172
|
+
| `alias` | Short aliases (e.g., `["f"]`). Not for `positional` |
|
|
173
|
+
| `valueHint` | Display hint in help (e.g., `"host"` renders `--name=<host>`) |
|
|
174
|
+
|
|
175
|
+
### Example
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
const main = defineCommand({
|
|
179
|
+
args: {
|
|
180
|
+
name: { type: "positional", description: "Your name", required: true },
|
|
181
|
+
friendly: { type: "boolean", description: "Use friendly greeting", alias: ["f"] },
|
|
182
|
+
greeting: { type: "string", description: "Custom greeting", default: "Hello" },
|
|
183
|
+
level: {
|
|
184
|
+
type: "enum",
|
|
185
|
+
description: "Log level",
|
|
186
|
+
options: ["debug", "info", "warn", "error"],
|
|
187
|
+
default: "info",
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
run({ args }) {
|
|
191
|
+
console.log(`${args.greeting} ${args.name}! (level: ${args.level})`);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
```
|
|
78
195
|
|
|
79
|
-
###
|
|
196
|
+
### Boolean Negation
|
|
80
197
|
|
|
81
|
-
|
|
198
|
+
Boolean args support `--no-` prefix. The negative variant appears in help when `default: true` or `negativeDescription` is set.
|
|
82
199
|
|
|
83
|
-
###
|
|
200
|
+
### Case-Agnostic Access
|
|
84
201
|
|
|
85
|
-
|
|
202
|
+
Kebab-case args can be accessed as camelCase: `args["output-dir"]` and `args.outputDir` both work.
|
|
86
203
|
|
|
87
|
-
|
|
204
|
+
## Built-in Flags
|
|
88
205
|
|
|
89
|
-
|
|
206
|
+
`--help` / `-h` and `--version` / `-v` are handled automatically. Disabled if your command defines args with the same names or aliases.
|
|
90
207
|
|
|
91
|
-
|
|
208
|
+
## API
|
|
92
209
|
|
|
93
|
-
|
|
210
|
+
| Function | Description |
|
|
211
|
+
| ----------------------------- | -------------------------------------------------------------------------- |
|
|
212
|
+
| `defineCommand(def)` | Type helper for defining commands |
|
|
213
|
+
| `runMain(cmd, opts?)` | Run a command with usage support and graceful error handling |
|
|
214
|
+
| `createMain(cmd)` | Create a wrapper that calls `runMain` when invoked |
|
|
215
|
+
| `runCommand(cmd, opts)` | Parse args and run command/sub-commands; access `result` from return value |
|
|
216
|
+
| `parseArgs(rawArgs, argsDef)` | Parse input arguments and apply defaults |
|
|
217
|
+
| `renderUsage(cmd, parent?)` | Render command usage to a string |
|
|
218
|
+
| `showUsage(cmd, parent?)` | Render usage and print to console |
|
|
219
|
+
| `defineCittyPlugin(def)` | Type helper for defining plugins |
|
|
94
220
|
|
|
95
221
|
## Development
|
|
96
222
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
//#region node_modules/.pnpm/scule@1.3.0/node_modules/scule/dist/index.mjs
|
|
1
2
|
const NUMBER_CHAR_RE = /\d/;
|
|
2
3
|
const STR_SPLITTERS = [
|
|
3
4
|
"-",
|
|
@@ -62,4 +63,8 @@ function camelCase(str, opts) {
|
|
|
62
63
|
function kebabCase(str, joiner) {
|
|
63
64
|
return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => p.toLowerCase()).join(joiner ?? "-") : "";
|
|
64
65
|
}
|
|
65
|
-
|
|
66
|
+
function snakeCase(str) {
|
|
67
|
+
return kebabCase(str || "", "_");
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { kebabCase as n, snakeCase as r, camelCase as t };
|
package/dist/index.d.mts
CHANGED
|
@@ -52,12 +52,15 @@ interface CommandMeta {
|
|
|
52
52
|
version?: string;
|
|
53
53
|
description?: string;
|
|
54
54
|
hidden?: boolean;
|
|
55
|
+
alias?: string | string[];
|
|
55
56
|
}
|
|
56
57
|
type SubCommandsDef = Record<string, Resolvable<CommandDef<any>>>;
|
|
57
58
|
type CommandDef<T extends ArgsDef = ArgsDef> = {
|
|
58
59
|
meta?: Resolvable<CommandMeta>;
|
|
59
60
|
args?: Resolvable<T>;
|
|
61
|
+
default?: Resolvable<string>;
|
|
60
62
|
subCommands?: Resolvable<SubCommandsDef>;
|
|
63
|
+
plugins?: Resolvable<CittyPlugin>[];
|
|
61
64
|
setup?: (context: CommandContext<T>) => any | Promise<any>;
|
|
62
65
|
cleanup?: (context: CommandContext<T>) => any | Promise<any>;
|
|
63
66
|
run?: (context: CommandContext<T>) => any | Promise<any>;
|
|
@@ -69,6 +72,11 @@ type CommandContext<T extends ArgsDef = ArgsDef> = {
|
|
|
69
72
|
subCommand?: CommandDef<T>;
|
|
70
73
|
data?: any;
|
|
71
74
|
};
|
|
75
|
+
type CittyPlugin = {
|
|
76
|
+
name: string;
|
|
77
|
+
setup?(context: CommandContext<any>): void | Promise<void>;
|
|
78
|
+
cleanup?(context: CommandContext<any>): void | Promise<void>;
|
|
79
|
+
};
|
|
72
80
|
type Awaitable<T> = () => T | Promise<T>;
|
|
73
81
|
type Resolvable<T> = T | Promise<T> | (() => T) | (() => Promise<T>);
|
|
74
82
|
//#endregion
|
|
@@ -98,4 +106,7 @@ declare function createMain<T extends ArgsDef = ArgsDef>(cmd: CommandDef<T>): (o
|
|
|
98
106
|
//#region src/args.d.ts
|
|
99
107
|
declare function parseArgs<T extends ArgsDef = ArgsDef>(rawArgs: string[], argsDef: ArgsDef): ParsedArgs<T>;
|
|
100
108
|
//#endregion
|
|
101
|
-
|
|
109
|
+
//#region src/plugin.d.ts
|
|
110
|
+
declare function defineCittyPlugin(plugin: Resolvable<CittyPlugin>): Resolvable<CittyPlugin>;
|
|
111
|
+
//#endregion
|
|
112
|
+
export { Arg, ArgDef, ArgType, ArgsDef, Awaitable, BooleanArgDef, CittyPlugin, CommandContext, CommandDef, CommandMeta, EnumArgDef, ParsedArgs, PositionalArgDef, Resolvable, type RunCommandOptions, type RunMainOptions, StringArgDef, SubCommandsDef, _ArgDef, createMain, defineCittyPlugin, defineCommand, parseArgs, renderUsage, runCommand, runMain, showUsage };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { n as kebabCase, t as camelCase } from "./_chunks/libs/scule.mjs";
|
|
1
|
+
import { n as kebabCase, r as snakeCase, t as camelCase } from "./_chunks/libs/scule.mjs";
|
|
2
2
|
import { parseArgs as parseArgs$1 } from "node:util";
|
|
3
|
+
//#region src/_utils.ts
|
|
3
4
|
function toArray(val) {
|
|
4
5
|
if (Array.isArray(val)) return val;
|
|
5
6
|
return val === void 0 ? [] : [val];
|
|
@@ -20,6 +21,8 @@ var CLIError = class extends Error {
|
|
|
20
21
|
this.code = code;
|
|
21
22
|
}
|
|
22
23
|
};
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/_parser.ts
|
|
23
26
|
function parseRawArgs(args = [], opts = {}) {
|
|
24
27
|
const booleans = new Set(opts.boolean || []);
|
|
25
28
|
const strings = new Set(opts.string || []);
|
|
@@ -45,6 +48,12 @@ function parseRawArgs(args = [], opts = {}) {
|
|
|
45
48
|
for (const alias of aliases) if (booleans.has(alias)) return "boolean";
|
|
46
49
|
return "string";
|
|
47
50
|
}
|
|
51
|
+
function isStringType(name) {
|
|
52
|
+
if (strings.has(name)) return true;
|
|
53
|
+
const aliases = mainToAliases.get(name) || [];
|
|
54
|
+
for (const alias of aliases) if (strings.has(alias)) return true;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
48
57
|
const allOptions = new Set([
|
|
49
58
|
...booleans,
|
|
50
59
|
...strings,
|
|
@@ -88,7 +97,12 @@ function parseRawArgs(args = [], opts = {}) {
|
|
|
88
97
|
}
|
|
89
98
|
const out = { _: [] };
|
|
90
99
|
out._ = parsed.positionals;
|
|
91
|
-
for (const [key, value] of Object.entries(parsed.values))
|
|
100
|
+
for (const [key, value] of Object.entries(parsed.values)) {
|
|
101
|
+
let coerced = value;
|
|
102
|
+
if (getType(key) === "boolean" && typeof value === "string") coerced = value !== "false";
|
|
103
|
+
else if (isStringType(key) && typeof value === "boolean") coerced = "";
|
|
104
|
+
out[key] = coerced;
|
|
105
|
+
}
|
|
92
106
|
for (const [name] of Object.entries(negatedFlags)) {
|
|
93
107
|
out[name] = false;
|
|
94
108
|
const mainName = aliasToMain.get(name);
|
|
@@ -99,9 +113,12 @@ function parseRawArgs(args = [], opts = {}) {
|
|
|
99
113
|
for (const [alias, main] of aliasToMain.entries()) {
|
|
100
114
|
if (out[alias] !== void 0 && out[main] === void 0) out[main] = out[alias];
|
|
101
115
|
if (out[main] !== void 0 && out[alias] === void 0) out[alias] = out[main];
|
|
116
|
+
if (out[alias] !== out[main] && defaults[main] === out[main]) out[main] = out[alias];
|
|
102
117
|
}
|
|
103
118
|
return out;
|
|
104
119
|
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/_color.ts
|
|
105
122
|
const noColor = /* @__PURE__ */ (() => {
|
|
106
123
|
const env = globalThis.process?.env ?? {};
|
|
107
124
|
return env.NO_COLOR === "1" || env.TERM === "dumb" || env.TEST || env.CI;
|
|
@@ -111,6 +128,8 @@ const bold = /* @__PURE__ */ _c(1, 22);
|
|
|
111
128
|
const cyan = /* @__PURE__ */ _c(36);
|
|
112
129
|
const gray = /* @__PURE__ */ _c(90);
|
|
113
130
|
const underline = /* @__PURE__ */ _c(4, 24);
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/args.ts
|
|
114
133
|
function parseArgs(rawArgs, argsDef) {
|
|
115
134
|
const parseOptions = {
|
|
116
135
|
boolean: [],
|
|
@@ -160,6 +179,16 @@ function resolveArgs(argsDef) {
|
|
|
160
179
|
});
|
|
161
180
|
return args;
|
|
162
181
|
}
|
|
182
|
+
//#endregion
|
|
183
|
+
//#region src/plugin.ts
|
|
184
|
+
function defineCittyPlugin(plugin) {
|
|
185
|
+
return plugin;
|
|
186
|
+
}
|
|
187
|
+
async function resolvePlugins(plugins) {
|
|
188
|
+
return Promise.all(plugins.map((p) => resolveValue(p)));
|
|
189
|
+
}
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/command.ts
|
|
163
192
|
function defineCommand(def) {
|
|
164
193
|
return def;
|
|
165
194
|
}
|
|
@@ -172,35 +201,94 @@ async function runCommand(cmd, opts) {
|
|
|
172
201
|
data: opts.data,
|
|
173
202
|
cmd
|
|
174
203
|
};
|
|
175
|
-
|
|
204
|
+
const plugins = await resolvePlugins(cmd.plugins ?? []);
|
|
176
205
|
let result;
|
|
206
|
+
let runError;
|
|
177
207
|
try {
|
|
208
|
+
for (const plugin of plugins) await plugin.setup?.(context);
|
|
209
|
+
if (typeof cmd.setup === "function") await cmd.setup(context);
|
|
178
210
|
const subCommands = await resolveValue(cmd.subCommands);
|
|
179
211
|
if (subCommands && Object.keys(subCommands).length > 0) {
|
|
180
|
-
const subCommandArgIndex = opts.rawArgs
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
} else
|
|
212
|
+
const subCommandArgIndex = findSubCommandIndex(opts.rawArgs, cmdArgs);
|
|
213
|
+
const explicitName = opts.rawArgs[subCommandArgIndex];
|
|
214
|
+
if (explicitName) {
|
|
215
|
+
const subCommand = await _findSubCommand(subCommands, explicitName);
|
|
216
|
+
if (!subCommand) throw new CLIError(`Unknown command ${cyan(explicitName)}`, "E_UNKNOWN_COMMAND");
|
|
217
|
+
await runCommand(subCommand, { rawArgs: opts.rawArgs.slice(subCommandArgIndex + 1) });
|
|
218
|
+
} else {
|
|
219
|
+
const defaultSubCommand = await resolveValue(cmd.default);
|
|
220
|
+
if (defaultSubCommand) {
|
|
221
|
+
if (cmd.run) throw new CLIError(`Cannot specify both 'run' and 'default' on the same command.`, "E_DEFAULT_CONFLICT");
|
|
222
|
+
const subCommand = await _findSubCommand(subCommands, defaultSubCommand);
|
|
223
|
+
if (!subCommand) throw new CLIError(`Default sub command ${cyan(defaultSubCommand)} not found in subCommands.`, "E_UNKNOWN_COMMAND");
|
|
224
|
+
await runCommand(subCommand, { rawArgs: opts.rawArgs });
|
|
225
|
+
} else if (!cmd.run) throw new CLIError(`No command specified.`, "E_NO_COMMAND");
|
|
226
|
+
}
|
|
187
227
|
}
|
|
188
228
|
if (typeof cmd.run === "function") result = await cmd.run(context);
|
|
189
|
-
}
|
|
190
|
-
|
|
229
|
+
} catch (error) {
|
|
230
|
+
runError = error;
|
|
191
231
|
}
|
|
232
|
+
const cleanupErrors = [];
|
|
233
|
+
if (typeof cmd.cleanup === "function") try {
|
|
234
|
+
await cmd.cleanup(context);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
cleanupErrors.push(error);
|
|
237
|
+
}
|
|
238
|
+
for (const plugin of [...plugins].reverse()) try {
|
|
239
|
+
await plugin.cleanup?.(context);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
cleanupErrors.push(error);
|
|
242
|
+
}
|
|
243
|
+
if (runError) throw runError;
|
|
244
|
+
if (cleanupErrors.length === 1) throw cleanupErrors[0];
|
|
245
|
+
if (cleanupErrors.length > 1) throw new Error("Multiple cleanup errors", { cause: cleanupErrors });
|
|
192
246
|
return { result };
|
|
193
247
|
}
|
|
194
248
|
async function resolveSubCommand(cmd, rawArgs, parent) {
|
|
195
249
|
const subCommands = await resolveValue(cmd.subCommands);
|
|
196
250
|
if (subCommands && Object.keys(subCommands).length > 0) {
|
|
197
|
-
const subCommandArgIndex = rawArgs.
|
|
251
|
+
const subCommandArgIndex = findSubCommandIndex(rawArgs, await resolveValue(cmd.args || {}));
|
|
198
252
|
const subCommandName = rawArgs[subCommandArgIndex];
|
|
199
|
-
const subCommand = await
|
|
253
|
+
const subCommand = await _findSubCommand(subCommands, subCommandName);
|
|
200
254
|
if (subCommand) return resolveSubCommand(subCommand, rawArgs.slice(subCommandArgIndex + 1), cmd);
|
|
201
255
|
}
|
|
202
256
|
return [cmd, parent];
|
|
203
257
|
}
|
|
258
|
+
async function _findSubCommand(subCommands, name) {
|
|
259
|
+
if (name in subCommands) return resolveValue(subCommands[name]);
|
|
260
|
+
for (const sub of Object.values(subCommands)) {
|
|
261
|
+
const resolved = await resolveValue(sub);
|
|
262
|
+
const meta = await resolveValue(resolved?.meta);
|
|
263
|
+
if (meta?.alias) {
|
|
264
|
+
if (toArray(meta.alias).includes(name)) return resolved;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function findSubCommandIndex(rawArgs, argsDef) {
|
|
269
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
270
|
+
const arg = rawArgs[i];
|
|
271
|
+
if (arg === "--") return -1;
|
|
272
|
+
if (arg.startsWith("-")) {
|
|
273
|
+
if (!arg.includes("=") && _isValueFlag(arg, argsDef)) i++;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
return i;
|
|
277
|
+
}
|
|
278
|
+
return -1;
|
|
279
|
+
}
|
|
280
|
+
function _isValueFlag(flag, argsDef) {
|
|
281
|
+
const name = flag.replace(/^-{1,2}/, "");
|
|
282
|
+
const normalized = camelCase(name);
|
|
283
|
+
for (const [key, def] of Object.entries(argsDef)) {
|
|
284
|
+
if (def.type !== "string" && def.type !== "enum") continue;
|
|
285
|
+
if (normalized === camelCase(key)) return true;
|
|
286
|
+
if ((Array.isArray(def.alias) ? def.alias : def.alias ? [def.alias] : []).includes(name)) return true;
|
|
287
|
+
}
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
//#endregion
|
|
291
|
+
//#region src/usage.ts
|
|
204
292
|
async function showUsage(cmd, parent) {
|
|
205
293
|
try {
|
|
206
294
|
console.log(await renderUsage(cmd, parent) + "\n");
|
|
@@ -221,22 +309,22 @@ async function renderUsage(cmd, parent) {
|
|
|
221
309
|
for (const arg of cmdArgs) if (arg.type === "positional") {
|
|
222
310
|
const name = arg.name.toUpperCase();
|
|
223
311
|
const isRequired = arg.required !== false && arg.default === void 0;
|
|
224
|
-
|
|
225
|
-
posLines.push([
|
|
226
|
-
cyan(name + defaultHint),
|
|
227
|
-
arg.description || "",
|
|
228
|
-
arg.valueHint ? `<${arg.valueHint}>` : ""
|
|
229
|
-
]);
|
|
312
|
+
posLines.push([cyan(name + renderValueHint(arg)), renderDescription(arg, isRequired)]);
|
|
230
313
|
usageLine.push(isRequired ? `<${name}>` : `[${name}]`);
|
|
231
314
|
} else {
|
|
232
315
|
const isRequired = arg.required === true && arg.default === void 0;
|
|
233
|
-
const argStr = [...(arg.alias || []).map((a) => `-${a}`), `--${arg.name}`].join(", ") + (arg
|
|
234
|
-
argLines.push([cyan(argStr
|
|
316
|
+
const argStr = [...(arg.alias || []).map((a) => `-${a}`), `--${arg.name}`].join(", ") + renderValueHint(arg);
|
|
317
|
+
argLines.push([cyan(argStr), renderDescription(arg, isRequired)]);
|
|
318
|
+
/**
|
|
319
|
+
* print negative boolean arg variant usage when
|
|
320
|
+
* - enabled by default or has `negativeDescription`
|
|
321
|
+
* - not prefixed with `no-` or `no[A-Z]`
|
|
322
|
+
*/
|
|
235
323
|
if (arg.type === "boolean" && (arg.default === true || arg.negativeDescription) && !negativePrefixRe.test(arg.name)) {
|
|
236
324
|
const negativeArgStr = [...(arg.alias || []).map((a) => `--no-${a}`), `--no-${arg.name}`].join(", ");
|
|
237
|
-
argLines.push([cyan(negativeArgStr
|
|
325
|
+
argLines.push([cyan(negativeArgStr), [arg.negativeDescription, isRequired ? gray("(Required)") : ""].filter(Boolean).join(" ")]);
|
|
238
326
|
}
|
|
239
|
-
if (isRequired) usageLine.push(
|
|
327
|
+
if (isRequired) usageLine.push(`--${arg.name}` + renderValueHint(arg));
|
|
240
328
|
}
|
|
241
329
|
if (cmd.subCommands) {
|
|
242
330
|
const commandNames = [];
|
|
@@ -244,8 +332,10 @@ async function renderUsage(cmd, parent) {
|
|
|
244
332
|
for (const [name, sub] of Object.entries(subCommands)) {
|
|
245
333
|
const meta = await resolveValue((await resolveValue(sub))?.meta);
|
|
246
334
|
if (meta?.hidden) continue;
|
|
247
|
-
|
|
248
|
-
|
|
335
|
+
const aliases = toArray(meta?.alias);
|
|
336
|
+
const label = [name, ...aliases].join(", ");
|
|
337
|
+
commandsLines.push([cyan(label), meta?.description || ""]);
|
|
338
|
+
commandNames.push(name, ...aliases);
|
|
249
339
|
}
|
|
250
340
|
usageLine.push(commandNames.join("|"));
|
|
251
341
|
}
|
|
@@ -271,14 +361,33 @@ async function renderUsage(cmd, parent) {
|
|
|
271
361
|
}
|
|
272
362
|
return usageLines.filter((l) => typeof l === "string").join("\n");
|
|
273
363
|
}
|
|
364
|
+
function renderValueHint(arg) {
|
|
365
|
+
const valueHint = arg.valueHint ? `=<${arg.valueHint}>` : "";
|
|
366
|
+
const fallbackValueHint = valueHint || `=<${snakeCase(arg.name)}>`;
|
|
367
|
+
if (!arg.type || arg.type === "positional" || arg.type === "boolean") return valueHint;
|
|
368
|
+
if (arg.type === "enum" && arg.options?.length) return `=<${arg.options.join("|")}>`;
|
|
369
|
+
return fallbackValueHint;
|
|
370
|
+
}
|
|
371
|
+
function renderDescription(arg, required) {
|
|
372
|
+
const requiredHint = required ? gray("(Required)") : "";
|
|
373
|
+
const defaultHint = arg.default === void 0 ? "" : gray(`(Default: ${arg.default})`);
|
|
374
|
+
return [
|
|
375
|
+
arg.description,
|
|
376
|
+
requiredHint,
|
|
377
|
+
defaultHint
|
|
378
|
+
].filter(Boolean).join(" ");
|
|
379
|
+
}
|
|
380
|
+
//#endregion
|
|
381
|
+
//#region src/main.ts
|
|
274
382
|
async function runMain(cmd, opts = {}) {
|
|
275
383
|
const rawArgs = opts.rawArgs || process.argv.slice(2);
|
|
276
384
|
const showUsage$1 = opts.showUsage || showUsage;
|
|
277
385
|
try {
|
|
278
|
-
|
|
386
|
+
const builtinFlags = await _resolveBuiltinFlags(cmd);
|
|
387
|
+
if (builtinFlags.help.length > 0 && rawArgs.some((arg) => builtinFlags.help.includes(arg))) {
|
|
279
388
|
await showUsage$1(...await resolveSubCommand(cmd, rawArgs));
|
|
280
389
|
process.exit(0);
|
|
281
|
-
} else if (rawArgs.length === 1 && rawArgs[0]
|
|
390
|
+
} else if (rawArgs.length === 1 && builtinFlags.version.includes(rawArgs[0])) {
|
|
282
391
|
const meta = typeof cmd.meta === "function" ? await cmd.meta() : await cmd.meta;
|
|
283
392
|
if (!meta?.version) throw new CLIError("No version specified", "E_NO_VERSION");
|
|
284
393
|
console.log(meta.version);
|
|
@@ -294,4 +403,23 @@ async function runMain(cmd, opts = {}) {
|
|
|
294
403
|
function createMain(cmd) {
|
|
295
404
|
return (opts = {}) => runMain(cmd, opts);
|
|
296
405
|
}
|
|
297
|
-
|
|
406
|
+
async function _resolveBuiltinFlags(cmd) {
|
|
407
|
+
const argsDef = await resolveValue(cmd.args || {});
|
|
408
|
+
const userNames = /* @__PURE__ */ new Set();
|
|
409
|
+
const userAliases = /* @__PURE__ */ new Set();
|
|
410
|
+
for (const [name, def] of Object.entries(argsDef)) {
|
|
411
|
+
userNames.add(name);
|
|
412
|
+
for (const alias of toArray(def.alias)) userAliases.add(alias);
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
help: _getBuiltinFlags("help", "h", userNames, userAliases),
|
|
416
|
+
version: _getBuiltinFlags("version", "v", userNames, userAliases)
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function _getBuiltinFlags(long, short, userNames, userAliases) {
|
|
420
|
+
if (userNames.has(long) || userAliases.has(long)) return [];
|
|
421
|
+
if (userNames.has(short) || userAliases.has(short)) return [`--${long}`];
|
|
422
|
+
return [`--${long}`, `-${short}`];
|
|
423
|
+
}
|
|
424
|
+
//#endregion
|
|
425
|
+
export { createMain, defineCittyPlugin, defineCommand, parseArgs, renderUsage, runCommand, runMain, showUsage };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "citty",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Elegant CLI Builder",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": "unjs/citty",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"build": "obuild",
|
|
18
18
|
"dev": "vitest dev",
|
|
19
19
|
"lint": "oxlint . && oxfmt --check",
|
|
20
|
-
"
|
|
20
|
+
"fmt": "oxlint . --fix && oxfmt",
|
|
21
21
|
"prepack": "pnpm run build",
|
|
22
22
|
"play": "node ./playground/cli.ts",
|
|
23
23
|
"release": "pnpm test && pnpm build && changelogen --release --push && npm publish",
|
|
@@ -25,18 +25,18 @@
|
|
|
25
25
|
"test:types": "tsgo --noEmit"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@types/node": "^25.
|
|
29
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
30
|
-
"@vitest/coverage-v8": "^4.
|
|
28
|
+
"@types/node": "^25.5.0",
|
|
29
|
+
"@typescript/native-preview": "^7.0.0-dev.20260401.1",
|
|
30
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
31
31
|
"automd": "^0.4.3",
|
|
32
32
|
"changelogen": "^0.6.2",
|
|
33
33
|
"eslint-config-unjs": "^0.6.2",
|
|
34
|
-
"obuild": "^0.4.
|
|
35
|
-
"oxfmt": "^0.
|
|
36
|
-
"oxlint": "^1.
|
|
34
|
+
"obuild": "^0.4.32",
|
|
35
|
+
"oxfmt": "^0.43.0",
|
|
36
|
+
"oxlint": "^1.58.0",
|
|
37
37
|
"scule": "^1.3.0",
|
|
38
|
-
"typescript": "^
|
|
39
|
-
"vitest": "^4.
|
|
38
|
+
"typescript": "^6.0.2",
|
|
39
|
+
"vitest": "^4.1.2"
|
|
40
40
|
},
|
|
41
|
-
"packageManager": "pnpm@10.
|
|
41
|
+
"packageManager": "pnpm@10.33.0"
|
|
42
42
|
}
|