letmecook 0.0.21 → 0.0.22

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.
@@ -1,288 +0,0 @@
1
- import { homedir } from "node:os";
2
- import { join } from "node:path";
3
- import { mkdir, symlink, readlink, lstat, readdir, rm } from "node:fs/promises";
4
- import type { RepoSpec } from "./types";
5
- import { readProcessOutputWithBuffer } from "./utils/stream";
6
-
7
- const LETMECOOK_DIR = join(homedir(), ".letmecook");
8
- export const REFERENCES_DIR = join(LETMECOOK_DIR, "references");
9
-
10
- export interface RefreshResult {
11
- repo: RepoSpec;
12
- status: "updated" | "up-to-date" | "skipped" | "error";
13
- reason?: string;
14
- }
15
-
16
- export type RefreshProgressStatus = "refreshing" | "updated" | "up-to-date" | "skipped" | "error";
17
-
18
- /**
19
- * Get the path where a reference repo should be cached.
20
- * Format: ~/.letmecook/references/{owner}/{name}/{branch|HEAD}
21
- */
22
- export function getReferencePath(repo: RepoSpec): string {
23
- const branchDir = repo.branch || "HEAD";
24
- return join(REFERENCES_DIR, repo.owner, repo.name, branchDir);
25
- }
26
-
27
- /**
28
- * Ensure the references directory exists.
29
- */
30
- export async function ensureReferencesDir(): Promise<void> {
31
- await mkdir(REFERENCES_DIR, { recursive: true });
32
- }
33
-
34
- /**
35
- * Check if a path is a symlink.
36
- */
37
- export async function isSymlink(path: string): Promise<boolean> {
38
- try {
39
- const stats = await lstat(path);
40
- return stats.isSymbolicLink();
41
- } catch {
42
- return false;
43
- }
44
- }
45
-
46
- /**
47
- * Clone a repo to the reference cache if not already present.
48
- * Returns the path to the cached repo.
49
- */
50
- export async function ensureReferenceRepo(
51
- repo: RepoSpec,
52
- onProgress?: (status: "cloning" | "done" | "error", outputLines?: string[]) => void,
53
- ): Promise<string> {
54
- const refPath = getReferencePath(repo);
55
-
56
- // Check if already cached
57
- const gitDir = join(refPath, ".git");
58
- const gitDirFile = Bun.file(gitDir);
59
- if (await gitDirFile.exists()) {
60
- onProgress?.("done", ["Already cached"]);
61
- return refPath;
62
- }
63
-
64
- // Ensure parent directories exist
65
- await mkdir(join(REFERENCES_DIR, repo.owner, repo.name), { recursive: true });
66
-
67
- onProgress?.("cloning");
68
-
69
- const url = `https://github.com/${repo.owner}/${repo.name}.git`;
70
- const args = repo.branch
71
- ? [
72
- "git",
73
- "clone",
74
- "--depth",
75
- "1",
76
- "--single-branch",
77
- "--branch",
78
- repo.branch,
79
- "--progress",
80
- url,
81
- refPath,
82
- ]
83
- : ["git", "clone", "--depth", "1", "--single-branch", "--progress", url, refPath];
84
-
85
- const proc = Bun.spawn(args, {
86
- stdout: "pipe",
87
- stderr: "pipe",
88
- });
89
-
90
- const { success, output } = await readProcessOutputWithBuffer(proc, {
91
- maxBufferLines: 5,
92
- onBufferUpdate: (buffer) => onProgress?.("cloning", buffer),
93
- });
94
-
95
- if (!success) {
96
- onProgress?.("error", output);
97
- throw new Error(`Failed to clone reference repo ${repo.owner}/${repo.name}`);
98
- }
99
-
100
- onProgress?.("done", output);
101
- return refPath;
102
- }
103
-
104
- /**
105
- * Refresh a cached reference repo with git pull.
106
- */
107
- export async function refreshReferenceRepo(
108
- repo: RepoSpec,
109
- onProgress?: (status: RefreshProgressStatus, outputLines?: string[]) => void,
110
- ): Promise<RefreshResult> {
111
- const refPath = getReferencePath(repo);
112
-
113
- // Check if cached repo exists
114
- const gitDir = join(refPath, ".git");
115
- const gitDirFile = Bun.file(gitDir);
116
- if (!(await gitDirFile.exists())) {
117
- // Try to clone it
118
- try {
119
- await ensureReferenceRepo(repo, (status, lines) => {
120
- if (status === "cloning") onProgress?.("refreshing", lines);
121
- else if (status === "done") onProgress?.("updated", lines);
122
- else onProgress?.("error", lines);
123
- });
124
- return { repo, status: "updated", reason: "newly cloned" };
125
- } catch (error) {
126
- return {
127
- repo,
128
- status: "error",
129
- reason: error instanceof Error ? error.message : String(error),
130
- };
131
- }
132
- }
133
-
134
- onProgress?.("refreshing", [`Pulling ${repo.owner}/${repo.name}...`]);
135
-
136
- const proc = Bun.spawn(["git", "-C", refPath, "pull", "--ff-only", "--depth", "1"], {
137
- stdout: "pipe",
138
- stderr: "pipe",
139
- });
140
-
141
- const { success, output, fullOutput } = await readProcessOutputWithBuffer(proc, {
142
- maxBufferLines: 5,
143
- onBufferUpdate: (buffer) => onProgress?.("refreshing", buffer),
144
- });
145
-
146
- if (!success) {
147
- const reason = fullOutput.trim() || "git pull failed";
148
- onProgress?.("error", output.length > 0 ? output : [reason]);
149
- return { repo, status: "error", reason };
150
- }
151
-
152
- const normalized = fullOutput.toLowerCase();
153
- const upToDate =
154
- normalized.includes("already up to date") || normalized.includes("already up-to-date");
155
-
156
- const status = upToDate ? "up-to-date" : "updated";
157
- onProgress?.(status, output);
158
- return { repo, status };
159
- }
160
-
161
- /**
162
- * Create a symlink from session directory to the cached reference repo.
163
- */
164
- export async function linkReferenceRepo(repo: RepoSpec, sessionPath: string): Promise<void> {
165
- const refPath = getReferencePath(repo);
166
- const linkPath = join(sessionPath, repo.dir);
167
-
168
- // Check if link already exists and is valid
169
- if (await isSymlink(linkPath)) {
170
- try {
171
- const target = await readlink(linkPath);
172
- if (target === refPath) {
173
- return; // Already correctly linked
174
- }
175
- // Remove incorrect symlink
176
- await rm(linkPath);
177
- } catch {
178
- // Remove broken symlink
179
- await rm(linkPath);
180
- }
181
- }
182
-
183
- await symlink(refPath, linkPath);
184
- }
185
-
186
- /**
187
- * Verify that a symlink is valid and points to the correct reference.
188
- * Returns true if valid, false if needs repair.
189
- */
190
- export async function verifyReferenceLink(repo: RepoSpec, sessionPath: string): Promise<boolean> {
191
- const refPath = getReferencePath(repo);
192
- const linkPath = join(sessionPath, repo.dir);
193
-
194
- if (!(await isSymlink(linkPath))) {
195
- return false;
196
- }
197
-
198
- try {
199
- const target = await readlink(linkPath);
200
- // Check if target matches expected reference path
201
- if (target !== refPath) {
202
- return false;
203
- }
204
- // Check if target actually exists
205
- const gitDir = Bun.file(join(refPath, ".git"));
206
- return await gitDir.exists();
207
- } catch {
208
- return false;
209
- }
210
- }
211
-
212
- /**
213
- * Repair a broken reference link by re-ensuring the cache and re-linking.
214
- */
215
- export async function repairReferenceLink(
216
- repo: RepoSpec,
217
- sessionPath: string,
218
- onProgress?: (status: "cloning" | "done" | "error", outputLines?: string[]) => void,
219
- ): Promise<void> {
220
- const linkPath = join(sessionPath, repo.dir);
221
-
222
- // Remove existing link/directory
223
- try {
224
- await rm(linkPath, { recursive: true, force: true });
225
- } catch {
226
- // Ignore
227
- }
228
-
229
- // Ensure reference is cached
230
- await ensureReferenceRepo(repo, onProgress);
231
-
232
- // Create symlink
233
- await linkReferenceRepo(repo, sessionPath);
234
- }
235
-
236
- /**
237
- * Clean up orphaned references that aren't used by any session.
238
- * Returns the number of references removed.
239
- */
240
- export async function cleanOrphanedReferences(): Promise<number> {
241
- // This is a placeholder for future implementation
242
- // Would need to:
243
- // 1. List all sessions
244
- // 2. Collect all reference paths in use
245
- // 3. Walk references dir and remove unused entries
246
- return 0;
247
- }
248
-
249
- /**
250
- * List all cached references.
251
- */
252
- export async function listCachedReferences(): Promise<
253
- Array<{ owner: string; name: string; branch: string; path: string }>
254
- > {
255
- const refs: Array<{ owner: string; name: string; branch: string; path: string }> = [];
256
-
257
- try {
258
- const owners = await readdir(REFERENCES_DIR);
259
-
260
- for (const owner of owners) {
261
- const ownerPath = join(REFERENCES_DIR, owner);
262
- const ownerStat = await lstat(ownerPath);
263
- if (!ownerStat.isDirectory()) continue;
264
-
265
- const names = await readdir(ownerPath);
266
-
267
- for (const name of names) {
268
- const namePath = join(ownerPath, name);
269
- const nameStat = await lstat(namePath);
270
- if (!nameStat.isDirectory()) continue;
271
-
272
- const branches = await readdir(namePath);
273
-
274
- for (const branch of branches) {
275
- const branchPath = join(namePath, branch);
276
- const branchStat = await lstat(branchPath);
277
- if (!branchStat.isDirectory()) continue;
278
-
279
- refs.push({ owner, name, branch, path: branchPath });
280
- }
281
- }
282
- }
283
- } catch {
284
- // References dir doesn't exist yet
285
- }
286
-
287
- return refs;
288
- }