routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Pooya Parsa <pooya@pi0.io>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
# 🌆 citty
|
||||
|
||||
<!-- automd:badges color=yellow bundlephobia -->
|
||||
|
||||
[](https://npmjs.com/package/citty)
|
||||
[](https://npmjs.com/package/citty)
|
||||
[](https://bundlephobia.com/package/citty)
|
||||
|
||||
<!-- /automd -->
|
||||
|
||||
Elegant CLI Builder
|
||||
|
||||
- Zero dependency, fast and lightweight (based on native [`util.parseArgs`](https://nodejs.org/api/util.html#utilparseargsconfig))
|
||||
- Smart value parsing with typecast and boolean shortcuts
|
||||
- Nested sub-commands with lazy and async loading
|
||||
- Pluggable and composable API with auto generated usage
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
npx nypm add -D citty
|
||||
```
|
||||
|
||||
```js
|
||||
import { defineCommand, runMain } from "citty";
|
||||
|
||||
const main = defineCommand({
|
||||
meta: {
|
||||
name: "hello",
|
||||
version: "1.0.0",
|
||||
description: "My Awesome CLI App",
|
||||
},
|
||||
args: {
|
||||
name: {
|
||||
type: "positional",
|
||||
description: "Your name",
|
||||
required: true,
|
||||
},
|
||||
friendly: {
|
||||
type: "boolean",
|
||||
description: "Use friendly greeting",
|
||||
},
|
||||
},
|
||||
setup({ args }) {
|
||||
console.log(`now setup ${args.command}`);
|
||||
},
|
||||
cleanup({ args }) {
|
||||
console.log(`now cleanup ${args.command}`);
|
||||
},
|
||||
run({ args }) {
|
||||
console.log(`${args.friendly ? "Hi" : "Greetings"} ${args.name}!`);
|
||||
},
|
||||
});
|
||||
|
||||
runMain(main);
|
||||
```
|
||||
|
||||
```sh
|
||||
node index.mjs john
|
||||
# Greetings john!
|
||||
```
|
||||
|
||||
### Sub Commands
|
||||
|
||||
Sub commands can be nested recursively. Use lazy imports for large CLIs to avoid loading all commands at once.
|
||||
|
||||
```js
|
||||
import { defineCommand, runMain } from "citty";
|
||||
|
||||
const sub = defineCommand({
|
||||
meta: { name: "sub", description: "Sub command" },
|
||||
args: {
|
||||
name: { type: "positional", description: "Your name", required: true },
|
||||
},
|
||||
run({ args }) {
|
||||
console.log(`Hello ${args.name}!`);
|
||||
},
|
||||
});
|
||||
|
||||
const main = defineCommand({
|
||||
meta: { name: "hello", version: "1.0.0", description: "My Awesome CLI App" },
|
||||
subCommands: { sub },
|
||||
});
|
||||
|
||||
runMain(main);
|
||||
```
|
||||
|
||||
Subcommands support `meta.alias` (e.g., `["i", "add"]`) and `meta.hidden: true` to hide from help output.
|
||||
|
||||
### Lazy Commands
|
||||
|
||||
For large CLIs, lazy load sub commands so only the executed command is imported:
|
||||
|
||||
```js
|
||||
const main = defineCommand({
|
||||
meta: { name: "hello", version: "1.0.0", description: "My Awesome CLI App" },
|
||||
subCommands: {
|
||||
sub: () => import("./sub.mjs").then((m) => m.default),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`meta`, `args`, and `subCommands` all accept `Resolvable<T>` values — a value, Promise, function, or async function — enabling lazy and dynamic resolution.
|
||||
|
||||
### Hooks
|
||||
|
||||
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.
|
||||
|
||||
```js
|
||||
const main = defineCommand({
|
||||
meta: { name: "hello", version: "1.0.0", description: "My Awesome CLI App" },
|
||||
setup() {
|
||||
console.log("Setting up...");
|
||||
},
|
||||
cleanup() {
|
||||
console.log("Cleaning up...");
|
||||
},
|
||||
run() {
|
||||
console.log("Hello World!");
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Plugins
|
||||
|
||||
Plugins extend commands with reusable `setup` and `cleanup` hooks:
|
||||
|
||||
```js
|
||||
import { defineCommand, defineCittyPlugin, runMain } from "citty";
|
||||
|
||||
const logger = defineCittyPlugin({
|
||||
name: "logger",
|
||||
setup({ args }) {
|
||||
console.log("Logger setup, args:", args);
|
||||
},
|
||||
cleanup() {
|
||||
console.log("Logger cleanup");
|
||||
},
|
||||
});
|
||||
|
||||
const main = defineCommand({
|
||||
meta: { name: "hello", description: "My CLI App" },
|
||||
plugins: [logger],
|
||||
run() {
|
||||
console.log("Hello!");
|
||||
},
|
||||
});
|
||||
|
||||
runMain(main);
|
||||
```
|
||||
|
||||
Plugin `setup` hooks run before the command's `setup` (in order), `cleanup` hooks run after (in reverse). Plugins can be async or factory functions.
|
||||
|
||||
## Arguments
|
||||
|
||||
### Argument Types
|
||||
|
||||
| Type | Description | Example |
|
||||
| ------------ | ---------------------------------------- | --------------------------- |
|
||||
| `positional` | Unnamed positional args | `cli <name>` |
|
||||
| `string` | Named string options | `--name value` |
|
||||
| `boolean` | Boolean flags, supports `--no-` negation | `--verbose` |
|
||||
| `enum` | Constrained to `options` array | `--level=info\|warn\|error` |
|
||||
|
||||
### Common Options
|
||||
|
||||
| Option | Description |
|
||||
| ------------- | ------------------------------------------------------------- |
|
||||
| `description` | Help text shown in usage output |
|
||||
| `required` | Whether the argument is required |
|
||||
| `default` | Default value when not provided |
|
||||
| `alias` | Short aliases (e.g., `["f"]`). Not for `positional` |
|
||||
| `valueHint` | Display hint in help (e.g., `"host"` renders `--name=<host>`) |
|
||||
|
||||
### Example
|
||||
|
||||
```js
|
||||
const main = defineCommand({
|
||||
args: {
|
||||
name: { type: "positional", description: "Your name", required: true },
|
||||
friendly: { type: "boolean", description: "Use friendly greeting", alias: ["f"] },
|
||||
greeting: { type: "string", description: "Custom greeting", default: "Hello" },
|
||||
level: {
|
||||
type: "enum",
|
||||
description: "Log level",
|
||||
options: ["debug", "info", "warn", "error"],
|
||||
default: "info",
|
||||
},
|
||||
},
|
||||
run({ args }) {
|
||||
console.log(`${args.greeting} ${args.name}! (level: ${args.level})`);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Boolean Negation
|
||||
|
||||
Boolean args support `--no-` prefix. The negative variant appears in help when `default: true` or `negativeDescription` is set.
|
||||
|
||||
### Case-Agnostic Access
|
||||
|
||||
Kebab-case args can be accessed as camelCase: `args["output-dir"]` and `args.outputDir` both work.
|
||||
|
||||
## Built-in Flags
|
||||
|
||||
`--help` / `-h` and `--version` / `-v` are handled automatically. Disabled if your command defines args with the same names or aliases.
|
||||
|
||||
## API
|
||||
|
||||
| Function | Description |
|
||||
| ----------------------------- | -------------------------------------------------------------------------- |
|
||||
| `defineCommand(def)` | Type helper for defining commands |
|
||||
| `runMain(cmd, opts?)` | Run a command with usage support and graceful error handling |
|
||||
| `createMain(cmd)` | Create a wrapper that calls `runMain` when invoked |
|
||||
| `runCommand(cmd, opts)` | Parse args and run command/sub-commands; access `result` from return value |
|
||||
| `parseArgs(rawArgs, argsDef)` | Parse input arguments and apply defaults |
|
||||
| `renderUsage(cmd, parent?)` | Render command usage to a string |
|
||||
| `showUsage(cmd, parent?)` | Render usage and print to console |
|
||||
| `defineCittyPlugin(def)` | Type helper for defining plugins |
|
||||
|
||||
## Development
|
||||
|
||||
- Clone this repository
|
||||
- Install latest LTS version of [Node.js](https://nodejs.org/en/)
|
||||
- Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`
|
||||
- Install dependencies using `pnpm install`
|
||||
- Run interactive tests using `pnpm dev`
|
||||
|
||||
## License
|
||||
|
||||
Made with 💛 Published under [MIT License](./LICENSE).
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Licenses of Bundled Dependencies
|
||||
|
||||
The published artifact additionally contains code with the following licenses:
|
||||
MIT
|
||||
|
||||
# Bundled Dependencies
|
||||
|
||||
## scule
|
||||
|
||||
License: MIT
|
||||
Repository: https://github.com/unjs/scule
|
||||
|
||||
> MIT License
|
||||
>
|
||||
> Copyright (c) Pooya Parsa <pooya@pi0.io>
|
||||
>
|
||||
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
> of this software and associated documentation files (the "Software"), to deal
|
||||
> in the Software without restriction, including without limitation the rights
|
||||
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
> copies of the Software, and to permit persons to whom the Software is
|
||||
> furnished to do so, subject to the following conditions:
|
||||
>
|
||||
> The above copyright notice and this permission notice shall be included in all
|
||||
> copies or substantial portions of the Software.
|
||||
>
|
||||
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
> SOFTWARE.
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
//#region node_modules/.pnpm/scule@1.3.0/node_modules/scule/dist/index.mjs
|
||||
const NUMBER_CHAR_RE = /\d/;
|
||||
const STR_SPLITTERS = [
|
||||
"-",
|
||||
"_",
|
||||
"/",
|
||||
"."
|
||||
];
|
||||
function isUppercase(char = "") {
|
||||
if (NUMBER_CHAR_RE.test(char)) return;
|
||||
return char !== char.toLowerCase();
|
||||
}
|
||||
function splitByCase(str, separators) {
|
||||
const splitters = separators ?? STR_SPLITTERS;
|
||||
const parts = [];
|
||||
if (!str || typeof str !== "string") return parts;
|
||||
let buff = "";
|
||||
let previousUpper;
|
||||
let previousSplitter;
|
||||
for (const char of str) {
|
||||
const isSplitter = splitters.includes(char);
|
||||
if (isSplitter === true) {
|
||||
parts.push(buff);
|
||||
buff = "";
|
||||
previousUpper = void 0;
|
||||
continue;
|
||||
}
|
||||
const isUpper = isUppercase(char);
|
||||
if (previousSplitter === false) {
|
||||
if (previousUpper === false && isUpper === true) {
|
||||
parts.push(buff);
|
||||
buff = char;
|
||||
previousUpper = isUpper;
|
||||
continue;
|
||||
}
|
||||
if (previousUpper === true && isUpper === false && buff.length > 1) {
|
||||
const lastChar = buff.at(-1);
|
||||
parts.push(buff.slice(0, Math.max(0, buff.length - 1)));
|
||||
buff = lastChar + char;
|
||||
previousUpper = isUpper;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
buff += char;
|
||||
previousUpper = isUpper;
|
||||
previousSplitter = isSplitter;
|
||||
}
|
||||
parts.push(buff);
|
||||
return parts;
|
||||
}
|
||||
function upperFirst(str) {
|
||||
return str ? str[0].toUpperCase() + str.slice(1) : "";
|
||||
}
|
||||
function lowerFirst(str) {
|
||||
return str ? str[0].toLowerCase() + str.slice(1) : "";
|
||||
}
|
||||
function pascalCase(str, opts) {
|
||||
return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => upperFirst(opts?.normalize ? p.toLowerCase() : p)).join("") : "";
|
||||
}
|
||||
function camelCase(str, opts) {
|
||||
return lowerFirst(pascalCase(str || "", opts));
|
||||
}
|
||||
function kebabCase(str, joiner) {
|
||||
return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => p.toLowerCase()).join(joiner ?? "-") : "";
|
||||
}
|
||||
function snakeCase(str) {
|
||||
return kebabCase(str || "", "_");
|
||||
}
|
||||
//#endregion
|
||||
export { kebabCase as n, snakeCase as r, camelCase as t };
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
//#region src/types.d.ts
|
||||
type ArgType = "boolean" | "string" | "enum" | "positional" | undefined;
|
||||
type _ArgDef<T extends ArgType, VT extends boolean | number | string> = {
|
||||
type?: T;
|
||||
description?: string;
|
||||
valueHint?: string;
|
||||
alias?: string | string[];
|
||||
default?: VT;
|
||||
required?: boolean;
|
||||
options?: string[];
|
||||
};
|
||||
type BooleanArgDef = Omit<_ArgDef<"boolean", boolean>, "options"> & {
|
||||
negativeDescription?: string;
|
||||
};
|
||||
type StringArgDef = Omit<_ArgDef<"string", string>, "options">;
|
||||
type EnumArgDef = _ArgDef<"enum", string>;
|
||||
type PositionalArgDef = Omit<_ArgDef<"positional", string>, "alias" | "options">;
|
||||
type ArgDef = BooleanArgDef | StringArgDef | PositionalArgDef | EnumArgDef;
|
||||
type ArgsDef = Record<string, ArgDef>;
|
||||
type Arg = ArgDef & {
|
||||
name: string;
|
||||
alias: string[];
|
||||
};
|
||||
type ResolveParsedArgType<T extends ArgDef, VT> = T extends {
|
||||
default?: any;
|
||||
required?: boolean;
|
||||
} ? T["default"] extends NonNullable<VT> ? VT : T["required"] extends true ? VT : VT | undefined : VT | undefined;
|
||||
type ParsedPositionalArg<T extends ArgDef> = T extends {
|
||||
type: "positional";
|
||||
} ? ResolveParsedArgType<T, string> : never;
|
||||
type ParsedStringArg<T extends ArgDef> = T extends {
|
||||
type: "string";
|
||||
} ? ResolveParsedArgType<T, string> : never;
|
||||
type ParsedBooleanArg<T extends ArgDef> = T extends {
|
||||
type: "boolean";
|
||||
} ? ResolveParsedArgType<T, boolean> : never;
|
||||
type ParsedEnumArg<T extends ArgDef> = T extends {
|
||||
type: "enum";
|
||||
options: infer U;
|
||||
} ? U extends Array<any> ? ResolveParsedArgType<T, U[number]> : never : never;
|
||||
type RawArgs = {
|
||||
_: string[];
|
||||
};
|
||||
type ParsedArg<T extends ArgDef> = T["type"] extends "positional" ? ParsedPositionalArg<T> : T["type"] extends "boolean" ? ParsedBooleanArg<T> : T["type"] extends "string" ? ParsedStringArg<T> : T["type"] extends "enum" ? ParsedEnumArg<T> : never;
|
||||
type ParsedArgs<T extends ArgsDef = ArgsDef> = RawArgs & { [K in keyof T]: ParsedArg<T[K]> } & { [K in keyof T as T[K] extends {
|
||||
alias: string;
|
||||
} ? T[K]["alias"] : never]: ParsedArg<T[K]> } & { [K in keyof T as T[K] extends {
|
||||
alias: string[];
|
||||
} ? T[K]["alias"][number] : never]: ParsedArg<T[K]> } & Record<string, string | number | boolean | string[]>;
|
||||
interface CommandMeta {
|
||||
name?: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
hidden?: boolean;
|
||||
alias?: string | string[];
|
||||
}
|
||||
type SubCommandsDef = Record<string, Resolvable<CommandDef<any>>>;
|
||||
type CommandDef<T extends ArgsDef = ArgsDef> = {
|
||||
meta?: Resolvable<CommandMeta>;
|
||||
args?: Resolvable<T>;
|
||||
default?: Resolvable<string>;
|
||||
subCommands?: Resolvable<SubCommandsDef>;
|
||||
plugins?: Resolvable<CittyPlugin>[];
|
||||
setup?: (context: CommandContext<T>) => any | Promise<any>;
|
||||
cleanup?: (context: CommandContext<T>) => any | Promise<any>;
|
||||
run?: (context: CommandContext<T>) => any | Promise<any>;
|
||||
};
|
||||
type CommandContext<T extends ArgsDef = ArgsDef> = {
|
||||
rawArgs: string[];
|
||||
args: ParsedArgs<T>;
|
||||
cmd: CommandDef<T>;
|
||||
subCommand?: CommandDef<T>;
|
||||
data?: any;
|
||||
};
|
||||
type CittyPlugin = {
|
||||
name: string;
|
||||
setup?(context: CommandContext<any>): void | Promise<void>;
|
||||
cleanup?(context: CommandContext<any>): void | Promise<void>;
|
||||
};
|
||||
type Awaitable<T> = () => T | Promise<T>;
|
||||
type Resolvable<T> = T | Promise<T> | (() => T) | (() => Promise<T>);
|
||||
//#endregion
|
||||
//#region src/command.d.ts
|
||||
declare function defineCommand<const T extends ArgsDef = ArgsDef>(def: CommandDef<T>): CommandDef<T>;
|
||||
interface RunCommandOptions {
|
||||
rawArgs: string[];
|
||||
data?: any;
|
||||
showUsage?: boolean;
|
||||
}
|
||||
declare function runCommand<T extends ArgsDef = ArgsDef>(cmd: CommandDef<T>, opts: RunCommandOptions): Promise<{
|
||||
result: unknown;
|
||||
}>;
|
||||
//#endregion
|
||||
//#region src/usage.d.ts
|
||||
declare function showUsage<T extends ArgsDef = ArgsDef>(cmd: CommandDef<T>, parent?: CommandDef<T>): Promise<void>;
|
||||
declare function renderUsage<T extends ArgsDef = ArgsDef>(cmd: CommandDef<T>, parent?: CommandDef<T>): Promise<string>;
|
||||
//#endregion
|
||||
//#region src/main.d.ts
|
||||
interface RunMainOptions {
|
||||
rawArgs?: string[];
|
||||
showUsage?: typeof showUsage;
|
||||
}
|
||||
declare function runMain<T extends ArgsDef = ArgsDef>(cmd: CommandDef<T>, opts?: RunMainOptions): Promise<void>;
|
||||
declare function createMain<T extends ArgsDef = ArgsDef>(cmd: CommandDef<T>): (opts?: RunMainOptions) => Promise<void>;
|
||||
//#endregion
|
||||
//#region src/args.d.ts
|
||||
declare function parseArgs<T extends ArgsDef = ArgsDef>(rawArgs: string[], argsDef: ArgsDef): ParsedArgs<T>;
|
||||
//#endregion
|
||||
//#region src/plugin.d.ts
|
||||
declare function defineCittyPlugin(plugin: Resolvable<CittyPlugin>): Resolvable<CittyPlugin>;
|
||||
//#endregion
|
||||
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 };
|
||||
+425
@@ -0,0 +1,425 @@
|
||||
import { n as kebabCase, r as snakeCase, t as camelCase } from "./_chunks/libs/scule.mjs";
|
||||
import { parseArgs as parseArgs$1 } from "node:util";
|
||||
//#region src/_utils.ts
|
||||
function toArray(val) {
|
||||
if (Array.isArray(val)) return val;
|
||||
return val === void 0 ? [] : [val];
|
||||
}
|
||||
function formatLineColumns(lines, linePrefix = "") {
|
||||
const maxLength = [];
|
||||
for (const line of lines) for (const [i, element] of line.entries()) maxLength[i] = Math.max(maxLength[i] || 0, element.length);
|
||||
return lines.map((l) => l.map((c, i) => linePrefix + c[i === 0 ? "padStart" : "padEnd"](maxLength[i])).join(" ")).join("\n");
|
||||
}
|
||||
function resolveValue(input) {
|
||||
return typeof input === "function" ? input() : input;
|
||||
}
|
||||
var CLIError = class extends Error {
|
||||
code;
|
||||
constructor(message, code) {
|
||||
super(message);
|
||||
this.name = "CLIError";
|
||||
this.code = code;
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
//#region src/_parser.ts
|
||||
function parseRawArgs(args = [], opts = {}) {
|
||||
const booleans = new Set(opts.boolean || []);
|
||||
const strings = new Set(opts.string || []);
|
||||
const aliasMap = opts.alias || {};
|
||||
const defaults = opts.default || {};
|
||||
const aliasToMain = /* @__PURE__ */ new Map();
|
||||
const mainToAliases = /* @__PURE__ */ new Map();
|
||||
for (const [key, value] of Object.entries(aliasMap)) {
|
||||
const targets = value;
|
||||
for (const target of targets) {
|
||||
aliasToMain.set(key, target);
|
||||
if (!mainToAliases.has(target)) mainToAliases.set(target, []);
|
||||
mainToAliases.get(target).push(key);
|
||||
aliasToMain.set(target, key);
|
||||
if (!mainToAliases.has(key)) mainToAliases.set(key, []);
|
||||
mainToAliases.get(key).push(target);
|
||||
}
|
||||
}
|
||||
const options = {};
|
||||
function getType(name) {
|
||||
if (booleans.has(name)) return "boolean";
|
||||
const aliases = mainToAliases.get(name) || [];
|
||||
for (const alias of aliases) if (booleans.has(alias)) return "boolean";
|
||||
return "string";
|
||||
}
|
||||
function isStringType(name) {
|
||||
if (strings.has(name)) return true;
|
||||
const aliases = mainToAliases.get(name) || [];
|
||||
for (const alias of aliases) if (strings.has(alias)) return true;
|
||||
return false;
|
||||
}
|
||||
const allOptions = new Set([
|
||||
...booleans,
|
||||
...strings,
|
||||
...Object.keys(aliasMap),
|
||||
...Object.values(aliasMap).flat(),
|
||||
...Object.keys(defaults)
|
||||
]);
|
||||
for (const name of allOptions) if (!options[name]) options[name] = {
|
||||
type: getType(name),
|
||||
default: defaults[name]
|
||||
};
|
||||
for (const [alias, main] of aliasToMain.entries()) if (alias.length === 1 && options[main] && !options[main].short) options[main].short = alias;
|
||||
const processedArgs = [];
|
||||
const negatedFlags = {};
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg === "--") {
|
||||
processedArgs.push(...args.slice(i));
|
||||
break;
|
||||
}
|
||||
if (arg.startsWith("--no-")) {
|
||||
const flagName = arg.slice(5);
|
||||
negatedFlags[flagName] = true;
|
||||
continue;
|
||||
}
|
||||
processedArgs.push(arg);
|
||||
}
|
||||
let parsed;
|
||||
try {
|
||||
parsed = parseArgs$1({
|
||||
args: processedArgs,
|
||||
options: Object.keys(options).length > 0 ? options : void 0,
|
||||
allowPositionals: true,
|
||||
strict: false
|
||||
});
|
||||
} catch {
|
||||
parsed = {
|
||||
values: {},
|
||||
positionals: processedArgs
|
||||
};
|
||||
}
|
||||
const out = { _: [] };
|
||||
out._ = parsed.positionals;
|
||||
for (const [key, value] of Object.entries(parsed.values)) {
|
||||
let coerced = value;
|
||||
if (getType(key) === "boolean" && typeof value === "string") coerced = value !== "false";
|
||||
else if (isStringType(key) && typeof value === "boolean") coerced = "";
|
||||
out[key] = coerced;
|
||||
}
|
||||
for (const [name] of Object.entries(negatedFlags)) {
|
||||
out[name] = false;
|
||||
const mainName = aliasToMain.get(name);
|
||||
if (mainName) out[mainName] = false;
|
||||
const aliases = mainToAliases.get(name);
|
||||
if (aliases) for (const alias of aliases) out[alias] = false;
|
||||
}
|
||||
for (const [alias, main] of aliasToMain.entries()) {
|
||||
if (out[alias] !== void 0 && out[main] === void 0) out[main] = out[alias];
|
||||
if (out[main] !== void 0 && out[alias] === void 0) out[alias] = out[main];
|
||||
if (out[alias] !== out[main] && defaults[main] === out[main]) out[main] = out[alias];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
//#endregion
|
||||
//#region src/_color.ts
|
||||
const noColor = /* @__PURE__ */ (() => {
|
||||
const env = globalThis.process?.env ?? {};
|
||||
return env.NO_COLOR === "1" || env.TERM === "dumb" || env.TEST || env.CI;
|
||||
})();
|
||||
const _c = (c, r = 39) => (t) => noColor ? t : `\u001B[${c}m${t}\u001B[${r}m`;
|
||||
const bold = /* @__PURE__ */ _c(1, 22);
|
||||
const cyan = /* @__PURE__ */ _c(36);
|
||||
const gray = /* @__PURE__ */ _c(90);
|
||||
const underline = /* @__PURE__ */ _c(4, 24);
|
||||
//#endregion
|
||||
//#region src/args.ts
|
||||
function parseArgs(rawArgs, argsDef) {
|
||||
const parseOptions = {
|
||||
boolean: [],
|
||||
string: [],
|
||||
alias: {},
|
||||
default: {}
|
||||
};
|
||||
const args = resolveArgs(argsDef);
|
||||
for (const arg of args) {
|
||||
if (arg.type === "positional") continue;
|
||||
if (arg.type === "string" || arg.type === "enum") parseOptions.string.push(arg.name);
|
||||
else if (arg.type === "boolean") parseOptions.boolean.push(arg.name);
|
||||
if (arg.default !== void 0) parseOptions.default[arg.name] = arg.default;
|
||||
if (arg.alias) parseOptions.alias[arg.name] = arg.alias;
|
||||
const camelName = camelCase(arg.name);
|
||||
const kebabName = kebabCase(arg.name);
|
||||
if (camelName !== arg.name || kebabName !== arg.name) {
|
||||
const existingAliases = toArray(parseOptions.alias[arg.name] || []);
|
||||
if (camelName !== arg.name && !existingAliases.includes(camelName)) existingAliases.push(camelName);
|
||||
if (kebabName !== arg.name && !existingAliases.includes(kebabName)) existingAliases.push(kebabName);
|
||||
if (existingAliases.length > 0) parseOptions.alias[arg.name] = existingAliases;
|
||||
}
|
||||
}
|
||||
const parsed = parseRawArgs(rawArgs, parseOptions);
|
||||
const [ ...positionalArguments] = parsed._;
|
||||
const parsedArgsProxy = new Proxy(parsed, { get(target, prop) {
|
||||
return target[prop] ?? target[camelCase(prop)] ?? target[kebabCase(prop)];
|
||||
} });
|
||||
for (const [, arg] of args.entries()) if (arg.type === "positional") {
|
||||
const nextPositionalArgument = positionalArguments.shift();
|
||||
if (nextPositionalArgument !== void 0) parsedArgsProxy[arg.name] = nextPositionalArgument;
|
||||
else if (arg.default === void 0 && arg.required !== false) throw new CLIError(`Missing required positional argument: ${arg.name.toUpperCase()}`, "EARG");
|
||||
else parsedArgsProxy[arg.name] = arg.default;
|
||||
} else if (arg.type === "enum") {
|
||||
const argument = parsedArgsProxy[arg.name];
|
||||
const options = arg.options || [];
|
||||
if (argument !== void 0 && options.length > 0 && !options.includes(argument)) throw new CLIError(`Invalid value for argument: ${cyan(`--${arg.name}`)} (${cyan(argument)}). Expected one of: ${options.map((o) => cyan(o)).join(", ")}.`, "EARG");
|
||||
} else if (arg.required && parsedArgsProxy[arg.name] === void 0) throw new CLIError(`Missing required argument: --${arg.name}`, "EARG");
|
||||
return parsedArgsProxy;
|
||||
}
|
||||
function resolveArgs(argsDef) {
|
||||
const args = [];
|
||||
for (const [name, argDef] of Object.entries(argsDef || {})) args.push({
|
||||
...argDef,
|
||||
name,
|
||||
alias: toArray(argDef.alias)
|
||||
});
|
||||
return args;
|
||||
}
|
||||
//#endregion
|
||||
//#region src/plugin.ts
|
||||
function defineCittyPlugin(plugin) {
|
||||
return plugin;
|
||||
}
|
||||
async function resolvePlugins(plugins) {
|
||||
return Promise.all(plugins.map((p) => resolveValue(p)));
|
||||
}
|
||||
//#endregion
|
||||
//#region src/command.ts
|
||||
function defineCommand(def) {
|
||||
return def;
|
||||
}
|
||||
async function runCommand(cmd, opts) {
|
||||
const cmdArgs = await resolveValue(cmd.args || {});
|
||||
const parsedArgs = parseArgs(opts.rawArgs, cmdArgs);
|
||||
const context = {
|
||||
rawArgs: opts.rawArgs,
|
||||
args: parsedArgs,
|
||||
data: opts.data,
|
||||
cmd
|
||||
};
|
||||
const plugins = await resolvePlugins(cmd.plugins ?? []);
|
||||
let result;
|
||||
let runError;
|
||||
try {
|
||||
for (const plugin of plugins) await plugin.setup?.(context);
|
||||
if (typeof cmd.setup === "function") await cmd.setup(context);
|
||||
const subCommands = await resolveValue(cmd.subCommands);
|
||||
if (subCommands && Object.keys(subCommands).length > 0) {
|
||||
const subCommandArgIndex = findSubCommandIndex(opts.rawArgs, cmdArgs);
|
||||
const explicitName = opts.rawArgs[subCommandArgIndex];
|
||||
if (explicitName) {
|
||||
const subCommand = await _findSubCommand(subCommands, explicitName);
|
||||
if (!subCommand) throw new CLIError(`Unknown command ${cyan(explicitName)}`, "E_UNKNOWN_COMMAND");
|
||||
await runCommand(subCommand, { rawArgs: opts.rawArgs.slice(subCommandArgIndex + 1) });
|
||||
} else {
|
||||
const defaultSubCommand = await resolveValue(cmd.default);
|
||||
if (defaultSubCommand) {
|
||||
if (cmd.run) throw new CLIError(`Cannot specify both 'run' and 'default' on the same command.`, "E_DEFAULT_CONFLICT");
|
||||
const subCommand = await _findSubCommand(subCommands, defaultSubCommand);
|
||||
if (!subCommand) throw new CLIError(`Default sub command ${cyan(defaultSubCommand)} not found in subCommands.`, "E_UNKNOWN_COMMAND");
|
||||
await runCommand(subCommand, { rawArgs: opts.rawArgs });
|
||||
} else if (!cmd.run) throw new CLIError(`No command specified.`, "E_NO_COMMAND");
|
||||
}
|
||||
}
|
||||
if (typeof cmd.run === "function") result = await cmd.run(context);
|
||||
} catch (error) {
|
||||
runError = error;
|
||||
}
|
||||
const cleanupErrors = [];
|
||||
if (typeof cmd.cleanup === "function") try {
|
||||
await cmd.cleanup(context);
|
||||
} catch (error) {
|
||||
cleanupErrors.push(error);
|
||||
}
|
||||
for (const plugin of [...plugins].reverse()) try {
|
||||
await plugin.cleanup?.(context);
|
||||
} catch (error) {
|
||||
cleanupErrors.push(error);
|
||||
}
|
||||
if (runError) throw runError;
|
||||
if (cleanupErrors.length === 1) throw cleanupErrors[0];
|
||||
if (cleanupErrors.length > 1) throw new Error("Multiple cleanup errors", { cause: cleanupErrors });
|
||||
return { result };
|
||||
}
|
||||
async function resolveSubCommand(cmd, rawArgs, parent) {
|
||||
const subCommands = await resolveValue(cmd.subCommands);
|
||||
if (subCommands && Object.keys(subCommands).length > 0) {
|
||||
const subCommandArgIndex = findSubCommandIndex(rawArgs, await resolveValue(cmd.args || {}));
|
||||
const subCommandName = rawArgs[subCommandArgIndex];
|
||||
const subCommand = await _findSubCommand(subCommands, subCommandName);
|
||||
if (subCommand) return resolveSubCommand(subCommand, rawArgs.slice(subCommandArgIndex + 1), cmd);
|
||||
}
|
||||
return [cmd, parent];
|
||||
}
|
||||
async function _findSubCommand(subCommands, name) {
|
||||
if (name in subCommands) return resolveValue(subCommands[name]);
|
||||
for (const sub of Object.values(subCommands)) {
|
||||
const resolved = await resolveValue(sub);
|
||||
const meta = await resolveValue(resolved?.meta);
|
||||
if (meta?.alias) {
|
||||
if (toArray(meta.alias).includes(name)) return resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
function findSubCommandIndex(rawArgs, argsDef) {
|
||||
for (let i = 0; i < rawArgs.length; i++) {
|
||||
const arg = rawArgs[i];
|
||||
if (arg === "--") return -1;
|
||||
if (arg.startsWith("-")) {
|
||||
if (!arg.includes("=") && _isValueFlag(arg, argsDef)) i++;
|
||||
continue;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
function _isValueFlag(flag, argsDef) {
|
||||
const name = flag.replace(/^-{1,2}/, "");
|
||||
const normalized = camelCase(name);
|
||||
for (const [key, def] of Object.entries(argsDef)) {
|
||||
if (def.type !== "string" && def.type !== "enum") continue;
|
||||
if (normalized === camelCase(key)) return true;
|
||||
if ((Array.isArray(def.alias) ? def.alias : def.alias ? [def.alias] : []).includes(name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
//#endregion
|
||||
//#region src/usage.ts
|
||||
async function showUsage(cmd, parent) {
|
||||
try {
|
||||
console.log(await renderUsage(cmd, parent) + "\n");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
const negativePrefixRe = /^no[-A-Z]/;
|
||||
async function renderUsage(cmd, parent) {
|
||||
const cmdMeta = await resolveValue(cmd.meta || {});
|
||||
const cmdArgs = resolveArgs(await resolveValue(cmd.args || {}));
|
||||
const parentMeta = await resolveValue(parent?.meta || {});
|
||||
const commandName = `${parentMeta.name ? `${parentMeta.name} ` : ""}` + (cmdMeta.name || process.argv[1]);
|
||||
const argLines = [];
|
||||
const posLines = [];
|
||||
const commandsLines = [];
|
||||
const usageLine = [];
|
||||
for (const arg of cmdArgs) if (arg.type === "positional") {
|
||||
const name = arg.name.toUpperCase();
|
||||
const isRequired = arg.required !== false && arg.default === void 0;
|
||||
posLines.push([cyan(name + renderValueHint(arg)), renderDescription(arg, isRequired)]);
|
||||
usageLine.push(isRequired ? `<${name}>` : `[${name}]`);
|
||||
} else {
|
||||
const isRequired = arg.required === true && arg.default === void 0;
|
||||
const argStr = [...(arg.alias || []).map((a) => `-${a}`), `--${arg.name}`].join(", ") + renderValueHint(arg);
|
||||
argLines.push([cyan(argStr), renderDescription(arg, isRequired)]);
|
||||
/**
|
||||
* print negative boolean arg variant usage when
|
||||
* - enabled by default or has `negativeDescription`
|
||||
* - not prefixed with `no-` or `no[A-Z]`
|
||||
*/
|
||||
if (arg.type === "boolean" && (arg.default === true || arg.negativeDescription) && !negativePrefixRe.test(arg.name)) {
|
||||
const negativeArgStr = [...(arg.alias || []).map((a) => `--no-${a}`), `--no-${arg.name}`].join(", ");
|
||||
argLines.push([cyan(negativeArgStr), [arg.negativeDescription, isRequired ? gray("(Required)") : ""].filter(Boolean).join(" ")]);
|
||||
}
|
||||
if (isRequired) usageLine.push(`--${arg.name}` + renderValueHint(arg));
|
||||
}
|
||||
if (cmd.subCommands) {
|
||||
const commandNames = [];
|
||||
const subCommands = await resolveValue(cmd.subCommands);
|
||||
for (const [name, sub] of Object.entries(subCommands)) {
|
||||
const meta = await resolveValue((await resolveValue(sub))?.meta);
|
||||
if (meta?.hidden) continue;
|
||||
const aliases = toArray(meta?.alias);
|
||||
const label = [name, ...aliases].join(", ");
|
||||
commandsLines.push([cyan(label), meta?.description || ""]);
|
||||
commandNames.push(name, ...aliases);
|
||||
}
|
||||
usageLine.push(commandNames.join("|"));
|
||||
}
|
||||
const usageLines = [];
|
||||
const version = cmdMeta.version || parentMeta.version;
|
||||
usageLines.push(gray(`${cmdMeta.description} (${commandName + (version ? ` v${version}` : "")})`), "");
|
||||
const hasOptions = argLines.length > 0 || posLines.length > 0;
|
||||
usageLines.push(`${underline(bold("USAGE"))} ${cyan(`${commandName}${hasOptions ? " [OPTIONS]" : ""} ${usageLine.join(" ")}`)}`, "");
|
||||
if (posLines.length > 0) {
|
||||
usageLines.push(underline(bold("ARGUMENTS")), "");
|
||||
usageLines.push(formatLineColumns(posLines, " "));
|
||||
usageLines.push("");
|
||||
}
|
||||
if (argLines.length > 0) {
|
||||
usageLines.push(underline(bold("OPTIONS")), "");
|
||||
usageLines.push(formatLineColumns(argLines, " "));
|
||||
usageLines.push("");
|
||||
}
|
||||
if (commandsLines.length > 0) {
|
||||
usageLines.push(underline(bold("COMMANDS")), "");
|
||||
usageLines.push(formatLineColumns(commandsLines, " "));
|
||||
usageLines.push("", `Use ${cyan(`${commandName} <command> --help`)} for more information about a command.`);
|
||||
}
|
||||
return usageLines.filter((l) => typeof l === "string").join("\n");
|
||||
}
|
||||
function renderValueHint(arg) {
|
||||
const valueHint = arg.valueHint ? `=<${arg.valueHint}>` : "";
|
||||
const fallbackValueHint = valueHint || `=<${snakeCase(arg.name)}>`;
|
||||
if (!arg.type || arg.type === "positional" || arg.type === "boolean") return valueHint;
|
||||
if (arg.type === "enum" && arg.options?.length) return `=<${arg.options.join("|")}>`;
|
||||
return fallbackValueHint;
|
||||
}
|
||||
function renderDescription(arg, required) {
|
||||
const requiredHint = required ? gray("(Required)") : "";
|
||||
const defaultHint = arg.default === void 0 ? "" : gray(`(Default: ${arg.default})`);
|
||||
return [
|
||||
arg.description,
|
||||
requiredHint,
|
||||
defaultHint
|
||||
].filter(Boolean).join(" ");
|
||||
}
|
||||
//#endregion
|
||||
//#region src/main.ts
|
||||
async function runMain(cmd, opts = {}) {
|
||||
const rawArgs = opts.rawArgs || process.argv.slice(2);
|
||||
const showUsage$1 = opts.showUsage || showUsage;
|
||||
try {
|
||||
const builtinFlags = await _resolveBuiltinFlags(cmd);
|
||||
if (builtinFlags.help.length > 0 && rawArgs.some((arg) => builtinFlags.help.includes(arg))) {
|
||||
await showUsage$1(...await resolveSubCommand(cmd, rawArgs));
|
||||
process.exit(0);
|
||||
} else if (rawArgs.length === 1 && builtinFlags.version.includes(rawArgs[0])) {
|
||||
const meta = typeof cmd.meta === "function" ? await cmd.meta() : await cmd.meta;
|
||||
if (!meta?.version) throw new CLIError("No version specified", "E_NO_VERSION");
|
||||
console.log(meta.version);
|
||||
} else await runCommand(cmd, { rawArgs });
|
||||
} catch (error) {
|
||||
if (error instanceof CLIError) {
|
||||
await showUsage$1(...await resolveSubCommand(cmd, rawArgs));
|
||||
console.error(error.message);
|
||||
} else console.error(error, "\n");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
function createMain(cmd) {
|
||||
return (opts = {}) => runMain(cmd, opts);
|
||||
}
|
||||
async function _resolveBuiltinFlags(cmd) {
|
||||
const argsDef = await resolveValue(cmd.args || {});
|
||||
const userNames = /* @__PURE__ */ new Set();
|
||||
const userAliases = /* @__PURE__ */ new Set();
|
||||
for (const [name, def] of Object.entries(argsDef)) {
|
||||
userNames.add(name);
|
||||
for (const alias of toArray(def.alias)) userAliases.add(alias);
|
||||
}
|
||||
return {
|
||||
help: _getBuiltinFlags("help", "h", userNames, userAliases),
|
||||
version: _getBuiltinFlags("version", "v", userNames, userAliases)
|
||||
};
|
||||
}
|
||||
function _getBuiltinFlags(long, short, userNames, userAliases) {
|
||||
if (userNames.has(long) || userAliases.has(long)) return [];
|
||||
if (userNames.has(short) || userAliases.has(short)) return [`--${long}`];
|
||||
return [`--${long}`, `-${short}`];
|
||||
}
|
||||
//#endregion
|
||||
export { createMain, defineCittyPlugin, defineCommand, parseArgs, renderUsage, runCommand, runMain, showUsage };
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "citty",
|
||||
"version": "0.2.2",
|
||||
"description": "Elegant CLI Builder",
|
||||
"license": "MIT",
|
||||
"repository": "unjs/citty",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"types": "./dist/index.d.mts",
|
||||
"exports": {
|
||||
".": "./dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "obuild",
|
||||
"dev": "vitest dev",
|
||||
"lint": "oxlint . && oxfmt --check",
|
||||
"fmt": "oxlint . --fix && oxfmt",
|
||||
"prepack": "pnpm run build",
|
||||
"play": "node ./playground/cli.ts",
|
||||
"release": "pnpm test && pnpm build && changelogen --release --push && npm publish",
|
||||
"test": "pnpm lint && pnpm test:types && vitest run --coverage",
|
||||
"test:types": "tsgo --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.5.0",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260401.1",
|
||||
"@vitest/coverage-v8": "^4.1.2",
|
||||
"automd": "^0.4.3",
|
||||
"changelogen": "^0.6.2",
|
||||
"eslint-config-unjs": "^0.6.2",
|
||||
"obuild": "^0.4.32",
|
||||
"oxfmt": "^0.43.0",
|
||||
"oxlint": "^1.58.0",
|
||||
"scule": "^1.3.0",
|
||||
"typescript": "^6.0.2",
|
||||
"vitest": "^4.1.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.0"
|
||||
}
|
||||
Reference in New Issue
Block a user