@spader/dotllm 1.3.1 → 1.3.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spader/dotllm",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "description": "A simple CLI to clone, manage, and link repositories for LLM reference",
5
5
  "type": "module",
6
6
  "repository": {
@@ -9,7 +9,14 @@ import { Prompt } from "@spader/dotllm/cli/prompt";
9
9
  var command = {
10
10
  description: "Re-create symlinks from .llm/dotllm.json",
11
11
  summary: "Sync symlinks from local config",
12
- handler: async () => {
12
+ options: {
13
+ force: {
14
+ alias: "f",
15
+ type: "boolean",
16
+ description: "Pull all repos regardless of recent access"
17
+ }
18
+ },
19
+ handler: async (argv) => {
13
20
  prompts.intro("dotllm sync");
14
21
  const result = sync();
15
22
  if (result.linked.length === 0 && result.removed.length === 0 && result.missing.length === 0 && result.unchanged.length === 0) {
@@ -22,15 +29,21 @@ var command = {
22
29
  if (refs.length === 0) {
23
30
  return;
24
31
  }
32
+ const force = argv.force === true;
25
33
  const spinner2 = prompts.spinner();
26
- spinner2.start(`Pulling ${refs.length} linked repo${refs.length === 1 ? "" : "s"}`);
27
- const pulled = await pull(refs);
34
+ spinner2.start(`Checking ${refs.length} linked repo${refs.length === 1 ? "" : "s"}`);
35
+ const pulled = await pull(refs, { force });
28
36
  if (pulled.failed.length > 0) {
29
37
  spinner2.stop(t.error(`pull failed for ${pulled.failed.length} repo${pulled.failed.length === 1 ? "" : "s"}`));
30
38
  process.exit(1);
31
39
  return;
32
40
  }
33
- spinner2.stop(`${t.success("pulled")} ${pulled.count} repo${pulled.count === 1 ? "" : "s"}`);
41
+ const parts = [];
42
+ if (pulled.pulled.length > 0)
43
+ parts.push(`${t.success("pulled")} ${pulled.pulled.length}`);
44
+ if (pulled.skipped.length > 0)
45
+ parts.push(t.dim(`${pulled.skipped.length} skipped`));
46
+ spinner2.stop(parts.length > 0 ? parts.join(" ") : t.dim("nothing to pull"));
34
47
  }
35
48
  };
36
49
  export {
package/src/core/add.js CHANGED
@@ -36,7 +36,7 @@ async function cloneUrl(url, name, description) {
36
36
  fs.mkdirSync(store, { recursive: true });
37
37
  const target = path.join(store, resolved);
38
38
  if (!fs.existsSync(target)) {
39
- const proc = Bun.spawn(["git", "clone", url, target], {
39
+ const proc = Bun.spawn(["git", "clone", "--depth=1", url, target], {
40
40
  stdout: "pipe",
41
41
  stderr: "pipe"
42
42
  });
package/src/core/sync.js CHANGED
@@ -5,31 +5,93 @@ var __require = import.meta.require;
5
5
  import fs from "fs";
6
6
  import path from "path";
7
7
  import { Config } from "@spader/dotllm/core/config";
8
- async function pull(names) {
8
+ var FRESH_MS = 24 * 60 * 60 * 1000;
9
+ var STALE_MS = 7 * FRESH_MS;
10
+ function shouldPull(storeDir, force) {
11
+ if (force)
12
+ return true;
13
+ const head = path.join(storeDir, ".git", "HEAD");
14
+ const headStat = fs.statSync(head, { throwIfNoEntry: false });
15
+ if (!headStat)
16
+ return true;
17
+ const age = Date.now() - headStat.mtimeMs;
18
+ if (age < FRESH_MS)
19
+ return false;
20
+ if (age > STALE_MS)
21
+ return true;
22
+ const dirStat = fs.statSync(storeDir, { throwIfNoEntry: false });
23
+ if (!dirStat)
24
+ return true;
25
+ return dirStat.atimeMs > headStat.mtimeMs;
26
+ }
27
+ async function pull(names, options = {}) {
28
+ const force = options.force === true;
29
+ const global = Config.Global.read();
9
30
  const states = await Promise.all(names.map(async (name) => {
10
31
  const cwd = path.join(Config.refDir(), name);
11
32
  if (!fs.existsSync(cwd)) {
12
- return { name, error: "reference directory missing" };
33
+ return { kind: "failed", name, error: "reference directory missing" };
34
+ }
35
+ const repo = Config.Global.find(global, name);
36
+ if (repo && repo.kind === "file") {
37
+ return { kind: "skipped", name };
38
+ }
39
+ const storeDir = path.join(Config.storeDir(), name);
40
+ const storeStat = fs.lstatSync(storeDir, { throwIfNoEntry: false });
41
+ if (!storeStat || storeStat.isSymbolicLink()) {
42
+ return { kind: "skipped", name };
43
+ }
44
+ if (!fs.existsSync(path.join(storeDir, ".git"))) {
45
+ return { kind: "failed", name, error: "store directory is not a git repo" };
46
+ }
47
+ if (!shouldPull(storeDir, force)) {
48
+ return { kind: "skipped", name };
49
+ }
50
+ const fetch = Bun.spawn(["git", "fetch", "--depth=1", "origin", "HEAD"], {
51
+ cwd,
52
+ stdout: "pipe",
53
+ stderr: "pipe"
54
+ });
55
+ const [fetchCode, fetchOut, fetchErr] = await Promise.all([
56
+ fetch.exited,
57
+ new Response(fetch.stdout).text(),
58
+ new Response(fetch.stderr).text()
59
+ ]);
60
+ if (fetchCode !== 0) {
61
+ const msg = `${fetchOut}
62
+ ${fetchErr}`.trim();
63
+ return { kind: "failed", name, error: msg || "git fetch failed" };
13
64
  }
14
- const proc = Bun.spawn(["git", "pull", "--ff-only"], {
65
+ const reset = Bun.spawn(["git", "reset", "--hard", "FETCH_HEAD"], {
15
66
  cwd,
16
67
  stdout: "pipe",
17
68
  stderr: "pipe"
18
69
  });
19
- const [code, out, err] = await Promise.all([
20
- proc.exited,
21
- new Response(proc.stdout).text(),
22
- new Response(proc.stderr).text()
70
+ const [resetCode, resetOut, resetErr] = await Promise.all([
71
+ reset.exited,
72
+ new Response(reset.stdout).text(),
73
+ new Response(reset.stderr).text()
23
74
  ]);
24
- const msg = `${out}
25
- ${err}`.trim();
26
- if (code !== 0) {
27
- return { name, error: msg || "git pull failed" };
75
+ if (resetCode !== 0) {
76
+ const msg = `${resetOut}
77
+ ${resetErr}`.trim();
78
+ return { kind: "failed", name, error: msg || "git reset failed" };
28
79
  }
29
- return null;
80
+ return { kind: "ok" };
30
81
  }));
31
- const failed = states.filter((state) => state !== null);
32
- return { count: names.length, failed };
82
+ const pulled = [];
83
+ const skipped = [];
84
+ const failed = [];
85
+ for (let i = 0;i < states.length; i++) {
86
+ const state = states[i];
87
+ if (state.kind === "ok")
88
+ pulled.push(names[i]);
89
+ if (state.kind === "skipped")
90
+ skipped.push(state.name);
91
+ if (state.kind === "failed")
92
+ failed.push({ name: state.name, error: state.error });
93
+ }
94
+ return { count: names.length, pulled, skipped, failed };
33
95
  }
34
96
  function sync() {
35
97
  const local = Config.Local.read();
@@ -58,11 +120,12 @@ function sync() {
58
120
  }
59
121
  for (const [name, repo] of Object.entries(local.refs)) {
60
122
  const store = path.join(Config.storeDir(), name);
61
- if (!fs.existsSync(store)) {
123
+ const storeBroken = repo.kind === "url" && fs.existsSync(store) && !fs.existsSync(path.join(store, ".git"));
124
+ if (storeBroken) {
62
125
  fs.rmSync(store, { recursive: true, force: true });
63
126
  }
64
127
  if (!fs.existsSync(store) && repo.kind === "url") {
65
- const clone = Bun.spawnSync(["git", "clone", repo.uri, store], {
128
+ const clone = Bun.spawnSync(["git", "clone", "--depth=1", repo.uri, store], {
66
129
  stdout: "pipe",
67
130
  stderr: "pipe"
68
131
  });