@bunizao/cli-kit 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 bunizao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # @bunizao/cli-kit
2
+
3
+ Small shared primitives for the `ontrack`, `moodle`, and `edstem` command-line tools. The package
4
+ owns their stable command contract: verbs, error codes, exit codes, output formats, mutation
5
+ confirmation, and command self-description.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install https://codeload.github.com/bunizao/cli-kit/tar.gz/refs/tags/v0.1.0 commander
11
+ ```
12
+
13
+ After the registry release, consumers can switch to the versioned package:
14
+
15
+ ```sh
16
+ npm install @bunizao/cli-kit commander
17
+ ```
18
+
19
+ Node.js 18 or newer is supported. Individual CLIs may require a newer runtime.
20
+
21
+ ## Program setup
22
+
23
+ ```ts
24
+ import {
25
+ commandsJson,
26
+ createProgram,
27
+ insertDefaultVerb,
28
+ mutating,
29
+ type NounSpec,
30
+ } from "@bunizao/cli-kit";
31
+
32
+ const program = createProgram({
33
+ name: "example",
34
+ version: "1.0.0",
35
+ description: "Example CLI",
36
+ });
37
+
38
+ const units = program.command("units").aliases(["courses", "projects"]);
39
+ units.command("list").action(listUnits);
40
+ units.command("show <unit>").action(showUnit);
41
+ mutating(units.command("set <unit> <state>")).action(setUnitState);
42
+
43
+ const nouns: readonly NounSpec[] = [
44
+ {
45
+ name: "units",
46
+ aliases: ["courses", "projects"],
47
+ verbs: ["list", "show", "set"],
48
+ defaultByArity: { 0: "list", 1: "show" },
49
+ valueFlags: [],
50
+ },
51
+ ];
52
+
53
+ const args = insertDefaultVerb(process.argv.slice(2), nouns);
54
+ await program.parseAsync(args, { from: "user" });
55
+ ```
56
+
57
+ `insertDefaultVerb` accepts user arguments, not the Node executable and script prefix. It is a
58
+ pure transform and does not mutate the provided array.
59
+
60
+ List option flags that consume a separate value in `valueFlags`. This lets the arity counter ignore
61
+ option values when flags are interleaved with positionals. Boolean flags and `--flag=value` do not
62
+ need to be listed.
63
+
64
+ ## Output and errors
65
+
66
+ `resolveFormat` applies explicit `--json`, `--yaml`, or `--table` flags, then defaults to `table`
67
+ for a TTY and `json` for a pipe. `render` serializes a value and can select top-level fields.
68
+ `writeOutput` writes to stdout or a file and treats a closed stdout pipe as successful.
69
+
70
+ Catch errors at the executable boundary and render them once:
71
+
72
+ ```ts
73
+ const format = resolveFormat(program.opts(), process.stdout.isTTY === true);
74
+
75
+ try {
76
+ await program.parseAsync(args, { from: "user" });
77
+ } catch (error) {
78
+ const isNormalExit = typeof error === "object" && error && "exitCode" in error && error.exitCode === 0;
79
+ if (!isNormalExit) {
80
+ const reported = reportError(error, format);
81
+ process.stderr.write(reported.text);
82
+ process.exitCode = reported.exitCode;
83
+ }
84
+ }
85
+ ```
86
+
87
+ Commander error text is suppressed by `createProgram`, so the shared reporter is the only error
88
+ renderer. Help and version requests render normally, then throw Commander's zero-exit signal;
89
+ preserve that status without reporting it. Usage failures throw for the boundary to report once.
90
+
91
+ ## Mutations
92
+
93
+ Call `mutating(command)` for `send`, `submit`, `set`, and `mark-read` commands. The marker is
94
+ included by `commandsJson`. Command actions call `confirm` before making an upstream request:
95
+
96
+ ```ts
97
+ const shouldApply = await confirm(
98
+ { summary: "Set FIT1045 task 1.1 to complete" },
99
+ { yes: options.yes, dryRun: options.dryRun, interactive: process.stdin.isTTY === true },
100
+ );
101
+
102
+ if (!shouldApply) return;
103
+ ```
104
+
105
+ The plan and prompt are written to stderr. A non-interactive mutation without `--yes` throws a
106
+ `usage` error. A dry run prints its plan and returns `false`.
107
+
108
+ ## Command description
109
+
110
+ `commandsJson(program)` returns the program metadata and full command tree. Every node includes
111
+ aliases, positional arguments, options, enum values, nested commands, and `mutating`. Domain
112
+ commands at noun depth must use a verb exported in `VERBS`; unsupported verbs throw. `auth` and
113
+ `skills` are action groups rather than domain nouns and are not assigned a `verb` field.
114
+
115
+ The exit-code table, error vocabulary, and verb set are public versioned API. Changing one requires
116
+ a major package release.
117
+
118
+ The conformance suite is isolated under `conformance/`. Until all three CLIs have published their
119
+ normalized releases, missing binaries are reported as skipped tests. Its CI workflow installs the
120
+ published packages before running the suite.
@@ -0,0 +1,8 @@
1
+ export interface MutationPlan {
2
+ readonly summary: string;
3
+ }
4
+ export declare function confirm(plan: MutationPlan, options: {
5
+ yes: boolean;
6
+ dryRun: boolean;
7
+ interactive: boolean;
8
+ }): Promise<boolean>;
@@ -0,0 +1,22 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { CliError } from "./errors.js";
3
+ export async function confirm(plan, options) {
4
+ if (options.dryRun) {
5
+ process.stderr.write(`${plan.summary}\n`);
6
+ return false;
7
+ }
8
+ if (options.yes)
9
+ return true;
10
+ if (!options.interactive) {
11
+ throw new CliError("usage", "Mutation requires --yes when stdin is not interactive.");
12
+ }
13
+ process.stderr.write(`${plan.summary}\n`);
14
+ const prompt = createInterface({ input: process.stdin, output: process.stderr });
15
+ try {
16
+ const answer = await prompt.question("Continue? y/N ");
17
+ return answer.trim().toLowerCase() === "y";
18
+ }
19
+ finally {
20
+ prompt.close();
21
+ }
22
+ }
@@ -0,0 +1,10 @@
1
+ export declare const ERROR_CODES: readonly ["usage", "auth", "config", "not_found", "upstream", "network", "cancelled", "unexpected"];
2
+ export type ErrorCode = (typeof ERROR_CODES)[number];
3
+ export declare const EXIT_CODES: Readonly<Record<ErrorCode, number>>;
4
+ export declare class CliError extends Error {
5
+ readonly code: ErrorCode;
6
+ readonly hint: string | undefined;
7
+ constructor(code: ErrorCode, message: string, hint?: string);
8
+ }
9
+ export declare function exitCodeFor(code: ErrorCode): number;
10
+ export declare function normalizeError(error: unknown): CliError;
package/dist/errors.js ADDED
@@ -0,0 +1,51 @@
1
+ export const ERROR_CODES = [
2
+ "usage",
3
+ "auth",
4
+ "config",
5
+ "not_found",
6
+ "upstream",
7
+ "network",
8
+ "cancelled",
9
+ "unexpected",
10
+ ];
11
+ export const EXIT_CODES = Object.freeze({
12
+ usage: 2,
13
+ auth: 3,
14
+ config: 1,
15
+ not_found: 4,
16
+ upstream: 5,
17
+ network: 1,
18
+ cancelled: 130,
19
+ unexpected: 1,
20
+ });
21
+ export class CliError extends Error {
22
+ code;
23
+ hint;
24
+ constructor(code, message, hint) {
25
+ super(message);
26
+ this.name = "CliError";
27
+ this.code = code;
28
+ this.hint = hint;
29
+ }
30
+ }
31
+ export function exitCodeFor(code) {
32
+ return EXIT_CODES[code];
33
+ }
34
+ export function normalizeError(error) {
35
+ if (error instanceof CliError)
36
+ return error;
37
+ if (error instanceof Error) {
38
+ const candidate = error;
39
+ if (typeof candidate.code === "string" && candidate.code.startsWith("commander.")) {
40
+ return new CliError("usage", error.message.replace(/^error:\s*/i, ""));
41
+ }
42
+ if (error.name === "AbortError")
43
+ return new CliError("cancelled", "Operation cancelled.");
44
+ if (typeof candidate.code === "string" && NETWORK_CODES.has(candidate.code)) {
45
+ return new CliError("network", error.message);
46
+ }
47
+ return new CliError("unexpected", error.message);
48
+ }
49
+ return new CliError("unexpected", String(error));
50
+ }
51
+ const NETWORK_CODES = new Set(["ECONNREFUSED", "ECONNRESET", "ENETUNREACH", "ETIMEDOUT", "EAI_AGAIN"]);
@@ -0,0 +1,6 @@
1
+ export * from "./confirm.js";
2
+ export * from "./errors.js";
3
+ export * from "./nouns.js";
4
+ export * from "./output.js";
5
+ export * from "./program.js";
6
+ export * from "./report.js";
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from "./confirm.js";
2
+ export * from "./errors.js";
3
+ export * from "./nouns.js";
4
+ export * from "./output.js";
5
+ export * from "./program.js";
6
+ export * from "./report.js";
@@ -0,0 +1,9 @@
1
+ export interface NounSpec {
2
+ readonly name: string;
3
+ readonly aliases?: readonly string[];
4
+ readonly verbs: readonly string[];
5
+ readonly defaultByArity: Readonly<Record<number, string>>;
6
+ /** Option flags whose following token is a value, not a positional argument. */
7
+ readonly valueFlags?: readonly string[];
8
+ }
9
+ export declare function insertDefaultVerb(argv: readonly string[], nouns: readonly NounSpec[]): string[];
package/dist/nouns.js ADDED
@@ -0,0 +1,57 @@
1
+ const GLOBAL_VALUE_FLAGS = new Set(["--fields", "-o", "--output"]);
2
+ export function insertDefaultVerb(argv, nouns) {
3
+ const result = [...argv];
4
+ const nounIndex = findNounIndex(result, nouns);
5
+ if (nounIndex === -1)
6
+ return result;
7
+ const spec = nouns.find((noun) => noun.name === result[nounIndex] || noun.aliases?.includes(result[nounIndex] ?? ""));
8
+ if (!spec)
9
+ return result;
10
+ result[nounIndex] = spec.name;
11
+ const next = result[nounIndex + 1];
12
+ if (next && spec.verbs.includes(next))
13
+ return result;
14
+ if (result.includes("--help") || result.includes("-h"))
15
+ return result;
16
+ const arity = countPositionals(result.slice(nounIndex + 1), spec.valueFlags ?? []);
17
+ const verb = spec.defaultByArity[arity];
18
+ if (!verb)
19
+ return result;
20
+ result.splice(nounIndex + 1, 0, verb);
21
+ return result;
22
+ }
23
+ function findNounIndex(argv, nouns) {
24
+ const names = new Set(nouns.flatMap((noun) => [noun.name, ...(noun.aliases ?? [])]));
25
+ for (let index = 0; index < argv.length; index += 1) {
26
+ const token = argv[index];
27
+ if (!token)
28
+ continue;
29
+ if (GLOBAL_VALUE_FLAGS.has(token)) {
30
+ index += 1;
31
+ continue;
32
+ }
33
+ if (token.startsWith("-"))
34
+ continue;
35
+ return names.has(token) ? index : -1;
36
+ }
37
+ return -1;
38
+ }
39
+ function countPositionals(argv, valueFlags) {
40
+ const flagsWithValues = new Set([...GLOBAL_VALUE_FLAGS, ...valueFlags]);
41
+ let count = 0;
42
+ let terminated = false;
43
+ for (let index = 0; index < argv.length; index += 1) {
44
+ const token = argv[index];
45
+ if (token === "--") {
46
+ terminated = true;
47
+ continue;
48
+ }
49
+ if (!terminated && token && flagsWithValues.has(token)) {
50
+ index += 1;
51
+ continue;
52
+ }
53
+ if (terminated || (token && !token.startsWith("-")))
54
+ count += 1;
55
+ }
56
+ return count;
57
+ }
@@ -0,0 +1,16 @@
1
+ export type OutputFormat = "table" | "json" | "yaml";
2
+ export interface FormatOptions {
3
+ readonly json?: boolean;
4
+ readonly yaml?: boolean;
5
+ readonly table?: boolean;
6
+ readonly fields?: string;
7
+ }
8
+ export declare function resolveFormat(options: FormatOptions, isTty: boolean): OutputFormat;
9
+ export declare function render(value: unknown, options: {
10
+ format: OutputFormat;
11
+ fields?: readonly string[];
12
+ columns?: readonly [string, string][];
13
+ }): string;
14
+ export declare function writeOutput(text: string, options: {
15
+ output?: string;
16
+ }): Promise<void>;
package/dist/output.js ADDED
@@ -0,0 +1,63 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { stringify as stringifyYaml } from "yaml";
3
+ import { CliError } from "./errors.js";
4
+ export function resolveFormat(options, isTty) {
5
+ const selected = [options.json && "json", options.yaml && "yaml", options.table && "table"].filter(Boolean);
6
+ if (selected.length > 1) {
7
+ throw new CliError("usage", "--json, --yaml, and --table are mutually exclusive.");
8
+ }
9
+ return selected[0] ?? (isTty ? "table" : "json");
10
+ }
11
+ export function render(value, options) {
12
+ const filtered = options.fields?.length ? selectFields(value, options.fields) : value;
13
+ if (options.format === "json")
14
+ return `${JSON.stringify(filtered, null, 2)}\n`;
15
+ if (options.format === "yaml")
16
+ return stringifyYaml(filtered);
17
+ return renderTable(filtered, options.columns);
18
+ }
19
+ export async function writeOutput(text, options) {
20
+ if (options.output) {
21
+ await writeFile(options.output, text, "utf8");
22
+ return;
23
+ }
24
+ await new Promise((resolve, reject) => {
25
+ const onError = (error) => finish(error);
26
+ const finish = (error) => {
27
+ process.stdout.off("error", onError);
28
+ if (!error || error.code === "EPIPE")
29
+ resolve();
30
+ else
31
+ reject(error);
32
+ };
33
+ process.stdout.once("error", onError);
34
+ process.stdout.write(text, finish);
35
+ });
36
+ }
37
+ function selectFields(value, fields) {
38
+ if (Array.isArray(value))
39
+ return value.map((item) => selectFields(item, fields));
40
+ if (!isRecord(value))
41
+ return value;
42
+ return Object.fromEntries(fields.filter((field) => field in value).map((field) => [field, value[field]]));
43
+ }
44
+ function renderTable(value, columns) {
45
+ const rows = Array.isArray(value) ? value : [value];
46
+ if (rows.length === 0)
47
+ return "";
48
+ if (!rows.every(isRecord))
49
+ return `${rows.map(String).join("\n")}\n`;
50
+ const selected = columns ?? Object.keys(rows[0] ?? {}).map((key) => [key, key]);
51
+ const values = rows.map((row) => selected.map(([key]) => formatCell(row[key])));
52
+ const widths = selected.map(([, label], index) => Math.max(label.length, ...values.map((row) => row[index]?.length ?? 0)));
53
+ const line = (cells) => cells.map((cell, index) => cell.padEnd(widths[index] ?? 0)).join(" ").trimEnd();
54
+ return `${line(selected.map(([, label]) => label))}\n${values.map(line).join("\n")}\n`;
55
+ }
56
+ function formatCell(value) {
57
+ if (value === null || value === undefined)
58
+ return "";
59
+ return typeof value === "object" ? JSON.stringify(value) : String(value);
60
+ }
61
+ function isRecord(value) {
62
+ return typeof value === "object" && value !== null && !Array.isArray(value);
63
+ }
@@ -0,0 +1,40 @@
1
+ import { Command } from "commander";
2
+ export declare const VERBS: readonly ["list", "show", "read", "get", "search", "send", "submit", "set", "mark-read"];
3
+ export interface CommandDescription {
4
+ readonly name: string;
5
+ readonly noun?: string;
6
+ readonly verb?: string;
7
+ readonly aliases: readonly string[];
8
+ readonly description: string;
9
+ readonly positionals: readonly PositionalDescription[];
10
+ readonly options: readonly OptionDescription[];
11
+ readonly mutating: boolean;
12
+ readonly commands: readonly CommandDescription[];
13
+ }
14
+ export interface PositionalDescription {
15
+ readonly name: string;
16
+ readonly description: string;
17
+ readonly required: boolean;
18
+ readonly variadic: boolean;
19
+ readonly enumValues?: readonly string[];
20
+ }
21
+ export interface OptionDescription {
22
+ readonly flags: string;
23
+ readonly description: string;
24
+ readonly required: boolean;
25
+ readonly variadic: boolean;
26
+ readonly enumValues?: readonly string[];
27
+ }
28
+ export interface ProgramDescription {
29
+ readonly name: string;
30
+ readonly version: string;
31
+ readonly description: string;
32
+ readonly commands: readonly CommandDescription[];
33
+ }
34
+ export declare function createProgram(meta: {
35
+ name: string;
36
+ version: string;
37
+ description: string;
38
+ }): Command;
39
+ export declare function mutating(command: Command): Command;
40
+ export declare function commandsJson(program: Command): ProgramDescription;
@@ -0,0 +1,81 @@
1
+ import { Command } from "commander";
2
+ export const VERBS = ["list", "show", "read", "get", "search", "send", "submit", "set", "mark-read"];
3
+ const VERB_SET = new Set(VERBS);
4
+ const mutations = new WeakSet();
5
+ const ACTION_GROUPS = new Set(["auth", "skills"]);
6
+ const ACTIONS = {
7
+ auth: new Set(["install", "uninstall", "login", "status", "logout", "keepalive"]),
8
+ skills: new Set(["generate", "add"]),
9
+ };
10
+ export function createProgram(meta) {
11
+ return new Command()
12
+ .name(meta.name)
13
+ .version(meta.version, "-V, --version")
14
+ .description(meta.description)
15
+ .helpOption("-h, --help")
16
+ .addHelpCommand(true)
17
+ .showSuggestionAfterError()
18
+ .exitOverride()
19
+ .configureOutput({ outputError: () => undefined })
20
+ .option("--json", "Emit JSON")
21
+ .option("--yaml", "Emit YAML")
22
+ .option("--table", "Emit a human-readable table")
23
+ .option("--fields <fields>", "Select top-level fields")
24
+ .option("-o, --output <file>", "Write output to a file")
25
+ .option("-q, --quiet", "Suppress non-essential diagnostics")
26
+ .option("--verbose", "Enable debug logging")
27
+ .option("--no-color", "Disable color output")
28
+ .option("-y, --yes", "Confirm mutations non-interactively")
29
+ .option("--dry-run", "Print the mutation plan without applying it");
30
+ }
31
+ export function mutating(command) {
32
+ mutations.add(command);
33
+ return command;
34
+ }
35
+ export function commandsJson(program) {
36
+ return {
37
+ name: program.name(),
38
+ version: program.version() ?? "",
39
+ description: program.description(),
40
+ commands: program.commands.filter((command) => command.name() !== "help").map((command) => describe(command)),
41
+ };
42
+ }
43
+ function describe(command, noun) {
44
+ const currentNoun = noun ?? command.name();
45
+ const verb = noun && !ACTION_GROUPS.has(noun) ? command.name() : undefined;
46
+ if (noun && ACTION_GROUPS.has(noun) && !ACTIONS[noun]?.has(command.name())) {
47
+ throw new Error(`Unsupported action "${command.name()}" on group "${noun}".`);
48
+ }
49
+ if (verb && !VERB_SET.has(verb)) {
50
+ throw new Error(`Unsupported verb "${verb}" on noun "${noun}".`);
51
+ }
52
+ return {
53
+ name: command.name(),
54
+ noun: currentNoun,
55
+ ...(verb ? { verb } : {}),
56
+ aliases: command.aliases(),
57
+ description: command.description(),
58
+ positionals: command.registeredArguments.map(describeArgument),
59
+ options: command.options.map(describeOption),
60
+ mutating: mutations.has(command),
61
+ commands: command.commands.filter((child) => child.name() !== "help").map((child) => describe(child, currentNoun)),
62
+ };
63
+ }
64
+ function describeArgument(argument) {
65
+ return {
66
+ name: argument.name(),
67
+ description: argument.description,
68
+ required: argument.required,
69
+ variadic: argument.variadic,
70
+ ...(argument.argChoices ? { enumValues: argument.argChoices } : {}),
71
+ };
72
+ }
73
+ function describeOption(option) {
74
+ return {
75
+ flags: option.flags,
76
+ description: option.description,
77
+ required: option.required,
78
+ variadic: option.variadic,
79
+ ...(option.argChoices ? { enumValues: option.argChoices } : {}),
80
+ };
81
+ }
@@ -0,0 +1,5 @@
1
+ import type { OutputFormat } from "./output.js";
2
+ export declare function reportError(error: unknown, format: OutputFormat): {
3
+ readonly text: string;
4
+ readonly exitCode: number;
5
+ };
package/dist/report.js ADDED
@@ -0,0 +1,23 @@
1
+ import { stringify as stringifyYaml } from "yaml";
2
+ import { exitCodeFor, normalizeError } from "./errors.js";
3
+ export function reportError(error, format) {
4
+ const normalized = normalizeError(error);
5
+ const exitCode = exitCodeFor(normalized.code);
6
+ if (format === "table") {
7
+ const hint = normalized.hint ? `\nhint: ${normalized.hint}` : "";
8
+ return { text: `error: ${normalized.message}${hint}\n`, exitCode };
9
+ }
10
+ const envelope = {
11
+ ok: false,
12
+ error: {
13
+ code: normalized.code,
14
+ message: normalized.message,
15
+ ...(normalized.hint ? { hint: normalized.hint } : {}),
16
+ },
17
+ exit_code: exitCode,
18
+ };
19
+ return {
20
+ text: format === "json" ? `${JSON.stringify(envelope, null, 2)}\n` : stringifyYaml(envelope),
21
+ exitCode,
22
+ };
23
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@bunizao/cli-kit",
3
+ "version": "0.1.0",
4
+ "description": "Shared command-line contract for Bunizao CLI tools",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ }
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json",
19
+ "check": "tsc -p tsconfig.json --noEmit",
20
+ "test": "npm run build && node --test test/*.test.js",
21
+ "test:conformance": "npm --prefix conformance test",
22
+ "prepack": "npm test"
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "dependencies": {
28
+ "commander": "^13.1.0",
29
+ "yaml": "^2.8.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^22.15.0",
33
+ "typescript": "^5.8.3"
34
+ },
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/bunizao/cli-kit.git"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ }
43
+ }