@leemour/cli-core 0.1.2 → 0.3.0

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
@@ -7,7 +7,7 @@ keyring behind a testable seam, and injectable clocks.
7
7
  Extracted from [`brazecli`](https://github.com/leemour/brazecli), where each piece earned its
8
8
  shape, and shared with [`max-cli`](https://github.com/leemour/max-cli).
9
9
 
10
- **Status: 0.1.2.** Published on npm, used by `max-cli`. Both extraction steps have landed: the
10
+ **Status: 0.3.0.** Published on npm, used by `max-cli`. Both extraction steps have landed: the
11
11
  files that move unchanged, and the ones that needed a parameter threaded through. 75 tests.
12
12
 
13
13
  ## The rule this package exists to keep
@@ -43,6 +43,8 @@ streams.stderr // []
43
43
  | `logging` | a Pino adapter writing JSON lines with secrets redacted by field name; a file that cannot be written is reported to `onError`, never thrown |
44
44
  | `retry` | full-jitter backoff, and the distinction between "no answer came" and "safe to repeat" |
45
45
  | `/testing` | `captureStreams`, `memoryKeyring`, `brokenKeyring`, `fakeClock` |
46
+ | `/commands` | the command registry: `describeProgram`, `annotate`, `flatten` — see below |
47
+ | `/completion` | shell completion over the registry: `suggest`, `formatSuggestions` — see below |
46
48
 
47
49
  **Nothing in the root export is HTTP.** Status classification, `Retry-After` parsing and the fetch
48
50
  seam live in `@leemour/cli-core/http`, so a CLI that speaks a socket never depends on a stack it
@@ -52,6 +54,40 @@ does not call:
52
54
  import { providerWaitMs, statusToCode } from "@leemour/cli-core/http"
53
55
  ```
54
56
 
57
+ **The command registry is `@leemour/cli-core/commands`** — the whole command tree as data, like
58
+ `rails routes`, for an agent to read instead of `--help` and for generated documentation. It walks
59
+ the live [Commander](https://github.com/tj/commander.js) tree, so a command built in a loop from a
60
+ catalog appears exactly like a handwritten one; `annotate` adds what the tree cannot say. Commander
61
+ is a type here, not a runtime dependency — an optional peer, 15 or newer.
62
+
63
+ ```ts
64
+ import { annotate, describeProgram } from "@leemour/cli-core/commands"
65
+
66
+ annotate(program.command("send"), { mutates: true, examples: ["max messages send 42 hi"] })
67
+ annotate(generated, { origin: "generated", operationId: "campaigns.list" })
68
+
69
+ describeProgram(program) // [{ path: ["send"], usage, origin, mutates, options: [...], commands: [...] }]
70
+ ```
71
+
72
+ Each option says whether it `takesValue` and, separately, whether it is `mandatory` — not
73
+ Commander's `required`, which means "takes a value when given" — plus its choices, default,
74
+ environment variable, the options it `conflicts` with and the values it `implies`.
75
+
76
+ **Shell completion is `@leemour/cli-core/completion`**, built on the registry. `suggest` takes the
77
+ words typed so far and answers what may come next: a command, an action, an option, one of its
78
+ values, or an argument's values from a source the CLI hands in — a local cache, never the network,
79
+ because a shell calls it on every Tab. `formatSuggestions` writes the answer in the protocol of the
80
+ shell scripts [`@bomb.sh/tab`](https://github.com/bombshell-dev/tab) generates, so the CLI prints
81
+ those scripts with tab and answers `<cli> complete -- <words>` with this; cli-core itself does not
82
+ depend on tab.
83
+
84
+ ```ts
85
+ import { formatSuggestions, suggest } from "@leemour/cli-core/completion"
86
+
87
+ const words = argv.slice(argv.indexOf("--") + 1)
88
+ streams.data(formatSuggestions(suggest({ commands, globalOptions, words, sources: { arguments: { chat: chatNames } } })))
89
+ ```
90
+
55
91
  **Two traps worth knowing before you use the credential store.** The OS keyring is global: an entry
56
92
  is addressed by service and account and knows nothing about which config directory asked for it, so
57
93
  a throwaway config directory silently overwrites the real secret unless you pass `isolated: true`.
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The command registry: the whole command tree of a CLI as data, however each command came to be.
3
+ *
4
+ * It walks the live Commander tree rather than a list written by hand, so a command built in a
5
+ * loop from a catalog shows up exactly like one written in a file. What the tree cannot say — where
6
+ * a command came from, whether it changes anything, whether it is on its way out — is attached with
7
+ * `annotate`.
8
+ *
9
+ * Behind its own entry point for the same reason `/http` is: Commander is only a type here, and the
10
+ * root of this package stays free of any command-line framework.
11
+ */
12
+ import type { Command } from "commander";
13
+ export type Origin = "handwritten" | "generated";
14
+ export type CommandState = "beta" | "deprecated";
15
+ export interface CommandMeta {
16
+ origin?: Origin;
17
+ /** The catalog operation a generated command was made from. */
18
+ operationId?: string;
19
+ /** True when running the command changes something outside this machine. */
20
+ mutates?: boolean;
21
+ state?: CommandState;
22
+ examples?: readonly string[];
23
+ }
24
+ export interface ArgumentInfo {
25
+ name: string;
26
+ required: boolean;
27
+ variadic: boolean;
28
+ description: string;
29
+ choices?: readonly string[];
30
+ default?: unknown;
31
+ }
32
+ export interface OptionInfo {
33
+ flags: string;
34
+ description: string;
35
+ /** False for a plain switch like `--json`, so an agent knows not to look for a value. */
36
+ takesValue: boolean;
37
+ /**
38
+ * Whether the option itself must be given. Deliberately not Commander's `required`, which means
39
+ * "takes a value when present" — reading that as "you must pass this" is the obvious misreading.
40
+ */
41
+ mandatory: boolean;
42
+ choices?: readonly string[];
43
+ default?: unknown;
44
+ env?: string;
45
+ /** Options that may not be given together with this one. */
46
+ conflicts?: readonly string[];
47
+ /** Values this option sets on others when they are not given. */
48
+ implies?: Readonly<Record<string, unknown>>;
49
+ }
50
+ export interface CommandInfo {
51
+ /** What to pass to the CLI, already split: `["runs", "list"]`. */
52
+ path: readonly string[];
53
+ name: string;
54
+ summary?: string;
55
+ description: string;
56
+ usage: string;
57
+ origin: Origin;
58
+ operationId?: string;
59
+ mutates?: boolean;
60
+ state?: CommandState;
61
+ examples?: readonly string[];
62
+ arguments: readonly ArgumentInfo[];
63
+ options: readonly OptionInfo[];
64
+ commands: readonly CommandInfo[];
65
+ }
66
+ /** Attaches what the tree cannot say. Repeated calls merge; the command is returned for chaining. */
67
+ export declare const annotate: <T extends Command>(command: T, meta: CommandMeta) => T;
68
+ export declare const metaOf: (command: Command) => CommandMeta;
69
+ export declare const describeProgram: (root: Command) => CommandInfo[];
70
+ export declare const describeOptions: (command: Command) => OptionInfo[];
71
+ export declare const flatten: (commands: readonly CommandInfo[]) => CommandInfo[];
72
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/commands/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAY,OAAO,EAAU,MAAM,WAAW,CAAA;AAE1D,MAAM,MAAM,MAAM,GAAG,aAAa,GAAG,WAAW,CAAA;AAChD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,YAAY,CAAA;AAEhD,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,4EAA4E;IAC5E,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,OAAO,CAAA;IACjB,QAAQ,EAAE,OAAO,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,yFAAyF;IACzF,UAAU,EAAE,OAAO,CAAA;IACnB;;;OAGG;IACH,SAAS,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,4DAA4D;IAC5D,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC7B,iEAAiE;IACjE,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CAC5C;AAED,MAAM,WAAW,WAAW;IAC1B,kEAAkE;IAClE,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,KAAK,CAAC,EAAE,YAAY,CAAA;IACpB,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5B,SAAS,EAAE,SAAS,YAAY,EAAE,CAAA;IAClC,OAAO,EAAE,SAAS,UAAU,EAAE,CAAA;IAC9B,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAA;CACjC;AAID,qGAAqG;AACrG,eAAO,MAAM,QAAQ,GAAI,CAAC,SAAS,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM,WAAW,KAAG,CAG3E,CAAA;AAED,eAAO,MAAM,MAAM,GAAI,SAAS,OAAO,KAAG,WAAwC,CAAA;AAElF,eAAO,MAAM,eAAe,GAAI,MAAM,OAAO,KAAG,WAAW,EACK,CAAA;AAEhE,eAAO,MAAM,eAAe,GAAI,SAAS,OAAO,KAAG,UAAU,EACW,CAAA;AAExE,eAAO,MAAM,OAAO,GAAI,UAAU,SAAS,WAAW,EAAE,KAAG,WAAW,EACE,CAAA"}
@@ -0,0 +1,61 @@
1
+ const labels = new WeakMap();
2
+ /** Attaches what the tree cannot say. Repeated calls merge; the command is returned for chaining. */
3
+ export const annotate = (command, meta) => {
4
+ labels.set(command, { ...labels.get(command), ...meta });
5
+ return command;
6
+ };
7
+ export const metaOf = (command) => labels.get(command) ?? {};
8
+ export const describeProgram = (root) => visible(root).map((child) => describe(child, root.name(), []));
9
+ export const describeOptions = (command) => command.options.filter((option) => !option.hidden).map(describeOption);
10
+ export const flatten = (commands) => commands.flatMap((command) => [command, ...flatten(command.commands)]);
11
+ const describe = (command, cli, parents) => {
12
+ const path = [...parents, command.name()];
13
+ const args = command.registeredArguments.map(describeArgument);
14
+ const options = describeOptions(command);
15
+ const { origin = "handwritten", ...meta } = metaOf(command);
16
+ const usage = [
17
+ cli,
18
+ ...path,
19
+ ...args.map((argument) => (argument.required ? `<${argument.name}>` : `[${argument.name}]`)),
20
+ options.length > 0 ? "[options]" : "",
21
+ ]
22
+ .filter(Boolean)
23
+ .join(" ");
24
+ return {
25
+ path,
26
+ name: command.name(),
27
+ ...(command.summary() ? { summary: command.summary() } : {}),
28
+ description: command.description(),
29
+ usage,
30
+ origin,
31
+ ...meta,
32
+ arguments: args,
33
+ options,
34
+ commands: visible(command).map((child) => describe(child, cli, path)),
35
+ };
36
+ };
37
+ // `addCommand(command, { hidden: true })` sets only this; Commander has no public getter for it.
38
+ const visible = (command) => command.commands.filter((child) => !child._hidden);
39
+ const describeArgument = (argument) => ({
40
+ name: argument.name(),
41
+ required: argument.required,
42
+ variadic: argument.variadic,
43
+ description: argument.description,
44
+ ...(argument.argChoices ? { choices: argument.argChoices } : {}),
45
+ ...(argument.defaultValue === undefined ? {} : { default: argument.defaultValue }),
46
+ });
47
+ const describeOption = (option) => {
48
+ const { conflictsWith = [], implied } = option;
49
+ return {
50
+ flags: option.flags,
51
+ description: option.description,
52
+ takesValue: option.required || option.optional,
53
+ mandatory: option.mandatory,
54
+ ...(option.argChoices ? { choices: option.argChoices } : {}),
55
+ ...(option.defaultValue === undefined ? {} : { default: option.defaultValue }),
56
+ ...(option.envVar ? { env: option.envVar } : {}),
57
+ ...(conflictsWith.length > 0 ? { conflicts: conflictsWith } : {}),
58
+ ...(implied ? { implies: implied } : {}),
59
+ };
60
+ };
61
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/commands/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Shell completion over the command registry: which word may come next, as data.
3
+ *
4
+ * It speaks the protocol of the shell scripts `@bomb.sh/tab` generates — the shell runs
5
+ * `<cli> complete -- <words>` and reads `value<TAB>description` lines ending in `:<directive>` —
6
+ * so a CLI prints those scripts with tab and answers the requests with this. Nothing here reads a
7
+ * terminal, a file or the network: the words come in, the suggestions go out, and where values come
8
+ * from (a local cache, a config file) is the CLI's business, handed in as `sources`.
9
+ */
10
+ import type { CommandInfo, OptionInfo } from "../commands/index.js";
11
+ export interface Suggestion {
12
+ value: string;
13
+ description: string;
14
+ }
15
+ /** Values for one argument or option. Called on every Tab, so it must be cheap and must not connect anywhere. */
16
+ export type Values = () => readonly (string | Suggestion)[];
17
+ export interface CompletionSources {
18
+ /** By argument name, as registered: `chat`, `person`. */
19
+ arguments?: Readonly<Record<string, Values>>;
20
+ /** By long option name without dashes: `chat` for `--chat <id>`. */
21
+ options?: Readonly<Record<string, Values>>;
22
+ /** Offered alongside the top-level commands — a profile name that may come first, say. */
23
+ firstWord?: Values;
24
+ }
25
+ export interface CompletionRequest {
26
+ commands: readonly CommandInfo[];
27
+ globalOptions?: readonly OptionInfo[];
28
+ /** Everything after `complete --`; the last word is the one being typed, `""` for a fresh one. */
29
+ words: readonly string[];
30
+ sources?: CompletionSources;
31
+ }
32
+ /** Tells the shell not to fall back to file names when nothing matched. */
33
+ export declare const NO_FILE_COMPLETION = 4;
34
+ export declare const suggest: ({ commands, globalOptions, words, sources }: CompletionRequest) => Suggestion[];
35
+ /** The answer in the form the shell script reads. */
36
+ export declare const formatSuggestions: (suggestions: readonly Suggestion[]) => string;
37
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/completion/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAgB,WAAW,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEjF,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,iHAAiH;AACjH,MAAM,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,MAAM,GAAG,UAAU,CAAC,EAAE,CAAA;AAE3D,MAAM,WAAW,iBAAiB;IAChC,yDAAyD;IACzD,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC5C,oEAAoE;IACpE,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC1C,0FAA0F;IAC1F,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAA;IAChC,aAAa,CAAC,EAAE,SAAS,UAAU,EAAE,CAAA;IACrC,kGAAkG;IAClG,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACxB,OAAO,CAAC,EAAE,iBAAiB,CAAA;CAC5B;AAED,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,IAAI,CAAA;AAEnC,eAAO,MAAM,OAAO,GAAI,6CAAuD,iBAAiB,KAAG,UAAU,EA2C5G,CAAA;AAED,qDAAqD;AACrD,eAAO,MAAM,iBAAiB,GAAI,aAAa,SAAS,UAAU,EAAE,KAAG,MAC4C,CAAA"}
@@ -0,0 +1,87 @@
1
+ /** Tells the shell not to fall back to file names when nothing matched. */
2
+ export const NO_FILE_COMPLETION = 4;
3
+ export const suggest = ({ commands, globalOptions = [], words, sources = {} }) => {
4
+ const typed = words.at(-1) ?? "";
5
+ const before = words.slice(0, -1);
6
+ let level = commands;
7
+ let current;
8
+ let positional = 0;
9
+ let pending;
10
+ for (const word of before) {
11
+ if (pending) {
12
+ pending = undefined;
13
+ continue;
14
+ }
15
+ if (word.startsWith("-")) {
16
+ const option = findOption(word, current, globalOptions);
17
+ if (option?.takesValue && !word.includes("="))
18
+ pending = option;
19
+ continue;
20
+ }
21
+ const child = level.find((command) => command.name === word);
22
+ if (child) {
23
+ current = child;
24
+ level = child.commands;
25
+ positional = 0;
26
+ }
27
+ else {
28
+ positional += 1;
29
+ }
30
+ }
31
+ const candidates = pending
32
+ ? optionValues(pending, sources)
33
+ : typed.startsWith("-")
34
+ ? typed.includes("=")
35
+ ? valuesAfterEquals(typed, current, globalOptions, sources)
36
+ : optionNames(current, globalOptions)
37
+ : [
38
+ ...level.filter((command) => command.state !== "deprecated").map(asSuggestion),
39
+ ...(current ? argumentValues(current.arguments, positional, sources) : []),
40
+ ...(current ? [] : resolve(sources.firstWord)),
41
+ ];
42
+ const unique = new Map(candidates.map((candidate) => [candidate.value, candidate]));
43
+ return [...unique.values()].filter(({ value }) => value.startsWith(typed));
44
+ };
45
+ /** The answer in the form the shell script reads. */
46
+ export const formatSuggestions = (suggestions) => [...suggestions.map(({ value, description }) => `${value}\t${description}`), `:${NO_FILE_COMPLETION}`].join("\n");
47
+ const asSuggestion = (command) => ({
48
+ value: command.name,
49
+ description: command.summary ?? command.description,
50
+ });
51
+ const longName = (option) => option.flags.match(/--([\w-]+)/)?.[1];
52
+ const findOption = (word, current, globals) => {
53
+ const name = word.replace(/^-+/, "").split("=")[0];
54
+ const matches = (option) => longName(option) === name || option.flags.split(/[ ,]+/).includes(`-${name}`);
55
+ return [...(current?.options ?? []), ...globals].find(matches);
56
+ };
57
+ const optionNames = (current, globals) => [...(current?.options ?? []), ...globals].flatMap((option) => {
58
+ const name = longName(option);
59
+ return name ? [{ value: `--${name}`, description: option.description }] : [];
60
+ });
61
+ /** `--kind <dialog|group|channel>`: Commander does not know these as choices, but the flags say them. */
62
+ const inlineChoices = (flags) => flags.match(/<([^>]*\|[^>]*)>/)?.[1]?.split("|") ?? [];
63
+ const optionValues = (option, sources) => {
64
+ const name = longName(option);
65
+ return [
66
+ ...[...(option.choices ?? []), ...inlineChoices(option.flags)].map((value) => ({ value, description: "" })),
67
+ ...resolve(name ? sources.options?.[name] : undefined),
68
+ ];
69
+ };
70
+ const valuesAfterEquals = (typed, current, globals, sources) => {
71
+ const [flag] = typed.split("=");
72
+ const option = findOption(flag ?? "", current, globals);
73
+ if (!option?.takesValue)
74
+ return [];
75
+ return optionValues(option, sources).map(({ value, description }) => ({ value: `${flag}=${value}`, description }));
76
+ };
77
+ const argumentValues = (args, position, sources) => {
78
+ const argument = args[position] ?? (args.at(-1)?.variadic ? args.at(-1) : undefined);
79
+ if (!argument)
80
+ return [];
81
+ return [
82
+ ...(argument.choices ?? []).map((value) => ({ value, description: "" })),
83
+ ...resolve(sources.arguments?.[argument.name]),
84
+ ];
85
+ };
86
+ const resolve = (values) => (values?.() ?? []).map((value) => (typeof value === "string" ? { value, description: "" } : value));
87
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/completion/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leemour/cli-core",
3
- "version": "0.1.2",
3
+ "version": "0.3.0",
4
4
  "description": "The parts every command line tool needs: output modes, terminal rendering, error model, exit codes, credentials, clocks",
5
5
  "license": "MIT",
6
6
  "author": "Viacheslav Ptsarev",
@@ -30,6 +30,14 @@
30
30
  "./http": {
31
31
  "types": "./dist/http/index.d.ts",
32
32
  "default": "./dist/http/index.js"
33
+ },
34
+ "./commands": {
35
+ "types": "./dist/commands/index.d.ts",
36
+ "default": "./dist/commands/index.js"
37
+ },
38
+ "./completion": {
39
+ "types": "./dist/completion/index.d.ts",
40
+ "default": "./dist/completion/index.js"
33
41
  }
34
42
  },
35
43
  "scripts": {
@@ -46,8 +54,8 @@
46
54
  },
47
55
  "dependencies": {
48
56
  "cli-table3": "^0.6.5",
49
- "picocolors": "^1.1.1",
50
57
  "env-paths": "^4.0.0",
58
+ "picocolors": "^1.1.1",
51
59
  "pino": "^10.3.1",
52
60
  "valibot": "^1.5.0"
53
61
  },
@@ -57,11 +65,20 @@
57
65
  "devDependencies": {
58
66
  "@biomejs/biome": "^2.3.14",
59
67
  "@types/node": "^22.10.2",
68
+ "commander": "^15.0.0",
69
+ "lefthook": "^2.1.8",
60
70
  "typescript": "^5.9.3",
61
- "vitest": "^3.2.4",
62
- "lefthook": "^2.1.8"
71
+ "vitest": "^3.2.4"
63
72
  },
64
73
  "publishConfig": {
65
74
  "access": "public"
75
+ },
76
+ "peerDependencies": {
77
+ "commander": ">=15"
78
+ },
79
+ "peerDependenciesMeta": {
80
+ "commander": {
81
+ "optional": true
82
+ }
66
83
  }
67
84
  }