@reliverse/rempts 1.7.12 → 1.7.14

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
@@ -6,19 +6,20 @@
6
6
 
7
7
  ## Features
8
8
 
9
+ - 😘 drop-in alternative to `citty` + built-in prompts
10
+ - 📂 file-based commands (app-router style by default)
9
11
  - 🫂 rempts keeps you from fighting with your CLI tool
12
+ - 🏎️ prompt engine that *feels* modern — and actually is
10
13
  - ✨ rempts is your end-to-end CLI UI + command framework
11
14
  - 🌿 multi-level file-based subcommands (sibling + nested)
12
15
  - 💪 built for DX precision and high-context terminal UX
13
- - 🏎️ prompt engine that *feels* modern — and actually is
14
- - 📂 file-based commands (app-router style by default)
15
16
  - 🎭 looks great in plain scripts or full CLI apps
16
- - 🧠 type-safe from args to prompts
17
- - ⚡ blazing-fast, zero runtime baggage
18
- - 🧩 router + argument parser built-in
19
17
  - 🎨 customizable themes and styled output
20
18
  - 📦 built-in output formatter and logger
21
19
  - 🚨 crash-safe (Ctrl+C, SIGINT, errors)
20
+ - ⚡ blazing-fast, zero runtime baggage
21
+ - 🧩 router + argument parser built-in
22
+ - 🧠 type-safe from args to prompts
22
23
  - 📐 smart layout for small terminals
23
24
  - 🎛️ override styles via prompt options
24
25
  - 🪄 minimal API surface, maximum expressiveness
@@ -1,8 +1,8 @@
1
+ import path from "@reliverse/pathkit";
1
2
  import { re } from "@reliverse/relico";
2
3
  import { relinka } from "@reliverse/relinka";
3
4
  import { loadConfig } from "c12";
4
5
  import fs from "fs-extra";
5
- import path from "pathe";
6
6
  import termkit from "terminal-kit";
7
7
  const { terminal: term } = termkit;
8
8
  let state = {
@@ -1,140 +1,5 @@
1
1
  import type { ReliArgParserOptions } from "@reliverse/reliarg";
2
- type EmptyArgs = Record<string, never>;
3
- type BaseArgProps = {
4
- description?: string;
5
- required?: boolean;
6
- allowed?: string[];
7
- };
8
- type PositionalArgDefinition = {
9
- type: "positional";
10
- default?: string;
11
- } & BaseArgProps;
12
- type BooleanArgDefinition = {
13
- type: "boolean";
14
- default?: boolean;
15
- allowed?: boolean[];
16
- } & BaseArgProps;
17
- type StringArgDefinition = {
18
- type: "string";
19
- default?: string;
20
- } & BaseArgProps;
21
- type NumberArgDefinition = {
22
- type: "number";
23
- default?: number;
24
- allowed?: number[];
25
- } & BaseArgProps;
26
- type ArrayArgDefinition = {
27
- type: "array";
28
- default?: string | readonly string[];
29
- } & BaseArgProps;
30
- export type ArgDefinition = PositionalArgDefinition | BooleanArgDefinition | StringArgDefinition | NumberArgDefinition | ArrayArgDefinition;
31
- export type ArgDefinitions = Record<string, ArgDefinition>;
32
- type CommandMeta = {
33
- name: string;
34
- version?: string;
35
- description?: string;
36
- hidden?: boolean;
37
- aliases?: string[];
38
- };
39
- /**
40
- * A subcommand can be either:
41
- * 1) A string path to a module with a default export of type Command.
42
- * 2) A lazy import function returning a Promise that resolves to
43
- * { default: Command<any> } or directly to a Command instance.
44
- */
45
- type CommandSpec = string | (() => Promise<{
46
- default: Command<any>;
47
- } | Command<any>>);
48
- export type CommandsMap = Record<string, CommandSpec>;
49
- type CommandContext<ARGS> = {
50
- args: ARGS;
51
- raw: string[];
52
- };
53
- type CommandRun<ARGS> = (ctx: CommandContext<ARGS>) => void | Promise<void>;
54
- type CommandHook<ARGS> = (ctx: CommandContext<ARGS>) => void | Promise<void>;
55
- type DefineCommandOptions<A extends ArgDefinitions = EmptyArgs> = {
56
- meta?: CommandMeta;
57
- args?: A;
58
- run?: CommandRun<InferArgTypes<A>>;
59
- /**
60
- * Object subcommands for this command.
61
- */
62
- commands?: CommandsMap;
63
- /**
64
- * @deprecated Use `commands` instead. Will be removed in a future major version.
65
- */
66
- subCommands?: CommandsMap;
67
- /**
68
- * Called before the command runs. Receives `{ args, raw }` (parsed args and raw argv).
69
- */
70
- onCmdInit?: CommandHook<InferArgTypes<A>>;
71
- /**
72
- * Called after the command finishes. Receives `{ args, raw }` (parsed args and raw argv).
73
- */
74
- onCmdExit?: CommandHook<InferArgTypes<A>>;
75
- /**
76
- * @deprecated Use onCmdInit instead
77
- */
78
- setup?: CommandHook<InferArgTypes<A>>;
79
- /**
80
- * @deprecated Use onCmdExit instead
81
- */
82
- cleanup?: CommandHook<InferArgTypes<A>>;
83
- /**
84
- * Called once per CLI process, before any command/run() is executed
85
- */
86
- onLauncherInit?: () => void | Promise<void>;
87
- /**
88
- * Called once per CLI process, after all command/run() logic is finished
89
- */
90
- onLauncherExit?: () => void | Promise<void>;
91
- };
92
- export type Command<A extends ArgDefinitions = EmptyArgs> = {
93
- meta?: CommandMeta;
94
- args: A;
95
- run?: CommandRun<InferArgTypes<A>>;
96
- /**
97
- * Object subcommands for this command.
98
- */
99
- commands?: CommandsMap;
100
- /**
101
- * @deprecated Use `commands` instead. Will be removed in a future major version.
102
- */
103
- subCommands?: CommandsMap;
104
- /**
105
- * Called before the command runs. Receives `{ args, raw }` (parsed args and raw argv).
106
- */
107
- onCmdInit?: CommandHook<InferArgTypes<A>>;
108
- /**
109
- * Called after the command finishes. Receives `{ args, raw }` (parsed args and raw argv).
110
- */
111
- onCmdExit?: CommandHook<InferArgTypes<A>>;
112
- /**
113
- * @deprecated Use onCmdInit instead
114
- */
115
- setup?: CommandHook<InferArgTypes<A>>;
116
- /**
117
- * @deprecated Use onCmdExit instead
118
- */
119
- cleanup?: CommandHook<InferArgTypes<A>>;
120
- /**
121
- * Called once per CLI process, before any command/run() is executed
122
- */
123
- onLauncherInit?: () => void | Promise<void>;
124
- /**
125
- * Called once per CLI process, after all command/run() logic is finished
126
- */
127
- onLauncherExit?: () => void | Promise<void>;
128
- };
129
- export type InferArgTypes<A extends ArgDefinitions> = {
130
- [K in keyof A]: A[K] extends PositionalArgDefinition ? string : A[K] extends BooleanArgDefinition ? boolean : A[K] extends StringArgDefinition ? string : A[K] extends NumberArgDefinition ? number : A[K] extends {
131
- type: "array";
132
- } ? string[] : never;
133
- };
134
- export type FileBasedCmdsOptions = {
135
- enable: boolean;
136
- cmdsRootPath: string;
137
- };
2
+ import type { ArgDefinitions, Command, DefineCommandOptions, EmptyArgs, FileBasedCmdsOptions } from "./launcher-types.js";
138
3
  /**
139
4
  * Defines a command with metadata, argument definitions,
140
5
  * an execution function, and (optional) subCommands.
@@ -185,4 +50,3 @@ export declare function defineArgs<A extends ArgDefinitions>(args: A): A;
185
50
  * @param parserOptions Optional reliArgParser options
186
51
  */
187
52
  export declare function runCmd<A extends ArgDefinitions = EmptyArgs>(command: Command<A>, argv?: string[], parserOptions?: ReliArgParserOptions): Promise<void>;
188
- export {};
@@ -1,9 +1,9 @@
1
+ import path from "@reliverse/pathkit";
1
2
  import { reliArgParser } from "@reliverse/reliarg";
2
3
  import { re } from "@reliverse/relico";
3
4
  import { relinka, relinkaConfig, relinkaShutdown } from "@reliverse/relinka";
4
5
  import fs from "fs-extra";
5
6
  import process from "node:process";
6
- import path from "pathe";
7
7
  import { readPackageJSON } from "pkg-types";
8
8
  function buildExampleArgs(args) {
9
9
  const parts = [];
@@ -524,8 +524,8 @@ async function runFileBasedSubCmd(subName, argv, fileCmdOpts, parserOptions, par
524
524
  }
525
525
  }
526
526
  throw new Error(
527
- `Unknown command or arguments: ${args.join(" ")}
528
-
527
+ `
528
+ Unknown command or arguments: ${args.join(" ")}
529
529
  Info for this CLI's developer: No valid command file found in ${baseDir}`
530
530
  );
531
531
  }
@@ -543,8 +543,8 @@ Info for this CLI's developer: No valid command file found in ${baseDir}`
543
543
  }
544
544
  }
545
545
  throw new Error(
546
- `Unknown command or arguments: ${args.join(" ")}
547
-
546
+ `
547
+ Unknown command or arguments: ${args.join(" ")}
548
548
  Info for this CLI's developer: No valid command file found in ${baseDir}`
549
549
  );
550
550
  }
@@ -555,9 +555,23 @@ Info for this CLI's developer: No valid command file found in ${baseDir}`
555
555
  process.cwd(),
556
556
  path.join(fileCmdOpts.cmdsRootPath, subName, "cmd.{ts,js}")
557
557
  );
558
+ const allCommands = await findRecursiveFileBasedCommands(
559
+ fileCmdOpts.cmdsRootPath
560
+ );
561
+ const commandNames = allCommands.map((cmd) => cmd.path.join(" "));
562
+ let closestMatch = "";
563
+ let minDistance = Number.POSITIVE_INFINITY;
564
+ for (const cmd of commandNames) {
565
+ const distance = levenshteinDistance(subName, cmd.split(" ")[0]);
566
+ if (distance < minDistance) {
567
+ minDistance = distance;
568
+ closestMatch = cmd;
569
+ }
570
+ }
571
+ const suggestion = minDistance <= 3 ? ` (Did you mean: \`${closestMatch}\`?)` : "";
558
572
  throw new Error(
559
- `Unknown command or arguments: ${attempted}
560
-
573
+ `
574
+ Unknown command or arguments: ${attempted}${suggestion}
561
575
  Info for this CLI's developer: No valid command directory found, expected: ${expectedPath}`
562
576
  );
563
577
  }
@@ -964,3 +978,31 @@ async function resolveFileBasedCommandPath(cmdsRoot, argv) {
964
978
  }
965
979
  return null;
966
980
  }
981
+ function levenshteinDistance(a, b) {
982
+ if (a.length === 0) return b.length;
983
+ if (b.length === 0) return a.length;
984
+ const matrix = [];
985
+ for (let i = 0; i <= b.length; i++) {
986
+ matrix[i] = [i];
987
+ }
988
+ for (let j = 0; j <= a.length; j++) {
989
+ matrix[0][j] = j;
990
+ }
991
+ for (let i = 1; i <= b.length; i++) {
992
+ for (let j = 1; j <= a.length; j++) {
993
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
994
+ matrix[i][j] = matrix[i - 1][j - 1];
995
+ } else {
996
+ matrix[i][j] = Math.min(
997
+ matrix[i - 1][j - 1] + 1,
998
+ // substitution
999
+ matrix[i][j - 1] + 1,
1000
+ // insertion
1001
+ matrix[i - 1][j] + 1
1002
+ // deletion
1003
+ );
1004
+ }
1005
+ }
1006
+ }
1007
+ return matrix[b.length][a.length];
1008
+ }
@@ -0,0 +1,136 @@
1
+ export type EmptyArgs = Record<string, never>;
2
+ export type BaseArgProps = {
3
+ description?: string;
4
+ required?: boolean;
5
+ allowed?: string[];
6
+ };
7
+ export type PositionalArgDefinition = {
8
+ type: "positional";
9
+ default?: string;
10
+ } & BaseArgProps;
11
+ export type BooleanArgDefinition = {
12
+ type: "boolean";
13
+ default?: boolean;
14
+ allowed?: boolean[];
15
+ } & BaseArgProps;
16
+ export type StringArgDefinition = {
17
+ type: "string";
18
+ default?: string;
19
+ } & BaseArgProps;
20
+ export type NumberArgDefinition = {
21
+ type: "number";
22
+ default?: number;
23
+ allowed?: number[];
24
+ } & BaseArgProps;
25
+ export type ArrayArgDefinition = {
26
+ type: "array";
27
+ default?: string | readonly string[];
28
+ } & BaseArgProps;
29
+ export type ArgDefinition = PositionalArgDefinition | BooleanArgDefinition | StringArgDefinition | NumberArgDefinition | ArrayArgDefinition;
30
+ export type ArgDefinitions = Record<string, ArgDefinition>;
31
+ export type CommandMeta = {
32
+ name: string;
33
+ version?: string;
34
+ description?: string;
35
+ hidden?: boolean;
36
+ aliases?: string[];
37
+ };
38
+ /**
39
+ * A subcommand can be either:
40
+ * 1) A string path to a module with a default export of type Command.
41
+ * 2) A lazy import function returning a Promise that resolves to
42
+ * { default: Command<any> } or directly to a Command instance.
43
+ */
44
+ export type CommandSpec = string | (() => Promise<{
45
+ default: Command<any>;
46
+ } | Command<any>>);
47
+ export type CommandsMap = Record<string, CommandSpec>;
48
+ export type CommandContext<ARGS> = {
49
+ args: ARGS;
50
+ raw: string[];
51
+ };
52
+ export type CommandRun<ARGS> = (ctx: CommandContext<ARGS>) => void | Promise<void>;
53
+ export type CommandHook<ARGS> = (ctx: CommandContext<ARGS>) => void | Promise<void>;
54
+ export type DefineCommandOptions<A extends ArgDefinitions = EmptyArgs> = {
55
+ meta?: CommandMeta;
56
+ args?: A;
57
+ run?: CommandRun<InferArgTypes<A>>;
58
+ /**
59
+ * Object subcommands for this command.
60
+ */
61
+ commands?: CommandsMap;
62
+ /**
63
+ * @deprecated Use `commands` instead. Will be removed in a future major version.
64
+ */
65
+ subCommands?: CommandsMap;
66
+ /**
67
+ * Called before the command runs. Receives `{ args, raw }` (parsed args and raw argv).
68
+ */
69
+ onCmdInit?: CommandHook<InferArgTypes<A>>;
70
+ /**
71
+ * Called after the command finishes. Receives `{ args, raw }` (parsed args and raw argv).
72
+ */
73
+ onCmdExit?: CommandHook<InferArgTypes<A>>;
74
+ /**
75
+ * @deprecated Use onCmdInit instead
76
+ */
77
+ setup?: CommandHook<InferArgTypes<A>>;
78
+ /**
79
+ * @deprecated Use onCmdExit instead
80
+ */
81
+ cleanup?: CommandHook<InferArgTypes<A>>;
82
+ /**
83
+ * Called once per CLI process, before any command/run() is executed
84
+ */
85
+ onLauncherInit?: () => void | Promise<void>;
86
+ /**
87
+ * Called once per CLI process, after all command/run() logic is finished
88
+ */
89
+ onLauncherExit?: () => void | Promise<void>;
90
+ };
91
+ export type Command<A extends ArgDefinitions = EmptyArgs> = {
92
+ meta?: CommandMeta;
93
+ args: A;
94
+ run?: CommandRun<InferArgTypes<A>>;
95
+ /**
96
+ * Object subcommands for this command.
97
+ */
98
+ commands?: CommandsMap;
99
+ /**
100
+ * @deprecated Use `commands` instead. Will be removed in a future major version.
101
+ */
102
+ subCommands?: CommandsMap;
103
+ /**
104
+ * Called before the command runs. Receives `{ args, raw }` (parsed args and raw argv).
105
+ */
106
+ onCmdInit?: CommandHook<InferArgTypes<A>>;
107
+ /**
108
+ * Called after the command finishes. Receives `{ args, raw }` (parsed args and raw argv).
109
+ */
110
+ onCmdExit?: CommandHook<InferArgTypes<A>>;
111
+ /**
112
+ * @deprecated Use onCmdInit instead
113
+ */
114
+ setup?: CommandHook<InferArgTypes<A>>;
115
+ /**
116
+ * @deprecated Use onCmdExit instead
117
+ */
118
+ cleanup?: CommandHook<InferArgTypes<A>>;
119
+ /**
120
+ * Called once per CLI process, before any command/run() is executed
121
+ */
122
+ onLauncherInit?: () => void | Promise<void>;
123
+ /**
124
+ * Called once per CLI process, after all command/run() logic is finished
125
+ */
126
+ onLauncherExit?: () => void | Promise<void>;
127
+ };
128
+ export type InferArgTypes<A extends ArgDefinitions> = {
129
+ [K in keyof A]: A[K] extends PositionalArgDefinition ? string : A[K] extends BooleanArgDefinition ? boolean : A[K] extends StringArgDefinition ? string : A[K] extends NumberArgDefinition ? number : A[K] extends {
130
+ type: "array";
131
+ } ? string[] : never;
132
+ };
133
+ export type FileBasedCmdsOptions = {
134
+ enable: boolean;
135
+ cmdsRootPath: string;
136
+ };
File without changes
@@ -0,0 +1,2 @@
1
+ import type { Command } from "./launcher-types.js";
2
+ export declare function loadCommand(cmdPath: string): Promise<Command>;
@@ -0,0 +1,50 @@
1
+ import { resolve } from "@reliverse/pathkit";
2
+ import { relinka } from "@reliverse/relinka";
3
+ import fs from "fs-extra";
4
+ import { createJiti } from "jiti";
5
+ const jiti = createJiti(import.meta.url, {
6
+ debug: process.env.NODE_ENV === "development",
7
+ fsCache: true,
8
+ sourceMaps: true
9
+ });
10
+ export async function loadCommand(cmdPath) {
11
+ try {
12
+ const resolvedPath = cmdPath.startsWith("./") || cmdPath.startsWith("../") ? resolve(process.cwd(), cmdPath) : cmdPath;
13
+ if (!resolvedPath.endsWith("cmd.ts") && !resolvedPath.endsWith("cmd.js")) {
14
+ const possiblePaths = [
15
+ resolve(resolvedPath, "cmd.ts"),
16
+ resolve(resolvedPath, "cmd.js")
17
+ ];
18
+ for (const path of possiblePaths) {
19
+ if (await fs.pathExists(path)) {
20
+ relinka("verbose", `Loading command from: ${path}`);
21
+ const cmd2 = await jiti.import(path, { default: true });
22
+ relinka("verbose", `Successfully loaded command from: ${path}`);
23
+ return cmd2;
24
+ }
25
+ }
26
+ throw new Error(
27
+ `No command file found in ${resolvedPath}. Expected to find either:
28
+ - ${possiblePaths[0]}
29
+ - ${possiblePaths[1]}
30
+ Please ensure one of these files exists and exports a default command.`
31
+ );
32
+ }
33
+ relinka("verbose", `Loading command from: ${resolvedPath}`);
34
+ const cmd = await jiti.import(resolvedPath, { default: true });
35
+ relinka("verbose", `Successfully loaded command from: ${resolvedPath}`);
36
+ return cmd;
37
+ } catch (error) {
38
+ if (error instanceof Error && error.message.includes("No command file found")) {
39
+ throw error;
40
+ }
41
+ relinka("error", `Failed to load command from ${cmdPath}:`, error);
42
+ throw new Error(
43
+ `Failed to load command from ${cmdPath}:
44
+ - Make sure the file exists and is accessible
45
+ - Ensure the file exports a default command
46
+ - Check that the file is a valid TypeScript/JavaScript module
47
+ Original error: ${error instanceof Error ? error.message : String(error)}`
48
+ );
49
+ }
50
+ }
package/bin/mod.d.ts CHANGED
@@ -4,8 +4,9 @@ export { startEditor } from "./components/editor/editor-mod.js";
4
4
  export { mainSymbols, fallbackSymbols, } from "./components/figures/figures-mod.js";
5
5
  export { confirmPrompt } from "./components/input/confirm-prompt.js";
6
6
  export { inputPrompt } from "./components/input/input-prompt.js";
7
- export type { ArgDefinition, ArgDefinitions, CommandsMap, Command, InferArgTypes, FileBasedCmdsOptions, } from "./components/launcher/launcher-mod.js";
7
+ export * from "./components/launcher/launcher-types.js";
8
8
  export { defineCommand, defineArgs, showUsage, runMain, runCmd, } from "./components/launcher/launcher-mod.js";
9
+ export { loadCommand } from "./components/launcher/run-command.js";
9
10
  export { toBaseColor, toSolidColor } from "./components/msg-fmt/colors.js";
10
11
  export { relinkaByRemptsDeprecated, relinkaAsyncByRemptsDeprecated, throwError, } from "./components/msg-fmt/logger.js";
11
12
  export { colorMap, typographyMap } from "./components/msg-fmt/mapping.js";
package/bin/mod.js CHANGED
@@ -7,6 +7,7 @@ export {
7
7
  } from "./components/figures/figures-mod.js";
8
8
  export { confirmPrompt } from "./components/input/confirm-prompt.js";
9
9
  export { inputPrompt } from "./components/input/input-prompt.js";
10
+ export * from "./components/launcher/launcher-types.js";
10
11
  export {
11
12
  defineCommand,
12
13
  defineArgs,
@@ -14,6 +15,7 @@ export {
14
15
  runMain,
15
16
  runCmd
16
17
  } from "./components/launcher/launcher-mod.js";
18
+ export { loadCommand } from "./components/launcher/run-command.js";
17
19
  export { toBaseColor, toSolidColor } from "./components/msg-fmt/colors.js";
18
20
  export {
19
21
  relinkaByRemptsDeprecated,
package/package.json CHANGED
@@ -1,21 +1,22 @@
1
1
  {
2
2
  "dependencies": {
3
3
  "@figliolia/chalk-animation": "^1.0.4",
4
+ "@reliverse/pathkit": "^1.2.1",
4
5
  "@reliverse/reliarg": "^1.0.3",
5
6
  "@reliverse/relico": "^1.1.2",
6
- "@reliverse/relinka": "^1.4.5",
7
+ "@reliverse/relinka": "^1.4.7",
7
8
  "@reliverse/runtime": "^1.0.3",
8
9
  "ansi-escapes": "^7.0.0",
9
- "c12": "^3.0.3",
10
+ "c12": "^3.0.4",
10
11
  "cli-spinners": "^3.2.0",
11
12
  "detect-package-manager": "^3.0.2",
12
13
  "figlet": "^1.8.1",
13
14
  "fs-extra": "^11.3.0",
14
15
  "gradient-string": "^3.0.0",
16
+ "jiti": "^2.4.2",
15
17
  "log-update": "^6.1.0",
16
18
  "node-emoji": "^2.2.0",
17
19
  "ora": "^8.2.0",
18
- "pathe": "^2.0.3",
19
20
  "pkg-types": "^2.1.0",
20
21
  "sisteransi": "^1.0.5",
21
22
  "terminal-kit": "^3.1.2",
@@ -28,7 +29,7 @@
28
29
  "license": "MIT",
29
30
  "name": "@reliverse/rempts",
30
31
  "type": "module",
31
- "version": "1.7.12",
32
+ "version": "1.7.14",
32
33
  "author": "reliverse",
33
34
  "bugs": {
34
35
  "email": "blefnk@gmail.com",
@@ -41,28 +42,7 @@
41
42
  "type": "git",
42
43
  "url": "git+https://github.com/reliverse/rempts.git"
43
44
  },
44
- "devDependencies": {
45
- "@biomejs/biome": "1.9.4",
46
- "@eslint/js": "^9.26.0",
47
- "@reliverse/dler": "^1.2.5",
48
- "@reliverse/relidler-cfg": "^1.1.3",
49
- "@stylistic/eslint-plugin": "^4.2.0",
50
- "@total-typescript/ts-reset": "^0.6.1",
51
- "@types/bun": "^1.2.13",
52
- "@types/figlet": "^1.7.0",
53
- "@types/fs-extra": "^11.0.4",
54
- "@types/node": "^22.15.18",
55
- "@types/terminal-kit": "^2.5.7",
56
- "@types/wrap-ansi": "^8.1.0",
57
- "eslint": "^9.26.0",
58
- "eslint-plugin-no-relative-import-paths": "^1.6.1",
59
- "eslint-plugin-perfectionist": "^4.13.0",
60
- "jiti": "^2.4.2",
61
- "knip": "^5.56.0",
62
- "typescript": "^5.8.3",
63
- "typescript-eslint": "^8.32.1",
64
- "vitest": "^3.1.3"
65
- },
45
+ "devDependencies": {},
66
46
  "exports": {
67
47
  ".": "./bin/mod.js"
68
48
  },
@@ -77,4 +57,4 @@
77
57
  "publishConfig": {
78
58
  "access": "public"
79
59
  }
80
- }
60
+ }
@@ -1,167 +0,0 @@
1
- import type {
2
- Default,
3
- ParserArgv,
4
- ParserOptions,
5
- } from "~/types.js";
6
-
7
- function toArr(any: any) {
8
- // biome-ignore lint/suspicious/noDoubleEquals: <explanation>
9
- return any == undefined ? [] : Array.isArray(any) ? any : [any];
10
- }
11
-
12
- function toVal(out, key, val, opts) {
13
- let x;
14
- const old = out[key];
15
- const nxt = ~opts.string.indexOf(key)
16
- ? // biome-ignore lint/suspicious/noDoubleEquals: <explanation>
17
- val == undefined || val === true
18
- ? ""
19
- : String(val)
20
- : typeof val === "boolean"
21
- ? val
22
- : ~opts.boolean.indexOf(key)
23
- ? val === "false"
24
- ? false
25
- : val === "true" ||
26
- // biome-ignore lint/style/noCommaOperator: <explanation>
27
- (out._.push(((x = +val), x * 0 === 0) ? x : val), !!val)
28
- : // biome-ignore lint/style/noCommaOperator: <explanation>
29
- ((x = +val), x * 0 === 0)
30
- ? x
31
- : val;
32
- out[key] =
33
- // biome-ignore lint/suspicious/noDoubleEquals: <explanation>
34
- old == undefined ? nxt : Array.isArray(old) ? old.concat(nxt) : [old, nxt];
35
- }
36
-
37
- export function parseRawArgs<T = Default>(
38
- args: string[] = [],
39
- opts: ParserOptions = {},
40
- ): ParserArgv<T> {
41
- let k;
42
- let arr;
43
- let arg;
44
- let name;
45
- let val;
46
- const out = { _: [] };
47
- let i = 0;
48
- let j = 0;
49
- let idx = 0;
50
- const len = args.length;
51
-
52
- const alibi = opts.alias !== void 0;
53
- const strict = opts.unknown !== void 0;
54
- const defaults = opts.default !== void 0;
55
-
56
- opts.alias = opts.alias || {};
57
- opts.string = toArr(opts.string);
58
- opts.boolean = toArr(opts.boolean);
59
-
60
- if (alibi) {
61
- for (k in opts.alias) {
62
- arr = opts.alias[k] = toArr(opts.alias[k]);
63
- for (i = 0; i < arr.length; i++) {
64
- (opts.alias[arr[i]] = arr.concat(k)).splice(i, 1);
65
- }
66
- }
67
- }
68
-
69
- for (i = opts.boolean.length; i-- > 0; ) {
70
- arr = opts.alias[opts.boolean[i]] || [];
71
- for (j = arr.length; j-- > 0; ) {
72
- opts.boolean.push(arr[j]);
73
- }
74
- }
75
-
76
- for (i = opts.string.length; i-- > 0; ) {
77
- arr = opts.alias[opts.string[i]] || [];
78
- for (j = arr.length; j-- > 0; ) {
79
- opts.string.push(arr[j]);
80
- }
81
- }
82
-
83
- if (defaults) {
84
- for (k in opts.default) {
85
- name = typeof opts.default[k];
86
- arr = opts.alias[k] = opts.alias[k] || [];
87
- if (opts[name] !== void 0) {
88
- opts[name].push(k);
89
- for (i = 0; i < arr.length; i++) {
90
- opts[name].push(arr[i]);
91
- }
92
- }
93
- }
94
- }
95
-
96
- const keys = strict ? Object.keys(opts.alias) : [];
97
-
98
- for (i = 0; i < len; i++) {
99
- arg = args[i];
100
-
101
- if (arg === "--") {
102
- out._ = out._.concat(args.slice(++i));
103
- break;
104
- }
105
-
106
- for (j = 0; j < arg.length; j++) {
107
- if (arg.charCodeAt(j) !== 45) {
108
- break;
109
- } // "-"
110
- }
111
-
112
- if (j === 0) {
113
- out._.push(arg);
114
- } else if (arg.substring(j, j + 3) === "no-") {
115
- name = arg.slice(Math.max(0, j + 3));
116
- if (strict && !~keys.indexOf(name)) {
117
- // @ts-expect-error TODO: fix ts
118
- return opts.unknown(arg);
119
- }
120
- out[name] = false;
121
- } else {
122
- for (idx = j + 1; idx < arg.length; idx++) {
123
- if (arg.charCodeAt(idx) === 61) {
124
- break;
125
- } // "="
126
- }
127
-
128
- name = arg.substring(j, idx);
129
- val =
130
- arg.slice(Math.max(0, ++idx)) ||
131
- i + 1 === len ||
132
- // biome-ignore lint/style/useTemplate: <explanation>
133
- ("" + args[i + 1]).charCodeAt(0) === 45 ||
134
- args[++i];
135
- arr = j === 2 ? [name] : name;
136
-
137
- for (idx = 0; idx < arr.length; idx++) {
138
- name = arr[idx];
139
- if (strict && !~keys.indexOf(name)) {
140
- // @ts-expect-error TODO: fix ts
141
- return opts.unknown("-".repeat(j) + name);
142
- }
143
- toVal(out, name, idx + 1 < arr.length || val, opts);
144
- }
145
- }
146
- }
147
-
148
- if (defaults) {
149
- for (k in opts.default) {
150
- if (out[k] === void 0) {
151
- out[k] = opts.default[k];
152
- }
153
- }
154
- }
155
-
156
- if (alibi) {
157
- for (k in out) {
158
- arr = opts.alias[k] || [];
159
- while (arr.length > 0) {
160
- out[arr.shift()] = out[k];
161
- }
162
- }
163
- }
164
-
165
- // @ts-expect-error TODO: fix ts
166
- return out;
167
- }
@@ -1,41 +0,0 @@
1
- import type { Resolvable } from "~/types.js";
2
-
3
- export function toArray(val: any) {
4
- if (Array.isArray(val)) {
5
- return val;
6
- }
7
- return val === undefined ? [] : [val];
8
- }
9
-
10
- export function formatLineColumns(lines: string[][], linePrefix = "") {
11
- const maxLength: number[] = [];
12
- for (const line of lines) {
13
- for (const [i, element] of line.entries()) {
14
- maxLength[i] = Math.max(maxLength[i] || 0, element.length);
15
- }
16
- }
17
- return lines
18
- .map((l) =>
19
- l
20
- .map(
21
- (c, i) =>
22
- linePrefix + c[i === 0 ? "padStart" : "padEnd"](maxLength[i]),
23
- )
24
- .join(" "),
25
- )
26
- .join("\n");
27
- }
28
-
29
- export function resolveValue<T>(input: Resolvable<T>): T | Promise<T> {
30
- return typeof input === "function" ? (input as any)() : input;
31
- }
32
-
33
- export class CLIError extends Error {
34
- constructor(
35
- message: string,
36
- public code?: string,
37
- ) {
38
- super(message);
39
- this.name = "CLIError";
40
- }
41
- }
@@ -1,108 +0,0 @@
1
- import { kebabCase, camelCase } from "scule";
2
-
3
- import type { Arg, ArgsDef, ParsedArgs } from "~/types.js";
4
-
5
- import { parseRawArgs } from "./_parser.js";
6
- import { CLIError, toArray } from "./_utils.js";
7
-
8
- export function parseArgs<T extends ArgsDef = ArgsDef>(
9
- rawArgs: string[],
10
- argsDef: ArgsDef,
11
- ): ParsedArgs<T> {
12
- const parseOptions = {
13
- boolean: [] as string[],
14
- string: [] as string[],
15
- number: [] as string[],
16
- enum: [] as (number | string)[],
17
- mixed: [] as string[],
18
- alias: {} as Record<string, string | string[]>,
19
- default: {} as Record<string, boolean | number | string>,
20
- };
21
-
22
- const args = resolveArgs(argsDef);
23
-
24
- for (const arg of args) {
25
- if (arg.type === "positional") {
26
- continue;
27
- }
28
- if (arg.type === "string" || arg.type === "number") {
29
- parseOptions.string.push(arg.name);
30
- } else if (arg.type === "boolean") {
31
- parseOptions.boolean.push(arg.name);
32
- } else if (arg.type === "enum") {
33
- parseOptions.enum.push(...(arg.options || []));
34
- }
35
-
36
- if (arg.default !== undefined) {
37
- parseOptions.default[arg.name] = arg.default;
38
- }
39
- if (arg.alias) {
40
- parseOptions.alias[arg.name] = arg.alias;
41
- }
42
- }
43
-
44
- const parsed = parseRawArgs(rawArgs, parseOptions);
45
- const [...positionalArguments] = parsed._;
46
-
47
- const parsedArgsProxy = new Proxy(parsed, {
48
- get(target: ParsedArgs<any>, prop: string) {
49
- return target[prop] ?? target[camelCase(prop)] ?? target[kebabCase(prop)];
50
- },
51
- });
52
-
53
- for (const [, arg] of args.entries()) {
54
- if (arg.type === "positional") {
55
- const nextPositionalArgument = positionalArguments.shift();
56
- if (nextPositionalArgument !== undefined) {
57
- parsedArgsProxy[arg.name] = nextPositionalArgument;
58
- } else if (arg.default === undefined && arg.required !== false) {
59
- throw new CLIError(
60
- `Missing required positional argument: ${arg.name.toUpperCase()}`,
61
- "EARG",
62
- );
63
- } else {
64
- parsedArgsProxy[arg.name] = arg.default;
65
- }
66
- } else if (arg.type === "enum") {
67
- const argument = parsedArgsProxy[arg.name];
68
- const options = arg.options || [];
69
- if (
70
- argument !== undefined &&
71
- options.length > 0 &&
72
- !options.includes(argument)
73
- ) {
74
- throw new CLIError(
75
- `Invalid value for argument: \`--${arg.name}\` (\`${argument}\`). Expected one of: ${options.map((o) => `\`${o}\``).join(", ")}.`,
76
- "EARG",
77
- );
78
- }
79
- } else if (arg.type === "number") {
80
- const _originalValue = parsedArgsProxy[arg.name];
81
- parsedArgsProxy[arg.name] = Number.parseFloat(
82
- parsedArgsProxy[arg.name] as string,
83
- );
84
- if (Number.isNaN(parsedArgsProxy[arg.name])) {
85
- throw new CLIError(
86
- `Invalid value for argument: \`--${arg.name}\` (\`${_originalValue}\`). Expected a number.`,
87
- "EARG",
88
- );
89
- }
90
- } else if (arg.required && parsedArgsProxy[arg.name] === undefined) {
91
- throw new CLIError(`Missing required argument: --${arg.name}`, "EARG");
92
- }
93
- }
94
-
95
- return parsedArgsProxy as ParsedArgs<T>;
96
- }
97
-
98
- export function resolveArgs(argsDef: ArgsDef): Arg[] {
99
- const args: Arg[] = [];
100
- for (const [name, argDef] of Object.entries(argsDef || {})) {
101
- args.push({
102
- ...argDef,
103
- name,
104
- alias: toArray((argDef as any).alias),
105
- });
106
- }
107
- return args;
108
- }
@@ -1,95 +0,0 @@
1
- import type {
2
- ArgsDef,
3
- CommandContext,
4
- CommandDef,
5
- RunCommandOptions,
6
- } from "~/types.js";
7
-
8
- import { CLIError, resolveValue } from "./_utils.js";
9
- import { parseArgs } from "./args.js";
10
-
11
- export function defineCommand<const T extends ArgsDef = ArgsDef>(
12
- def: CommandDef<T>,
13
- ): CommandDef<T> {
14
- return def;
15
- }
16
-
17
- export async function runCommand<T extends ArgsDef = ArgsDef>(
18
- cmd: CommandDef<T>,
19
- opts: RunCommandOptions,
20
- ): Promise<{ result: unknown }> {
21
- const cmdArgs = await resolveValue(cmd.args || {});
22
- const parsedArgs = parseArgs<T>(opts.rawArgs, cmdArgs);
23
-
24
- const context: CommandContext<T> = {
25
- rawArgs: opts.rawArgs,
26
- args: parsedArgs,
27
- data: opts.data,
28
- cmd,
29
- };
30
-
31
- // Setup hook
32
- if (typeof cmd.setup === "function") {
33
- await cmd.setup(context);
34
- }
35
-
36
- // Handle sub command
37
- let result: unknown;
38
- try {
39
- const subCommands = await resolveValue(cmd.subCommands);
40
- if (subCommands && Object.keys(subCommands).length > 0) {
41
- const subCommandArgIndex = opts.rawArgs.findIndex(
42
- (arg) => !arg.startsWith("-"),
43
- );
44
- const subCommandName = opts.rawArgs[subCommandArgIndex];
45
- if (subCommandName) {
46
- if (!subCommands[subCommandName]) {
47
- throw new CLIError(
48
- `Unknown command \`${subCommandName}\``,
49
- "E_UNKNOWN_COMMAND",
50
- );
51
- }
52
- const subCommand = await resolveValue(subCommands[subCommandName]);
53
- if (subCommand) {
54
- await runCommand(subCommand, {
55
- rawArgs: opts.rawArgs.slice(subCommandArgIndex + 1),
56
- });
57
- }
58
- } else if (!cmd.run) {
59
- // biome-ignore lint/style/noUnusedTemplateLiteral: <explanation>
60
- throw new CLIError(`No command specified.`, "E_NO_COMMAND");
61
- }
62
- }
63
-
64
- // Handle main command
65
- if (typeof cmd.run === "function") {
66
- result = await cmd.run(context);
67
- }
68
- } finally {
69
- if (typeof cmd.cleanup === "function") {
70
- await cmd.cleanup(context);
71
- }
72
- }
73
- return { result };
74
- }
75
-
76
- export async function resolveSubCommand<T extends ArgsDef = ArgsDef>(
77
- cmd: CommandDef<T>,
78
- rawArgs: string[],
79
- parent?: CommandDef<T>,
80
- ): Promise<[CommandDef<T>, CommandDef<T>?]> {
81
- const subCommands = await resolveValue(cmd.subCommands);
82
- if (subCommands && Object.keys(subCommands).length > 0) {
83
- const subCommandArgIndex = rawArgs.findIndex((arg) => !arg.startsWith("-"));
84
- const subCommandName = rawArgs[subCommandArgIndex];
85
- const subCommand = await resolveValue(subCommands[subCommandName]);
86
- if (subCommand) {
87
- return resolveSubCommand(
88
- subCommand,
89
- rawArgs.slice(subCommandArgIndex + 1),
90
- cmd,
91
- );
92
- }
93
- }
94
- return [cmd, parent];
95
- }
@@ -1,50 +0,0 @@
1
- import { relinka } from "@reliverse/relinka";
2
-
3
- import type { ArgsDef, CommandDef } from "~/types.js";
4
-
5
- import { CLIError } from "./_utils.js";
6
- import { resolveSubCommand, runCommand } from "./command.js";
7
- import { showUsage as _showUsage } from "./usage.js";
8
-
9
- export type RunMainOptions = {
10
- rawArgs?: string[];
11
- showUsage?: typeof _showUsage;
12
- };
13
-
14
- export async function runMain<T extends ArgsDef = ArgsDef>(
15
- cmd: CommandDef<T>,
16
- opts: RunMainOptions = {},
17
- ) {
18
- const rawArgs = opts.rawArgs || process.argv.slice(2);
19
- const showUsage = opts.showUsage || _showUsage;
20
- try {
21
- if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
22
- await showUsage(...(await resolveSubCommand(cmd, rawArgs)));
23
- process.exit(0);
24
- } else if (rawArgs.length === 1 && rawArgs[0] === "--version") {
25
- const meta =
26
- typeof cmd.meta === "function" ? await cmd.meta() : await cmd.meta;
27
- if (!meta?.version) {
28
- throw new CLIError("No version specified", "E_NO_VERSION");
29
- }
30
- relinka("log", meta.version);
31
- } else {
32
- await runCommand(cmd, { rawArgs });
33
- }
34
- } catch (error: any) {
35
- const isCLIError = error instanceof CLIError;
36
- if (isCLIError) {
37
- await showUsage(...(await resolveSubCommand(cmd, rawArgs)));
38
- relinka("error", error.message);
39
- } else {
40
- relinka("error", error, "\n");
41
- }
42
- process.exit(1);
43
- }
44
- }
45
-
46
- export function createMain<T extends ArgsDef = ArgsDef>(
47
- cmd: CommandDef<T>,
48
- ): (opts?: RunMainOptions) => Promise<void> {
49
- return (opts: RunMainOptions = {}) => runMain(cmd, opts);
50
- }
@@ -1,157 +0,0 @@
1
- import { re } from "@reliverse/relico";
2
- import { relinka } from "@reliverse/relinka";
3
-
4
- import type { ArgsDef, CommandDef } from "~/types.js";
5
-
6
- import { formatLineColumns, resolveValue } from "./_utils.js";
7
- import { resolveArgs } from "./args.js";
8
-
9
- export async function showUsage<T extends ArgsDef = ArgsDef>(
10
- cmd: CommandDef<T>,
11
- parent?: CommandDef<T>,
12
- ) {
13
- try {
14
- // biome-ignore lint/style/useTemplate: <explanation>
15
- relinka("log", (await renderUsage(cmd, parent)) + "\n");
16
- } catch (error) {
17
- relinka("error", String(error));
18
- }
19
- }
20
-
21
- // `no` prefix matcher (kebab-case or camelCase)
22
- const negativePrefixRe = /^no[-A-Z]/;
23
-
24
- export async function renderUsage<T extends ArgsDef = ArgsDef>(
25
- cmd: CommandDef<T>,
26
- parent?: CommandDef<T>,
27
- ) {
28
- const cmdMeta = await resolveValue(cmd.meta || {});
29
- const cmdArgs = resolveArgs(await resolveValue(cmd.args || {}));
30
- const parentMeta = await resolveValue(parent?.meta || {});
31
-
32
- const commandName =
33
- (parentMeta.name ? `${parentMeta.name} ` : "") +
34
- (cmdMeta.name || process.argv[1]);
35
-
36
- const argLines: string[][] = [];
37
- const posLines: string[][] = [];
38
- const commandsLines: string[][] = [];
39
- const usageLine = [];
40
-
41
- for (const arg of cmdArgs) {
42
- if (arg.type === "positional") {
43
- const name = arg.name.toUpperCase();
44
- const isRequired = arg.required !== false && arg.default === undefined;
45
- // (isRequired ? " (required)" : " (optional)"
46
- const defaultHint = arg.default ? `="${arg.default}"` : "";
47
- posLines.push([
48
- // biome-ignore lint/style/useTemplate: <explanation>
49
- "`" + name + defaultHint + "`",
50
- arg.description || "",
51
- arg.valueHint ? `<${arg.valueHint}>` : "",
52
- ]);
53
- usageLine.push(isRequired ? `<${name}>` : `[${name}]`);
54
- } else {
55
- const isRequired = arg.required === true && arg.default === undefined;
56
- const argStr =
57
- [...(arg.alias || []).map((a) => `-${a}`), `--${arg.name}`].join(", ") +
58
- (arg.type === "string" && (arg.valueHint || arg.default)
59
- ? `=${
60
- arg.valueHint ? `<${arg.valueHint}>` : `"${arg.default || ""}"`
61
- }`
62
- : "") +
63
- (arg.type === "enum" && arg.options
64
- ? `=<${arg.options.join("|")}>`
65
- : "");
66
- argLines.push([
67
- // biome-ignore lint/style/useTemplate: <explanation>
68
- "`" + argStr + (isRequired ? " (required)" : "") + "`",
69
- arg.description || "",
70
- ]);
71
-
72
- /**
73
- * print negative boolean arg variant usage when
74
- * - enabled by default or has `negativeDescription`
75
- * - not prefixed with `no-` or `no[A-Z]`
76
- */
77
- if (
78
- arg.type === "boolean" &&
79
- (arg.default === true || arg.negativeDescription) &&
80
- !negativePrefixRe.test(arg.name)
81
- ) {
82
- const negativeArgStr = [
83
- ...(arg.alias || []).map((a) => `--no-${a}`),
84
- `--no-${arg.name}`,
85
- ].join(", ");
86
- argLines.push([
87
- // biome-ignore lint/style/useTemplate: <explanation>
88
- "`" + negativeArgStr + (isRequired ? " (required)" : "") + "`",
89
- arg.negativeDescription || "",
90
- ]);
91
- }
92
-
93
- if (isRequired) {
94
- usageLine.push(argStr);
95
- }
96
- }
97
- }
98
-
99
- if (cmd.subCommands) {
100
- const commandNames: string[] = [];
101
- const subCommands = await resolveValue(cmd.subCommands);
102
- for (const [name, sub] of Object.entries(subCommands)) {
103
- const subCmd = await resolveValue(sub);
104
- const meta = await resolveValue(subCmd?.meta);
105
- if (meta?.hidden) {
106
- continue;
107
- }
108
- commandsLines.push([`\`${name}\``, meta?.description || ""]);
109
- commandNames.push(name);
110
- }
111
- usageLine.push(commandNames.join("|"));
112
- }
113
-
114
- const usageLines: (string | undefined)[] = [];
115
-
116
- const version = cmdMeta.version || parentMeta.version;
117
-
118
- usageLines.push(
119
- re.gray(
120
- `${cmdMeta.description} (${
121
- commandName + (version ? ` v${version}` : "")
122
- })`,
123
- ),
124
- "",
125
- );
126
-
127
- const hasOptions = argLines.length > 0 || posLines.length > 0;
128
- usageLines.push(
129
- `${re.underline(re.bold("USAGE"))} \`${commandName}${
130
- hasOptions ? " [OPTIONS]" : ""
131
- } ${usageLine.join(" ")}\``,
132
- "",
133
- );
134
-
135
- if (posLines.length > 0) {
136
- usageLines.push(re.underline(re.bold("ARGUMENTS")), "");
137
- usageLines.push(formatLineColumns(posLines, " "));
138
- usageLines.push("");
139
- }
140
-
141
- if (argLines.length > 0) {
142
- usageLines.push(re.underline(re.bold("OPTIONS")), "");
143
- usageLines.push(formatLineColumns(argLines, " "));
144
- usageLines.push("");
145
- }
146
-
147
- if (commandsLines.length > 0) {
148
- usageLines.push(re.underline(re.bold("COMMANDS")), "");
149
- usageLines.push(formatLineColumns(commandsLines, " "));
150
- usageLines.push(
151
- "",
152
- `Use \`${commandName} <command> --help\` for more information about a command.`,
153
- );
154
- }
155
-
156
- return usageLines.filter((l) => typeof l === "string").join("\n");
157
- }