citty 0.2.0 → 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/THIRD-PARTY-LICENSES.md +33 -0
- package/dist/_chunks/libs/scule.mjs +4 -2
- package/dist/index.d.mts +12 -1
- package/dist/index.mjs +145 -38
- package/package.json +20 -19
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 utils](arg parser](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
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Licenses of Bundled Dependencies
|
|
2
|
+
|
|
3
|
+
The published artifact additionally contains code with the following licenses:
|
|
4
|
+
MIT
|
|
5
|
+
|
|
6
|
+
# Bundled Dependencies
|
|
7
|
+
|
|
8
|
+
## scule
|
|
9
|
+
|
|
10
|
+
License: MIT
|
|
11
|
+
Repository: https://github.com/unjs/scule
|
|
12
|
+
|
|
13
|
+
> MIT License
|
|
14
|
+
>
|
|
15
|
+
> Copyright (c) Pooya Parsa <pooya@pi0.io>
|
|
16
|
+
>
|
|
17
|
+
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
18
|
+
> of this software and associated documentation files (the "Software"), to deal
|
|
19
|
+
> in the Software without restriction, including without limitation the rights
|
|
20
|
+
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
21
|
+
> copies of the Software, and to permit persons to whom the Software is
|
|
22
|
+
> furnished to do so, subject to the following conditions:
|
|
23
|
+
>
|
|
24
|
+
> The above copyright notice and this permission notice shall be included in all
|
|
25
|
+
> copies or substantial portions of the Software.
|
|
26
|
+
>
|
|
27
|
+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
28
|
+
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
29
|
+
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
30
|
+
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
31
|
+
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
32
|
+
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
33
|
+
> SOFTWARE.
|
|
@@ -63,6 +63,8 @@ function camelCase(str, opts) {
|
|
|
63
63
|
function kebabCase(str, joiner) {
|
|
64
64
|
return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => p.toLowerCase()).join(joiner ?? "-") : "";
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
function snakeCase(str) {
|
|
67
|
+
return kebabCase(str || "", "_");
|
|
68
|
+
}
|
|
67
69
|
//#endregion
|
|
68
|
-
export { kebabCase as n, camelCase as t };
|
|
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,6 +1,5 @@
|
|
|
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
|
-
|
|
4
3
|
//#region src/_utils.ts
|
|
5
4
|
function toArray(val) {
|
|
6
5
|
if (Array.isArray(val)) return val;
|
|
@@ -22,7 +21,6 @@ var CLIError = class extends Error {
|
|
|
22
21
|
this.code = code;
|
|
23
22
|
}
|
|
24
23
|
};
|
|
25
|
-
|
|
26
24
|
//#endregion
|
|
27
25
|
//#region src/_parser.ts
|
|
28
26
|
function parseRawArgs(args = [], opts = {}) {
|
|
@@ -50,6 +48,12 @@ function parseRawArgs(args = [], opts = {}) {
|
|
|
50
48
|
for (const alias of aliases) if (booleans.has(alias)) return "boolean";
|
|
51
49
|
return "string";
|
|
52
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
|
+
}
|
|
53
57
|
const allOptions = new Set([
|
|
54
58
|
...booleans,
|
|
55
59
|
...strings,
|
|
@@ -93,15 +97,26 @@ function parseRawArgs(args = [], opts = {}) {
|
|
|
93
97
|
}
|
|
94
98
|
const out = { _: [] };
|
|
95
99
|
out._ = parsed.positionals;
|
|
96
|
-
for (const [key, value] of Object.entries(parsed.values))
|
|
97
|
-
|
|
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
|
+
}
|
|
106
|
+
for (const [name] of Object.entries(negatedFlags)) {
|
|
107
|
+
out[name] = false;
|
|
108
|
+
const mainName = aliasToMain.get(name);
|
|
109
|
+
if (mainName) out[mainName] = false;
|
|
110
|
+
const aliases = mainToAliases.get(name);
|
|
111
|
+
if (aliases) for (const alias of aliases) out[alias] = false;
|
|
112
|
+
}
|
|
98
113
|
for (const [alias, main] of aliasToMain.entries()) {
|
|
99
114
|
if (out[alias] !== void 0 && out[main] === void 0) out[main] = out[alias];
|
|
100
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];
|
|
101
117
|
}
|
|
102
118
|
return out;
|
|
103
119
|
}
|
|
104
|
-
|
|
105
120
|
//#endregion
|
|
106
121
|
//#region src/_color.ts
|
|
107
122
|
const noColor = /* @__PURE__ */ (() => {
|
|
@@ -113,7 +128,6 @@ const bold = /* @__PURE__ */ _c(1, 22);
|
|
|
113
128
|
const cyan = /* @__PURE__ */ _c(36);
|
|
114
129
|
const gray = /* @__PURE__ */ _c(90);
|
|
115
130
|
const underline = /* @__PURE__ */ _c(4, 24);
|
|
116
|
-
|
|
117
131
|
//#endregion
|
|
118
132
|
//#region src/args.ts
|
|
119
133
|
function parseArgs(rawArgs, argsDef) {
|
|
@@ -165,7 +179,14 @@ function resolveArgs(argsDef) {
|
|
|
165
179
|
});
|
|
166
180
|
return args;
|
|
167
181
|
}
|
|
168
|
-
|
|
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
|
+
}
|
|
169
190
|
//#endregion
|
|
170
191
|
//#region src/command.ts
|
|
171
192
|
function defineCommand(def) {
|
|
@@ -180,36 +201,92 @@ async function runCommand(cmd, opts) {
|
|
|
180
201
|
data: opts.data,
|
|
181
202
|
cmd
|
|
182
203
|
};
|
|
183
|
-
|
|
204
|
+
const plugins = await resolvePlugins(cmd.plugins ?? []);
|
|
184
205
|
let result;
|
|
206
|
+
let runError;
|
|
185
207
|
try {
|
|
208
|
+
for (const plugin of plugins) await plugin.setup?.(context);
|
|
209
|
+
if (typeof cmd.setup === "function") await cmd.setup(context);
|
|
186
210
|
const subCommands = await resolveValue(cmd.subCommands);
|
|
187
211
|
if (subCommands && Object.keys(subCommands).length > 0) {
|
|
188
|
-
const subCommandArgIndex = opts.rawArgs
|
|
189
|
-
const
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
} 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
|
+
}
|
|
195
227
|
}
|
|
196
228
|
if (typeof cmd.run === "function") result = await cmd.run(context);
|
|
197
|
-
}
|
|
198
|
-
|
|
229
|
+
} catch (error) {
|
|
230
|
+
runError = error;
|
|
231
|
+
}
|
|
232
|
+
const cleanupErrors = [];
|
|
233
|
+
if (typeof cmd.cleanup === "function") try {
|
|
234
|
+
await cmd.cleanup(context);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
cleanupErrors.push(error);
|
|
199
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 });
|
|
200
246
|
return { result };
|
|
201
247
|
}
|
|
202
248
|
async function resolveSubCommand(cmd, rawArgs, parent) {
|
|
203
249
|
const subCommands = await resolveValue(cmd.subCommands);
|
|
204
250
|
if (subCommands && Object.keys(subCommands).length > 0) {
|
|
205
|
-
const subCommandArgIndex = rawArgs.
|
|
251
|
+
const subCommandArgIndex = findSubCommandIndex(rawArgs, await resolveValue(cmd.args || {}));
|
|
206
252
|
const subCommandName = rawArgs[subCommandArgIndex];
|
|
207
|
-
const subCommand = await
|
|
253
|
+
const subCommand = await _findSubCommand(subCommands, subCommandName);
|
|
208
254
|
if (subCommand) return resolveSubCommand(subCommand, rawArgs.slice(subCommandArgIndex + 1), cmd);
|
|
209
255
|
}
|
|
210
256
|
return [cmd, parent];
|
|
211
257
|
}
|
|
212
|
-
|
|
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
|
+
}
|
|
213
290
|
//#endregion
|
|
214
291
|
//#region src/usage.ts
|
|
215
292
|
async function showUsage(cmd, parent) {
|
|
@@ -232,17 +309,12 @@ async function renderUsage(cmd, parent) {
|
|
|
232
309
|
for (const arg of cmdArgs) if (arg.type === "positional") {
|
|
233
310
|
const name = arg.name.toUpperCase();
|
|
234
311
|
const isRequired = arg.required !== false && arg.default === void 0;
|
|
235
|
-
|
|
236
|
-
posLines.push([
|
|
237
|
-
cyan(name + defaultHint),
|
|
238
|
-
arg.description || "",
|
|
239
|
-
arg.valueHint ? `<${arg.valueHint}>` : ""
|
|
240
|
-
]);
|
|
312
|
+
posLines.push([cyan(name + renderValueHint(arg)), renderDescription(arg, isRequired)]);
|
|
241
313
|
usageLine.push(isRequired ? `<${name}>` : `[${name}]`);
|
|
242
314
|
} else {
|
|
243
315
|
const isRequired = arg.required === true && arg.default === void 0;
|
|
244
|
-
const argStr = [...(arg.alias || []).map((a) => `-${a}`), `--${arg.name}`].join(", ") + (arg
|
|
245
|
-
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)]);
|
|
246
318
|
/**
|
|
247
319
|
* print negative boolean arg variant usage when
|
|
248
320
|
* - enabled by default or has `negativeDescription`
|
|
@@ -250,9 +322,9 @@ async function renderUsage(cmd, parent) {
|
|
|
250
322
|
*/
|
|
251
323
|
if (arg.type === "boolean" && (arg.default === true || arg.negativeDescription) && !negativePrefixRe.test(arg.name)) {
|
|
252
324
|
const negativeArgStr = [...(arg.alias || []).map((a) => `--no-${a}`), `--no-${arg.name}`].join(", ");
|
|
253
|
-
argLines.push([cyan(negativeArgStr
|
|
325
|
+
argLines.push([cyan(negativeArgStr), [arg.negativeDescription, isRequired ? gray("(Required)") : ""].filter(Boolean).join(" ")]);
|
|
254
326
|
}
|
|
255
|
-
if (isRequired) usageLine.push(
|
|
327
|
+
if (isRequired) usageLine.push(`--${arg.name}` + renderValueHint(arg));
|
|
256
328
|
}
|
|
257
329
|
if (cmd.subCommands) {
|
|
258
330
|
const commandNames = [];
|
|
@@ -260,8 +332,10 @@ async function renderUsage(cmd, parent) {
|
|
|
260
332
|
for (const [name, sub] of Object.entries(subCommands)) {
|
|
261
333
|
const meta = await resolveValue((await resolveValue(sub))?.meta);
|
|
262
334
|
if (meta?.hidden) continue;
|
|
263
|
-
|
|
264
|
-
|
|
335
|
+
const aliases = toArray(meta?.alias);
|
|
336
|
+
const label = [name, ...aliases].join(", ");
|
|
337
|
+
commandsLines.push([cyan(label), meta?.description || ""]);
|
|
338
|
+
commandNames.push(name, ...aliases);
|
|
265
339
|
}
|
|
266
340
|
usageLine.push(commandNames.join("|"));
|
|
267
341
|
}
|
|
@@ -287,17 +361,33 @@ async function renderUsage(cmd, parent) {
|
|
|
287
361
|
}
|
|
288
362
|
return usageLines.filter((l) => typeof l === "string").join("\n");
|
|
289
363
|
}
|
|
290
|
-
|
|
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
|
+
}
|
|
291
380
|
//#endregion
|
|
292
381
|
//#region src/main.ts
|
|
293
382
|
async function runMain(cmd, opts = {}) {
|
|
294
383
|
const rawArgs = opts.rawArgs || process.argv.slice(2);
|
|
295
384
|
const showUsage$1 = opts.showUsage || showUsage;
|
|
296
385
|
try {
|
|
297
|
-
|
|
386
|
+
const builtinFlags = await _resolveBuiltinFlags(cmd);
|
|
387
|
+
if (builtinFlags.help.length > 0 && rawArgs.some((arg) => builtinFlags.help.includes(arg))) {
|
|
298
388
|
await showUsage$1(...await resolveSubCommand(cmd, rawArgs));
|
|
299
389
|
process.exit(0);
|
|
300
|
-
} else if (rawArgs.length === 1 && rawArgs[0]
|
|
390
|
+
} else if (rawArgs.length === 1 && builtinFlags.version.includes(rawArgs[0])) {
|
|
301
391
|
const meta = typeof cmd.meta === "function" ? await cmd.meta() : await cmd.meta;
|
|
302
392
|
if (!meta?.version) throw new CLIError("No version specified", "E_NO_VERSION");
|
|
303
393
|
console.log(meta.version);
|
|
@@ -313,6 +403,23 @@ async function runMain(cmd, opts = {}) {
|
|
|
313
403
|
function createMain(cmd) {
|
|
314
404
|
return (opts = {}) => runMain(cmd, opts);
|
|
315
405
|
}
|
|
316
|
-
|
|
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
|
+
}
|
|
317
424
|
//#endregion
|
|
318
|
-
export { createMain, defineCommand, parseArgs, renderUsage, runCommand, runMain, showUsage };
|
|
425
|
+
export { createMain, defineCittyPlugin, defineCommand, parseArgs, renderUsage, runCommand, runMain, showUsage };
|
package/package.json
CHANGED
|
@@ -1,41 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "citty",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Elegant CLI Builder",
|
|
5
|
-
"repository": "unjs/citty",
|
|
6
5
|
"license": "MIT",
|
|
7
|
-
"
|
|
6
|
+
"repository": "unjs/citty",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
8
10
|
"type": "module",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"types": "./dist/index.d.mts",
|
|
9
13
|
"exports": {
|
|
10
14
|
".": "./dist/index.mjs"
|
|
11
15
|
},
|
|
12
|
-
"types": "./dist/index.d.mts",
|
|
13
|
-
"files": [
|
|
14
|
-
"dist"
|
|
15
|
-
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "obuild",
|
|
18
18
|
"dev": "vitest dev",
|
|
19
|
-
"lint": "
|
|
20
|
-
"
|
|
19
|
+
"lint": "oxlint . && oxfmt --check",
|
|
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",
|
|
24
24
|
"test": "pnpm lint && pnpm test:types && vitest run --coverage",
|
|
25
|
-
"test:types": "
|
|
25
|
+
"test:types": "tsgo --noEmit"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@types/node": "^25.0
|
|
29
|
-
"@
|
|
30
|
-
"
|
|
28
|
+
"@types/node": "^25.5.0",
|
|
29
|
+
"@typescript/native-preview": "^7.0.0-dev.20260401.1",
|
|
30
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
31
|
+
"automd": "^0.4.3",
|
|
31
32
|
"changelogen": "^0.6.2",
|
|
32
|
-
"eslint": "^9.39.2",
|
|
33
33
|
"eslint-config-unjs": "^0.6.2",
|
|
34
|
-
"obuild": "^0.4.
|
|
35
|
-
"
|
|
34
|
+
"obuild": "^0.4.32",
|
|
35
|
+
"oxfmt": "^0.43.0",
|
|
36
|
+
"oxlint": "^1.58.0",
|
|
36
37
|
"scule": "^1.3.0",
|
|
37
|
-
"typescript": "^
|
|
38
|
-
"vitest": "^4.
|
|
38
|
+
"typescript": "^6.0.2",
|
|
39
|
+
"vitest": "^4.1.2"
|
|
39
40
|
},
|
|
40
|
-
"packageManager": "pnpm@10.
|
|
41
|
+
"packageManager": "pnpm@10.33.0"
|
|
41
42
|
}
|