@spader/dotllm 1.0.0 → 1.2.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.
@@ -0,0 +1,112 @@
1
+ // @bun
2
+ // src/core/config.ts
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { z } from "zod";
6
+ var LOCAL_DIR = ".llm";
7
+ var LOCAL_FILE = path.join(LOCAL_DIR, "dotllm.json");
8
+ var REF_DIR = path.join(LOCAL_DIR, "reference");
9
+ function home() {
10
+ return path.join(process.env.HOME ?? "", ".local", "share", "dotllm");
11
+ }
12
+ var RepoEntry = z.object({
13
+ kind: z.enum(["url", "file"]),
14
+ name: z.string(),
15
+ uri: z.string(),
16
+ description: z.string()
17
+ });
18
+ var GlobalShape = z.object({
19
+ repos: z.array(RepoEntry)
20
+ });
21
+ var LocalShape = z.object({
22
+ refs: z.record(z.string(), RepoEntry)
23
+ });
24
+ var Config;
25
+ ((Config) => {
26
+ function storeDir() {
27
+ return path.join(home(), "store");
28
+ }
29
+ Config.storeDir = storeDir;
30
+ function refDir() {
31
+ return REF_DIR;
32
+ }
33
+ Config.refDir = refDir;
34
+ let Global;
35
+ ((Global) => {
36
+ function read() {
37
+ const raw = readJson(path.join(home(), "dotllm.json"));
38
+ if (!raw)
39
+ return { repos: [] };
40
+ const result = GlobalShape.safeParse(raw);
41
+ if (!result.success)
42
+ return { repos: [] };
43
+ return result.data;
44
+ }
45
+ Global.read = read;
46
+ function write(config) {
47
+ const dir = home();
48
+ fs.mkdirSync(dir, { recursive: true });
49
+ fs.writeFileSync(path.join(dir, "dotllm.json"), JSON.stringify(config, null, 2) + `
50
+ `);
51
+ }
52
+ Global.write = write;
53
+ function find(config, name) {
54
+ return config.repos.find((r) => r.name === name);
55
+ }
56
+ Global.find = find;
57
+ function add(config, entry) {
58
+ const filtered = config.repos.filter((r) => r.name !== entry.name);
59
+ return { repos: [...filtered, entry] };
60
+ }
61
+ Global.add = add;
62
+ function remove(config, name) {
63
+ return { repos: config.repos.filter((r) => r.name !== name) };
64
+ }
65
+ Global.remove = remove;
66
+ })(Global = Config.Global ||= {});
67
+ let Local;
68
+ ((Local) => {
69
+ function read() {
70
+ const raw = readJson(LOCAL_FILE);
71
+ if (!raw)
72
+ return { refs: {} };
73
+ const result = LocalShape.safeParse(raw);
74
+ if (!result.success)
75
+ return { refs: {} };
76
+ return result.data;
77
+ }
78
+ Local.read = read;
79
+ function write(config) {
80
+ fs.mkdirSync(LOCAL_DIR, { recursive: true });
81
+ fs.writeFileSync(LOCAL_FILE, JSON.stringify(config, null, 2) + `
82
+ `);
83
+ }
84
+ Local.write = write;
85
+ function has(config, name) {
86
+ return Object.prototype.hasOwnProperty.call(config.refs, name);
87
+ }
88
+ Local.has = has;
89
+ function add(config, repo) {
90
+ return { refs: { ...config.refs, [repo.name]: repo } };
91
+ }
92
+ Local.add = add;
93
+ function remove(config, name) {
94
+ const refs = Object.fromEntries(Object.entries(config.refs).filter(([key]) => key !== name));
95
+ return { refs };
96
+ }
97
+ Local.remove = remove;
98
+ })(Local = Config.Local ||= {});
99
+ })(Config ||= {});
100
+ function readJson(filepath) {
101
+ if (!fs.existsSync(filepath))
102
+ return null;
103
+ const raw = fs.readFileSync(filepath, "utf-8");
104
+ try {
105
+ return JSON.parse(raw);
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+ export {
111
+ Config
112
+ };
@@ -0,0 +1,17 @@
1
+ // @bun
2
+ // src/core/index.ts
3
+ import { Config } from "@spader/dotllm/core/config";
4
+ import { add } from "@spader/dotllm/core/add";
5
+ import { remove } from "@spader/dotllm/core/remove";
6
+ import { link } from "@spader/dotllm/core/link";
7
+ import { unlink } from "@spader/dotllm/core/unlink";
8
+ import { pull, sync } from "@spader/dotllm/core/sync";
9
+ export {
10
+ unlink,
11
+ sync,
12
+ remove,
13
+ pull,
14
+ link,
15
+ add,
16
+ Config
17
+ };
@@ -0,0 +1,18 @@
1
+ // @bun
2
+ // src/core/link.ts
3
+ import { Config } from "@spader/dotllm/core/config";
4
+ import { sync } from "@spader/dotllm/core/sync";
5
+ function link(names) {
6
+ const global = Config.Global.read();
7
+ const rows = names.map((name) => {
8
+ const repo = Config.Global.find(global, name);
9
+ if (!repo)
10
+ return null;
11
+ return [name, repo];
12
+ }).filter((row) => row !== null);
13
+ Config.Local.write({ refs: Object.fromEntries(rows) });
14
+ return sync();
15
+ }
16
+ export {
17
+ link
18
+ };
@@ -1,22 +1,14 @@
1
+ // @bun
2
+ // src/core/remove.ts
1
3
  import fs from "fs";
2
4
  import path from "path";
3
5
  import { Config } from "@spader/dotllm/core/config";
4
-
5
- export type RemoveResult = {
6
- ok: true;
7
- } | {
8
- ok: false;
9
- error: string;
10
- };
11
-
12
- export function remove(name: string): RemoveResult {
6
+ function remove(name) {
13
7
  const global = Config.Global.read();
14
8
  const found = Config.Global.find(global, name);
15
-
16
9
  if (!found) {
17
10
  return { ok: false, error: `No repo named "${name}" in registry` };
18
11
  }
19
-
20
12
  const target = path.join(Config.storeDir(), name);
21
13
  if (fs.existsSync(target)) {
22
14
  const stat = fs.lstatSync(target);
@@ -27,7 +19,9 @@ export function remove(name: string): RemoveResult {
27
19
  fs.rmSync(target, { recursive: true, force: true });
28
20
  }
29
21
  }
30
-
31
22
  Config.Global.write(Config.Global.remove(global, name));
32
23
  return { ok: true };
33
24
  }
25
+ export {
26
+ remove
27
+ };
@@ -1,85 +1,52 @@
1
+ // @bun
2
+ // src/core/sync.ts
1
3
  import fs from "fs";
2
4
  import path from "path";
3
5
  import { Config } from "@spader/dotllm/core/config";
4
-
5
- export type SyncResult = {
6
- linked: string[];
7
- removed: string[];
8
- missing: string[];
9
- unchanged: string[];
10
- };
11
-
12
- type PullState = {
13
- name: string;
14
- error: string;
15
- } | null;
16
-
17
- export type PullError = {
18
- name: string;
19
- error: string;
20
- };
21
-
22
- export type PullResult = {
23
- count: number;
24
- failed: PullError[];
25
- };
26
-
27
- export async function pull(names: string[]): Promise<PullResult> {
28
- const states = await Promise.all(
29
- names.map(async (name): Promise<PullState> => {
30
- const cwd = path.join(Config.refDir(), name);
31
- if (!fs.existsSync(cwd)) {
32
- return { name, error: "reference directory missing" };
33
- }
34
-
35
- const proc = Bun.spawn(["git", "pull", "--ff-only"], {
36
- cwd,
37
- stdout: "pipe",
38
- stderr: "pipe",
39
- });
40
- const [code, out, err] = await Promise.all([
41
- proc.exited,
42
- new Response(proc.stdout).text(),
43
- new Response(proc.stderr).text(),
44
- ]);
45
- const msg = `${out}\n${err}`.trim();
46
-
47
- if (code !== 0) {
48
- return { name, error: msg || "git pull failed" };
49
- }
50
-
51
- return null;
52
- }),
53
- );
54
-
55
- const failed = states.filter((state): state is PullError => state !== null);
56
-
6
+ async function pull(names) {
7
+ const states = await Promise.all(names.map(async (name) => {
8
+ const cwd = path.join(Config.refDir(), name);
9
+ if (!fs.existsSync(cwd)) {
10
+ return { name, error: "reference directory missing" };
11
+ }
12
+ const proc = Bun.spawn(["git", "pull", "--ff-only"], {
13
+ cwd,
14
+ stdout: "pipe",
15
+ stderr: "pipe"
16
+ });
17
+ const [code, out, err] = await Promise.all([
18
+ proc.exited,
19
+ new Response(proc.stdout).text(),
20
+ new Response(proc.stderr).text()
21
+ ]);
22
+ const msg = `${out}
23
+ ${err}`.trim();
24
+ if (code !== 0) {
25
+ return { name, error: msg || "git pull failed" };
26
+ }
27
+ return null;
28
+ }));
29
+ const failed = states.filter((state) => state !== null);
57
30
  return { count: names.length, failed };
58
31
  }
59
-
60
- export function sync(): SyncResult {
32
+ function sync() {
61
33
  const local = Config.Local.read();
62
34
  const global = Config.Global.read();
63
- const merged = Object.values(local.refs).reduce(
64
- (config, repo) => Config.Global.add(config, repo),
65
- global,
66
- );
35
+ const merged = Object.values(local.refs).reduce((config, repo) => Config.Global.add(config, repo), global);
67
36
  if (Object.keys(local.refs).length > 0) {
68
37
  Config.Global.write(merged);
69
38
  }
70
-
71
39
  const refDir = Config.refDir();
72
40
  fs.mkdirSync(refDir, { recursive: true });
73
41
  fs.mkdirSync(Config.storeDir(), { recursive: true });
74
-
75
- const linked: string[] = [];
76
- const removed: string[] = [];
77
- const missing: string[] = [];
78
- const unchanged: string[] = [];
79
-
42
+ const linked = [];
43
+ const removed = [];
44
+ const missing = [];
45
+ const unchanged = [];
80
46
  const wanted = new Set(Object.keys(local.refs));
81
47
  for (const entry of fs.readdirSync(refDir)) {
82
- if (wanted.has(entry)) continue;
48
+ if (wanted.has(entry))
49
+ continue;
83
50
  const target = path.join(refDir, entry);
84
51
  const stat = fs.lstatSync(target);
85
52
  if (stat.isSymbolicLink()) {
@@ -87,24 +54,21 @@ export function sync(): SyncResult {
87
54
  removed.push(entry);
88
55
  }
89
56
  }
90
-
91
57
  for (const [name, repo] of Object.entries(local.refs)) {
92
58
  const store = path.join(Config.storeDir(), name);
93
59
  if (!fs.existsSync(store)) {
94
60
  fs.rmSync(store, { recursive: true, force: true });
95
61
  }
96
-
97
62
  if (!fs.existsSync(store) && repo.kind === "url") {
98
63
  const clone = Bun.spawnSync(["git", "clone", repo.uri, store], {
99
64
  stdout: "pipe",
100
- stderr: "pipe",
65
+ stderr: "pipe"
101
66
  });
102
67
  if (clone.exitCode !== 0) {
103
68
  missing.push(name);
104
69
  continue;
105
70
  }
106
71
  }
107
-
108
72
  if (!fs.existsSync(store) && repo.kind === "file") {
109
73
  if (!fs.existsSync(repo.uri)) {
110
74
  missing.push(name);
@@ -116,21 +80,21 @@ export function sync(): SyncResult {
116
80
  }
117
81
  fs.symlinkSync(repo.uri, store, "dir");
118
82
  }
119
-
120
83
  if (!fs.existsSync(store)) {
121
84
  missing.push(name);
122
85
  continue;
123
86
  }
124
-
125
87
  const target = path.join(refDir, name);
126
88
  if (fs.existsSync(target)) {
127
89
  unchanged.push(name);
128
90
  continue;
129
91
  }
130
-
131
92
  fs.symlinkSync(store, target, "dir");
132
93
  linked.push(name);
133
94
  }
134
-
135
95
  return { linked, removed, missing, unchanged };
136
96
  }
97
+ export {
98
+ sync,
99
+ pull
100
+ };
@@ -1,26 +1,20 @@
1
+ // @bun
2
+ // src/core/unlink.ts
1
3
  import fs from "fs";
2
4
  import path from "path";
3
5
  import { Config } from "@spader/dotllm/core/config";
4
-
5
- export type UnlinkResult = {
6
- ok: true;
7
- } | {
8
- ok: false;
9
- error: string;
10
- };
11
-
12
- export function unlink(name: string): UnlinkResult {
6
+ function unlink(name) {
13
7
  const local = Config.Local.read();
14
-
15
8
  if (!Config.Local.has(local, name)) {
16
9
  return { ok: false, error: `"${name}" is not linked in local config` };
17
10
  }
18
-
19
11
  const target = path.join(Config.refDir(), name);
20
12
  if (fs.existsSync(target)) {
21
13
  fs.unlinkSync(target);
22
14
  }
23
-
24
15
  Config.Local.write(Config.Local.remove(local, name));
25
16
  return { ok: true };
26
17
  }
18
+ export {
19
+ unlink
20
+ };
@@ -1,5 +0,0 @@
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";
@@ -1,54 +0,0 @@
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
- };
@@ -1,44 +0,0 @@
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
- };
package/src/cli/index.ts DELETED
@@ -1,24 +0,0 @@
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();