@spader/dotllm 1.3.1 → 1.3.3

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/src/core/sync.js CHANGED
@@ -1,35 +1,246 @@
1
1
  // @bun
2
- var __require = import.meta.require;
2
+ var __esm = (fn, res, err) => () => {
3
+ if (fn)
4
+ try {
5
+ res = fn(fn = 0);
6
+ } catch (e) {
7
+ err = [e];
8
+ }
9
+ if (err)
10
+ throw err[0];
11
+ return res;
12
+ };
3
13
 
4
- // src/core/sync.ts
14
+ // src/core/config.ts
5
15
  import fs from "fs";
16
+ import os from "os";
6
17
  import path from "path";
7
- import { Config } from "@spader/dotllm/core/config";
8
- async function pull(names) {
18
+ import { z } from "zod";
19
+ function readJson(filepath) {
20
+ if (!fs.existsSync(filepath))
21
+ return null;
22
+ const raw = fs.readFileSync(filepath, "utf-8");
23
+ try {
24
+ return JSON.parse(raw);
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+ var LOCAL_DIR = ".llm", LOCAL_FILE, REF_DIR, RepoEntry, GlobalShape, LocalShape, Config;
30
+ var init_config = __esm(() => {
31
+ LOCAL_FILE = path.join(LOCAL_DIR, "dotllm.json");
32
+ REF_DIR = path.join(LOCAL_DIR, "reference");
33
+ RepoEntry = z.object({
34
+ kind: z.enum(["url", "file"]),
35
+ name: z.string(),
36
+ uri: z.string(),
37
+ description: z.string()
38
+ });
39
+ GlobalShape = z.object({
40
+ store: z.string().optional(),
41
+ repos: z.array(RepoEntry)
42
+ });
43
+ LocalShape = z.object({
44
+ refs: z.record(z.string(), RepoEntry)
45
+ });
46
+ ((Config) => {
47
+ function home() {
48
+ if (process.platform === "win32") {
49
+ const appData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local");
50
+ return path.join(appData, "dotllm");
51
+ }
52
+ return path.join(process.env.HOME ?? os.homedir(), ".local", "share", "dotllm");
53
+ }
54
+ Config.home = home;
55
+ function storeDir() {
56
+ const store = Global.read().store;
57
+ if (!store)
58
+ return path.join(home(), "store");
59
+ return expand(store);
60
+ }
61
+ Config.storeDir = storeDir;
62
+ function refDir() {
63
+ return REF_DIR;
64
+ }
65
+ Config.refDir = refDir;
66
+ let Global;
67
+ ((Global) => {
68
+ function read() {
69
+ const raw = readJson(path.join(home(), "dotllm.json"));
70
+ if (!raw)
71
+ return { repos: [] };
72
+ const result = GlobalShape.safeParse(raw);
73
+ if (!result.success)
74
+ return { repos: [] };
75
+ return result.data;
76
+ }
77
+ Global.read = read;
78
+ function write(config) {
79
+ const dir = home();
80
+ fs.mkdirSync(dir, { recursive: true });
81
+ fs.writeFileSync(path.join(dir, "dotllm.json"), JSON.stringify(config, null, 2) + `
82
+ `);
83
+ }
84
+ Global.write = write;
85
+ function find(config, name) {
86
+ const lower = name.toLowerCase();
87
+ return config.repos.find((r) => r.name.toLowerCase() === lower);
88
+ }
89
+ Global.find = find;
90
+ function add(config, entry) {
91
+ const lower = entry.name.toLowerCase();
92
+ const filtered = config.repos.filter((r) => r.name.toLowerCase() !== lower);
93
+ return { ...config, repos: [...filtered, entry] };
94
+ }
95
+ Global.add = add;
96
+ function remove(config, name) {
97
+ const lower = name.toLowerCase();
98
+ return { ...config, repos: config.repos.filter((r) => r.name.toLowerCase() !== lower) };
99
+ }
100
+ Global.remove = remove;
101
+ })(Global = Config.Global ||= {});
102
+ let Local;
103
+ ((Local) => {
104
+ function read() {
105
+ const raw = readJson(LOCAL_FILE);
106
+ if (!raw)
107
+ return { refs: {} };
108
+ const result = LocalShape.safeParse(raw);
109
+ if (!result.success)
110
+ return { refs: {} };
111
+ return result.data;
112
+ }
113
+ Local.read = read;
114
+ function write(config) {
115
+ fs.mkdirSync(LOCAL_DIR, { recursive: true });
116
+ fs.writeFileSync(LOCAL_FILE, JSON.stringify(config, null, 2) + `
117
+ `);
118
+ }
119
+ Local.write = write;
120
+ function find(config, name) {
121
+ const lower = name.toLowerCase();
122
+ for (const [key, value] of Object.entries(config.refs)) {
123
+ if (key.toLowerCase() === lower)
124
+ return value;
125
+ }
126
+ return;
127
+ }
128
+ Local.find = find;
129
+ function has(config, name) {
130
+ return find(config, name) !== undefined;
131
+ }
132
+ Local.has = has;
133
+ function add(config, repo) {
134
+ const lower = repo.name.toLowerCase();
135
+ const refs = Object.fromEntries(Object.entries(config.refs).filter(([key]) => key.toLowerCase() !== lower));
136
+ refs[repo.name] = repo;
137
+ return { refs };
138
+ }
139
+ Local.add = add;
140
+ function remove(config, name) {
141
+ const lower = name.toLowerCase();
142
+ const refs = Object.fromEntries(Object.entries(config.refs).filter(([key]) => key.toLowerCase() !== lower));
143
+ return { refs };
144
+ }
145
+ Local.remove = remove;
146
+ })(Local = Config.Local ||= {});
147
+ function expand(dir) {
148
+ const tilde = dir === "~" || dir.startsWith("~/") || dir.startsWith("~\\");
149
+ if (!tilde)
150
+ return path.resolve(home(), dir);
151
+ return path.join(os.homedir(), dir.slice(1));
152
+ }
153
+ })(Config ||= {});
154
+ });
155
+
156
+ // src/core/sync.ts
157
+ import fs2 from "fs";
158
+ import path2 from "path";
159
+ function shouldPull(storeDir, force) {
160
+ if (force)
161
+ return true;
162
+ const head = path2.join(storeDir, ".git", "HEAD");
163
+ const headStat = fs2.statSync(head, { throwIfNoEntry: false });
164
+ if (!headStat)
165
+ return true;
166
+ const age = Date.now() - headStat.mtimeMs;
167
+ if (age < FRESH_MS)
168
+ return false;
169
+ if (age > STALE_MS)
170
+ return true;
171
+ const dirStat = fs2.statSync(storeDir, { throwIfNoEntry: false });
172
+ if (!dirStat)
173
+ return true;
174
+ return dirStat.atimeMs > headStat.mtimeMs;
175
+ }
176
+ async function pull(names, options = {}) {
177
+ const force = options.force === true;
178
+ const global = Config.Global.read();
9
179
  const states = await Promise.all(names.map(async (name) => {
10
- const cwd = path.join(Config.refDir(), name);
11
- if (!fs.existsSync(cwd)) {
12
- return { name, error: "reference directory missing" };
180
+ const cwd = path2.join(Config.refDir(), name);
181
+ if (!fs2.existsSync(cwd)) {
182
+ return { kind: "failed", name, error: "reference directory missing" };
183
+ }
184
+ const repo = Config.Global.find(global, name);
185
+ if (repo && repo.kind === "file") {
186
+ return { kind: "skipped", name };
187
+ }
188
+ const storeDir = path2.join(Config.storeDir(), name);
189
+ const storeStat = fs2.lstatSync(storeDir, { throwIfNoEntry: false });
190
+ if (!storeStat || storeStat.isSymbolicLink()) {
191
+ return { kind: "skipped", name };
192
+ }
193
+ if (!fs2.existsSync(path2.join(storeDir, ".git"))) {
194
+ return { kind: "failed", name, error: "store directory is not a git repo" };
13
195
  }
14
- const proc = Bun.spawn(["git", "pull", "--ff-only"], {
196
+ if (!shouldPull(storeDir, force)) {
197
+ return { kind: "skipped", name };
198
+ }
199
+ const fetch = Bun.spawn(["git", "fetch", "--depth=1", "origin", "HEAD"], {
15
200
  cwd,
16
201
  stdout: "pipe",
17
202
  stderr: "pipe"
18
203
  });
19
- const [code, out, err] = await Promise.all([
20
- proc.exited,
21
- new Response(proc.stdout).text(),
22
- new Response(proc.stderr).text()
204
+ const [fetchCode, fetchOut, fetchErr] = await Promise.all([
205
+ fetch.exited,
206
+ new Response(fetch.stdout).text(),
207
+ new Response(fetch.stderr).text()
23
208
  ]);
24
- const msg = `${out}
25
- ${err}`.trim();
26
- if (code !== 0) {
27
- return { name, error: msg || "git pull failed" };
209
+ if (fetchCode !== 0) {
210
+ const msg = `${fetchOut}
211
+ ${fetchErr}`.trim();
212
+ return { kind: "failed", name, error: msg || "git fetch failed" };
28
213
  }
29
- return null;
214
+ const reset = Bun.spawn(["git", "reset", "--hard", "FETCH_HEAD"], {
215
+ cwd,
216
+ stdout: "pipe",
217
+ stderr: "pipe"
218
+ });
219
+ const [resetCode, resetOut, resetErr] = await Promise.all([
220
+ reset.exited,
221
+ new Response(reset.stdout).text(),
222
+ new Response(reset.stderr).text()
223
+ ]);
224
+ if (resetCode !== 0) {
225
+ const msg = `${resetOut}
226
+ ${resetErr}`.trim();
227
+ return { kind: "failed", name, error: msg || "git reset failed" };
228
+ }
229
+ return { kind: "ok" };
30
230
  }));
31
- const failed = states.filter((state) => state !== null);
32
- return { count: names.length, failed };
231
+ const pulled = [];
232
+ const skipped = [];
233
+ const failed = [];
234
+ for (let i = 0;i < states.length; i++) {
235
+ const state = states[i];
236
+ if (state.kind === "ok")
237
+ pulled.push(names[i]);
238
+ if (state.kind === "skipped")
239
+ skipped.push(state.name);
240
+ if (state.kind === "failed")
241
+ failed.push({ name: state.name, error: state.error });
242
+ }
243
+ return { count: names.length, pulled, skipped, failed };
33
244
  }
34
245
  function sync() {
35
246
  const local = Config.Local.read();
@@ -39,30 +250,31 @@ function sync() {
39
250
  Config.Global.write(merged);
40
251
  }
41
252
  const refDir = Config.refDir();
42
- fs.mkdirSync(refDir, { recursive: true });
43
- fs.mkdirSync(Config.storeDir(), { recursive: true });
253
+ fs2.mkdirSync(refDir, { recursive: true });
254
+ fs2.mkdirSync(Config.storeDir(), { recursive: true });
44
255
  const linked = [];
45
256
  const removed = [];
46
257
  const missing = [];
47
258
  const unchanged = [];
48
259
  const wanted = new Set(Object.keys(local.refs));
49
- for (const entry of fs.readdirSync(refDir)) {
260
+ for (const entry of fs2.readdirSync(refDir)) {
50
261
  if (wanted.has(entry))
51
262
  continue;
52
- const target = path.join(refDir, entry);
53
- const stat = fs.lstatSync(target);
263
+ const target = path2.join(refDir, entry);
264
+ const stat = fs2.lstatSync(target);
54
265
  if (stat.isSymbolicLink()) {
55
- fs.unlinkSync(target);
266
+ fs2.unlinkSync(target);
56
267
  removed.push(entry);
57
268
  }
58
269
  }
59
270
  for (const [name, repo] of Object.entries(local.refs)) {
60
- const store = path.join(Config.storeDir(), name);
61
- if (!fs.existsSync(store)) {
62
- fs.rmSync(store, { recursive: true, force: true });
271
+ const store = path2.join(Config.storeDir(), name);
272
+ const storeBroken = repo.kind === "url" && fs2.existsSync(store) && !fs2.existsSync(path2.join(store, ".git"));
273
+ if (storeBroken) {
274
+ fs2.rmSync(store, { recursive: true, force: true });
63
275
  }
64
- if (!fs.existsSync(store) && repo.kind === "url") {
65
- const clone = Bun.spawnSync(["git", "clone", repo.uri, store], {
276
+ if (!fs2.existsSync(store) && repo.kind === "url") {
277
+ const clone = Bun.spawnSync(["git", "clone", "--depth=1", repo.uri, store], {
66
278
  stdout: "pipe",
67
279
  stderr: "pipe"
68
280
  });
@@ -71,32 +283,40 @@ function sync() {
71
283
  continue;
72
284
  }
73
285
  }
74
- if (!fs.existsSync(store) && repo.kind === "file") {
75
- if (!fs.existsSync(repo.uri)) {
286
+ if (!fs2.existsSync(store) && repo.kind === "file") {
287
+ if (!fs2.existsSync(repo.uri)) {
76
288
  missing.push(name);
77
289
  continue;
78
290
  }
79
- if (!fs.statSync(repo.uri).isDirectory()) {
291
+ if (!fs2.statSync(repo.uri).isDirectory()) {
80
292
  missing.push(name);
81
293
  continue;
82
294
  }
83
- fs.symlinkSync(repo.uri, store, "dir");
295
+ fs2.symlinkSync(repo.uri, store, "dir");
84
296
  }
85
- if (!fs.existsSync(store)) {
297
+ if (!fs2.existsSync(store)) {
86
298
  missing.push(name);
87
299
  continue;
88
300
  }
89
- const target = path.join(refDir, name);
90
- if (fs.existsSync(target)) {
301
+ const target = path2.join(refDir, name);
302
+ if (fs2.existsSync(target)) {
91
303
  unchanged.push(name);
92
304
  continue;
93
305
  }
94
- fs.symlinkSync(store, target, "dir");
306
+ fs2.symlinkSync(store, target, "dir");
95
307
  linked.push(name);
96
308
  }
97
309
  return { linked, removed, missing, unchanged };
98
310
  }
311
+ var FRESH_MS, STALE_MS;
312
+ var init_sync = __esm(() => {
313
+ init_config();
314
+ FRESH_MS = 24 * 60 * 60 * 1000;
315
+ STALE_MS = 7 * FRESH_MS;
316
+ });
317
+ init_sync();
318
+
99
319
  export {
100
- sync,
101
- pull
320
+ pull,
321
+ sync
102
322
  };
@@ -1,23 +1,179 @@
1
1
  // @bun
2
- var __require = import.meta.require;
2
+ var __esm = (fn, res, err) => () => {
3
+ if (fn)
4
+ try {
5
+ res = fn(fn = 0);
6
+ } catch (e) {
7
+ err = [e];
8
+ }
9
+ if (err)
10
+ throw err[0];
11
+ return res;
12
+ };
3
13
 
4
- // src/core/unlink.ts
14
+ // src/core/config.ts
5
15
  import fs from "fs";
16
+ import os from "os";
6
17
  import path from "path";
7
- import { Config } from "@spader/dotllm/core/config";
18
+ import { z } from "zod";
19
+ function readJson(filepath) {
20
+ if (!fs.existsSync(filepath))
21
+ return null;
22
+ const raw = fs.readFileSync(filepath, "utf-8");
23
+ try {
24
+ return JSON.parse(raw);
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+ var LOCAL_DIR = ".llm", LOCAL_FILE, REF_DIR, RepoEntry, GlobalShape, LocalShape, Config;
30
+ var init_config = __esm(() => {
31
+ LOCAL_FILE = path.join(LOCAL_DIR, "dotllm.json");
32
+ REF_DIR = path.join(LOCAL_DIR, "reference");
33
+ RepoEntry = z.object({
34
+ kind: z.enum(["url", "file"]),
35
+ name: z.string(),
36
+ uri: z.string(),
37
+ description: z.string()
38
+ });
39
+ GlobalShape = z.object({
40
+ store: z.string().optional(),
41
+ repos: z.array(RepoEntry)
42
+ });
43
+ LocalShape = z.object({
44
+ refs: z.record(z.string(), RepoEntry)
45
+ });
46
+ ((Config) => {
47
+ function home() {
48
+ if (process.platform === "win32") {
49
+ const appData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local");
50
+ return path.join(appData, "dotllm");
51
+ }
52
+ return path.join(process.env.HOME ?? os.homedir(), ".local", "share", "dotllm");
53
+ }
54
+ Config.home = home;
55
+ function storeDir() {
56
+ const store = Global.read().store;
57
+ if (!store)
58
+ return path.join(home(), "store");
59
+ return expand(store);
60
+ }
61
+ Config.storeDir = storeDir;
62
+ function refDir() {
63
+ return REF_DIR;
64
+ }
65
+ Config.refDir = refDir;
66
+ let Global;
67
+ ((Global) => {
68
+ function read() {
69
+ const raw = readJson(path.join(home(), "dotllm.json"));
70
+ if (!raw)
71
+ return { repos: [] };
72
+ const result = GlobalShape.safeParse(raw);
73
+ if (!result.success)
74
+ return { repos: [] };
75
+ return result.data;
76
+ }
77
+ Global.read = read;
78
+ function write(config) {
79
+ const dir = home();
80
+ fs.mkdirSync(dir, { recursive: true });
81
+ fs.writeFileSync(path.join(dir, "dotllm.json"), JSON.stringify(config, null, 2) + `
82
+ `);
83
+ }
84
+ Global.write = write;
85
+ function find(config, name) {
86
+ const lower = name.toLowerCase();
87
+ return config.repos.find((r) => r.name.toLowerCase() === lower);
88
+ }
89
+ Global.find = find;
90
+ function add(config, entry) {
91
+ const lower = entry.name.toLowerCase();
92
+ const filtered = config.repos.filter((r) => r.name.toLowerCase() !== lower);
93
+ return { ...config, repos: [...filtered, entry] };
94
+ }
95
+ Global.add = add;
96
+ function remove(config, name) {
97
+ const lower = name.toLowerCase();
98
+ return { ...config, repos: config.repos.filter((r) => r.name.toLowerCase() !== lower) };
99
+ }
100
+ Global.remove = remove;
101
+ })(Global = Config.Global ||= {});
102
+ let Local;
103
+ ((Local) => {
104
+ function read() {
105
+ const raw = readJson(LOCAL_FILE);
106
+ if (!raw)
107
+ return { refs: {} };
108
+ const result = LocalShape.safeParse(raw);
109
+ if (!result.success)
110
+ return { refs: {} };
111
+ return result.data;
112
+ }
113
+ Local.read = read;
114
+ function write(config) {
115
+ fs.mkdirSync(LOCAL_DIR, { recursive: true });
116
+ fs.writeFileSync(LOCAL_FILE, JSON.stringify(config, null, 2) + `
117
+ `);
118
+ }
119
+ Local.write = write;
120
+ function find(config, name) {
121
+ const lower = name.toLowerCase();
122
+ for (const [key, value] of Object.entries(config.refs)) {
123
+ if (key.toLowerCase() === lower)
124
+ return value;
125
+ }
126
+ return;
127
+ }
128
+ Local.find = find;
129
+ function has(config, name) {
130
+ return find(config, name) !== undefined;
131
+ }
132
+ Local.has = has;
133
+ function add(config, repo) {
134
+ const lower = repo.name.toLowerCase();
135
+ const refs = Object.fromEntries(Object.entries(config.refs).filter(([key]) => key.toLowerCase() !== lower));
136
+ refs[repo.name] = repo;
137
+ return { refs };
138
+ }
139
+ Local.add = add;
140
+ function remove(config, name) {
141
+ const lower = name.toLowerCase();
142
+ const refs = Object.fromEntries(Object.entries(config.refs).filter(([key]) => key.toLowerCase() !== lower));
143
+ return { refs };
144
+ }
145
+ Local.remove = remove;
146
+ })(Local = Config.Local ||= {});
147
+ function expand(dir) {
148
+ const tilde = dir === "~" || dir.startsWith("~/") || dir.startsWith("~\\");
149
+ if (!tilde)
150
+ return path.resolve(home(), dir);
151
+ return path.join(os.homedir(), dir.slice(1));
152
+ }
153
+ })(Config ||= {});
154
+ });
155
+
156
+ // src/core/unlink.ts
157
+ import fs2 from "fs";
158
+ import path2 from "path";
8
159
  function unlink(name) {
9
160
  const local = Config.Local.read();
10
161
  const found = Config.Local.find(local, name);
11
162
  if (!found) {
12
163
  return { ok: false, error: `"${name}" is not linked in local config` };
13
164
  }
14
- const target = path.join(Config.refDir(), found.name);
15
- if (fs.existsSync(target)) {
16
- fs.unlinkSync(target);
165
+ const target = path2.join(Config.refDir(), found.name);
166
+ if (fs2.existsSync(target)) {
167
+ fs2.unlinkSync(target);
17
168
  }
18
169
  Config.Local.write(Config.Local.remove(local, found.name));
19
170
  return { ok: true };
20
171
  }
172
+ var init_unlink = __esm(() => {
173
+ init_config();
174
+ });
175
+ init_unlink();
176
+
21
177
  export {
22
178
  unlink
23
179
  };