index.mjs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. import consola from 'consola';
  2. import { colors } from 'consola/utils';
  3. function toArray(val) {
  4. if (Array.isArray(val)) {
  5. return val;
  6. }
  7. return val === void 0 ? [] : [val];
  8. }
  9. function formatLineColumns(lines, linePrefix = "") {
  10. const maxLengh = [];
  11. for (const line of lines) {
  12. for (const [i, element] of line.entries()) {
  13. maxLengh[i] = Math.max(maxLengh[i] || 0, element.length);
  14. }
  15. }
  16. return lines.map(
  17. (l) => l.map(
  18. (c, i) => linePrefix + c[i === 0 ? "padStart" : "padEnd"](maxLengh[i])
  19. ).join(" ")
  20. ).join("\n");
  21. }
  22. function resolveValue(input) {
  23. return typeof input === "function" ? input() : input;
  24. }
  25. class CLIError extends Error {
  26. constructor(message, code) {
  27. super(message);
  28. this.code = code;
  29. this.name = "CLIError";
  30. }
  31. }
  32. const NUMBER_CHAR_RE = /\d/;
  33. const STR_SPLITTERS = ["-", "_", "/", "."];
  34. function isUppercase(char = "") {
  35. if (NUMBER_CHAR_RE.test(char)) {
  36. return void 0;
  37. }
  38. return char !== char.toLowerCase();
  39. }
  40. function splitByCase(str, separators) {
  41. const splitters = separators ?? STR_SPLITTERS;
  42. const parts = [];
  43. if (!str || typeof str !== "string") {
  44. return parts;
  45. }
  46. let buff = "";
  47. let previousUpper;
  48. let previousSplitter;
  49. for (const char of str) {
  50. const isSplitter = splitters.includes(char);
  51. if (isSplitter === true) {
  52. parts.push(buff);
  53. buff = "";
  54. previousUpper = void 0;
  55. continue;
  56. }
  57. const isUpper = isUppercase(char);
  58. if (previousSplitter === false) {
  59. if (previousUpper === false && isUpper === true) {
  60. parts.push(buff);
  61. buff = char;
  62. previousUpper = isUpper;
  63. continue;
  64. }
  65. if (previousUpper === true && isUpper === false && buff.length > 1) {
  66. const lastChar = buff.at(-1);
  67. parts.push(buff.slice(0, Math.max(0, buff.length - 1)));
  68. buff = lastChar + char;
  69. previousUpper = isUpper;
  70. continue;
  71. }
  72. }
  73. buff += char;
  74. previousUpper = isUpper;
  75. previousSplitter = isSplitter;
  76. }
  77. parts.push(buff);
  78. return parts;
  79. }
  80. function upperFirst(str) {
  81. return str ? str[0].toUpperCase() + str.slice(1) : "";
  82. }
  83. function lowerFirst(str) {
  84. return str ? str[0].toLowerCase() + str.slice(1) : "";
  85. }
  86. function pascalCase(str, opts) {
  87. return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => upperFirst(opts?.normalize ? p.toLowerCase() : p)).join("") : "";
  88. }
  89. function camelCase(str, opts) {
  90. return lowerFirst(pascalCase(str || "", opts));
  91. }
  92. function kebabCase(str, joiner) {
  93. return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => p.toLowerCase()).join(joiner ?? "-") : "";
  94. }
  95. function toArr(any) {
  96. return any == void 0 ? [] : Array.isArray(any) ? any : [any];
  97. }
  98. function toVal(out, key, val, opts) {
  99. let x;
  100. const old = out[key];
  101. const nxt = ~opts.string.indexOf(key) ? val == void 0 || val === true ? "" : String(val) : typeof val === "boolean" ? val : ~opts.boolean.indexOf(key) ? val === "false" ? false : val === "true" || (out._.push((x = +val, x * 0 === 0) ? x : val), !!val) : (x = +val, x * 0 === 0) ? x : val;
  102. out[key] = old == void 0 ? nxt : Array.isArray(old) ? old.concat(nxt) : [old, nxt];
  103. }
  104. function parseRawArgs(args = [], opts = {}) {
  105. let k;
  106. let arr;
  107. let arg;
  108. let name;
  109. let val;
  110. const out = { _: [] };
  111. let i = 0;
  112. let j = 0;
  113. let idx = 0;
  114. const len = args.length;
  115. const alibi = opts.alias !== void 0;
  116. const strict = opts.unknown !== void 0;
  117. const defaults = opts.default !== void 0;
  118. opts.alias = opts.alias || {};
  119. opts.string = toArr(opts.string);
  120. opts.boolean = toArr(opts.boolean);
  121. if (alibi) {
  122. for (k in opts.alias) {
  123. arr = opts.alias[k] = toArr(opts.alias[k]);
  124. for (i = 0; i < arr.length; i++) {
  125. (opts.alias[arr[i]] = arr.concat(k)).splice(i, 1);
  126. }
  127. }
  128. }
  129. for (i = opts.boolean.length; i-- > 0; ) {
  130. arr = opts.alias[opts.boolean[i]] || [];
  131. for (j = arr.length; j-- > 0; ) {
  132. opts.boolean.push(arr[j]);
  133. }
  134. }
  135. for (i = opts.string.length; i-- > 0; ) {
  136. arr = opts.alias[opts.string[i]] || [];
  137. for (j = arr.length; j-- > 0; ) {
  138. opts.string.push(arr[j]);
  139. }
  140. }
  141. if (defaults) {
  142. for (k in opts.default) {
  143. name = typeof opts.default[k];
  144. arr = opts.alias[k] = opts.alias[k] || [];
  145. if (opts[name] !== void 0) {
  146. opts[name].push(k);
  147. for (i = 0; i < arr.length; i++) {
  148. opts[name].push(arr[i]);
  149. }
  150. }
  151. }
  152. }
  153. const keys = strict ? Object.keys(opts.alias) : [];
  154. for (i = 0; i < len; i++) {
  155. arg = args[i];
  156. if (arg === "--") {
  157. out._ = out._.concat(args.slice(++i));
  158. break;
  159. }
  160. for (j = 0; j < arg.length; j++) {
  161. if (arg.charCodeAt(j) !== 45) {
  162. break;
  163. }
  164. }
  165. if (j === 0) {
  166. out._.push(arg);
  167. } else if (arg.substring(j, j + 3) === "no-") {
  168. name = arg.slice(Math.max(0, j + 3));
  169. if (strict && !~keys.indexOf(name)) {
  170. return opts.unknown(arg);
  171. }
  172. out[name] = false;
  173. } else {
  174. for (idx = j + 1; idx < arg.length; idx++) {
  175. if (arg.charCodeAt(idx) === 61) {
  176. break;
  177. }
  178. }
  179. name = arg.substring(j, idx);
  180. val = arg.slice(Math.max(0, ++idx)) || i + 1 === len || ("" + args[i + 1]).charCodeAt(0) === 45 || args[++i];
  181. arr = j === 2 ? [name] : name;
  182. for (idx = 0; idx < arr.length; idx++) {
  183. name = arr[idx];
  184. if (strict && !~keys.indexOf(name)) {
  185. return opts.unknown("-".repeat(j) + name);
  186. }
  187. toVal(out, name, idx + 1 < arr.length || val, opts);
  188. }
  189. }
  190. }
  191. if (defaults) {
  192. for (k in opts.default) {
  193. if (out[k] === void 0) {
  194. out[k] = opts.default[k];
  195. }
  196. }
  197. }
  198. if (alibi) {
  199. for (k in out) {
  200. arr = opts.alias[k] || [];
  201. while (arr.length > 0) {
  202. out[arr.shift()] = out[k];
  203. }
  204. }
  205. }
  206. return out;
  207. }
  208. function parseArgs(rawArgs, argsDef) {
  209. const parseOptions = {
  210. boolean: [],
  211. string: [],
  212. mixed: [],
  213. alias: {},
  214. default: {}
  215. };
  216. const args = resolveArgs(argsDef);
  217. for (const arg of args) {
  218. if (arg.type === "positional") {
  219. continue;
  220. }
  221. if (arg.type === "string") {
  222. parseOptions.string.push(arg.name);
  223. } else if (arg.type === "boolean") {
  224. parseOptions.boolean.push(arg.name);
  225. }
  226. if (arg.default !== void 0) {
  227. parseOptions.default[arg.name] = arg.default;
  228. }
  229. if (arg.alias) {
  230. parseOptions.alias[arg.name] = arg.alias;
  231. }
  232. }
  233. const parsed = parseRawArgs(rawArgs, parseOptions);
  234. const [...positionalArguments] = parsed._;
  235. const parsedArgsProxy = new Proxy(parsed, {
  236. get(target, prop) {
  237. return target[prop] ?? target[camelCase(prop)] ?? target[kebabCase(prop)];
  238. }
  239. });
  240. for (const [, arg] of args.entries()) {
  241. if (arg.type === "positional") {
  242. const nextPositionalArgument = positionalArguments.shift();
  243. if (nextPositionalArgument !== void 0) {
  244. parsedArgsProxy[arg.name] = nextPositionalArgument;
  245. } else if (arg.default === void 0 && arg.required !== false) {
  246. throw new CLIError(
  247. `Missing required positional argument: ${arg.name.toUpperCase()}`,
  248. "EARG"
  249. );
  250. } else {
  251. parsedArgsProxy[arg.name] = arg.default;
  252. }
  253. } else if (arg.required && parsedArgsProxy[arg.name] === void 0) {
  254. throw new CLIError(`Missing required argument: --${arg.name}`, "EARG");
  255. }
  256. }
  257. return parsedArgsProxy;
  258. }
  259. function resolveArgs(argsDef) {
  260. const args = [];
  261. for (const [name, argDef] of Object.entries(argsDef || {})) {
  262. args.push({
  263. ...argDef,
  264. name,
  265. alias: toArray(argDef.alias)
  266. });
  267. }
  268. return args;
  269. }
  270. function defineCommand(def) {
  271. return def;
  272. }
  273. async function runCommand(cmd, opts) {
  274. const cmdArgs = await resolveValue(cmd.args || {});
  275. const parsedArgs = parseArgs(opts.rawArgs, cmdArgs);
  276. const context = {
  277. rawArgs: opts.rawArgs,
  278. args: parsedArgs,
  279. data: opts.data,
  280. cmd
  281. };
  282. if (typeof cmd.setup === "function") {
  283. await cmd.setup(context);
  284. }
  285. let result;
  286. try {
  287. const subCommands = await resolveValue(cmd.subCommands);
  288. if (subCommands && Object.keys(subCommands).length > 0) {
  289. const subCommandArgIndex = opts.rawArgs.findIndex(
  290. (arg) => !arg.startsWith("-")
  291. );
  292. const subCommandName = opts.rawArgs[subCommandArgIndex];
  293. if (subCommandName) {
  294. if (!subCommands[subCommandName]) {
  295. throw new CLIError(
  296. `Unknown command \`${subCommandName}\``,
  297. "E_UNKNOWN_COMMAND"
  298. );
  299. }
  300. const subCommand = await resolveValue(subCommands[subCommandName]);
  301. if (subCommand) {
  302. await runCommand(subCommand, {
  303. rawArgs: opts.rawArgs.slice(subCommandArgIndex + 1)
  304. });
  305. }
  306. } else if (!cmd.run) {
  307. throw new CLIError(`No command specified.`, "E_NO_COMMAND");
  308. }
  309. }
  310. if (typeof cmd.run === "function") {
  311. result = await cmd.run(context);
  312. }
  313. } finally {
  314. if (typeof cmd.cleanup === "function") {
  315. await cmd.cleanup(context);
  316. }
  317. }
  318. return { result };
  319. }
  320. async function resolveSubCommand(cmd, rawArgs, parent) {
  321. const subCommands = await resolveValue(cmd.subCommands);
  322. if (subCommands && Object.keys(subCommands).length > 0) {
  323. const subCommandArgIndex = rawArgs.findIndex((arg) => !arg.startsWith("-"));
  324. const subCommandName = rawArgs[subCommandArgIndex];
  325. const subCommand = await resolveValue(subCommands[subCommandName]);
  326. if (subCommand) {
  327. return resolveSubCommand(
  328. subCommand,
  329. rawArgs.slice(subCommandArgIndex + 1),
  330. cmd
  331. );
  332. }
  333. }
  334. return [cmd, parent];
  335. }
  336. async function showUsage(cmd, parent) {
  337. try {
  338. consola.log(await renderUsage(cmd, parent) + "\n");
  339. } catch (error) {
  340. consola.error(error);
  341. }
  342. }
  343. async function renderUsage(cmd, parent) {
  344. const cmdMeta = await resolveValue(cmd.meta || {});
  345. const cmdArgs = resolveArgs(await resolveValue(cmd.args || {}));
  346. const parentMeta = await resolveValue(parent?.meta || {});
  347. const commandName = `${parentMeta.name ? `${parentMeta.name} ` : ""}` + (cmdMeta.name || process.argv[1]);
  348. const argLines = [];
  349. const posLines = [];
  350. const commandsLines = [];
  351. const usageLine = [];
  352. for (const arg of cmdArgs) {
  353. if (arg.type === "positional") {
  354. const name = arg.name.toUpperCase();
  355. const isRequired = arg.required !== false && arg.default === void 0;
  356. const defaultHint = arg.default ? `="${arg.default}"` : "";
  357. posLines.push([
  358. "`" + name + defaultHint + "`",
  359. arg.description || "",
  360. arg.valueHint ? `<${arg.valueHint}>` : ""
  361. ]);
  362. usageLine.push(isRequired ? `<${name}>` : `[${name}]`);
  363. } else {
  364. const isRequired = arg.required === true && arg.default === void 0;
  365. const argStr = (arg.type === "boolean" && arg.default === true ? [
  366. ...(arg.alias || []).map((a) => `--no-${a}`),
  367. `--no-${arg.name}`
  368. ].join(", ") : [...(arg.alias || []).map((a) => `-${a}`), `--${arg.name}`].join(
  369. ", "
  370. )) + (arg.type === "string" && (arg.valueHint || arg.default) ? `=${arg.valueHint ? `<${arg.valueHint}>` : `"${arg.default || ""}"`}` : "");
  371. argLines.push([
  372. "`" + argStr + (isRequired ? " (required)" : "") + "`",
  373. arg.description || ""
  374. ]);
  375. if (isRequired) {
  376. usageLine.push(argStr);
  377. }
  378. }
  379. }
  380. if (cmd.subCommands) {
  381. const commandNames = [];
  382. const subCommands = await resolveValue(cmd.subCommands);
  383. for (const [name, sub] of Object.entries(subCommands)) {
  384. const subCmd = await resolveValue(sub);
  385. const meta = await resolveValue(subCmd?.meta);
  386. commandsLines.push([`\`${name}\``, meta?.description || ""]);
  387. commandNames.push(name);
  388. }
  389. usageLine.push(commandNames.join("|"));
  390. }
  391. const usageLines = [];
  392. const version = cmdMeta.version || parentMeta.version;
  393. usageLines.push(
  394. colors.gray(
  395. `${cmdMeta.description} (${commandName + (version ? ` v${version}` : "")})`
  396. ),
  397. ""
  398. );
  399. const hasOptions = argLines.length > 0 || posLines.length > 0;
  400. usageLines.push(
  401. `${colors.underline(colors.bold("USAGE"))} \`${commandName}${hasOptions ? " [OPTIONS]" : ""} ${usageLine.join(" ")}\``,
  402. ""
  403. );
  404. if (posLines.length > 0) {
  405. usageLines.push(colors.underline(colors.bold("ARGUMENTS")), "");
  406. usageLines.push(formatLineColumns(posLines, " "));
  407. usageLines.push("");
  408. }
  409. if (argLines.length > 0) {
  410. usageLines.push(colors.underline(colors.bold("OPTIONS")), "");
  411. usageLines.push(formatLineColumns(argLines, " "));
  412. usageLines.push("");
  413. }
  414. if (commandsLines.length > 0) {
  415. usageLines.push(colors.underline(colors.bold("COMMANDS")), "");
  416. usageLines.push(formatLineColumns(commandsLines, " "));
  417. usageLines.push(
  418. "",
  419. `Use \`${commandName} <command> --help\` for more information about a command.`
  420. );
  421. }
  422. return usageLines.filter((l) => typeof l === "string").join("\n");
  423. }
  424. async function runMain(cmd, opts = {}) {
  425. const rawArgs = opts.rawArgs || process.argv.slice(2);
  426. const showUsage$1 = opts.showUsage || showUsage;
  427. try {
  428. if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
  429. await showUsage$1(...await resolveSubCommand(cmd, rawArgs));
  430. process.exit(0);
  431. } else if (rawArgs.length === 1 && rawArgs[0] === "--version") {
  432. const meta = typeof cmd.meta === "function" ? await cmd.meta() : await cmd.meta;
  433. if (!meta?.version) {
  434. throw new CLIError("No version specified", "E_NO_VERSION");
  435. }
  436. consola.log(meta.version);
  437. } else {
  438. await runCommand(cmd, { rawArgs });
  439. }
  440. } catch (error) {
  441. const isCLIError = error instanceof CLIError;
  442. if (!isCLIError) {
  443. consola.error(error, "\n");
  444. }
  445. if (isCLIError) {
  446. await showUsage$1(...await resolveSubCommand(cmd, rawArgs));
  447. }
  448. consola.error(error.message);
  449. process.exit(1);
  450. }
  451. }
  452. function createMain(cmd) {
  453. return (opts = {}) => runMain(cmd, opts);
  454. }
  455. export { createMain, defineCommand, parseArgs, renderUsage, runCommand, runMain, showUsage };