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 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
- - Lazy and Async commands
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
- ## Utils
58
+ ```sh
59
+ node index.mjs john
60
+ # Greetings john!
61
+ ```
66
62
 
67
- ### `defineCommand`
63
+ ### Sub Commands
68
64
 
69
- `defineCommand` is a type helper for defining commands.
65
+ Sub commands can be nested recursively. Use lazy imports for large CLIs to avoid loading all commands at once.
70
66
 
71
- ### `runMain`
67
+ ```js
68
+ import { defineCommand, runMain } from "citty";
72
69
 
73
- Runs a command with usage support and graceful error handling.
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
- ### `createMain`
80
+ const main = defineCommand({
81
+ meta: { name: "hello", version: "1.0.0", description: "My Awesome CLI App" },
82
+ subCommands: { sub },
83
+ });
76
84
 
77
- Create a wrapper around command that calls `runMain` when called.
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
- ### `runCommand`
196
+ ### Boolean Negation
80
197
 
81
- Parses input args and runs command and sub-commands (unsupervised). You can access `result` key from returnd/awaited value to access command's result.
198
+ Boolean args support `--no-` prefix. The negative variant appears in help when `default: true` or `negativeDescription` is set.
82
199
 
83
- ### `parseArgs`
200
+ ### Case-Agnostic Access
84
201
 
85
- Parses input arguments and applies defaults.
202
+ Kebab-case args can be accessed as camelCase: `args["output-dir"]` and `args.outputDir` both work.
86
203
 
87
- ### `renderUsage`
204
+ ## Built-in Flags
88
205
 
89
- Renders command usage to a string value.
206
+ `--help` / `-h` and `--version` / `-v` are handled automatically. Disabled if your command defines args with the same names or aliases.
90
207
 
91
- ### `showUsage`
208
+ ## API
92
209
 
93
- Renders usage and prints to the console
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
- export { Arg, ArgDef, ArgType, ArgsDef, Awaitable, BooleanArgDef, CommandContext, CommandDef, CommandMeta, EnumArgDef, ParsedArgs, PositionalArgDef, Resolvable, type RunCommandOptions, type RunMainOptions, StringArgDef, SubCommandsDef, _ArgDef, createMain, defineCommand, parseArgs, renderUsage, runCommand, runMain, showUsage };
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)) out[key] = value;
97
- for (const [name] of Object.entries(negatedFlags)) out[name] = false;
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
- if (typeof cmd.setup === "function") await cmd.setup(context);
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.findIndex((arg) => !arg.startsWith("-"));
189
- const subCommandName = opts.rawArgs[subCommandArgIndex];
190
- if (subCommandName) {
191
- if (!subCommands[subCommandName]) throw new CLIError(`Unknown command ${cyan(subCommandName)}`, "E_UNKNOWN_COMMAND");
192
- const subCommand = await resolveValue(subCommands[subCommandName]);
193
- if (subCommand) await runCommand(subCommand, { rawArgs: opts.rawArgs.slice(subCommandArgIndex + 1) });
194
- } else if (!cmd.run) throw new CLIError(`No command specified.`, "E_NO_COMMAND");
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
- } finally {
198
- if (typeof cmd.cleanup === "function") await cmd.cleanup(context);
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.findIndex((arg) => !arg.startsWith("-"));
251
+ const subCommandArgIndex = findSubCommandIndex(rawArgs, await resolveValue(cmd.args || {}));
206
252
  const subCommandName = rawArgs[subCommandArgIndex];
207
- const subCommand = await resolveValue(subCommands[subCommandName]);
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
- const defaultHint = arg.default ? `="${arg.default}"` : "";
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.type === "string" && (arg.valueHint || arg.default) ? `=${arg.valueHint ? `<${arg.valueHint}>` : `"${arg.default || ""}"`}` : "") + (arg.type === "enum" && arg.options ? `=<${arg.options.join("|")}>` : "");
245
- argLines.push([cyan(argStr + (isRequired ? " (required)" : "")), arg.description || ""]);
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 + (isRequired ? " (required)" : "")), arg.negativeDescription || ""]);
325
+ argLines.push([cyan(negativeArgStr), [arg.negativeDescription, isRequired ? gray("(Required)") : ""].filter(Boolean).join(" ")]);
254
326
  }
255
- if (isRequired) usageLine.push(argStr);
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
- commandsLines.push([cyan(name), meta?.description || ""]);
264
- commandNames.push(name);
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
- if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
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] === "--version") {
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.0",
3
+ "version": "0.2.2",
4
4
  "description": "Elegant CLI Builder",
5
- "repository": "unjs/citty",
6
5
  "license": "MIT",
7
- "sideEffects": false,
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": "eslint --cache . && prettier -c src test",
20
- "lint:fix": "eslint --cache . --fix && prettier -c src test -w",
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": "tsc --noEmit"
25
+ "test:types": "tsgo --noEmit"
26
26
  },
27
27
  "devDependencies": {
28
- "@types/node": "^25.0.9",
29
- "@vitest/coverage-v8": "^4.0.17",
30
- "automd": "^0.4.2",
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.18",
35
- "prettier": "^3.8.0",
34
+ "obuild": "^0.4.32",
35
+ "oxfmt": "^0.43.0",
36
+ "oxlint": "^1.58.0",
36
37
  "scule": "^1.3.0",
37
- "typescript": "^5.9.3",
38
- "vitest": "^4.0.17"
38
+ "typescript": "^6.0.2",
39
+ "vitest": "^4.1.2"
39
40
  },
40
- "packageManager": "pnpm@10.28.1"
41
+ "packageManager": "pnpm@10.33.0"
41
42
  }