@spader/dotllm 1.0.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 ADDED
@@ -0,0 +1,25 @@
1
+ # motivation
2
+ LLMs work best with references. If you're like me, every project has loose repositories lying around half a dozen places because some LLM needed it.
3
+
4
+ Instead, keep a cache of such repositories and use `dotllm` to link them to `.llm`. Check in `.llm/dotllm.json`, and a simple `dotllm sync` will restore all of your references.
5
+
6
+ # installation
7
+ ```bash
8
+ bun install -g @spader/dotllm
9
+ ```
10
+
11
+ # usage
12
+ Register a repository (by HTTPS, SSH, or path)
13
+ ```bash
14
+ dotllm add https://github.com/tspader/dotllm.git
15
+ ```
16
+
17
+ In any directory, use the interactive CLI to edit which repositories are linked to `.llm/reference`
18
+ ```bash
19
+ dotllm link
20
+ ```
21
+
22
+ Check in `.llm/dotllm.json`. Then, on a fresh clone, link the repositories to your local copy
23
+ ```bash
24
+ dotllm sync
25
+ ```
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@spader/dotllm",
3
+ "version": "1.0.0",
4
+ "description": "A simple CLI to clone, manage, and link repositories for LLM reference",
5
+ "type": "module",
6
+ "bin": {
7
+ "dotllm": "src/cli/index.ts"
8
+ },
9
+ "scripts": {
10
+ "check": "bunx tsc --noEmit",
11
+ "prepublishOnly": "bun run check"
12
+ },
13
+ "files": [
14
+ "src/**/*.ts",
15
+ "README.md"
16
+ ],
17
+ "exports": {
18
+ "./cli": "./src/cli/index.ts",
19
+ "./cli/*": "./src/cli/*.ts",
20
+ "./core": "./src/core/index.ts",
21
+ "./core/*": "./src/core/*.ts"
22
+ },
23
+ "engines": {
24
+ "bun": ">=1.1.0"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "keywords": [
30
+ "cli",
31
+ "bun",
32
+ "dotfiles",
33
+ "symlink",
34
+ "llm"
35
+ ],
36
+ "license": "UNLICENSED",
37
+ "dependencies": {
38
+ "@clack/prompts": "^0.10.0",
39
+ "picocolors": "^1.1.1",
40
+ "yargs": "^17.7.2",
41
+ "zod": "^4.3.6"
42
+ },
43
+ "devDependencies": {
44
+ "@types/bun": "latest",
45
+ "@types/yargs": "^17.0.33"
46
+ }
47
+ }
@@ -0,0 +1,177 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import * as prompts from "@clack/prompts";
4
+ import { z } from "zod";
5
+ import { add } from "@spader/dotllm/core";
6
+ import { defaultTheme as t } from "@spader/dotllm/cli/theme";
7
+ import type { CommandDef } from "@spader/dotllm/cli/yargs";
8
+
9
+ const RepoShape = z.object({
10
+ description: z.string().nullable().optional(),
11
+ });
12
+
13
+ function isUrl(value: string): boolean {
14
+ return value.startsWith("http://") ||
15
+ value.startsWith("https://") ||
16
+ value.startsWith("git@") ||
17
+ value.startsWith("ssh://");
18
+ }
19
+
20
+ function stem(value: string): string {
21
+ const clean = value.trim().replace(/\/+$/, "");
22
+ if (clean.length === 0) return "";
23
+
24
+ if (clean.startsWith("git@")) {
25
+ const raw = clean.split(":").slice(1).join(":");
26
+ const seg = raw.split("/").filter(Boolean).pop() ?? raw;
27
+ return seg.replace(/\.git$/, "");
28
+ }
29
+
30
+ if (isUrl(clean)) {
31
+ const seg = clean.split("/").filter(Boolean).pop() ?? clean;
32
+ const raw = seg.split("?")[0] ?? seg;
33
+ const full = raw.split("#")[0] ?? raw;
34
+ return full.replace(/\.git$/, "");
35
+ }
36
+
37
+ const base = path.basename(clean);
38
+ const parsed = path.parse(base);
39
+ if (parsed.name.length > 0) return parsed.name;
40
+ return base;
41
+ }
42
+
43
+ function github(uri: string): { owner: string; repo: string } | null {
44
+ const https = uri.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
45
+ if (https) {
46
+ return { owner: https[1]!, repo: https[2]! };
47
+ }
48
+
49
+ const ssh = uri.match(/^ssh:\/\/git@github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
50
+ if (ssh) {
51
+ return { owner: ssh[1]!, repo: ssh[2]! };
52
+ }
53
+
54
+ const scp = uri.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/);
55
+ if (scp) {
56
+ return { owner: scp[1]!, repo: scp[2]! };
57
+ }
58
+
59
+ return null;
60
+ }
61
+
62
+ function gitName(uri: string): string {
63
+ const dir = path.resolve(uri);
64
+ if (!fs.existsSync(dir)) return "";
65
+ if (!fs.statSync(dir).isDirectory()) return "";
66
+
67
+ const proc = Bun.spawnSync(["git", "remote", "get-url", "origin"], {
68
+ cwd: dir,
69
+ stdout: "pipe",
70
+ stderr: "pipe",
71
+ });
72
+ if (proc.exitCode !== 0) return "";
73
+
74
+ const out = proc.stdout.toString().trim();
75
+ if (out.length === 0) return "";
76
+ return stem(out);
77
+ }
78
+
79
+ async function remoteDescription(uri: string): Promise<string> {
80
+ const repo = github(uri);
81
+ if (!repo) return "";
82
+
83
+ const url = `https://api.github.com/repos/${repo.owner}/${repo.repo}`;
84
+ const response = await fetch(url, {
85
+ headers: {
86
+ accept: "application/vnd.github+json",
87
+ "user-agent": "dotllm",
88
+ },
89
+ });
90
+ if (!response.ok) return "";
91
+
92
+ const raw = await response.json();
93
+ const result = RepoShape.safeParse(raw);
94
+ if (!result.success) return "";
95
+ return result.data.description ?? "";
96
+ }
97
+
98
+ async function prefill(uri: string): Promise<{ name: string; description: string }> {
99
+ const remote = isUrl(uri);
100
+ const git = remote ? "" : gitName(uri);
101
+ const name = git.length > 0 ? git : stem(uri);
102
+ const description = remote ? await remoteDescription(uri) : "";
103
+ return { name, description };
104
+ }
105
+
106
+ async function interactive(): Promise<void> {
107
+ const uri = await prompts.text({
108
+ message: "URL or local path to a git repo",
109
+ });
110
+ if (prompts.isCancel(uri)) return;
111
+
112
+ const input = uri.trim();
113
+ const seed = await prefill(input);
114
+
115
+ const name = await prompts.text({
116
+ message: "Name (leave empty to auto-detect)",
117
+ defaultValue: seed.name,
118
+ });
119
+ if (prompts.isCancel(name)) return;
120
+
121
+ const description = await prompts.text({
122
+ message: "Description",
123
+ defaultValue: seed.description,
124
+ });
125
+ if (prompts.isCancel(description)) return;
126
+
127
+ await run(input, name || undefined, description || undefined);
128
+ }
129
+
130
+ async function run(uri: string, name?: string, description?: string): Promise<void> {
131
+ const spinner = prompts.spinner();
132
+ spinner.start(`Adding ${uri}`);
133
+
134
+ const result = await add(uri, name, description);
135
+ if (!result.ok) {
136
+ spinner.stop(t.error(result.error), 1);
137
+ process.exit(1);
138
+ return;
139
+ }
140
+
141
+ spinner.stop(`${t.success("added")} ${t.primary(result.entry.name)} ${t.link(result.storePath)}`);
142
+ }
143
+
144
+ export const command: CommandDef = {
145
+ description: "Register a git repo as a reference",
146
+ summary: "Add a repo to the registry",
147
+ positionals: {
148
+ uri: {
149
+ type: "string",
150
+ description: "URL or local path to a git repo",
151
+ },
152
+ },
153
+ options: {
154
+ name: {
155
+ alias: "n",
156
+ type: "string",
157
+ description: "Name override (defaults to repo name from git)",
158
+ },
159
+ description: {
160
+ alias: "d",
161
+ type: "string",
162
+ description: "Description of the reference",
163
+ },
164
+ },
165
+ handler: async (argv) => {
166
+ const uri = typeof argv.uri === "string" && argv.uri.length > 0 ? argv.uri : undefined;
167
+
168
+ if (!uri) {
169
+ await interactive();
170
+ return;
171
+ }
172
+
173
+ const name = typeof argv.name === "string" && argv.name.length > 0 ? argv.name : undefined;
174
+ const description = typeof argv.description === "string" ? argv.description : undefined;
175
+ await run(uri, name, description);
176
+ },
177
+ };
@@ -0,0 +1,5 @@
1
+ export { command as add } from "@spader/dotllm/cli/commands/add";
2
+ export { command as remove } from "@spader/dotllm/cli/commands/remove";
3
+ export { command as list } from "@spader/dotllm/cli/commands/list";
4
+ export { command as link } from "@spader/dotllm/cli/commands/link";
5
+ export { command as sync } from "@spader/dotllm/cli/commands/sync";
@@ -0,0 +1,54 @@
1
+ import * as prompts from "@clack/prompts";
2
+ import { Config, link, type SyncResult } from "@spader/dotllm/core";
3
+ import { defaultTheme as t } from "@spader/dotllm/cli/theme";
4
+ import type { CommandDef } from "@spader/dotllm/cli/yargs";
5
+
6
+ function printResult(result: SyncResult): void {
7
+ for (const name of result.removed) {
8
+ prompts.log.step(`unlinked ${t.primary(name)} -> ${t.link(`.llm/reference/${name}`)}`);
9
+ }
10
+ for (const name of result.linked) {
11
+ prompts.log.step(`linked ${t.primary(name)} -> ${t.link(`.llm/reference/${name}`)}`);
12
+ }
13
+ }
14
+
15
+ export const command: CommandDef = {
16
+ description: "Interactively select repos to symlink into .llm/reference/",
17
+ summary: "Select and link references",
18
+ handler: async () => {
19
+ const global = Config.Global.read();
20
+
21
+ if (global.repos.length === 0) {
22
+ console.log(t.dim("(no repos registered)"));
23
+ console.log(t.dim("use `dotllm add <path>` to register one"));
24
+ return;
25
+ }
26
+
27
+ const local = Config.Local.read();
28
+ const current = new Set(Object.keys(local.refs));
29
+
30
+ const selected = await prompts.multiselect({
31
+ message: "Select repos to link into .llm/reference/",
32
+ options: global.repos.map((r) => ({
33
+ value: r.name,
34
+ label: r.name,
35
+ hint: r.uri,
36
+ })),
37
+ initialValues: global.repos
38
+ .filter((r) => current.has(r.name))
39
+ .map((r) => r.name),
40
+ required: false,
41
+ });
42
+
43
+ if (prompts.isCancel(selected)) {
44
+ prompts.cancel("cancelled");
45
+ return;
46
+ }
47
+
48
+ const names = Array.isArray(selected)
49
+ ? selected.filter((value): value is string => typeof value === "string")
50
+ : [];
51
+
52
+ printResult(link(names));
53
+ },
54
+ };
@@ -0,0 +1,44 @@
1
+ import { Config } from "@spader/dotllm/core";
2
+ import { table } from "@spader/dotllm/cli/layout";
3
+ import { defaultTheme as t } from "@spader/dotllm/cli/theme";
4
+ import type { CommandDef } from "@spader/dotllm/cli/yargs";
5
+
6
+ export const command: CommandDef = {
7
+ description: "List all registered repos",
8
+ summary: "Show the registry",
9
+ handler: () => {
10
+ const global = Config.Global.read();
11
+
12
+ if (global.repos.length === 0) {
13
+ console.log(t.dim("(no repos registered)"));
14
+ console.log(t.dim("use `dotllm add <path>` to register one"));
15
+ return;
16
+ }
17
+
18
+ const local = Config.Local.read();
19
+ const linked = new Set(Object.keys(local.refs));
20
+
21
+ table(
22
+ ["name", "kind", "uri", "description", "linked"],
23
+ [
24
+ global.repos.map((r) => r.name),
25
+ global.repos.map((r) => r.kind),
26
+ global.repos.map((r) => r.uri),
27
+ global.repos.map((r) => r.description),
28
+ global.repos.map((r) => linked.has(r.name) ? "yes" : "no"),
29
+ ],
30
+ {
31
+ flex: [0, 0, 1, 1, 0],
32
+ noTruncate: [true, true, false, false, true],
33
+ truncate: ["end", "end", "start", "end", "end"],
34
+ format: [
35
+ (s) => t.primary(s),
36
+ (s) => s,
37
+ (s) => t.link(s),
38
+ (s) => s,
39
+ (s) => s.trim() === "yes" ? t.success(s) : s,
40
+ ],
41
+ },
42
+ );
43
+ },
44
+ };
@@ -0,0 +1,28 @@
1
+ import { log } from "@clack/prompts";
2
+ import { remove } from "@spader/dotllm/core";
3
+ import { defaultTheme as t } from "@spader/dotllm/cli/theme";
4
+ import type { CommandDef } from "@spader/dotllm/cli/yargs";
5
+
6
+ export const command: CommandDef = {
7
+ description: "Remove a repo from the registry",
8
+ summary: "Remove a registered repo",
9
+ positionals: {
10
+ name: {
11
+ type: "string",
12
+ description: "Name of the reference to remove",
13
+ required: true,
14
+ },
15
+ },
16
+ handler: (argv) => {
17
+ const name = String(argv.name);
18
+
19
+ const result = remove(name);
20
+ if (!result.ok) {
21
+ log.error(t.error(result.error));
22
+ process.exit(1);
23
+ return;
24
+ }
25
+
26
+ log.step(`removed ${t.primary(name)}`);
27
+ },
28
+ };
@@ -0,0 +1,45 @@
1
+ import * as prompts from "@clack/prompts";
2
+ import { pull, sync } from "@spader/dotllm/core";
3
+ import { defaultTheme as t } from "@spader/dotllm/cli/theme";
4
+ import type { CommandDef } from "@spader/dotllm/cli/yargs";
5
+
6
+ export const command: CommandDef = {
7
+ description: "Re-create symlinks from .llm/dotllm.json",
8
+ summary: "Sync symlinks from local config",
9
+ handler: async () => {
10
+ const result = sync();
11
+
12
+ if (result.linked.length === 0 && result.removed.length === 0 && result.missing.length === 0 && result.unchanged.length === 0) {
13
+ console.log(t.dim("(no refs in local config)"));
14
+ console.log(t.dim("use `dotllm link` to select repos"));
15
+ return;
16
+ }
17
+
18
+ for (const name of result.removed) {
19
+ prompts.log.step(`removed stale ${t.primary(name)}`);
20
+ }
21
+ for (const name of result.missing) {
22
+ prompts.log.warn(`${t.primary(name)} missing cache entry`);
23
+ }
24
+ for (const name of result.linked) {
25
+ prompts.log.step(`linked ${t.primary(name)} -> ${t.link(`.llm/reference/${name}`)}`);
26
+ }
27
+
28
+ const refs = [...new Set([...result.unchanged, ...result.linked])];
29
+ if (refs.length === 0) {
30
+ return;
31
+ }
32
+
33
+ const spinner = prompts.spinner();
34
+ spinner.start(`Pulling ${refs.length} linked repo${refs.length === 1 ? "" : "s"}`);
35
+
36
+ const pulled = await pull(refs);
37
+ if (pulled.failed.length > 0) {
38
+ spinner.stop(t.error(`pull failed for ${pulled.failed.length} repo${pulled.failed.length === 1 ? "" : "s"}`), 1);
39
+ process.exit(1);
40
+ return;
41
+ }
42
+
43
+ spinner.stop(`${t.success("pulled")} ${pulled.count} repo${pulled.count === 1 ? "" : "s"}`);
44
+ },
45
+ };
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { build, type CliDef } from "@spader/dotllm/cli/yargs";
4
+ import { add, remove, list, link, sync } from "@spader/dotllm/cli/commands/index";
5
+
6
+ export namespace DotLlmCli {
7
+ export const def: CliDef = {
8
+ name: "dotllm",
9
+ description: "Manage git repo references symlinked into .llm/reference/",
10
+ commands: {
11
+ add,
12
+ remove,
13
+ list,
14
+ link,
15
+ sync,
16
+ },
17
+ };
18
+
19
+ export function run(): void {
20
+ build(def).parse();
21
+ }
22
+ }
23
+
24
+ DotLlmCli.run();
@@ -0,0 +1,200 @@
1
+ import { defaultTheme as theme } from "@spader/dotllm/cli/theme";
2
+
3
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
4
+
5
+ type TruncateMode = "start" | "middle" | "end";
6
+
7
+ export type TableOptions = {
8
+ maxWidth?: number;
9
+ flex?: number[];
10
+ noTruncate?: boolean[];
11
+ truncate?: TruncateMode[];
12
+ format?: Array<(value: string, row: number, col: number) => string>;
13
+ maxRows?: number;
14
+ };
15
+
16
+ function clean(value: string): string {
17
+ return value.replace(/[\t\n]/g, " ").replace(ANSI_RE, "");
18
+ }
19
+
20
+ function truncateMiddle(value: string, width: number): string {
21
+ if (width <= 0) return "";
22
+ if (value.length <= width) return value;
23
+ if (width <= 3) return value.slice(0, width);
24
+
25
+ const tail = Math.floor((width - 3) / 2);
26
+ const head = width - 3 - tail;
27
+ return `${value.slice(0, head)}...${value.slice(value.length - tail)}`;
28
+ }
29
+
30
+ function truncateStart(value: string, width: number): string {
31
+ if (width <= 0) return "";
32
+ if (value.length <= width) return value;
33
+ if (width <= 3) return "...".slice(0, width);
34
+
35
+ return `...${value.slice(value.length - (width - 3))}`;
36
+ }
37
+
38
+ function truncateEnd(value: string, width: number): string {
39
+ if (width <= 0) return "";
40
+ if (value.length <= width) return value;
41
+ if (width <= 3) return "...".slice(0, width);
42
+
43
+ return `${value.slice(0, width - 3)}...`;
44
+ }
45
+
46
+ function truncate(value: string, width: number, mode: TruncateMode): string {
47
+ if (mode === "start") return truncateStart(value, width);
48
+ if (mode === "end") return truncateEnd(value, width);
49
+ return truncateMiddle(value, width);
50
+ }
51
+
52
+ function fitWidths(
53
+ natural: number[],
54
+ available: number,
55
+ flex: number[],
56
+ noTruncate: boolean[],
57
+ ): number[] {
58
+ const widths = [...natural];
59
+ const totalNatural = natural.reduce((sum, width) => sum + width, 0);
60
+
61
+ if (totalNatural <= available) {
62
+ return widths;
63
+ }
64
+
65
+ let fixedWidth = 0;
66
+ const dynamic: Array<{ index: number; weight: number }> = [];
67
+
68
+ for (let index = 0; index < widths.length; index++) {
69
+ if (noTruncate[index] || flex[index] === 0) {
70
+ fixedWidth += widths[index] ?? 0;
71
+ continue;
72
+ }
73
+
74
+ dynamic.push({ index, weight: flex[index] ?? 1 });
75
+ }
76
+
77
+ const remaining = Math.max(0, available - fixedWidth);
78
+
79
+ if (dynamic.length === 0) {
80
+ return widths;
81
+ }
82
+
83
+ const totalWeight = dynamic.reduce((sum, item) => sum + item.weight, 0);
84
+ let used = 0;
85
+
86
+ for (const item of dynamic) {
87
+ const share = Math.floor((remaining * item.weight) / totalWeight);
88
+ const width = Math.max(1, share);
89
+ widths[item.index] = width;
90
+ used += width;
91
+ }
92
+
93
+ let extra = remaining - used;
94
+ let cursor = 0;
95
+ while (extra > 0) {
96
+ const item = dynamic[cursor % dynamic.length]!;
97
+ widths[item.index] = (widths[item.index] ?? 0) + 1;
98
+ extra--;
99
+ cursor++;
100
+ }
101
+
102
+ return widths;
103
+ }
104
+
105
+ export function table(
106
+ headers: string[],
107
+ columns: string[][],
108
+ options: TableOptions = {},
109
+ ): void {
110
+ if (headers.length === 0) return;
111
+
112
+ const count = headers.length;
113
+ const gap = 2;
114
+ const rows = columns.reduce((max, column) => Math.max(max, column.length), 0);
115
+ const visibleRows = Math.min(rows, options.maxRows ?? rows);
116
+
117
+ const natural: number[] = [];
118
+
119
+ for (let col = 0; col < count; col++) {
120
+ const headerWidth = clean(headers[col] ?? "").length;
121
+ let width = headerWidth;
122
+
123
+ for (let row = 0; row < visibleRows; row++) {
124
+ const value = clean(columns[col]?.[row] ?? "");
125
+ width = Math.max(width, value.length);
126
+ }
127
+
128
+ natural[col] = width;
129
+ }
130
+
131
+ const maxWidth =
132
+ options.maxWidth ??
133
+ (process.stdout.columns == null ? 120 : process.stdout.columns);
134
+ const available = Math.max(0, maxWidth - gap * (count - 1) - 1);
135
+ const widths = fitWidths(
136
+ natural,
137
+ available,
138
+ options.flex ?? [],
139
+ options.noTruncate ?? [],
140
+ );
141
+
142
+ const header = headers
143
+ .map((title, col) =>
144
+ truncateMiddle(clean(title), widths[col] ?? 0).padEnd(widths[col] ?? 0),
145
+ )
146
+ .join(" ");
147
+
148
+ process.stdout.write(`${theme.dim(header)}\n`);
149
+
150
+ for (let row = 0; row < visibleRows; row++) {
151
+ const cells: string[] = [];
152
+
153
+ for (let col = 0; col < count; col++) {
154
+ const width = widths[col] ?? 0;
155
+ const source = clean(columns[col]?.[row] ?? "");
156
+ const mode = options.truncate?.[col] ?? "middle";
157
+ const value = options.noTruncate?.[col]
158
+ ? source
159
+ : truncate(source, width, mode);
160
+ const padded = width > 0 ? value.padEnd(width) : value;
161
+ const formatted = options.format?.[col]?.(padded, row, col) ?? padded;
162
+
163
+ cells.push(formatted);
164
+ }
165
+
166
+ process.stdout.write(`${cells.join(" ")}\n`);
167
+ }
168
+
169
+ if (rows > visibleRows) {
170
+ process.stdout.write(`${theme.dim("(...truncated)")}\n`);
171
+ }
172
+ }
173
+
174
+ export function cols(
175
+ rows: string[][],
176
+ colorFns?: Array<(value: string) => string>,
177
+ ): void {
178
+ if (rows.length === 0) return;
179
+
180
+ const widths = rows[0]!.map((_, col) => {
181
+ let width = 0;
182
+
183
+ for (const row of rows) {
184
+ width = Math.max(width, clean(row[col] ?? "").length);
185
+ }
186
+
187
+ return width;
188
+ });
189
+
190
+ for (const row of rows) {
191
+ const line = row
192
+ .map((value, col) => {
193
+ const padded = clean(value).padEnd(widths[col] ?? 0);
194
+ return colorFns?.[col]?.(padded) ?? padded;
195
+ })
196
+ .join(" ");
197
+
198
+ process.stdout.write(`${line}\n`);
199
+ }
200
+ }
@@ -0,0 +1,35 @@
1
+ type Formatter = (value: string) => string;
2
+
3
+ export type Theme = {
4
+ primary: Formatter;
5
+ link: Formatter;
6
+ header: Formatter;
7
+ command: Formatter;
8
+ arg: Formatter;
9
+ option: Formatter;
10
+ type: Formatter;
11
+ description: Formatter;
12
+ dim: Formatter;
13
+ error: Formatter;
14
+ success: Formatter;
15
+ };
16
+
17
+ function rgb(r: number, g: number, b: number): Formatter {
18
+ return (value: string) => `\x1b[38;2;${r};${g};${b}m${value}\x1b[39m`;
19
+ }
20
+
21
+ const gray = (value: number) => rgb(value, value, value);
22
+
23
+ export const defaultTheme: Theme = {
24
+ primary: rgb(114, 161, 136),
25
+ link: rgb(114, 140, 212),
26
+ header: gray(128),
27
+ command: rgb(114, 161, 136),
28
+ arg: rgb(161, 212, 212),
29
+ option: rgb(212, 212, 161),
30
+ type: gray(128),
31
+ description: (value) => value,
32
+ dim: gray(128),
33
+ error: rgb(212, 114, 114),
34
+ success: rgb(114, 212, 136),
35
+ };