@spader/dotllm 1.3.2 → 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/README.md +12 -0
- package/package.json +1 -1
- package/src/cli/commands/add.js +529 -30
- package/src/cli/commands/completions.js +506 -26
- package/src/cli/commands/index.js +1268 -16
- package/src/cli/commands/link.js +516 -17
- package/src/cli/commands/list.js +628 -9
- package/src/cli/commands/remove.js +485 -5
- package/src/cli/commands/sync.js +511 -12
- package/src/cli/commands/which.js +488 -8
- package/src/cli/index.js +1438 -10
- package/src/cli/layout.js +22 -6
- package/src/cli/prompt.js +0 -2
- package/src/cli/theme.js +0 -2
- package/src/cli/yargs.js +163 -6
- package/src/core/add.js +170 -14
- package/src/core/config.js +139 -115
- package/src/core/index.js +467 -13
- package/src/core/link.js +321 -3
- package/src/core/remove.js +164 -8
- package/src/core/sync.js +190 -33
- package/src/core/unlink.js +162 -6
package/src/cli/index.js
CHANGED
|
@@ -1,10 +1,1438 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
var
|
|
3
|
+
var __esm = (fn, res, err) => () => {
|
|
4
|
+
if (fn)
|
|
5
|
+
try {
|
|
6
|
+
res = fn(fn = 0);
|
|
7
|
+
} catch (e) {
|
|
8
|
+
err = [e];
|
|
9
|
+
}
|
|
10
|
+
if (err)
|
|
11
|
+
throw err[0];
|
|
12
|
+
return res;
|
|
13
|
+
};
|
|
4
14
|
|
|
15
|
+
// src/core/config.ts
|
|
16
|
+
import fs from "fs";
|
|
17
|
+
import os from "os";
|
|
18
|
+
import path from "path";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
function readJson(filepath) {
|
|
21
|
+
if (!fs.existsSync(filepath))
|
|
22
|
+
return null;
|
|
23
|
+
const raw = fs.readFileSync(filepath, "utf-8");
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(raw);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
var LOCAL_DIR = ".llm", LOCAL_FILE, REF_DIR, RepoEntry, GlobalShape, LocalShape, Config;
|
|
31
|
+
var init_config = __esm(() => {
|
|
32
|
+
LOCAL_FILE = path.join(LOCAL_DIR, "dotllm.json");
|
|
33
|
+
REF_DIR = path.join(LOCAL_DIR, "reference");
|
|
34
|
+
RepoEntry = z.object({
|
|
35
|
+
kind: z.enum(["url", "file"]),
|
|
36
|
+
name: z.string(),
|
|
37
|
+
uri: z.string(),
|
|
38
|
+
description: z.string()
|
|
39
|
+
});
|
|
40
|
+
GlobalShape = z.object({
|
|
41
|
+
store: z.string().optional(),
|
|
42
|
+
repos: z.array(RepoEntry)
|
|
43
|
+
});
|
|
44
|
+
LocalShape = z.object({
|
|
45
|
+
refs: z.record(z.string(), RepoEntry)
|
|
46
|
+
});
|
|
47
|
+
((Config) => {
|
|
48
|
+
function home() {
|
|
49
|
+
if (process.platform === "win32") {
|
|
50
|
+
const appData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), "AppData", "Local");
|
|
51
|
+
return path.join(appData, "dotllm");
|
|
52
|
+
}
|
|
53
|
+
return path.join(process.env.HOME ?? os.homedir(), ".local", "share", "dotllm");
|
|
54
|
+
}
|
|
55
|
+
Config.home = home;
|
|
56
|
+
function storeDir() {
|
|
57
|
+
const store = Global.read().store;
|
|
58
|
+
if (!store)
|
|
59
|
+
return path.join(home(), "store");
|
|
60
|
+
return expand(store);
|
|
61
|
+
}
|
|
62
|
+
Config.storeDir = storeDir;
|
|
63
|
+
function refDir() {
|
|
64
|
+
return REF_DIR;
|
|
65
|
+
}
|
|
66
|
+
Config.refDir = refDir;
|
|
67
|
+
let Global;
|
|
68
|
+
((Global) => {
|
|
69
|
+
function read() {
|
|
70
|
+
const raw = readJson(path.join(home(), "dotllm.json"));
|
|
71
|
+
if (!raw)
|
|
72
|
+
return { repos: [] };
|
|
73
|
+
const result = GlobalShape.safeParse(raw);
|
|
74
|
+
if (!result.success)
|
|
75
|
+
return { repos: [] };
|
|
76
|
+
return result.data;
|
|
77
|
+
}
|
|
78
|
+
Global.read = read;
|
|
79
|
+
function write(config) {
|
|
80
|
+
const dir = home();
|
|
81
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
82
|
+
fs.writeFileSync(path.join(dir, "dotllm.json"), JSON.stringify(config, null, 2) + `
|
|
83
|
+
`);
|
|
84
|
+
}
|
|
85
|
+
Global.write = write;
|
|
86
|
+
function find(config, name) {
|
|
87
|
+
const lower = name.toLowerCase();
|
|
88
|
+
return config.repos.find((r) => r.name.toLowerCase() === lower);
|
|
89
|
+
}
|
|
90
|
+
Global.find = find;
|
|
91
|
+
function add(config, entry) {
|
|
92
|
+
const lower = entry.name.toLowerCase();
|
|
93
|
+
const filtered = config.repos.filter((r) => r.name.toLowerCase() !== lower);
|
|
94
|
+
return { ...config, repos: [...filtered, entry] };
|
|
95
|
+
}
|
|
96
|
+
Global.add = add;
|
|
97
|
+
function remove(config, name) {
|
|
98
|
+
const lower = name.toLowerCase();
|
|
99
|
+
return { ...config, repos: config.repos.filter((r) => r.name.toLowerCase() !== lower) };
|
|
100
|
+
}
|
|
101
|
+
Global.remove = remove;
|
|
102
|
+
})(Global = Config.Global ||= {});
|
|
103
|
+
let Local;
|
|
104
|
+
((Local) => {
|
|
105
|
+
function read() {
|
|
106
|
+
const raw = readJson(LOCAL_FILE);
|
|
107
|
+
if (!raw)
|
|
108
|
+
return { refs: {} };
|
|
109
|
+
const result = LocalShape.safeParse(raw);
|
|
110
|
+
if (!result.success)
|
|
111
|
+
return { refs: {} };
|
|
112
|
+
return result.data;
|
|
113
|
+
}
|
|
114
|
+
Local.read = read;
|
|
115
|
+
function write(config) {
|
|
116
|
+
fs.mkdirSync(LOCAL_DIR, { recursive: true });
|
|
117
|
+
fs.writeFileSync(LOCAL_FILE, JSON.stringify(config, null, 2) + `
|
|
118
|
+
`);
|
|
119
|
+
}
|
|
120
|
+
Local.write = write;
|
|
121
|
+
function find(config, name) {
|
|
122
|
+
const lower = name.toLowerCase();
|
|
123
|
+
for (const [key, value] of Object.entries(config.refs)) {
|
|
124
|
+
if (key.toLowerCase() === lower)
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
Local.find = find;
|
|
130
|
+
function has(config, name) {
|
|
131
|
+
return find(config, name) !== undefined;
|
|
132
|
+
}
|
|
133
|
+
Local.has = has;
|
|
134
|
+
function add(config, repo) {
|
|
135
|
+
const lower = repo.name.toLowerCase();
|
|
136
|
+
const refs = Object.fromEntries(Object.entries(config.refs).filter(([key]) => key.toLowerCase() !== lower));
|
|
137
|
+
refs[repo.name] = repo;
|
|
138
|
+
return { refs };
|
|
139
|
+
}
|
|
140
|
+
Local.add = add;
|
|
141
|
+
function remove(config, name) {
|
|
142
|
+
const lower = name.toLowerCase();
|
|
143
|
+
const refs = Object.fromEntries(Object.entries(config.refs).filter(([key]) => key.toLowerCase() !== lower));
|
|
144
|
+
return { refs };
|
|
145
|
+
}
|
|
146
|
+
Local.remove = remove;
|
|
147
|
+
})(Local = Config.Local ||= {});
|
|
148
|
+
function expand(dir) {
|
|
149
|
+
const tilde = dir === "~" || dir.startsWith("~/") || dir.startsWith("~\\");
|
|
150
|
+
if (!tilde)
|
|
151
|
+
return path.resolve(home(), dir);
|
|
152
|
+
return path.join(os.homedir(), dir.slice(1));
|
|
153
|
+
}
|
|
154
|
+
})(Config ||= {});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// src/core/add.ts
|
|
158
|
+
import fs2 from "fs";
|
|
159
|
+
import path2 from "path";
|
|
160
|
+
function isUrl(value) {
|
|
161
|
+
return value.startsWith("http://") || value.startsWith("https://") || value.startsWith("git@") || value.startsWith("ssh://");
|
|
162
|
+
}
|
|
163
|
+
function nameFromGitRemote(dir) {
|
|
164
|
+
const result = Bun.spawnSync(["git", "remote", "get-url", "origin"], {
|
|
165
|
+
cwd: dir,
|
|
166
|
+
stdout: "pipe",
|
|
167
|
+
stderr: "pipe"
|
|
168
|
+
});
|
|
169
|
+
if (result.exitCode !== 0)
|
|
170
|
+
return null;
|
|
171
|
+
const url = result.stdout.toString().trim();
|
|
172
|
+
return nameFromUrl(url);
|
|
173
|
+
}
|
|
174
|
+
function nameFromUrl(url) {
|
|
175
|
+
const base = url.split("/").pop() ?? url;
|
|
176
|
+
return base.replace(/\.git$/, "");
|
|
177
|
+
}
|
|
178
|
+
async function add(uri, name, description) {
|
|
179
|
+
const desc = description ?? "";
|
|
180
|
+
if (isUrl(uri)) {
|
|
181
|
+
return cloneUrl(uri, name, desc);
|
|
182
|
+
}
|
|
183
|
+
return linkLocal(uri, name, desc);
|
|
184
|
+
}
|
|
185
|
+
async function cloneUrl(url, name, description) {
|
|
186
|
+
const resolved = name ?? nameFromUrl(url);
|
|
187
|
+
const store = Config.storeDir();
|
|
188
|
+
fs2.mkdirSync(store, { recursive: true });
|
|
189
|
+
const target = path2.join(store, resolved);
|
|
190
|
+
if (!fs2.existsSync(target)) {
|
|
191
|
+
const proc = Bun.spawn(["git", "clone", "--depth=1", url, target], {
|
|
192
|
+
stdout: "pipe",
|
|
193
|
+
stderr: "pipe"
|
|
194
|
+
});
|
|
195
|
+
const code = await proc.exited;
|
|
196
|
+
if (code !== 0) {
|
|
197
|
+
const msg = await new Response(proc.stderr).text();
|
|
198
|
+
return { ok: false, error: `git clone failed: ${msg.trim()}` };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const entry = { kind: "url", name: resolved, uri: url, description };
|
|
202
|
+
const global = Config.Global.read();
|
|
203
|
+
Config.Global.write(Config.Global.add(global, entry));
|
|
204
|
+
return { ok: true, entry, storePath: target };
|
|
205
|
+
}
|
|
206
|
+
function linkLocal(raw, name, description) {
|
|
207
|
+
const resolved = path2.resolve(raw);
|
|
208
|
+
if (!fs2.existsSync(resolved)) {
|
|
209
|
+
return { ok: false, error: `Path does not exist: ${resolved}` };
|
|
210
|
+
}
|
|
211
|
+
if (!fs2.statSync(resolved).isDirectory()) {
|
|
212
|
+
return { ok: false, error: `Not a directory: ${resolved}` };
|
|
213
|
+
}
|
|
214
|
+
const store = Config.storeDir();
|
|
215
|
+
fs2.mkdirSync(store, { recursive: true });
|
|
216
|
+
const finalName = name ?? nameFromGitRemote(resolved) ?? path2.basename(resolved);
|
|
217
|
+
const target = path2.join(store, finalName);
|
|
218
|
+
if (!fs2.existsSync(target)) {
|
|
219
|
+
fs2.symlinkSync(resolved, target, "dir");
|
|
220
|
+
}
|
|
221
|
+
const entry = { kind: "file", name: finalName, uri: resolved, description };
|
|
222
|
+
const global = Config.Global.read();
|
|
223
|
+
Config.Global.write(Config.Global.add(global, entry));
|
|
224
|
+
return { ok: true, entry, storePath: target };
|
|
225
|
+
}
|
|
226
|
+
var init_add = __esm(() => {
|
|
227
|
+
init_config();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// src/core/remove.ts
|
|
231
|
+
import fs3 from "fs";
|
|
232
|
+
import path3 from "path";
|
|
233
|
+
function remove(name) {
|
|
234
|
+
const global = Config.Global.read();
|
|
235
|
+
const found = Config.Global.find(global, name);
|
|
236
|
+
if (!found) {
|
|
237
|
+
return { ok: false, error: `No repo named "${name}" in registry` };
|
|
238
|
+
}
|
|
239
|
+
const target = path3.join(Config.storeDir(), found.name);
|
|
240
|
+
if (fs3.existsSync(target)) {
|
|
241
|
+
const stat = fs3.lstatSync(target);
|
|
242
|
+
if (stat.isSymbolicLink()) {
|
|
243
|
+
fs3.unlinkSync(target);
|
|
244
|
+
}
|
|
245
|
+
if (stat.isDirectory()) {
|
|
246
|
+
fs3.rmSync(target, { recursive: true, force: true });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
Config.Global.write(Config.Global.remove(global, found.name));
|
|
250
|
+
return { ok: true };
|
|
251
|
+
}
|
|
252
|
+
var init_remove = __esm(() => {
|
|
253
|
+
init_config();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// src/core/sync.ts
|
|
257
|
+
import fs4 from "fs";
|
|
258
|
+
import path4 from "path";
|
|
259
|
+
function shouldPull(storeDir, force) {
|
|
260
|
+
if (force)
|
|
261
|
+
return true;
|
|
262
|
+
const head = path4.join(storeDir, ".git", "HEAD");
|
|
263
|
+
const headStat = fs4.statSync(head, { throwIfNoEntry: false });
|
|
264
|
+
if (!headStat)
|
|
265
|
+
return true;
|
|
266
|
+
const age = Date.now() - headStat.mtimeMs;
|
|
267
|
+
if (age < FRESH_MS)
|
|
268
|
+
return false;
|
|
269
|
+
if (age > STALE_MS)
|
|
270
|
+
return true;
|
|
271
|
+
const dirStat = fs4.statSync(storeDir, { throwIfNoEntry: false });
|
|
272
|
+
if (!dirStat)
|
|
273
|
+
return true;
|
|
274
|
+
return dirStat.atimeMs > headStat.mtimeMs;
|
|
275
|
+
}
|
|
276
|
+
async function pull(names, options = {}) {
|
|
277
|
+
const force = options.force === true;
|
|
278
|
+
const global = Config.Global.read();
|
|
279
|
+
const states = await Promise.all(names.map(async (name) => {
|
|
280
|
+
const cwd = path4.join(Config.refDir(), name);
|
|
281
|
+
if (!fs4.existsSync(cwd)) {
|
|
282
|
+
return { kind: "failed", name, error: "reference directory missing" };
|
|
283
|
+
}
|
|
284
|
+
const repo = Config.Global.find(global, name);
|
|
285
|
+
if (repo && repo.kind === "file") {
|
|
286
|
+
return { kind: "skipped", name };
|
|
287
|
+
}
|
|
288
|
+
const storeDir = path4.join(Config.storeDir(), name);
|
|
289
|
+
const storeStat = fs4.lstatSync(storeDir, { throwIfNoEntry: false });
|
|
290
|
+
if (!storeStat || storeStat.isSymbolicLink()) {
|
|
291
|
+
return { kind: "skipped", name };
|
|
292
|
+
}
|
|
293
|
+
if (!fs4.existsSync(path4.join(storeDir, ".git"))) {
|
|
294
|
+
return { kind: "failed", name, error: "store directory is not a git repo" };
|
|
295
|
+
}
|
|
296
|
+
if (!shouldPull(storeDir, force)) {
|
|
297
|
+
return { kind: "skipped", name };
|
|
298
|
+
}
|
|
299
|
+
const fetch2 = Bun.spawn(["git", "fetch", "--depth=1", "origin", "HEAD"], {
|
|
300
|
+
cwd,
|
|
301
|
+
stdout: "pipe",
|
|
302
|
+
stderr: "pipe"
|
|
303
|
+
});
|
|
304
|
+
const [fetchCode, fetchOut, fetchErr] = await Promise.all([
|
|
305
|
+
fetch2.exited,
|
|
306
|
+
new Response(fetch2.stdout).text(),
|
|
307
|
+
new Response(fetch2.stderr).text()
|
|
308
|
+
]);
|
|
309
|
+
if (fetchCode !== 0) {
|
|
310
|
+
const msg = `${fetchOut}
|
|
311
|
+
${fetchErr}`.trim();
|
|
312
|
+
return { kind: "failed", name, error: msg || "git fetch failed" };
|
|
313
|
+
}
|
|
314
|
+
const reset = Bun.spawn(["git", "reset", "--hard", "FETCH_HEAD"], {
|
|
315
|
+
cwd,
|
|
316
|
+
stdout: "pipe",
|
|
317
|
+
stderr: "pipe"
|
|
318
|
+
});
|
|
319
|
+
const [resetCode, resetOut, resetErr] = await Promise.all([
|
|
320
|
+
reset.exited,
|
|
321
|
+
new Response(reset.stdout).text(),
|
|
322
|
+
new Response(reset.stderr).text()
|
|
323
|
+
]);
|
|
324
|
+
if (resetCode !== 0) {
|
|
325
|
+
const msg = `${resetOut}
|
|
326
|
+
${resetErr}`.trim();
|
|
327
|
+
return { kind: "failed", name, error: msg || "git reset failed" };
|
|
328
|
+
}
|
|
329
|
+
return { kind: "ok" };
|
|
330
|
+
}));
|
|
331
|
+
const pulled = [];
|
|
332
|
+
const skipped = [];
|
|
333
|
+
const failed = [];
|
|
334
|
+
for (let i = 0;i < states.length; i++) {
|
|
335
|
+
const state = states[i];
|
|
336
|
+
if (state.kind === "ok")
|
|
337
|
+
pulled.push(names[i]);
|
|
338
|
+
if (state.kind === "skipped")
|
|
339
|
+
skipped.push(state.name);
|
|
340
|
+
if (state.kind === "failed")
|
|
341
|
+
failed.push({ name: state.name, error: state.error });
|
|
342
|
+
}
|
|
343
|
+
return { count: names.length, pulled, skipped, failed };
|
|
344
|
+
}
|
|
345
|
+
function sync() {
|
|
346
|
+
const local = Config.Local.read();
|
|
347
|
+
const global = Config.Global.read();
|
|
348
|
+
const merged = Object.values(local.refs).reduce((config, repo) => Config.Global.add(config, repo), global);
|
|
349
|
+
if (Object.keys(local.refs).length > 0) {
|
|
350
|
+
Config.Global.write(merged);
|
|
351
|
+
}
|
|
352
|
+
const refDir = Config.refDir();
|
|
353
|
+
fs4.mkdirSync(refDir, { recursive: true });
|
|
354
|
+
fs4.mkdirSync(Config.storeDir(), { recursive: true });
|
|
355
|
+
const linked = [];
|
|
356
|
+
const removed = [];
|
|
357
|
+
const missing = [];
|
|
358
|
+
const unchanged = [];
|
|
359
|
+
const wanted = new Set(Object.keys(local.refs));
|
|
360
|
+
for (const entry of fs4.readdirSync(refDir)) {
|
|
361
|
+
if (wanted.has(entry))
|
|
362
|
+
continue;
|
|
363
|
+
const target = path4.join(refDir, entry);
|
|
364
|
+
const stat = fs4.lstatSync(target);
|
|
365
|
+
if (stat.isSymbolicLink()) {
|
|
366
|
+
fs4.unlinkSync(target);
|
|
367
|
+
removed.push(entry);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
for (const [name, repo] of Object.entries(local.refs)) {
|
|
371
|
+
const store = path4.join(Config.storeDir(), name);
|
|
372
|
+
const storeBroken = repo.kind === "url" && fs4.existsSync(store) && !fs4.existsSync(path4.join(store, ".git"));
|
|
373
|
+
if (storeBroken) {
|
|
374
|
+
fs4.rmSync(store, { recursive: true, force: true });
|
|
375
|
+
}
|
|
376
|
+
if (!fs4.existsSync(store) && repo.kind === "url") {
|
|
377
|
+
const clone = Bun.spawnSync(["git", "clone", "--depth=1", repo.uri, store], {
|
|
378
|
+
stdout: "pipe",
|
|
379
|
+
stderr: "pipe"
|
|
380
|
+
});
|
|
381
|
+
if (clone.exitCode !== 0) {
|
|
382
|
+
missing.push(name);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (!fs4.existsSync(store) && repo.kind === "file") {
|
|
387
|
+
if (!fs4.existsSync(repo.uri)) {
|
|
388
|
+
missing.push(name);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (!fs4.statSync(repo.uri).isDirectory()) {
|
|
392
|
+
missing.push(name);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
fs4.symlinkSync(repo.uri, store, "dir");
|
|
396
|
+
}
|
|
397
|
+
if (!fs4.existsSync(store)) {
|
|
398
|
+
missing.push(name);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const target = path4.join(refDir, name);
|
|
402
|
+
if (fs4.existsSync(target)) {
|
|
403
|
+
unchanged.push(name);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
fs4.symlinkSync(store, target, "dir");
|
|
407
|
+
linked.push(name);
|
|
408
|
+
}
|
|
409
|
+
return { linked, removed, missing, unchanged };
|
|
410
|
+
}
|
|
411
|
+
var FRESH_MS, STALE_MS;
|
|
412
|
+
var init_sync = __esm(() => {
|
|
413
|
+
init_config();
|
|
414
|
+
FRESH_MS = 24 * 60 * 60 * 1000;
|
|
415
|
+
STALE_MS = 7 * FRESH_MS;
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// src/core/link.ts
|
|
419
|
+
function link(names) {
|
|
420
|
+
const global = Config.Global.read();
|
|
421
|
+
const rows = names.map((name) => {
|
|
422
|
+
const repo = Config.Global.find(global, name);
|
|
423
|
+
if (!repo)
|
|
424
|
+
return null;
|
|
425
|
+
return [repo.name, repo];
|
|
426
|
+
}).filter((row) => row !== null);
|
|
427
|
+
Config.Local.write({ refs: Object.fromEntries(rows) });
|
|
428
|
+
return sync();
|
|
429
|
+
}
|
|
430
|
+
var init_link = __esm(() => {
|
|
431
|
+
init_config();
|
|
432
|
+
init_sync();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// src/core/unlink.ts
|
|
436
|
+
import fs5 from "fs";
|
|
437
|
+
import path5 from "path";
|
|
438
|
+
function unlink(name) {
|
|
439
|
+
const local = Config.Local.read();
|
|
440
|
+
const found = Config.Local.find(local, name);
|
|
441
|
+
if (!found) {
|
|
442
|
+
return { ok: false, error: `"${name}" is not linked in local config` };
|
|
443
|
+
}
|
|
444
|
+
const target = path5.join(Config.refDir(), found.name);
|
|
445
|
+
if (fs5.existsSync(target)) {
|
|
446
|
+
fs5.unlinkSync(target);
|
|
447
|
+
}
|
|
448
|
+
Config.Local.write(Config.Local.remove(local, found.name));
|
|
449
|
+
return { ok: true };
|
|
450
|
+
}
|
|
451
|
+
var init_unlink = __esm(() => {
|
|
452
|
+
init_config();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// src/core/index.ts
|
|
456
|
+
var init_core = __esm(() => {
|
|
457
|
+
init_config();
|
|
458
|
+
init_add();
|
|
459
|
+
init_remove();
|
|
460
|
+
init_link();
|
|
461
|
+
init_unlink();
|
|
462
|
+
init_sync();
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// src/cli/yargs.ts
|
|
466
|
+
import yargs from "yargs";
|
|
467
|
+
import { hideBin } from "yargs/helpers";
|
|
468
|
+
import pc from "picocolors";
|
|
469
|
+
|
|
470
|
+
// src/cli/theme.ts
|
|
471
|
+
function rgb(r, g, b) {
|
|
472
|
+
return (value) => `\x1B[38;2;${r};${g};${b}m${value}\x1B[39m`;
|
|
473
|
+
}
|
|
474
|
+
var gray = (value) => rgb(value, value, value);
|
|
475
|
+
var defaultTheme = {
|
|
476
|
+
primary: rgb(114, 161, 136),
|
|
477
|
+
link: rgb(114, 140, 212),
|
|
478
|
+
header: gray(128),
|
|
479
|
+
command: rgb(114, 161, 136),
|
|
480
|
+
arg: rgb(161, 212, 212),
|
|
481
|
+
option: rgb(212, 212, 161),
|
|
482
|
+
type: gray(128),
|
|
483
|
+
description: (value) => value,
|
|
484
|
+
dim: gray(128),
|
|
485
|
+
error: rgb(212, 114, 114),
|
|
486
|
+
success: rgb(114, 212, 136)
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// src/cli/layout.ts
|
|
490
|
+
var ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
491
|
+
function clean(value) {
|
|
492
|
+
return value.replace(/[\t\n]/g, " ").replace(ANSI_RE, "");
|
|
493
|
+
}
|
|
494
|
+
function truncateMiddle(value, width) {
|
|
495
|
+
if (width <= 0)
|
|
496
|
+
return "";
|
|
497
|
+
if (value.length <= width)
|
|
498
|
+
return value;
|
|
499
|
+
if (width <= 3)
|
|
500
|
+
return value.slice(0, width);
|
|
501
|
+
const tail = Math.floor((width - 3) / 2);
|
|
502
|
+
const head = width - 3 - tail;
|
|
503
|
+
return `${value.slice(0, head)}...${value.slice(value.length - tail)}`;
|
|
504
|
+
}
|
|
505
|
+
function truncateStart(value, width) {
|
|
506
|
+
if (width <= 0)
|
|
507
|
+
return "";
|
|
508
|
+
if (value.length <= width)
|
|
509
|
+
return value;
|
|
510
|
+
if (width <= 3)
|
|
511
|
+
return "...".slice(0, width);
|
|
512
|
+
return `...${value.slice(value.length - (width - 3))}`;
|
|
513
|
+
}
|
|
514
|
+
function truncateEnd(value, width) {
|
|
515
|
+
if (width <= 0)
|
|
516
|
+
return "";
|
|
517
|
+
if (value.length <= width)
|
|
518
|
+
return value;
|
|
519
|
+
if (width <= 3)
|
|
520
|
+
return "...".slice(0, width);
|
|
521
|
+
return `${value.slice(0, width - 3)}...`;
|
|
522
|
+
}
|
|
523
|
+
function truncate(value, width, mode) {
|
|
524
|
+
if (mode === "start")
|
|
525
|
+
return truncateStart(value, width);
|
|
526
|
+
if (mode === "end")
|
|
527
|
+
return truncateEnd(value, width);
|
|
528
|
+
return truncateMiddle(value, width);
|
|
529
|
+
}
|
|
530
|
+
function fitWidths(natural, available, flex, noTruncate) {
|
|
531
|
+
const widths = [...natural];
|
|
532
|
+
const totalNatural = natural.reduce((sum, width) => sum + width, 0);
|
|
533
|
+
if (totalNatural <= available) {
|
|
534
|
+
return widths;
|
|
535
|
+
}
|
|
536
|
+
let fixedWidth = 0;
|
|
537
|
+
const dynamic = [];
|
|
538
|
+
for (let index = 0;index < widths.length; index++) {
|
|
539
|
+
if (noTruncate[index] || flex[index] === 0) {
|
|
540
|
+
fixedWidth += widths[index] ?? 0;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
dynamic.push({ index, weight: flex[index] ?? 1 });
|
|
544
|
+
}
|
|
545
|
+
const remaining = Math.max(0, available - fixedWidth);
|
|
546
|
+
if (dynamic.length === 0) {
|
|
547
|
+
return widths;
|
|
548
|
+
}
|
|
549
|
+
const totalWeight = dynamic.reduce((sum, item) => sum + item.weight, 0);
|
|
550
|
+
let used = 0;
|
|
551
|
+
for (const item of dynamic) {
|
|
552
|
+
const share = Math.floor(remaining * item.weight / totalWeight);
|
|
553
|
+
const width = Math.max(1, share);
|
|
554
|
+
widths[item.index] = width;
|
|
555
|
+
used += width;
|
|
556
|
+
}
|
|
557
|
+
let extra = remaining - used;
|
|
558
|
+
let cursor = 0;
|
|
559
|
+
while (extra > 0) {
|
|
560
|
+
const item = dynamic[cursor % dynamic.length];
|
|
561
|
+
widths[item.index] = (widths[item.index] ?? 0) + 1;
|
|
562
|
+
extra--;
|
|
563
|
+
cursor++;
|
|
564
|
+
}
|
|
565
|
+
return widths;
|
|
566
|
+
}
|
|
567
|
+
function table(headers, columns, options = {}) {
|
|
568
|
+
if (headers.length === 0)
|
|
569
|
+
return;
|
|
570
|
+
const count = headers.length;
|
|
571
|
+
const gap = 2;
|
|
572
|
+
const rows = columns.reduce((max, column) => Math.max(max, column.length), 0);
|
|
573
|
+
const visibleRows = Math.min(rows, options.maxRows ?? rows);
|
|
574
|
+
const natural = [];
|
|
575
|
+
for (let col = 0;col < count; col++) {
|
|
576
|
+
const headerWidth = clean(headers[col] ?? "").length;
|
|
577
|
+
let width = headerWidth;
|
|
578
|
+
for (let row = 0;row < visibleRows; row++) {
|
|
579
|
+
const value = clean(columns[col]?.[row] ?? "");
|
|
580
|
+
width = Math.max(width, value.length);
|
|
581
|
+
}
|
|
582
|
+
natural[col] = width;
|
|
583
|
+
}
|
|
584
|
+
const maxWidth = options.maxWidth ?? (process.stdout.columns == null ? 120 : process.stdout.columns);
|
|
585
|
+
const available = Math.max(0, maxWidth - gap * (count - 1) - 1);
|
|
586
|
+
const widths = fitWidths(natural, available, options.flex ?? [], options.noTruncate ?? []);
|
|
587
|
+
const header = headers.map((title, col) => truncateMiddle(clean(title), widths[col] ?? 0).padEnd(widths[col] ?? 0)).join(" ");
|
|
588
|
+
process.stdout.write(`${defaultTheme.dim(header)}
|
|
589
|
+
`);
|
|
590
|
+
for (let row = 0;row < visibleRows; row++) {
|
|
591
|
+
const cells = [];
|
|
592
|
+
for (let col = 0;col < count; col++) {
|
|
593
|
+
const width = widths[col] ?? 0;
|
|
594
|
+
const source = clean(columns[col]?.[row] ?? "");
|
|
595
|
+
const mode = options.truncate?.[col] ?? "middle";
|
|
596
|
+
const value = options.noTruncate?.[col] ? source : truncate(source, width, mode);
|
|
597
|
+
const padded = width > 0 ? value.padEnd(width) : value;
|
|
598
|
+
const formatted = options.format?.[col]?.(padded, row, col) ?? padded;
|
|
599
|
+
cells.push(formatted);
|
|
600
|
+
}
|
|
601
|
+
process.stdout.write(`${cells.join(" ")}
|
|
602
|
+
`);
|
|
603
|
+
}
|
|
604
|
+
if (rows > visibleRows) {
|
|
605
|
+
process.stdout.write(`${defaultTheme.dim("(...truncated)")}
|
|
606
|
+
`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function cols(rows, colorFns) {
|
|
610
|
+
if (rows.length === 0)
|
|
611
|
+
return;
|
|
612
|
+
const widths = rows[0].map((_, col) => {
|
|
613
|
+
let width = 0;
|
|
614
|
+
for (const row of rows) {
|
|
615
|
+
width = Math.max(width, clean(row[col] ?? "").length);
|
|
616
|
+
}
|
|
617
|
+
return width;
|
|
618
|
+
});
|
|
619
|
+
for (const row of rows) {
|
|
620
|
+
const line = row.map((value, col) => {
|
|
621
|
+
const padded = clean(value).padEnd(widths[col] ?? 0);
|
|
622
|
+
return colorFns?.[col]?.(padded) ?? padded;
|
|
623
|
+
}).join(" ");
|
|
624
|
+
process.stdout.write(`${line}
|
|
625
|
+
`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/cli/yargs.ts
|
|
630
|
+
function usage(def, path, t) {
|
|
631
|
+
const parts = [];
|
|
632
|
+
const last = path.length - 1;
|
|
633
|
+
for (let i = 0;i < path.length; i++) {
|
|
634
|
+
const fmt = t.command;
|
|
635
|
+
parts.push(i === last ? fmt(path[i]) : path[i]);
|
|
636
|
+
}
|
|
637
|
+
if ("positionals" in def && def.positionals) {
|
|
638
|
+
for (const [k, v] of Object.entries(def.positionals)) {
|
|
639
|
+
const name = t.arg(`$${k}`);
|
|
640
|
+
parts.push(v.required ? name : `[${name}]`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (def.options && Object.keys(def.options).length > 0) {
|
|
644
|
+
parts.push(t.dim("[options]"));
|
|
645
|
+
}
|
|
646
|
+
if ("commands" in def && def.commands && Object.keys(def.commands).length > 0) {
|
|
647
|
+
parts.push(t.arg("$command"));
|
|
648
|
+
}
|
|
649
|
+
return parts.join(" ");
|
|
650
|
+
}
|
|
651
|
+
function help(def, name, path = [], t = defaultTheme) {
|
|
652
|
+
let prev = false;
|
|
653
|
+
if (def.description) {
|
|
654
|
+
console.log(t.description(def.description));
|
|
655
|
+
prev = true;
|
|
656
|
+
}
|
|
657
|
+
if (prev)
|
|
658
|
+
console.log("");
|
|
659
|
+
console.log(t.header("usage:"));
|
|
660
|
+
console.log(` ${usage(def, [name, ...path], t)}`);
|
|
661
|
+
prev = true;
|
|
662
|
+
const pos = "positionals" in def ? def.positionals : undefined;
|
|
663
|
+
if (pos && Object.keys(pos).length > 0) {
|
|
664
|
+
if (prev)
|
|
665
|
+
console.log("");
|
|
666
|
+
console.log(t.header("arguments"));
|
|
667
|
+
prev = true;
|
|
668
|
+
const rows = [];
|
|
669
|
+
for (const [k, v] of Object.entries(pos)) {
|
|
670
|
+
let desc = v.description;
|
|
671
|
+
if (v.default !== undefined)
|
|
672
|
+
desc += ` ${t.dim(`(default: ${v.default})`)}`;
|
|
673
|
+
if (v.required)
|
|
674
|
+
desc += ` ${t.dim("(required)")}`;
|
|
675
|
+
rows.push([` ${k}`, v.type, desc]);
|
|
676
|
+
}
|
|
677
|
+
cols(rows, [t.arg, t.type, t.description]);
|
|
678
|
+
}
|
|
679
|
+
const opts = {
|
|
680
|
+
...def.options ?? {},
|
|
681
|
+
help: { alias: "h", type: "boolean", description: "Show help" }
|
|
682
|
+
};
|
|
683
|
+
if ("version" in def && def.version) {
|
|
684
|
+
opts.version = { alias: "v", type: "boolean", description: "Show version" };
|
|
685
|
+
}
|
|
686
|
+
if (Object.keys(opts).length > 0) {
|
|
687
|
+
if (prev)
|
|
688
|
+
console.log("");
|
|
689
|
+
console.log(t.header("options"));
|
|
690
|
+
prev = true;
|
|
691
|
+
const rows = [];
|
|
692
|
+
for (const [k, v] of Object.entries(opts)) {
|
|
693
|
+
const short = v.alias ? `-${v.alias}, ` : " ";
|
|
694
|
+
let desc = v.description;
|
|
695
|
+
if (v.default !== undefined && v.type !== "boolean") {
|
|
696
|
+
desc += ` ${t.dim(`(default: ${v.default})`)}`;
|
|
697
|
+
}
|
|
698
|
+
rows.push([` ${short}--${k}`, v.type, desc]);
|
|
699
|
+
}
|
|
700
|
+
cols(rows, [t.option, t.type, t.description]);
|
|
701
|
+
}
|
|
702
|
+
const cmds = "commands" in def ? def.commands : undefined;
|
|
703
|
+
if (cmds && Object.keys(cmds).length > 0) {
|
|
704
|
+
if (prev)
|
|
705
|
+
console.log("");
|
|
706
|
+
console.log(t.header("commands"));
|
|
707
|
+
const rows = [];
|
|
708
|
+
const rowCmdFmt = [];
|
|
709
|
+
for (const [k, v] of Object.entries(cmds)) {
|
|
710
|
+
if (v.hidden)
|
|
711
|
+
continue;
|
|
712
|
+
const args = v.positionals ? Object.keys(v.positionals).join(" ") : "";
|
|
713
|
+
rows.push([` ${k}`, args, v.summary ?? v.description]);
|
|
714
|
+
rowCmdFmt.push(t.command);
|
|
715
|
+
}
|
|
716
|
+
let ri = 0;
|
|
717
|
+
cols(rows, [(s) => rowCmdFmt[ri++](s), t.arg, t.description]);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function fail(def, name, path = []) {
|
|
721
|
+
return (msg) => {
|
|
722
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
723
|
+
help(def, name, path);
|
|
724
|
+
process.exit(0);
|
|
725
|
+
}
|
|
726
|
+
if (msg?.includes("You must specify") || msg?.includes("Not enough non-option arguments")) {
|
|
727
|
+
help(def, name, path);
|
|
728
|
+
process.exit(1);
|
|
729
|
+
}
|
|
730
|
+
console.error(pc.red(msg ?? "Unknown error"));
|
|
731
|
+
process.exit(1);
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
function check(def, name, path = []) {
|
|
735
|
+
return (argv) => {
|
|
736
|
+
const args = argv;
|
|
737
|
+
if (args.help && args._.length === path.length) {
|
|
738
|
+
help(def, name, path);
|
|
739
|
+
process.exit(0);
|
|
740
|
+
}
|
|
741
|
+
return true;
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function configure(y, def, root, path) {
|
|
745
|
+
if ("positionals" in def && def.positionals) {
|
|
746
|
+
for (const [k, v] of Object.entries(def.positionals)) {
|
|
747
|
+
y.positional(k, {
|
|
748
|
+
type: v.type,
|
|
749
|
+
describe: v.description,
|
|
750
|
+
demandOption: v.required,
|
|
751
|
+
default: v.default
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (def.options) {
|
|
756
|
+
for (const [k, v] of Object.entries(def.options)) {
|
|
757
|
+
y.option(k, {
|
|
758
|
+
alias: v.alias,
|
|
759
|
+
type: v.type,
|
|
760
|
+
describe: v.description,
|
|
761
|
+
demandOption: v.required,
|
|
762
|
+
default: v.default
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if ("commands" in def && def.commands) {
|
|
767
|
+
for (const [k, v] of Object.entries(def.commands)) {
|
|
768
|
+
command(y, k, v, root, path);
|
|
769
|
+
}
|
|
770
|
+
const hasHandler = "handler" in def && typeof def.handler === "function";
|
|
771
|
+
if (!hasHandler) {
|
|
772
|
+
y.demandCommand(1, "You must specify a command");
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
y.help(false).option("help", { alias: "h", type: "boolean", describe: "Show help" }).check(check(def, root, path)).fail(fail(def, root, path));
|
|
776
|
+
if (path.length === 0 && "version" in def && def.version) {
|
|
777
|
+
y.version(def.version).alias("version", "v");
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function command(y, name, def, root, path) {
|
|
781
|
+
let cmd = name;
|
|
782
|
+
if (def.positionals) {
|
|
783
|
+
for (const [k, v] of Object.entries(def.positionals)) {
|
|
784
|
+
cmd += v.required ? ` <${k}>` : ` [${k}]`;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
y.command(cmd, def.description, (inner) => configure(inner, def, root, [...path, name]), def.handler);
|
|
788
|
+
}
|
|
789
|
+
function build(def) {
|
|
790
|
+
const y = yargs(hideBin(process.argv)).scriptName(def.name);
|
|
791
|
+
configure(y, def, def.name, []);
|
|
792
|
+
y.strict();
|
|
793
|
+
return y;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/cli/commands/add.ts
|
|
797
|
+
init_core();
|
|
798
|
+
import fs6 from "fs";
|
|
799
|
+
import path6 from "path";
|
|
800
|
+
import * as prompts2 from "@clack/prompts";
|
|
801
|
+
import { z as z2 } from "zod";
|
|
802
|
+
|
|
803
|
+
// src/cli/prompt.ts
|
|
804
|
+
import * as prompts from "@clack/prompts";
|
|
805
|
+
var Prompt;
|
|
806
|
+
((Prompt) => {
|
|
807
|
+
function sync(result) {
|
|
808
|
+
const parts = [];
|
|
809
|
+
if (result.linked.length > 0)
|
|
810
|
+
parts.push(`${result.linked.length} added`);
|
|
811
|
+
if (result.removed.length > 0)
|
|
812
|
+
parts.push(`${result.removed.length} removed`);
|
|
813
|
+
if (result.unchanged.length > 0)
|
|
814
|
+
parts.push(`${result.unchanged.length} unchanged`);
|
|
815
|
+
if (result.missing.length > 0)
|
|
816
|
+
parts.push(`${result.missing.length} missing`);
|
|
817
|
+
if (parts.length > 0)
|
|
818
|
+
prompts.log.step(parts.join(", "));
|
|
819
|
+
}
|
|
820
|
+
Prompt.sync = sync;
|
|
821
|
+
})(Prompt ||= {});
|
|
822
|
+
|
|
823
|
+
// src/cli/commands/add.ts
|
|
824
|
+
var RepoShape = z2.object({
|
|
825
|
+
description: z2.string().nullable().optional()
|
|
826
|
+
});
|
|
827
|
+
function isUrl2(value) {
|
|
828
|
+
return value.startsWith("http://") || value.startsWith("https://") || value.startsWith("git@") || value.startsWith("ssh://");
|
|
829
|
+
}
|
|
830
|
+
function stem(value) {
|
|
831
|
+
const clean = value.trim().replace(/\/+$/, "");
|
|
832
|
+
if (clean.length === 0)
|
|
833
|
+
return "";
|
|
834
|
+
if (clean.startsWith("git@")) {
|
|
835
|
+
const raw = clean.split(":").slice(1).join(":");
|
|
836
|
+
const seg = raw.split("/").filter(Boolean).pop() ?? raw;
|
|
837
|
+
return seg.replace(/\.git$/, "");
|
|
838
|
+
}
|
|
839
|
+
if (isUrl2(clean)) {
|
|
840
|
+
const seg = clean.split("/").filter(Boolean).pop() ?? clean;
|
|
841
|
+
const raw = seg.split("?")[0] ?? seg;
|
|
842
|
+
const full = raw.split("#")[0] ?? raw;
|
|
843
|
+
return full.replace(/\.git$/, "");
|
|
844
|
+
}
|
|
845
|
+
const base = path6.basename(clean);
|
|
846
|
+
const parsed = path6.parse(base);
|
|
847
|
+
if (parsed.name.length > 0)
|
|
848
|
+
return parsed.name;
|
|
849
|
+
return base;
|
|
850
|
+
}
|
|
851
|
+
function github(uri) {
|
|
852
|
+
return hosted(uri, "github.com");
|
|
853
|
+
}
|
|
854
|
+
function codeberg(uri) {
|
|
855
|
+
return hosted(uri, "codeberg.org");
|
|
856
|
+
}
|
|
857
|
+
function hosted(uri, host) {
|
|
858
|
+
const escaped = host.replace(/\./g, "\\.");
|
|
859
|
+
const https = uri.match(new RegExp(`^https?:\\/\\/${escaped}\\/([^/]+)\\/([^/]+?)(?:\\.git)?\\/?$`));
|
|
860
|
+
if (https) {
|
|
861
|
+
return { owner: https[1], repo: https[2] };
|
|
862
|
+
}
|
|
863
|
+
const ssh = uri.match(new RegExp(`^ssh:\\/\\/git@${escaped}\\/([^/]+)\\/([^/]+?)(?:\\.git)?\\/?$`));
|
|
864
|
+
if (ssh) {
|
|
865
|
+
return { owner: ssh[1], repo: ssh[2] };
|
|
866
|
+
}
|
|
867
|
+
const scp = uri.match(new RegExp(`^git@${escaped}:([^/]+)\\/([^/]+?)(?:\\.git)?$`));
|
|
868
|
+
if (scp) {
|
|
869
|
+
return { owner: scp[1], repo: scp[2] };
|
|
870
|
+
}
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
async function apiDescription(url, accept) {
|
|
874
|
+
const response = await fetch(url, {
|
|
875
|
+
headers: {
|
|
876
|
+
accept,
|
|
877
|
+
"user-agent": "dotllm"
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
if (!response.ok)
|
|
881
|
+
return "";
|
|
882
|
+
const raw = await response.json();
|
|
883
|
+
const result = RepoShape.safeParse(raw);
|
|
884
|
+
if (!result.success)
|
|
885
|
+
return "";
|
|
886
|
+
return result.data.description ?? "";
|
|
887
|
+
}
|
|
888
|
+
function gitName(uri) {
|
|
889
|
+
const dir = path6.resolve(uri);
|
|
890
|
+
if (!fs6.existsSync(dir))
|
|
891
|
+
return "";
|
|
892
|
+
if (!fs6.statSync(dir).isDirectory())
|
|
893
|
+
return "";
|
|
894
|
+
const proc = Bun.spawnSync(["git", "remote", "get-url", "origin"], {
|
|
895
|
+
cwd: dir,
|
|
896
|
+
stdout: "pipe",
|
|
897
|
+
stderr: "pipe"
|
|
898
|
+
});
|
|
899
|
+
if (proc.exitCode !== 0)
|
|
900
|
+
return "";
|
|
901
|
+
const out = proc.stdout.toString().trim();
|
|
902
|
+
if (out.length === 0)
|
|
903
|
+
return "";
|
|
904
|
+
return stem(out);
|
|
905
|
+
}
|
|
906
|
+
async function remoteDescription(uri) {
|
|
907
|
+
const gh = github(uri);
|
|
908
|
+
if (gh) {
|
|
909
|
+
const url = `https://api.github.com/repos/${gh.owner}/${gh.repo}`;
|
|
910
|
+
return apiDescription(url, "application/vnd.github+json");
|
|
911
|
+
}
|
|
912
|
+
const cb = codeberg(uri);
|
|
913
|
+
if (cb) {
|
|
914
|
+
const url = `https://codeberg.org/api/v1/repos/${cb.owner}/${cb.repo}`;
|
|
915
|
+
return apiDescription(url, "application/json");
|
|
916
|
+
}
|
|
917
|
+
return "";
|
|
918
|
+
}
|
|
919
|
+
async function prefill(uri) {
|
|
920
|
+
const remote = isUrl2(uri);
|
|
921
|
+
const git = remote ? "" : gitName(uri);
|
|
922
|
+
const name = git.length > 0 ? git : stem(uri);
|
|
923
|
+
const description = remote ? await remoteDescription(uri) : "";
|
|
924
|
+
return { name, description };
|
|
925
|
+
}
|
|
926
|
+
async function interactive(namePrefill, descPrefill) {
|
|
927
|
+
const uri = await prompts2.text({
|
|
928
|
+
message: "URL or local path to a git repo"
|
|
929
|
+
});
|
|
930
|
+
if (prompts2.isCancel(uri))
|
|
931
|
+
return;
|
|
932
|
+
const input = uri.trim();
|
|
933
|
+
const seed = await prefill(input);
|
|
934
|
+
const name = await prompts2.text({
|
|
935
|
+
message: "Name",
|
|
936
|
+
initialValue: namePrefill ?? seed.name
|
|
937
|
+
});
|
|
938
|
+
if (prompts2.isCancel(name))
|
|
939
|
+
return;
|
|
940
|
+
const description = await prompts2.text({
|
|
941
|
+
message: "Description",
|
|
942
|
+
initialValue: descPrefill ?? seed.description
|
|
943
|
+
});
|
|
944
|
+
if (prompts2.isCancel(description))
|
|
945
|
+
return;
|
|
946
|
+
await run(input, name || undefined, description || undefined);
|
|
947
|
+
}
|
|
948
|
+
function autoLink(name) {
|
|
949
|
+
const localFile = path6.join(".llm", "dotllm.json");
|
|
950
|
+
if (!fs6.existsSync(localFile))
|
|
951
|
+
return;
|
|
952
|
+
const local = Config.Local.read();
|
|
953
|
+
if (Config.Local.has(local, name))
|
|
954
|
+
return;
|
|
955
|
+
const global = Config.Global.read();
|
|
956
|
+
const repo = Config.Global.find(global, name);
|
|
957
|
+
if (!repo)
|
|
958
|
+
return;
|
|
959
|
+
Config.Local.write(Config.Local.add(local, repo));
|
|
960
|
+
Prompt.sync(sync());
|
|
961
|
+
}
|
|
962
|
+
async function run(uri, name, description) {
|
|
963
|
+
const spinner2 = prompts2.spinner();
|
|
964
|
+
spinner2.start(`Adding ${uri}`);
|
|
965
|
+
const needSeed = !name || !description;
|
|
966
|
+
const seed = needSeed ? await prefill(uri) : { name: "", description: "" };
|
|
967
|
+
const resolved = name ?? seed.name;
|
|
968
|
+
const desc = description ?? seed.description;
|
|
969
|
+
const result = await add(uri, resolved || undefined, desc || undefined);
|
|
970
|
+
if (!result.ok) {
|
|
971
|
+
spinner2.stop(defaultTheme.error(result.error));
|
|
972
|
+
process.exit(1);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
spinner2.stop(`${defaultTheme.success("added")} ${defaultTheme.primary(result.entry.name)} ${defaultTheme.link(result.storePath)}`);
|
|
976
|
+
autoLink(result.entry.name);
|
|
977
|
+
}
|
|
978
|
+
var command2 = {
|
|
979
|
+
description: "Register a git repo as a reference",
|
|
980
|
+
summary: "Add a repo to the registry",
|
|
981
|
+
positionals: {
|
|
982
|
+
uri: {
|
|
983
|
+
type: "string",
|
|
984
|
+
description: "URL or local path to a git repo"
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
options: {
|
|
988
|
+
name: {
|
|
989
|
+
alias: "n",
|
|
990
|
+
type: "string",
|
|
991
|
+
description: "Name override (defaults to repo name from git)"
|
|
992
|
+
},
|
|
993
|
+
description: {
|
|
994
|
+
alias: "d",
|
|
995
|
+
type: "string",
|
|
996
|
+
description: "Description of the reference"
|
|
997
|
+
}
|
|
998
|
+
},
|
|
999
|
+
handler: async (argv) => {
|
|
1000
|
+
prompts2.intro("dotllm add");
|
|
1001
|
+
const uri = typeof argv.uri === "string" && argv.uri.length > 0 ? argv.uri : undefined;
|
|
1002
|
+
const name = typeof argv.name === "string" && argv.name.length > 0 ? argv.name : undefined;
|
|
1003
|
+
const description = typeof argv.description === "string" && argv.description.length > 0 ? argv.description : undefined;
|
|
1004
|
+
if (!uri) {
|
|
1005
|
+
await interactive(name, description);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
await run(uri, name, description);
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
// src/cli/commands/remove.ts
|
|
1012
|
+
init_core();
|
|
1013
|
+
import * as prompts3 from "@clack/prompts";
|
|
1014
|
+
var command3 = {
|
|
1015
|
+
description: "Remove a repo from the registry",
|
|
1016
|
+
summary: "Remove a registered repo",
|
|
1017
|
+
positionals: {
|
|
1018
|
+
name: {
|
|
1019
|
+
type: "string",
|
|
1020
|
+
description: "Name of the reference to remove",
|
|
1021
|
+
required: true
|
|
1022
|
+
}
|
|
1023
|
+
},
|
|
1024
|
+
handler: (argv) => {
|
|
1025
|
+
prompts3.intro("dotllm remove");
|
|
1026
|
+
const name = String(argv.name);
|
|
1027
|
+
const result = remove(name);
|
|
1028
|
+
if (!result.ok) {
|
|
1029
|
+
prompts3.log.error(defaultTheme.error(result.error));
|
|
1030
|
+
process.exit(1);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
prompts3.log.step(`removed ${defaultTheme.primary(name)}`);
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
// src/cli/commands/list.ts
|
|
1037
|
+
init_core();
|
|
1038
|
+
import * as prompts4 from "@clack/prompts";
|
|
1039
|
+
var command4 = {
|
|
1040
|
+
description: "List all registered repos",
|
|
1041
|
+
summary: "Show the registry",
|
|
1042
|
+
handler: () => {
|
|
1043
|
+
prompts4.intro("dotllm list");
|
|
1044
|
+
const global = Config.Global.read();
|
|
1045
|
+
if (global.repos.length === 0) {
|
|
1046
|
+
console.log(defaultTheme.dim("(no repos registered)"));
|
|
1047
|
+
console.log(defaultTheme.dim("use `dotllm add <path>` to register one"));
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
const local = Config.Local.read();
|
|
1051
|
+
const linked = new Set(Object.keys(local.refs));
|
|
1052
|
+
table(["name", "kind", "uri", "description", "linked"], [
|
|
1053
|
+
global.repos.map((r) => r.name),
|
|
1054
|
+
global.repos.map((r) => r.kind),
|
|
1055
|
+
global.repos.map((r) => r.uri),
|
|
1056
|
+
global.repos.map((r) => r.description),
|
|
1057
|
+
global.repos.map((r) => linked.has(r.name) ? "yes" : "no")
|
|
1058
|
+
], {
|
|
1059
|
+
flex: [0, 0, 1, 1, 0],
|
|
1060
|
+
noTruncate: [true, true, false, false, true],
|
|
1061
|
+
truncate: ["end", "end", "start", "end", "end"],
|
|
1062
|
+
format: [
|
|
1063
|
+
(s) => defaultTheme.primary(s),
|
|
1064
|
+
(s) => s,
|
|
1065
|
+
(s) => defaultTheme.link(s),
|
|
1066
|
+
(s) => s,
|
|
1067
|
+
(s) => s.trim() === "yes" ? defaultTheme.success(s) : s
|
|
1068
|
+
]
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
// src/cli/commands/link.ts
|
|
1073
|
+
init_core();
|
|
1074
|
+
import * as prompts5 from "@clack/prompts";
|
|
1075
|
+
var command5 = {
|
|
1076
|
+
description: "Interactively pick repos to link into .llm/reference/, or add/remove one by name",
|
|
1077
|
+
summary: "Link references",
|
|
1078
|
+
positionals: {
|
|
1079
|
+
name: {
|
|
1080
|
+
type: "string",
|
|
1081
|
+
description: "Name of the repo to link, or omit for interactive"
|
|
1082
|
+
}
|
|
1083
|
+
},
|
|
1084
|
+
options: {
|
|
1085
|
+
remove: {
|
|
1086
|
+
alias: "r",
|
|
1087
|
+
type: "boolean",
|
|
1088
|
+
description: "Remove the link instead of adding it"
|
|
1089
|
+
}
|
|
1090
|
+
},
|
|
1091
|
+
handler: async (argv) => {
|
|
1092
|
+
prompts5.intro("dotllm link");
|
|
1093
|
+
const name = typeof argv.name === "string" && argv.name.length > 0 ? argv.name : undefined;
|
|
1094
|
+
const shouldRemove = argv.remove === true;
|
|
1095
|
+
if (name) {
|
|
1096
|
+
if (shouldRemove) {
|
|
1097
|
+
const result = unlink(name);
|
|
1098
|
+
if (!result.ok) {
|
|
1099
|
+
prompts5.log.error(defaultTheme.error(result.error));
|
|
1100
|
+
process.exit(1);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
prompts5.log.step(`unlinked ${defaultTheme.primary(name)}`);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
const global = Config.Global.read();
|
|
1107
|
+
const repo = Config.Global.find(global, name);
|
|
1108
|
+
if (!repo) {
|
|
1109
|
+
prompts5.log.error(defaultTheme.error(`No repo named "${name}" in registry`));
|
|
1110
|
+
process.exit(1);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
const local = Config.Local.read();
|
|
1114
|
+
Config.Local.write(Config.Local.add(local, repo));
|
|
1115
|
+
Prompt.sync(sync());
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
const global = Config.Global.read();
|
|
1119
|
+
if (global.repos.length === 0) {
|
|
1120
|
+
console.log(defaultTheme.dim("(no repos registered)"));
|
|
1121
|
+
console.log(defaultTheme.dim("use `dotllm add <path>` to register one"));
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const local = Config.Local.read();
|
|
1125
|
+
const current = new Set(Object.keys(local.refs));
|
|
1126
|
+
const repos = [...global.repos].sort((a, b) => a.name.localeCompare(b.name));
|
|
1127
|
+
const initialValues = repos.filter((r) => current.has(r.name)).map((r) => r.name).sort((a, b) => b.localeCompare(a));
|
|
1128
|
+
const selected = await prompts5.autocompleteMultiselect({
|
|
1129
|
+
message: "Select repos to link into .llm/reference/",
|
|
1130
|
+
options: repos.map((r) => ({
|
|
1131
|
+
value: r.name,
|
|
1132
|
+
label: r.name,
|
|
1133
|
+
hint: r.uri
|
|
1134
|
+
})),
|
|
1135
|
+
initialValues,
|
|
1136
|
+
required: false
|
|
1137
|
+
});
|
|
1138
|
+
if (prompts5.isCancel(selected)) {
|
|
1139
|
+
prompts5.cancel("cancelled");
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
const names = Array.isArray(selected) ? selected.filter((value) => typeof value === "string") : [];
|
|
1143
|
+
Prompt.sync(link(names));
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
// src/cli/commands/sync.ts
|
|
1147
|
+
init_core();
|
|
1148
|
+
import * as prompts6 from "@clack/prompts";
|
|
1149
|
+
var command6 = {
|
|
1150
|
+
description: "Re-create symlinks from .llm/dotllm.json",
|
|
1151
|
+
summary: "Sync symlinks from local config",
|
|
1152
|
+
options: {
|
|
1153
|
+
force: {
|
|
1154
|
+
alias: "f",
|
|
1155
|
+
type: "boolean",
|
|
1156
|
+
description: "Pull all repos regardless of recent access"
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
handler: async (argv) => {
|
|
1160
|
+
prompts6.intro("dotllm sync");
|
|
1161
|
+
const result = sync();
|
|
1162
|
+
if (result.linked.length === 0 && result.removed.length === 0 && result.missing.length === 0 && result.unchanged.length === 0) {
|
|
1163
|
+
console.log(defaultTheme.dim("(no refs in local config)"));
|
|
1164
|
+
console.log(defaultTheme.dim("use `dotllm link` to select repos"));
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
Prompt.sync(result);
|
|
1168
|
+
const refs = [...new Set([...result.unchanged, ...result.linked])];
|
|
1169
|
+
if (refs.length === 0) {
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
const force = argv.force === true;
|
|
1173
|
+
const spinner = prompts6.spinner();
|
|
1174
|
+
spinner.start(`Checking ${refs.length} linked repo${refs.length === 1 ? "" : "s"}`);
|
|
1175
|
+
const pulled = await pull(refs, { force });
|
|
1176
|
+
if (pulled.failed.length > 0) {
|
|
1177
|
+
spinner.stop(defaultTheme.error(`pull failed for ${pulled.failed.length} repo${pulled.failed.length === 1 ? "" : "s"}`));
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
const parts = [];
|
|
1182
|
+
if (pulled.pulled.length > 0)
|
|
1183
|
+
parts.push(`${defaultTheme.success("pulled")} ${pulled.pulled.length}`);
|
|
1184
|
+
if (pulled.skipped.length > 0)
|
|
1185
|
+
parts.push(defaultTheme.dim(`${pulled.skipped.length} skipped`));
|
|
1186
|
+
spinner.stop(parts.length > 0 ? parts.join(" ") : defaultTheme.dim("nothing to pull"));
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
// src/cli/commands/which.ts
|
|
1190
|
+
init_core();
|
|
1191
|
+
import path7 from "path";
|
|
1192
|
+
var command7 = {
|
|
1193
|
+
description: "Print the absolute path to a repo in the store (prefix match, shortest wins)",
|
|
1194
|
+
summary: "Show repo store path",
|
|
1195
|
+
positionals: {
|
|
1196
|
+
name: {
|
|
1197
|
+
type: "string",
|
|
1198
|
+
description: "Name (or prefix) of the repo",
|
|
1199
|
+
required: true
|
|
1200
|
+
}
|
|
1201
|
+
},
|
|
1202
|
+
handler: (argv) => {
|
|
1203
|
+
const name = String(argv.name);
|
|
1204
|
+
const global = Config.Global.read();
|
|
1205
|
+
const exact = Config.Global.find(global, name);
|
|
1206
|
+
if (exact) {
|
|
1207
|
+
console.log(path7.join(Config.storeDir(), exact.name));
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
const lower = name.toLowerCase();
|
|
1211
|
+
const matches = global.repos.filter((r) => r.name.toLowerCase().startsWith(lower)).sort((a, b) => a.name.length - b.name.length);
|
|
1212
|
+
if (matches.length === 0) {
|
|
1213
|
+
console.error(defaultTheme.error(`No repo matching "${name}" in registry`));
|
|
1214
|
+
process.exit(1);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (matches.length > 1 && matches[0].name.length === matches[1].name.length) {
|
|
1218
|
+
const names = matches.filter((m) => m.name.length === matches[0].name.length).map((m) => m.name);
|
|
1219
|
+
console.error(defaultTheme.error(`Ambiguous prefix "${name}": ${names.join(", ")}`));
|
|
1220
|
+
process.exit(1);
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
console.log(path7.join(Config.storeDir(), matches[0].name));
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
// src/cli/commands/completions.ts
|
|
1227
|
+
init_core();
|
|
1228
|
+
import fs7 from "fs";
|
|
1229
|
+
import os2 from "os";
|
|
1230
|
+
import path8 from "path";
|
|
1231
|
+
var BASH = `_dotllm() {
|
|
1232
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
1233
|
+
|
|
1234
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
1235
|
+
COMPREPLY=( $(compgen -W "add remove list link sync which completions" -- "\${cur}") )
|
|
1236
|
+
return
|
|
1237
|
+
fi
|
|
1238
|
+
|
|
1239
|
+
case "\${COMP_WORDS[1]}" in
|
|
1240
|
+
which|link)
|
|
1241
|
+
local repos
|
|
1242
|
+
repos=$(dotllm completions --names 2>/dev/null)
|
|
1243
|
+
COMPREPLY=( $(compgen -W "\${repos}" -- "\${cur}") )
|
|
1244
|
+
;;
|
|
1245
|
+
esac
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
complete -F _dotllm dotllm`;
|
|
1249
|
+
var ZSH = `#compdef dotllm
|
|
1250
|
+
|
|
1251
|
+
_dotllm() {
|
|
1252
|
+
local -a commands
|
|
1253
|
+
commands=(
|
|
1254
|
+
'add:Register a new repo'
|
|
1255
|
+
'remove:Remove a repo from the registry'
|
|
1256
|
+
'list:Show the registry'
|
|
1257
|
+
'link:Link references'
|
|
1258
|
+
'sync:Sync linked repos'
|
|
1259
|
+
'which:Show repo store path'
|
|
1260
|
+
'completions:Output shell completions'
|
|
1261
|
+
)
|
|
1262
|
+
|
|
1263
|
+
_arguments -C '1:command:->cmd' '*::arg:->args'
|
|
1264
|
+
|
|
1265
|
+
case "$state" in
|
|
1266
|
+
cmd)
|
|
1267
|
+
_describe 'command' commands
|
|
1268
|
+
;;
|
|
1269
|
+
args)
|
|
1270
|
+
case "\${words[1]}" in
|
|
1271
|
+
which|link)
|
|
1272
|
+
local -a repos
|
|
1273
|
+
repos=(\${(f)"$(dotllm completions --names 2>/dev/null)"})
|
|
1274
|
+
_describe 'repo' repos
|
|
1275
|
+
;;
|
|
1276
|
+
esac
|
|
1277
|
+
;;
|
|
1278
|
+
esac
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
_dotllm`;
|
|
1282
|
+
var FISH = `complete -c dotllm -f
|
|
1283
|
+
complete -c dotllm -n "__fish_use_subcommand" -a add -d "Register a new repo"
|
|
1284
|
+
complete -c dotllm -n "__fish_use_subcommand" -a remove -d "Remove a repo from the registry"
|
|
1285
|
+
complete -c dotllm -n "__fish_use_subcommand" -a list -d "Show the registry"
|
|
1286
|
+
complete -c dotllm -n "__fish_use_subcommand" -a link -d "Link references"
|
|
1287
|
+
complete -c dotllm -n "__fish_use_subcommand" -a sync -d "Sync linked repos"
|
|
1288
|
+
complete -c dotllm -n "__fish_use_subcommand" -a which -d "Show repo store path"
|
|
1289
|
+
complete -c dotllm -n "__fish_use_subcommand" -a completions -d "Output shell completions"
|
|
1290
|
+
complete -c dotllm -n "__fish_seen_subcommand_from which link" -a "(dotllm completions --names 2>/dev/null)"`;
|
|
1291
|
+
var CANARY = "@dotllm_completions";
|
|
1292
|
+
function completionFile(shell) {
|
|
1293
|
+
return path8.join(Config.home(), `completions.${shell}`);
|
|
1294
|
+
}
|
|
1295
|
+
function hookSnippet(shell) {
|
|
1296
|
+
const file = completionFile(shell);
|
|
1297
|
+
switch (shell) {
|
|
1298
|
+
case "bash":
|
|
1299
|
+
return `
|
|
1300
|
+
# ${CANARY}
|
|
1301
|
+
# Installed by the dotllm CLI
|
|
1302
|
+
[ -f "${file}" ] && source "${file}"
|
|
1303
|
+
`;
|
|
1304
|
+
case "zsh":
|
|
1305
|
+
return `
|
|
1306
|
+
# ${CANARY}
|
|
1307
|
+
# Installed by the dotllm CLI
|
|
1308
|
+
[ -f "${file}" ] && source "${file}"
|
|
1309
|
+
`;
|
|
1310
|
+
case "fish":
|
|
1311
|
+
return `
|
|
1312
|
+
# ${CANARY}
|
|
1313
|
+
# Installed by the dotllm CLI
|
|
1314
|
+
test -f "${file}"; and source "${file}"
|
|
1315
|
+
`;
|
|
1316
|
+
default:
|
|
1317
|
+
return "";
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
function rcPath(shell) {
|
|
1321
|
+
const home = os2.homedir();
|
|
1322
|
+
switch (shell) {
|
|
1323
|
+
case "bash": {
|
|
1324
|
+
const bashrc = path8.join(home, ".bashrc");
|
|
1325
|
+
if (fs7.existsSync(bashrc))
|
|
1326
|
+
return bashrc;
|
|
1327
|
+
return path8.join(home, ".bash_profile");
|
|
1328
|
+
}
|
|
1329
|
+
case "zsh":
|
|
1330
|
+
return path8.join(home, ".zshrc");
|
|
1331
|
+
case "fish":
|
|
1332
|
+
return path8.join(home, ".config", "fish", "config.fish");
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
var install = {
|
|
1336
|
+
description: "Add completions to your shell rc file",
|
|
1337
|
+
summary: "Install completions",
|
|
1338
|
+
handler: async (argv) => {
|
|
1339
|
+
if (process.platform === "win32") {
|
|
1340
|
+
console.log(defaultTheme.dim("Shell completions are not supported on Windows."));
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
const shell = detectShell();
|
|
1344
|
+
const rc = rcPath(shell);
|
|
1345
|
+
if (!rc) {
|
|
1346
|
+
console.error(defaultTheme.error(`Could not determine rc file for shell: "${shell}"`));
|
|
1347
|
+
process.exit(1);
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
const script = shellScript(shell);
|
|
1351
|
+
if (!script) {
|
|
1352
|
+
console.error(defaultTheme.error(`Unknown shell: "${shell}"`));
|
|
1353
|
+
process.exit(1);
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
const cached = completionFile(shell);
|
|
1357
|
+
fs7.mkdirSync(path8.dirname(cached), { recursive: true });
|
|
1358
|
+
fs7.writeFileSync(cached, script + `
|
|
1359
|
+
`);
|
|
1360
|
+
const existing = fs7.existsSync(rc) ? fs7.readFileSync(rc, "utf-8") : "";
|
|
1361
|
+
if (existing.includes(CANARY)) {
|
|
1362
|
+
console.log(defaultTheme.dim(`Updated ${cached}`));
|
|
1363
|
+
console.log(defaultTheme.dim(`Already sourced from ${rc}`));
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
fs7.appendFileSync(rc, hookSnippet(shell));
|
|
1367
|
+
console.log(`Installed completions in ${defaultTheme.link(rc)}`);
|
|
1368
|
+
console.log(defaultTheme.dim(`Restart your shell or run: source ${rc}`));
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
var command8 = {
|
|
1372
|
+
description: "Output shell completion script for bash, zsh, or fish",
|
|
1373
|
+
summary: "Shell completions",
|
|
1374
|
+
options: {
|
|
1375
|
+
shell: {
|
|
1376
|
+
alias: "s",
|
|
1377
|
+
type: "string",
|
|
1378
|
+
description: "Shell type: bash, zsh, or fish"
|
|
1379
|
+
},
|
|
1380
|
+
names: {
|
|
1381
|
+
type: "boolean",
|
|
1382
|
+
description: "Print repo names (used internally by completions)"
|
|
1383
|
+
}
|
|
1384
|
+
},
|
|
1385
|
+
commands: {
|
|
1386
|
+
install
|
|
1387
|
+
},
|
|
1388
|
+
handler: async (argv) => {
|
|
1389
|
+
if (process.platform === "win32") {
|
|
1390
|
+
console.log(defaultTheme.dim("Shell completions are not supported on Windows."));
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
if (argv.names) {
|
|
1394
|
+
await Promise.resolve().then(() => init_core());
|
|
1395
|
+
const global = Config.Global.read();
|
|
1396
|
+
for (const r of global.repos)
|
|
1397
|
+
console.log(r.name);
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
const shell = typeof argv.shell === "string" && argv.shell.length > 0 ? argv.shell : detectShell();
|
|
1401
|
+
switch (shell) {
|
|
1402
|
+
case "bash":
|
|
1403
|
+
console.log(BASH);
|
|
1404
|
+
break;
|
|
1405
|
+
case "zsh":
|
|
1406
|
+
console.log(ZSH);
|
|
1407
|
+
break;
|
|
1408
|
+
case "fish":
|
|
1409
|
+
console.log(FISH);
|
|
1410
|
+
break;
|
|
1411
|
+
default:
|
|
1412
|
+
console.error(defaultTheme.error(`Unknown shell: "${shell}". Use bash, zsh, or fish.`));
|
|
1413
|
+
process.exit(1);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
function shellScript(shell) {
|
|
1418
|
+
switch (shell) {
|
|
1419
|
+
case "bash":
|
|
1420
|
+
return BASH;
|
|
1421
|
+
case "zsh":
|
|
1422
|
+
return ZSH;
|
|
1423
|
+
case "fish":
|
|
1424
|
+
return FISH;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
function detectShell() {
|
|
1428
|
+
const login = process.env.SHELL ?? "";
|
|
1429
|
+
if (login.endsWith("/fish"))
|
|
1430
|
+
return "fish";
|
|
1431
|
+
if (login.endsWith("/zsh"))
|
|
1432
|
+
return "zsh";
|
|
1433
|
+
return "bash";
|
|
1434
|
+
}
|
|
5
1435
|
// src/cli/index.ts
|
|
6
|
-
import { build } from "@spader/dotllm/cli/yargs";
|
|
7
|
-
import { add, remove, list, link, sync, which, completions } from "@spader/dotllm/cli/commands/index";
|
|
8
1436
|
var DotLlmCli;
|
|
9
1437
|
((DotLlmCli) => {
|
|
10
1438
|
async function run() {
|
|
@@ -15,13 +1443,13 @@ var DotLlmCli;
|
|
|
15
1443
|
description: "Manage git repo references symlinked into .llm/reference/",
|
|
16
1444
|
version,
|
|
17
1445
|
commands: {
|
|
18
|
-
add,
|
|
19
|
-
remove,
|
|
20
|
-
list,
|
|
21
|
-
link,
|
|
22
|
-
sync,
|
|
23
|
-
which,
|
|
24
|
-
completions
|
|
1446
|
+
add: command2,
|
|
1447
|
+
remove: command3,
|
|
1448
|
+
list: command4,
|
|
1449
|
+
link: command5,
|
|
1450
|
+
sync: command6,
|
|
1451
|
+
which: command7,
|
|
1452
|
+
completions: command8
|
|
25
1453
|
}
|
|
26
1454
|
};
|
|
27
1455
|
build(def).parse();
|