@spader/dotllm 1.3.0 → 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.0",
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": {
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/cli/commands/add.ts
3
5
  import fs from "fs";
4
6
  import path from "path";
@@ -0,0 +1,216 @@
1
+ // @bun
2
+ var __require = import.meta.require;
3
+
4
+ // src/cli/commands/completions.ts
5
+ import fs from "fs";
6
+ import os from "os";
7
+ import path from "path";
8
+ import { Config } from "@spader/dotllm/core";
9
+ import { defaultTheme as t } from "@spader/dotllm/cli/theme";
10
+ var BASH = `_dotllm() {
11
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
12
+
13
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
14
+ COMPREPLY=( $(compgen -W "add remove list link sync which completions" -- "\${cur}") )
15
+ return
16
+ fi
17
+
18
+ case "\${COMP_WORDS[1]}" in
19
+ which|link)
20
+ local repos
21
+ repos=$(dotllm completions --names 2>/dev/null)
22
+ COMPREPLY=( $(compgen -W "\${repos}" -- "\${cur}") )
23
+ ;;
24
+ esac
25
+ }
26
+
27
+ complete -F _dotllm dotllm`;
28
+ var ZSH = `#compdef dotllm
29
+
30
+ _dotllm() {
31
+ local -a commands
32
+ commands=(
33
+ 'add:Register a new repo'
34
+ 'remove:Remove a repo from the registry'
35
+ 'list:Show the registry'
36
+ 'link:Link references'
37
+ 'sync:Sync linked repos'
38
+ 'which:Show repo store path'
39
+ 'completions:Output shell completions'
40
+ )
41
+
42
+ _arguments -C '1:command:->cmd' '*::arg:->args'
43
+
44
+ case "$state" in
45
+ cmd)
46
+ _describe 'command' commands
47
+ ;;
48
+ args)
49
+ case "\${words[1]}" in
50
+ which|link)
51
+ local -a repos
52
+ repos=(\${(f)"$(dotllm completions --names 2>/dev/null)"})
53
+ _describe 'repo' repos
54
+ ;;
55
+ esac
56
+ ;;
57
+ esac
58
+ }
59
+
60
+ _dotllm`;
61
+ var FISH = `complete -c dotllm -f
62
+ complete -c dotllm -n "__fish_use_subcommand" -a add -d "Register a new repo"
63
+ complete -c dotllm -n "__fish_use_subcommand" -a remove -d "Remove a repo from the registry"
64
+ complete -c dotllm -n "__fish_use_subcommand" -a list -d "Show the registry"
65
+ complete -c dotllm -n "__fish_use_subcommand" -a link -d "Link references"
66
+ complete -c dotllm -n "__fish_use_subcommand" -a sync -d "Sync linked repos"
67
+ complete -c dotllm -n "__fish_use_subcommand" -a which -d "Show repo store path"
68
+ complete -c dotllm -n "__fish_use_subcommand" -a completions -d "Output shell completions"
69
+ complete -c dotllm -n "__fish_seen_subcommand_from which link" -a "(dotllm completions --names 2>/dev/null)"`;
70
+ var CANARY = "@dotllm_completions";
71
+ function completionFile(shell) {
72
+ return path.join(path.dirname(Config.storeDir()), `completions.${shell}`);
73
+ }
74
+ function hookSnippet(shell) {
75
+ const file = completionFile(shell);
76
+ switch (shell) {
77
+ case "bash":
78
+ return `
79
+ # ${CANARY}
80
+ # Installed by the dotllm CLI
81
+ [ -f "${file}" ] && source "${file}"
82
+ `;
83
+ case "zsh":
84
+ return `
85
+ # ${CANARY}
86
+ # Installed by the dotllm CLI
87
+ [ -f "${file}" ] && source "${file}"
88
+ `;
89
+ case "fish":
90
+ return `
91
+ # ${CANARY}
92
+ # Installed by the dotllm CLI
93
+ test -f "${file}"; and source "${file}"
94
+ `;
95
+ default:
96
+ return "";
97
+ }
98
+ }
99
+ function rcPath(shell) {
100
+ const home = os.homedir();
101
+ switch (shell) {
102
+ case "bash": {
103
+ const bashrc = path.join(home, ".bashrc");
104
+ if (fs.existsSync(bashrc))
105
+ return bashrc;
106
+ return path.join(home, ".bash_profile");
107
+ }
108
+ case "zsh":
109
+ return path.join(home, ".zshrc");
110
+ case "fish":
111
+ return path.join(home, ".config", "fish", "config.fish");
112
+ }
113
+ }
114
+ var install = {
115
+ description: "Add completions to your shell rc file",
116
+ summary: "Install completions",
117
+ handler: async (argv) => {
118
+ if (process.platform === "win32") {
119
+ console.log(t.dim("Shell completions are not supported on Windows."));
120
+ return;
121
+ }
122
+ const shell = detectShell();
123
+ const rc = rcPath(shell);
124
+ if (!rc) {
125
+ console.error(t.error(`Could not determine rc file for shell: "${shell}"`));
126
+ process.exit(1);
127
+ return;
128
+ }
129
+ const script = shellScript(shell);
130
+ if (!script) {
131
+ console.error(t.error(`Unknown shell: "${shell}"`));
132
+ process.exit(1);
133
+ return;
134
+ }
135
+ const cached = completionFile(shell);
136
+ fs.mkdirSync(path.dirname(cached), { recursive: true });
137
+ fs.writeFileSync(cached, script + `
138
+ `);
139
+ const existing = fs.existsSync(rc) ? fs.readFileSync(rc, "utf-8") : "";
140
+ if (existing.includes(CANARY)) {
141
+ console.log(t.dim(`Updated ${cached}`));
142
+ console.log(t.dim(`Already sourced from ${rc}`));
143
+ return;
144
+ }
145
+ fs.appendFileSync(rc, hookSnippet(shell));
146
+ console.log(`Installed completions in ${t.link(rc)}`);
147
+ console.log(t.dim(`Restart your shell or run: source ${rc}`));
148
+ }
149
+ };
150
+ var command = {
151
+ description: "Output shell completion script for bash, zsh, or fish",
152
+ summary: "Shell completions",
153
+ options: {
154
+ shell: {
155
+ alias: "s",
156
+ type: "string",
157
+ description: "Shell type: bash, zsh, or fish"
158
+ },
159
+ names: {
160
+ type: "boolean",
161
+ description: "Print repo names (used internally by completions)"
162
+ }
163
+ },
164
+ commands: {
165
+ install
166
+ },
167
+ handler: async (argv) => {
168
+ if (process.platform === "win32") {
169
+ console.log(t.dim("Shell completions are not supported on Windows."));
170
+ return;
171
+ }
172
+ if (argv.names) {
173
+ const { Config: Config2 } = await import("@spader/dotllm/core");
174
+ const global = Config2.Global.read();
175
+ for (const r of global.repos)
176
+ console.log(r.name);
177
+ return;
178
+ }
179
+ const shell = typeof argv.shell === "string" && argv.shell.length > 0 ? argv.shell : detectShell();
180
+ switch (shell) {
181
+ case "bash":
182
+ console.log(BASH);
183
+ break;
184
+ case "zsh":
185
+ console.log(ZSH);
186
+ break;
187
+ case "fish":
188
+ console.log(FISH);
189
+ break;
190
+ default:
191
+ console.error(t.error(`Unknown shell: "${shell}". Use bash, zsh, or fish.`));
192
+ process.exit(1);
193
+ }
194
+ }
195
+ };
196
+ function shellScript(shell) {
197
+ switch (shell) {
198
+ case "bash":
199
+ return BASH;
200
+ case "zsh":
201
+ return ZSH;
202
+ case "fish":
203
+ return FISH;
204
+ }
205
+ }
206
+ function detectShell() {
207
+ const login = process.env.SHELL ?? "";
208
+ if (login.endsWith("/fish"))
209
+ return "fish";
210
+ if (login.endsWith("/zsh"))
211
+ return "zsh";
212
+ return "bash";
213
+ }
214
+ export {
215
+ command
216
+ };
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/cli/commands/index.ts
3
5
  import { command } from "@spader/dotllm/cli/commands/add";
4
6
  import { command as command2 } from "@spader/dotllm/cli/commands/remove";
@@ -6,13 +8,13 @@ import { command as command3 } from "@spader/dotllm/cli/commands/list";
6
8
  import { command as command4 } from "@spader/dotllm/cli/commands/link";
7
9
  import { command as command5 } from "@spader/dotllm/cli/commands/sync";
8
10
  import { command as command6 } from "@spader/dotllm/cli/commands/which";
9
- import { command as command7 } from "@spader/dotllm/cli/commands/cd";
11
+ import { command as command7 } from "@spader/dotllm/cli/commands/completions";
10
12
  export {
11
13
  command6 as which,
12
14
  command5 as sync,
13
15
  command2 as remove,
14
16
  command3 as list,
15
17
  command4 as link,
16
- command7 as cd,
18
+ command7 as completions,
17
19
  command as add
18
20
  };
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/cli/commands/link.ts
3
5
  import * as prompts from "@clack/prompts";
4
6
  import { Config, link, unlink, sync } from "@spader/dotllm/core";
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/cli/commands/list.ts
3
5
  import * as prompts from "@clack/prompts";
4
6
  import { Config } from "@spader/dotllm/core";
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/cli/commands/remove.ts
3
5
  import * as prompts from "@clack/prompts";
4
6
  import { remove } from "@spader/dotllm/core";
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/cli/commands/sync.ts
3
5
  import * as prompts from "@clack/prompts";
4
6
  import { pull, sync } from "@spader/dotllm/core";
@@ -7,7 +9,14 @@ import { Prompt } from "@spader/dotllm/cli/prompt";
7
9
  var command = {
8
10
  description: "Re-create symlinks from .llm/dotllm.json",
9
11
  summary: "Sync symlinks from local config",
10
- 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) => {
11
20
  prompts.intro("dotllm sync");
12
21
  const result = sync();
13
22
  if (result.linked.length === 0 && result.removed.length === 0 && result.missing.length === 0 && result.unchanged.length === 0) {
@@ -20,15 +29,21 @@ var command = {
20
29
  if (refs.length === 0) {
21
30
  return;
22
31
  }
32
+ const force = argv.force === true;
23
33
  const spinner2 = prompts.spinner();
24
- spinner2.start(`Pulling ${refs.length} linked repo${refs.length === 1 ? "" : "s"}`);
25
- const pulled = await pull(refs);
34
+ spinner2.start(`Checking ${refs.length} linked repo${refs.length === 1 ? "" : "s"}`);
35
+ const pulled = await pull(refs, { force });
26
36
  if (pulled.failed.length > 0) {
27
37
  spinner2.stop(t.error(`pull failed for ${pulled.failed.length} repo${pulled.failed.length === 1 ? "" : "s"}`));
28
38
  process.exit(1);
29
39
  return;
30
40
  }
31
- 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"));
32
47
  }
33
48
  };
34
49
  export {
@@ -1,28 +1,42 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/cli/commands/which.ts
3
5
  import path from "path";
4
6
  import { Config } from "@spader/dotllm/core";
5
7
  import { defaultTheme as t } from "@spader/dotllm/cli/theme";
6
8
  var command = {
7
- description: "Print the absolute path to a repo in the store",
9
+ description: "Print the absolute path to a repo in the store (prefix match, shortest wins)",
8
10
  summary: "Show repo store path",
9
11
  positionals: {
10
12
  name: {
11
13
  type: "string",
12
- description: "Name of the repo",
14
+ description: "Name (or prefix) of the repo",
13
15
  required: true
14
16
  }
15
17
  },
16
18
  handler: (argv) => {
17
19
  const name = String(argv.name);
18
20
  const global = Config.Global.read();
19
- const repo = Config.Global.find(global, name);
20
- if (!repo) {
21
- console.error(t.error(`No repo named "${name}" in registry`));
21
+ const exact = Config.Global.find(global, name);
22
+ if (exact) {
23
+ console.log(path.join(Config.storeDir(), exact.name));
24
+ return;
25
+ }
26
+ const lower = name.toLowerCase();
27
+ const matches = global.repos.filter((r) => r.name.toLowerCase().startsWith(lower)).sort((a, b) => a.name.length - b.name.length);
28
+ if (matches.length === 0) {
29
+ console.error(t.error(`No repo matching "${name}" in registry`));
30
+ process.exit(1);
31
+ return;
32
+ }
33
+ if (matches.length > 1 && matches[0].name.length === matches[1].name.length) {
34
+ const names = matches.filter((m) => m.name.length === matches[0].name.length).map((m) => m.name);
35
+ console.error(t.error(`Ambiguous prefix "${name}": ${names.join(", ")}`));
22
36
  process.exit(1);
23
37
  return;
24
38
  }
25
- console.log(path.join(Config.storeDir(), name));
39
+ console.log(path.join(Config.storeDir(), matches[0].name));
26
40
  }
27
41
  };
28
42
  export {
package/src/cli/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ var __require = import.meta.require;
3
4
 
4
5
  // src/cli/index.ts
5
6
  import { build } from "@spader/dotllm/cli/yargs";
6
- import { add, remove, list, link, sync, which, cd } from "@spader/dotllm/cli/commands/index";
7
+ import { add, remove, list, link, sync, which, completions } from "@spader/dotllm/cli/commands/index";
7
8
  var DotLlmCli;
8
9
  ((DotLlmCli) => {
9
10
  async function run() {
@@ -20,7 +21,7 @@ var DotLlmCli;
20
21
  link,
21
22
  sync,
22
23
  which,
23
- cd
24
+ completions
24
25
  }
25
26
  };
26
27
  build(def).parse();
package/src/cli/layout.js CHANGED
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/cli/layout.ts
3
5
  import { defaultTheme as theme } from "@spader/dotllm/cli/theme";
4
6
  var ANSI_RE = /\x1b\[[0-9;]*m/g;
package/src/cli/prompt.js CHANGED
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/cli/prompt.ts
3
5
  import * as prompts from "@clack/prompts";
4
6
  var Prompt;
package/src/cli/theme.js CHANGED
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/cli/theme.ts
3
5
  function rgb(r, g, b) {
4
6
  return (value) => `\x1B[38;2;${r};${g};${b}m${value}\x1B[39m`;
package/src/cli/yargs.js CHANGED
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/cli/yargs.ts
3
5
  import yargs from "yargs";
4
6
  import { hideBin } from "yargs/helpers";
@@ -145,7 +147,10 @@ function configure(y, def, root, path) {
145
147
  for (const [k, v] of Object.entries(def.commands)) {
146
148
  command(y, k, v, root, path);
147
149
  }
148
- y.demandCommand(1, "You must specify a command");
150
+ const hasHandler = "handler" in def && typeof def.handler === "function";
151
+ if (!hasHandler) {
152
+ y.demandCommand(1, "You must specify a command");
153
+ }
149
154
  }
150
155
  y.help(false).option("help", { alias: "h", type: "boolean", describe: "Show help" }).check(check(def, root, path)).fail(fail(def, root, path));
151
156
  if (path.length === 0 && "version" in def && def.version) {
package/src/core/add.js CHANGED
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/core/add.ts
3
5
  import fs from "fs";
4
6
  import path from "path";
@@ -34,7 +36,7 @@ async function cloneUrl(url, name, description) {
34
36
  fs.mkdirSync(store, { recursive: true });
35
37
  const target = path.join(store, resolved);
36
38
  if (!fs.existsSync(target)) {
37
- const proc = Bun.spawn(["git", "clone", url, target], {
39
+ const proc = Bun.spawn(["git", "clone", "--depth=1", url, target], {
38
40
  stdout: "pipe",
39
41
  stderr: "pipe"
40
42
  });
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/core/config.ts
3
5
  import fs from "fs";
4
6
  import os from "os";
@@ -56,16 +58,19 @@ var Config;
56
58
  }
57
59
  Global.write = write;
58
60
  function find(config, name) {
59
- return config.repos.find((r) => r.name === name);
61
+ const lower = name.toLowerCase();
62
+ return config.repos.find((r) => r.name.toLowerCase() === lower);
60
63
  }
61
64
  Global.find = find;
62
65
  function add(config, entry) {
63
- const filtered = config.repos.filter((r) => r.name !== entry.name);
66
+ const lower = entry.name.toLowerCase();
67
+ const filtered = config.repos.filter((r) => r.name.toLowerCase() !== lower);
64
68
  return { repos: [...filtered, entry] };
65
69
  }
66
70
  Global.add = add;
67
71
  function remove(config, name) {
68
- return { repos: config.repos.filter((r) => r.name !== name) };
72
+ const lower = name.toLowerCase();
73
+ return { repos: config.repos.filter((r) => r.name.toLowerCase() !== lower) };
69
74
  }
70
75
  Global.remove = remove;
71
76
  })(Global = Config.Global ||= {});
@@ -87,16 +92,29 @@ var Config;
87
92
  `);
88
93
  }
89
94
  Local.write = write;
95
+ function find(config, name) {
96
+ const lower = name.toLowerCase();
97
+ for (const [key, value] of Object.entries(config.refs)) {
98
+ if (key.toLowerCase() === lower)
99
+ return value;
100
+ }
101
+ return;
102
+ }
103
+ Local.find = find;
90
104
  function has(config, name) {
91
- return Object.prototype.hasOwnProperty.call(config.refs, name);
105
+ return find(config, name) !== undefined;
92
106
  }
93
107
  Local.has = has;
94
108
  function add(config, repo) {
95
- return { refs: { ...config.refs, [repo.name]: repo } };
109
+ const lower = repo.name.toLowerCase();
110
+ const refs = Object.fromEntries(Object.entries(config.refs).filter(([key]) => key.toLowerCase() !== lower));
111
+ refs[repo.name] = repo;
112
+ return { refs };
96
113
  }
97
114
  Local.add = add;
98
115
  function remove(config, name) {
99
- const refs = Object.fromEntries(Object.entries(config.refs).filter(([key]) => key !== name));
116
+ const lower = name.toLowerCase();
117
+ const refs = Object.fromEntries(Object.entries(config.refs).filter(([key]) => key.toLowerCase() !== lower));
100
118
  return { refs };
101
119
  }
102
120
  Local.remove = remove;
package/src/core/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/core/index.ts
3
5
  import { Config } from "@spader/dotllm/core/config";
4
6
  import { add } from "@spader/dotllm/core/add";
package/src/core/link.js CHANGED
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/core/link.ts
3
5
  import { Config } from "@spader/dotllm/core/config";
4
6
  import { sync } from "@spader/dotllm/core/sync";
@@ -8,7 +10,7 @@ function link(names) {
8
10
  const repo = Config.Global.find(global, name);
9
11
  if (!repo)
10
12
  return null;
11
- return [name, repo];
13
+ return [repo.name, repo];
12
14
  }).filter((row) => row !== null);
13
15
  Config.Local.write({ refs: Object.fromEntries(rows) });
14
16
  return sync();
@@ -1,4 +1,6 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/core/remove.ts
3
5
  import fs from "fs";
4
6
  import path from "path";
@@ -9,7 +11,7 @@ function remove(name) {
9
11
  if (!found) {
10
12
  return { ok: false, error: `No repo named "${name}" in registry` };
11
13
  }
12
- const target = path.join(Config.storeDir(), name);
14
+ const target = path.join(Config.storeDir(), found.name);
13
15
  if (fs.existsSync(target)) {
14
16
  const stat = fs.lstatSync(target);
15
17
  if (stat.isSymbolicLink()) {
@@ -19,7 +21,7 @@ function remove(name) {
19
21
  fs.rmSync(target, { recursive: true, force: true });
20
22
  }
21
23
  }
22
- Config.Global.write(Config.Global.remove(global, name));
24
+ Config.Global.write(Config.Global.remove(global, found.name));
23
25
  return { ok: true };
24
26
  }
25
27
  export {
package/src/core/sync.js CHANGED
@@ -1,33 +1,97 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/core/sync.ts
3
5
  import fs from "fs";
4
6
  import path from "path";
5
7
  import { Config } from "@spader/dotllm/core/config";
6
- 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();
7
30
  const states = await Promise.all(names.map(async (name) => {
8
31
  const cwd = path.join(Config.refDir(), name);
9
32
  if (!fs.existsSync(cwd)) {
10
- 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" };
11
64
  }
12
- const proc = Bun.spawn(["git", "pull", "--ff-only"], {
65
+ const reset = Bun.spawn(["git", "reset", "--hard", "FETCH_HEAD"], {
13
66
  cwd,
14
67
  stdout: "pipe",
15
68
  stderr: "pipe"
16
69
  });
17
- const [code, out, err] = await Promise.all([
18
- proc.exited,
19
- new Response(proc.stdout).text(),
20
- 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()
21
74
  ]);
22
- const msg = `${out}
23
- ${err}`.trim();
24
- if (code !== 0) {
25
- 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" };
26
79
  }
27
- return null;
80
+ return { kind: "ok" };
28
81
  }));
29
- const failed = states.filter((state) => state !== null);
30
- 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 };
31
95
  }
32
96
  function sync() {
33
97
  const local = Config.Local.read();
@@ -56,11 +120,12 @@ function sync() {
56
120
  }
57
121
  for (const [name, repo] of Object.entries(local.refs)) {
58
122
  const store = path.join(Config.storeDir(), name);
59
- if (!fs.existsSync(store)) {
123
+ const storeBroken = repo.kind === "url" && fs.existsSync(store) && !fs.existsSync(path.join(store, ".git"));
124
+ if (storeBroken) {
60
125
  fs.rmSync(store, { recursive: true, force: true });
61
126
  }
62
127
  if (!fs.existsSync(store) && repo.kind === "url") {
63
- const clone = Bun.spawnSync(["git", "clone", repo.uri, store], {
128
+ const clone = Bun.spawnSync(["git", "clone", "--depth=1", repo.uri, store], {
64
129
  stdout: "pipe",
65
130
  stderr: "pipe"
66
131
  });
@@ -1,18 +1,21 @@
1
1
  // @bun
2
+ var __require = import.meta.require;
3
+
2
4
  // src/core/unlink.ts
3
5
  import fs from "fs";
4
6
  import path from "path";
5
7
  import { Config } from "@spader/dotllm/core/config";
6
8
  function unlink(name) {
7
9
  const local = Config.Local.read();
8
- if (!Config.Local.has(local, name)) {
10
+ const found = Config.Local.find(local, name);
11
+ if (!found) {
9
12
  return { ok: false, error: `"${name}" is not linked in local config` };
10
13
  }
11
- const target = path.join(Config.refDir(), name);
14
+ const target = path.join(Config.refDir(), found.name);
12
15
  if (fs.existsSync(target)) {
13
16
  fs.unlinkSync(target);
14
17
  }
15
- Config.Local.write(Config.Local.remove(local, name));
18
+ Config.Local.write(Config.Local.remove(local, found.name));
16
19
  return { ok: true };
17
20
  }
18
21
  export {
@@ -1,45 +0,0 @@
1
- // @bun
2
- // src/cli/commands/cd.ts
3
- import path from "path";
4
- import fs from "fs";
5
- import { Config } from "@spader/dotllm/core";
6
- import { defaultTheme as t } from "@spader/dotllm/cli/theme";
7
- var command = {
8
- description: "Open a subshell in a repo's store directory",
9
- summary: "cd into a repo",
10
- positionals: {
11
- name: {
12
- type: "string",
13
- description: "Name of the repo",
14
- required: true
15
- }
16
- },
17
- handler: async (argv) => {
18
- const name = String(argv.name);
19
- const global = Config.Global.read();
20
- const repo = Config.Global.find(global, name);
21
- if (!repo) {
22
- console.error(t.error(`No repo named "${name}" in registry`));
23
- process.exit(1);
24
- return;
25
- }
26
- const dir = path.join(Config.storeDir(), name);
27
- if (!fs.existsSync(dir)) {
28
- console.error(t.error(`Store path does not exist: ${dir}`));
29
- process.exit(1);
30
- return;
31
- }
32
- const shell = process.env.SHELL ?? "/bin/sh";
33
- const proc = Bun.spawn([shell], {
34
- cwd: dir,
35
- stdin: "inherit",
36
- stdout: "inherit",
37
- stderr: "inherit"
38
- });
39
- const code = await proc.exited;
40
- process.exit(code);
41
- }
42
- };
43
- export {
44
- command
45
- };