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 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
- - 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
 
@@ -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
- export { kebabCase as n, camelCase as t };
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
- 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,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)) out[key] = value;
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
- if (typeof cmd.setup === "function") await cmd.setup(context);
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.findIndex((arg) => !arg.startsWith("-"));
181
- const subCommandName = opts.rawArgs[subCommandArgIndex];
182
- if (subCommandName) {
183
- if (!subCommands[subCommandName]) throw new CLIError(`Unknown command ${cyan(subCommandName)}`, "E_UNKNOWN_COMMAND");
184
- const subCommand = await resolveValue(subCommands[subCommandName]);
185
- if (subCommand) await runCommand(subCommand, { rawArgs: opts.rawArgs.slice(subCommandArgIndex + 1) });
186
- } 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
+ }
187
227
  }
188
228
  if (typeof cmd.run === "function") result = await cmd.run(context);
189
- } finally {
190
- if (typeof cmd.cleanup === "function") await cmd.cleanup(context);
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.findIndex((arg) => !arg.startsWith("-"));
251
+ const subCommandArgIndex = findSubCommandIndex(rawArgs, await resolveValue(cmd.args || {}));
198
252
  const subCommandName = rawArgs[subCommandArgIndex];
199
- const subCommand = await resolveValue(subCommands[subCommandName]);
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
- const defaultHint = arg.default ? `="${arg.default}"` : "";
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.type === "string" && (arg.valueHint || arg.default) ? `=${arg.valueHint ? `<${arg.valueHint}>` : `"${arg.default || ""}"`}` : "") + (arg.type === "enum" && arg.options ? `=<${arg.options.join("|")}>` : "");
234
- 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)]);
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 + (isRequired ? " (required)" : "")), arg.negativeDescription || ""]);
325
+ argLines.push([cyan(negativeArgStr), [arg.negativeDescription, isRequired ? gray("(Required)") : ""].filter(Boolean).join(" ")]);
238
326
  }
239
- if (isRequired) usageLine.push(argStr);
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
- commandsLines.push([cyan(name), meta?.description || ""]);
248
- 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);
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
- 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))) {
279
388
  await showUsage$1(...await resolveSubCommand(cmd, rawArgs));
280
389
  process.exit(0);
281
- } else if (rawArgs.length === 1 && rawArgs[0] === "--version") {
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
- export { createMain, defineCommand, parseArgs, renderUsage, runCommand, runMain, showUsage };
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.1",
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
- "lint:fix": "oxlint . --fix && oxfmt",
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.2.3",
29
- "@typescript/native-preview": "7.0.0-dev.20260212.1",
30
- "@vitest/coverage-v8": "^4.0.18",
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.27",
35
- "oxfmt": "^0.32.0",
36
- "oxlint": "^1.47.0",
34
+ "obuild": "^0.4.32",
35
+ "oxfmt": "^0.43.0",
36
+ "oxlint": "^1.58.0",
37
37
  "scule": "^1.3.0",
38
- "typescript": "^5.9.3",
39
- "vitest": "^4.0.18"
38
+ "typescript": "^6.0.2",
39
+ "vitest": "^4.1.2"
40
40
  },
41
- "packageManager": "pnpm@10.29.3"
41
+ "packageManager": "pnpm@10.33.0"
42
42
  }