@aigne/afs-git 1.1.0 → 1.11.0-beta.1

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/dist/index.cjs ADDED
@@ -0,0 +1,610 @@
1
+ let node_child_process = require("node:child_process");
2
+ let node_crypto = require("node:crypto");
3
+ let node_fs_promises = require("node:fs/promises");
4
+ let node_os = require("node:os");
5
+ let node_path = require("node:path");
6
+ let node_util = require("node:util");
7
+ let _aigne_afs_utils_zod = require("@aigne/afs/utils/zod");
8
+ let simple_git = require("simple-git");
9
+ let zod = require("zod");
10
+
11
+ //#region src/index.ts
12
+ const LIST_MAX_LIMIT = 1e3;
13
+ const execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
14
+ const afsGitOptionsSchema = (0, _aigne_afs_utils_zod.camelize)(zod.z.object({
15
+ name: (0, _aigne_afs_utils_zod.optionalize)(zod.z.string()),
16
+ repoPath: (0, _aigne_afs_utils_zod.optionalize)(zod.z.string().describe("The path to the git repository")),
17
+ remoteUrl: (0, _aigne_afs_utils_zod.optionalize)(zod.z.string().describe("Remote repository URL (https or git protocol)")),
18
+ description: (0, _aigne_afs_utils_zod.optionalize)(zod.z.string().describe("A description of the repository")),
19
+ branches: (0, _aigne_afs_utils_zod.optionalize)(zod.z.array(zod.z.string()).describe("List of branches to expose")),
20
+ accessMode: (0, _aigne_afs_utils_zod.optionalize)(zod.z.enum(["readonly", "readwrite"]).describe("Access mode for this module")),
21
+ autoCommit: (0, _aigne_afs_utils_zod.optionalize)(zod.z.boolean().describe("Automatically commit changes after write operations")),
22
+ commitAuthor: (0, _aigne_afs_utils_zod.optionalize)(zod.z.object({
23
+ name: zod.z.string(),
24
+ email: zod.z.string()
25
+ })),
26
+ depth: (0, _aigne_afs_utils_zod.optionalize)(zod.z.number().describe("Clone depth for shallow clone")),
27
+ autoCleanup: (0, _aigne_afs_utils_zod.optionalize)(zod.z.boolean().describe("Automatically clean up cloned repository on cleanup()")),
28
+ cloneOptions: (0, _aigne_afs_utils_zod.optionalize)(zod.z.object({ auth: (0, _aigne_afs_utils_zod.optionalize)(zod.z.object({
29
+ username: (0, _aigne_afs_utils_zod.optionalize)(zod.z.string()),
30
+ password: (0, _aigne_afs_utils_zod.optionalize)(zod.z.string())
31
+ })) }))
32
+ }).refine((data) => data.repoPath || data.remoteUrl, { message: "Either repoPath or remoteUrl must be provided" }));
33
+ var AFSGit = class AFSGit {
34
+ static schema() {
35
+ return afsGitOptionsSchema;
36
+ }
37
+ static async load({ filepath, parsed }) {
38
+ const instance = new AFSGit({
39
+ ...await AFSGit.schema().parseAsync(parsed),
40
+ cwd: (0, node_path.dirname)(filepath)
41
+ });
42
+ await instance.ready();
43
+ return instance;
44
+ }
45
+ initPromise;
46
+ git;
47
+ tempBase;
48
+ worktrees = /* @__PURE__ */ new Map();
49
+ repoHash;
50
+ isAutoCloned = false;
51
+ clonedPath;
52
+ repoPath;
53
+ constructor(options) {
54
+ this.options = options;
55
+ (0, _aigne_afs_utils_zod.zodParse)(afsGitOptionsSchema, options);
56
+ let repoPath;
57
+ let repoName;
58
+ if (options.repoPath) {
59
+ repoPath = (0, node_path.isAbsolute)(options.repoPath) ? options.repoPath : (0, node_path.join)(options.cwd || process.cwd(), options.repoPath);
60
+ repoName = (0, node_path.basename)(repoPath);
61
+ } else if (options.remoteUrl) {
62
+ const urlParts = options.remoteUrl.split("/");
63
+ repoName = urlParts[urlParts.length - 1]?.replace(/\.git$/, "") || "git";
64
+ const repoHash = (0, node_crypto.createHash)("md5").update(options.remoteUrl).digest("hex").substring(0, 8);
65
+ repoPath = (0, node_path.join)((0, node_os.tmpdir)(), `afs-git-remote-${repoHash}`);
66
+ } else throw new Error("Either repoPath or remoteUrl must be provided");
67
+ this.repoPath = repoPath;
68
+ this.name = options.name || repoName;
69
+ this.description = options.description;
70
+ this.accessMode = options.accessMode ?? "readonly";
71
+ this.repoHash = (0, node_crypto.createHash)("md5").update(repoPath).digest("hex").substring(0, 8);
72
+ this.tempBase = (0, node_path.join)((0, node_os.tmpdir)(), `afs-git-${this.repoHash}`);
73
+ this.git = null;
74
+ this.initPromise = this.initialize();
75
+ }
76
+ /**
77
+ * Wait for async initialization to complete
78
+ */
79
+ async ready() {
80
+ await this.initPromise;
81
+ }
82
+ /**
83
+ * Async initialization logic (runs in constructor)
84
+ * Handles cloning remote repositories if needed
85
+ */
86
+ async initialize() {
87
+ const options = this.options;
88
+ if (options.remoteUrl) {
89
+ const targetPath = options.repoPath ? (0, node_path.isAbsolute)(options.repoPath) ? options.repoPath : (0, node_path.join)(options.cwd || process.cwd(), options.repoPath) : this.repoPath;
90
+ if (!options.repoPath) this.isAutoCloned = true;
91
+ if (!await (0, node_fs_promises.stat)(targetPath).then(() => true).catch(() => false)) {
92
+ const singleBranch = options.branches?.length === 1 ? options.branches[0] : void 0;
93
+ await AFSGit.cloneRepository(options.remoteUrl, targetPath, {
94
+ depth: options.depth ?? 1,
95
+ branch: singleBranch,
96
+ auth: options.cloneOptions?.auth
97
+ });
98
+ }
99
+ if (targetPath !== this.repoPath) {
100
+ this.repoPath = targetPath;
101
+ this.repoHash = (0, node_crypto.createHash)("md5").update(targetPath).digest("hex").substring(0, 8);
102
+ this.tempBase = (0, node_path.join)((0, node_os.tmpdir)(), `afs-git-${this.repoHash}`);
103
+ }
104
+ this.clonedPath = this.isAutoCloned ? targetPath : void 0;
105
+ }
106
+ this.git = (0, simple_git.simpleGit)(this.repoPath);
107
+ }
108
+ /**
109
+ * Clone a remote repository to local path
110
+ */
111
+ static async cloneRepository(remoteUrl, targetPath, options = {}) {
112
+ const git = (0, simple_git.simpleGit)();
113
+ const cloneArgs = [];
114
+ if (options.depth) cloneArgs.push("--depth", options.depth.toString());
115
+ if (options.branch) cloneArgs.push("--branch", options.branch, "--single-branch");
116
+ let cloneUrl = remoteUrl;
117
+ if (options.auth?.username && options.auth?.password) {
118
+ if (remoteUrl.startsWith("https://")) {
119
+ const url = new URL(remoteUrl);
120
+ url.username = encodeURIComponent(options.auth.username);
121
+ url.password = encodeURIComponent(options.auth.password);
122
+ cloneUrl = url.toString();
123
+ }
124
+ }
125
+ await git.clone(cloneUrl, targetPath, cloneArgs);
126
+ }
127
+ name;
128
+ description;
129
+ accessMode;
130
+ /**
131
+ * Parse AFS path into branch and file path
132
+ * Branch names may contain slashes and are encoded with ~ in paths
133
+ * Examples:
134
+ * "/" -> { branch: undefined, filePath: "" }
135
+ * "/main" -> { branch: "main", filePath: "" }
136
+ * "/feature~new-feature" -> { branch: "feature/new-feature", filePath: "" }
137
+ * "/main/src/index.ts" -> { branch: "main", filePath: "src/index.ts" }
138
+ */
139
+ parsePath(path) {
140
+ const segments = (0, node_path.join)("/", path).split("/").filter(Boolean);
141
+ if (segments.length === 0) return {
142
+ branch: void 0,
143
+ filePath: ""
144
+ };
145
+ return {
146
+ branch: segments[0].replace(/~/g, "/"),
147
+ filePath: segments.slice(1).join("/")
148
+ };
149
+ }
150
+ /**
151
+ * Detect MIME type based on file extension
152
+ */
153
+ getMimeType(filePath) {
154
+ return {
155
+ png: "image/png",
156
+ jpg: "image/jpeg",
157
+ jpeg: "image/jpeg",
158
+ gif: "image/gif",
159
+ bmp: "image/bmp",
160
+ webp: "image/webp",
161
+ svg: "image/svg+xml",
162
+ ico: "image/x-icon",
163
+ pdf: "application/pdf",
164
+ txt: "text/plain",
165
+ md: "text/markdown",
166
+ js: "text/javascript",
167
+ ts: "text/typescript",
168
+ json: "application/json",
169
+ html: "text/html",
170
+ css: "text/css",
171
+ xml: "text/xml"
172
+ }[filePath.split(".").pop()?.toLowerCase() || ""] || "application/octet-stream";
173
+ }
174
+ /**
175
+ * Check if file is likely binary based on extension
176
+ */
177
+ isBinaryFile(filePath) {
178
+ const ext = filePath.split(".").pop()?.toLowerCase();
179
+ return [
180
+ "png",
181
+ "jpg",
182
+ "jpeg",
183
+ "gif",
184
+ "bmp",
185
+ "webp",
186
+ "ico",
187
+ "pdf",
188
+ "zip",
189
+ "tar",
190
+ "gz",
191
+ "exe",
192
+ "dll",
193
+ "so",
194
+ "dylib",
195
+ "wasm"
196
+ ].includes(ext || "");
197
+ }
198
+ /**
199
+ * Get list of available branches
200
+ */
201
+ async getBranches() {
202
+ const allBranches = (await this.git.branchLocal()).all;
203
+ if (this.options.branches && this.options.branches.length > 0) return allBranches.filter((branch) => this.options.branches.includes(branch));
204
+ return allBranches;
205
+ }
206
+ /**
207
+ * Ensure worktree exists for a branch (lazy creation)
208
+ */
209
+ async ensureWorktree(branch) {
210
+ if (this.worktrees.has(branch)) return this.worktrees.get(branch);
211
+ if ((await this.git.revparse(["--abbrev-ref", "HEAD"])).trim() === branch) {
212
+ this.worktrees.set(branch, this.repoPath);
213
+ return this.repoPath;
214
+ }
215
+ const worktreePath = (0, node_path.join)(this.tempBase, branch);
216
+ if (!await (0, node_fs_promises.stat)(worktreePath).then(() => true).catch(() => false)) {
217
+ await (0, node_fs_promises.mkdir)(this.tempBase, { recursive: true });
218
+ await this.git.raw([
219
+ "worktree",
220
+ "add",
221
+ worktreePath,
222
+ branch
223
+ ]);
224
+ }
225
+ this.worktrees.set(branch, worktreePath);
226
+ return worktreePath;
227
+ }
228
+ /**
229
+ * List files using git ls-tree (no worktree needed)
230
+ */
231
+ async listWithGitLsTree(branch, path, options) {
232
+ const maxDepth = options?.maxDepth ?? 1;
233
+ const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
234
+ const entries = [];
235
+ const targetPath = path || "";
236
+ const treeish = targetPath ? `${branch}:${targetPath}` : branch;
237
+ try {
238
+ const pathType = await this.git.raw([
239
+ "cat-file",
240
+ "-t",
241
+ treeish
242
+ ]).then((t) => t.trim()).catch(() => null);
243
+ if (pathType === null) return { data: [] };
244
+ if (pathType === "blob") {
245
+ const size = await this.git.raw([
246
+ "cat-file",
247
+ "-s",
248
+ treeish
249
+ ]).then((s) => Number.parseInt(s.trim(), 10));
250
+ const afsPath = this.buildPath(branch, path);
251
+ entries.push({
252
+ id: afsPath,
253
+ path: afsPath,
254
+ metadata: {
255
+ type: "file",
256
+ size
257
+ }
258
+ });
259
+ return { data: entries };
260
+ }
261
+ const queue = [{
262
+ path: targetPath,
263
+ depth: 0
264
+ }];
265
+ while (queue.length > 0) {
266
+ const { path: itemPath, depth } = queue.shift();
267
+ const itemTreeish = itemPath ? `${branch}:${itemPath}` : branch;
268
+ const lines = (await this.git.raw([
269
+ "ls-tree",
270
+ "-l",
271
+ itemTreeish
272
+ ])).split("\n").filter((line) => line.trim()).slice(0, limit - entries.length);
273
+ for (const line of lines) {
274
+ const match = line.match(/^(\d+)\s+(blob|tree)\s+(\w+)\s+(-|\d+)\s+(.+)$/);
275
+ if (!match) continue;
276
+ const type = match[2];
277
+ const sizeStr = match[4];
278
+ const name = match[5];
279
+ const isDirectory = type === "tree";
280
+ const size = sizeStr === "-" ? void 0 : Number.parseInt(sizeStr, 10);
281
+ const fullPath = itemPath ? `${itemPath}/${name}` : name;
282
+ const afsPath = this.buildPath(branch, fullPath);
283
+ entries.push({
284
+ id: afsPath,
285
+ path: afsPath,
286
+ metadata: {
287
+ type: isDirectory ? "directory" : "file",
288
+ size
289
+ }
290
+ });
291
+ if (isDirectory && depth + 1 < maxDepth) queue.push({
292
+ path: fullPath,
293
+ depth: depth + 1
294
+ });
295
+ if (entries.length >= limit) return { data: entries };
296
+ }
297
+ }
298
+ return { data: entries };
299
+ } catch (error) {
300
+ return {
301
+ data: [],
302
+ message: error.message
303
+ };
304
+ }
305
+ }
306
+ /**
307
+ * Build AFS path with encoded branch name
308
+ * Branch names with slashes are encoded by replacing / with ~
309
+ * @param branch Branch name (may contain slashes)
310
+ * @param filePath File path within branch
311
+ */
312
+ buildPath(branch, filePath) {
313
+ const encodedBranch = branch.replace(/\//g, "~");
314
+ if (!filePath) return `/${encodedBranch}`;
315
+ return `/${encodedBranch}/${filePath}`;
316
+ }
317
+ async list(path, options) {
318
+ await this.ready();
319
+ const { branch, filePath } = this.parsePath(path);
320
+ if (!branch) return { data: (await this.getBranches()).map((name) => {
321
+ const encodedPath = this.buildPath(name);
322
+ return {
323
+ id: encodedPath,
324
+ path: encodedPath,
325
+ metadata: { type: "directory" }
326
+ };
327
+ }) };
328
+ return this.listWithGitLsTree(branch, filePath, options);
329
+ }
330
+ async read(path, _options) {
331
+ await this.ready();
332
+ const { branch, filePath } = this.parsePath(path);
333
+ if (!branch) return { data: {
334
+ id: "/",
335
+ path: "/",
336
+ metadata: { type: "directory" }
337
+ } };
338
+ if (!filePath) {
339
+ const branchPath = this.buildPath(branch);
340
+ return { data: {
341
+ id: branchPath,
342
+ path: branchPath,
343
+ metadata: { type: "directory" }
344
+ } };
345
+ }
346
+ try {
347
+ if (await this.git.raw([
348
+ "cat-file",
349
+ "-t",
350
+ `${branch}:${filePath}`
351
+ ]).then((t) => t.trim()) === "tree") {
352
+ const afsPath$1 = this.buildPath(branch, filePath);
353
+ return { data: {
354
+ id: afsPath$1,
355
+ path: afsPath$1,
356
+ metadata: { type: "directory" }
357
+ } };
358
+ }
359
+ const size = await this.git.raw([
360
+ "cat-file",
361
+ "-s",
362
+ `${branch}:${filePath}`
363
+ ]).then((s) => Number.parseInt(s.trim(), 10));
364
+ const mimeType = this.getMimeType(filePath);
365
+ const isBinary = this.isBinaryFile(filePath);
366
+ let content;
367
+ const metadata = {
368
+ type: "file",
369
+ size,
370
+ mimeType
371
+ };
372
+ if (isBinary) {
373
+ const { stdout } = await execFileAsync("git", [
374
+ "cat-file",
375
+ "-p",
376
+ `${branch}:${filePath}`
377
+ ], {
378
+ cwd: this.options.repoPath,
379
+ encoding: "buffer",
380
+ maxBuffer: 10 * 1024 * 1024
381
+ });
382
+ content = stdout.toString("base64");
383
+ metadata.contentType = "base64";
384
+ } else content = await this.git.show([`${branch}:${filePath}`]);
385
+ const afsPath = this.buildPath(branch, filePath);
386
+ return { data: {
387
+ id: afsPath,
388
+ path: afsPath,
389
+ content,
390
+ metadata
391
+ } };
392
+ } catch (error) {
393
+ return {
394
+ data: void 0,
395
+ message: error.message
396
+ };
397
+ }
398
+ }
399
+ async write(path, entry, options) {
400
+ await this.ready();
401
+ const { branch, filePath } = this.parsePath(path);
402
+ if (!branch || !filePath) throw new Error("Cannot write to root or branch root");
403
+ const worktreePath = await this.ensureWorktree(branch);
404
+ const fullPath = (0, node_path.join)(worktreePath, filePath);
405
+ const append = options?.append ?? false;
406
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
407
+ if (entry.content !== void 0) {
408
+ let contentToWrite;
409
+ if (typeof entry.content === "string") contentToWrite = entry.content;
410
+ else contentToWrite = JSON.stringify(entry.content, null, 2);
411
+ await (0, node_fs_promises.writeFile)(fullPath, contentToWrite, {
412
+ encoding: "utf8",
413
+ flag: append ? "a" : "w"
414
+ });
415
+ }
416
+ if (this.options.autoCommit) {
417
+ const gitInstance = (0, simple_git.simpleGit)(worktreePath);
418
+ await gitInstance.add(filePath);
419
+ if (this.options.commitAuthor) {
420
+ await gitInstance.addConfig("user.name", this.options.commitAuthor.name, void 0, "local");
421
+ await gitInstance.addConfig("user.email", this.options.commitAuthor.email, void 0, "local");
422
+ }
423
+ await gitInstance.commit(`Update ${filePath}`);
424
+ }
425
+ const stats = await (0, node_fs_promises.stat)(fullPath);
426
+ const afsPath = this.buildPath(branch, filePath);
427
+ return { data: {
428
+ id: afsPath,
429
+ path: afsPath,
430
+ content: entry.content,
431
+ summary: entry.summary,
432
+ createdAt: stats.birthtime,
433
+ updatedAt: stats.mtime,
434
+ metadata: {
435
+ ...entry.metadata,
436
+ type: stats.isDirectory() ? "directory" : "file",
437
+ size: stats.size
438
+ },
439
+ userId: entry.userId,
440
+ sessionId: entry.sessionId,
441
+ linkTo: entry.linkTo
442
+ } };
443
+ }
444
+ async delete(path, options) {
445
+ await this.ready();
446
+ const { branch, filePath } = this.parsePath(path);
447
+ if (!branch || !filePath) throw new Error("Cannot delete root or branch root");
448
+ const worktreePath = await this.ensureWorktree(branch);
449
+ const fullPath = (0, node_path.join)(worktreePath, filePath);
450
+ const recursive = options?.recursive ?? false;
451
+ if ((await (0, node_fs_promises.stat)(fullPath)).isDirectory() && !recursive) throw new Error(`Cannot delete directory '${path}' without recursive option. Set recursive: true to delete directories.`);
452
+ await (0, node_fs_promises.rm)(fullPath, {
453
+ recursive,
454
+ force: true
455
+ });
456
+ if (this.options.autoCommit) {
457
+ const gitInstance = (0, simple_git.simpleGit)(worktreePath);
458
+ await gitInstance.add(filePath);
459
+ if (this.options.commitAuthor) {
460
+ await gitInstance.addConfig("user.name", this.options.commitAuthor.name, void 0, "local");
461
+ await gitInstance.addConfig("user.email", this.options.commitAuthor.email, void 0, "local");
462
+ }
463
+ await gitInstance.commit(`Delete ${filePath}`);
464
+ }
465
+ return { message: `Successfully deleted: ${path}` };
466
+ }
467
+ async rename(oldPath, newPath, options) {
468
+ await this.ready();
469
+ const { branch: oldBranch, filePath: oldFilePath } = this.parsePath(oldPath);
470
+ const { branch: newBranch, filePath: newFilePath } = this.parsePath(newPath);
471
+ if (!oldBranch || !oldFilePath) throw new Error("Cannot rename from root or branch root");
472
+ if (!newBranch || !newFilePath) throw new Error("Cannot rename to root or branch root");
473
+ if (oldBranch !== newBranch) throw new Error("Cannot rename across branches");
474
+ const worktreePath = await this.ensureWorktree(oldBranch);
475
+ const oldFullPath = (0, node_path.join)(worktreePath, oldFilePath);
476
+ const newFullPath = (0, node_path.join)(worktreePath, newFilePath);
477
+ const overwrite = options?.overwrite ?? false;
478
+ await (0, node_fs_promises.stat)(oldFullPath);
479
+ try {
480
+ await (0, node_fs_promises.stat)(newFullPath);
481
+ if (!overwrite) throw new Error(`Destination '${newPath}' already exists. Set overwrite: true to replace it.`);
482
+ } catch (error) {
483
+ if (error.code !== "ENOENT") throw error;
484
+ }
485
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(newFullPath), { recursive: true });
486
+ await (0, node_fs_promises.rename)(oldFullPath, newFullPath);
487
+ if (this.options.autoCommit) {
488
+ const gitInstance = (0, simple_git.simpleGit)(worktreePath);
489
+ await gitInstance.add([oldFilePath, newFilePath]);
490
+ if (this.options.commitAuthor) {
491
+ await gitInstance.addConfig("user.name", this.options.commitAuthor.name, void 0, "local");
492
+ await gitInstance.addConfig("user.email", this.options.commitAuthor.email, void 0, "local");
493
+ }
494
+ await gitInstance.commit(`Rename ${oldFilePath} to ${newFilePath}`);
495
+ }
496
+ return { message: `Successfully renamed '${oldPath}' to '${newPath}'` };
497
+ }
498
+ async search(path, query, options) {
499
+ await this.ready();
500
+ const { branch, filePath } = this.parsePath(path);
501
+ if (!branch) return {
502
+ data: [],
503
+ message: "Search requires a branch path"
504
+ };
505
+ const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
506
+ try {
507
+ const args = [
508
+ "grep",
509
+ "-n",
510
+ "-I"
511
+ ];
512
+ if (options?.caseSensitive === false) args.push("-i");
513
+ args.push(query, branch);
514
+ if (filePath) args.push("--", filePath);
515
+ const lines = (await this.git.raw(args)).split("\n").filter((line) => line.trim());
516
+ const entries = [];
517
+ const processedFiles = /* @__PURE__ */ new Set();
518
+ for (const line of lines) {
519
+ let matchPath;
520
+ let lineNum;
521
+ let content;
522
+ const matchWithBranch = line.match(/^[^:]+:([^:]+):(\d+):(.+)$/);
523
+ if (matchWithBranch) {
524
+ matchPath = matchWithBranch[1];
525
+ lineNum = matchWithBranch[2];
526
+ content = matchWithBranch[3];
527
+ } else {
528
+ const matchNoBranch = line.match(/^([^:]+):(\d+):(.+)$/);
529
+ if (!matchNoBranch) continue;
530
+ matchPath = matchNoBranch[1];
531
+ lineNum = matchNoBranch[2];
532
+ content = matchNoBranch[3];
533
+ }
534
+ const afsPath = this.buildPath(branch, matchPath);
535
+ if (processedFiles.has(afsPath)) continue;
536
+ processedFiles.add(afsPath);
537
+ entries.push({
538
+ id: afsPath,
539
+ path: afsPath,
540
+ summary: `Line ${lineNum}: ${content}`,
541
+ metadata: { type: "file" }
542
+ });
543
+ if (entries.length >= limit) break;
544
+ }
545
+ return {
546
+ data: entries,
547
+ message: entries.length >= limit ? `Results truncated to limit ${limit}` : void 0
548
+ };
549
+ } catch (error) {
550
+ if (error.message.includes("did not match any file(s)")) return { data: [] };
551
+ return {
552
+ data: [],
553
+ message: error.message
554
+ };
555
+ }
556
+ }
557
+ /**
558
+ * Fetch latest changes from remote
559
+ */
560
+ async fetch() {
561
+ await this.ready();
562
+ await this.git.fetch();
563
+ }
564
+ /**
565
+ * Pull latest changes from remote for current branch
566
+ */
567
+ async pull() {
568
+ await this.ready();
569
+ await this.git.pull();
570
+ }
571
+ /**
572
+ * Push local changes to remote
573
+ */
574
+ async push(branch) {
575
+ await this.ready();
576
+ if (branch) await this.git.push("origin", branch);
577
+ else await this.git.push();
578
+ }
579
+ /**
580
+ * Cleanup all worktrees (useful when unmounting)
581
+ */
582
+ async cleanup() {
583
+ await this.ready();
584
+ for (const [_branch, worktreePath] of this.worktrees) try {
585
+ await this.git.raw([
586
+ "worktree",
587
+ "remove",
588
+ worktreePath,
589
+ "--force"
590
+ ]);
591
+ } catch (_error) {}
592
+ this.worktrees.clear();
593
+ try {
594
+ await (0, node_fs_promises.rm)(this.tempBase, {
595
+ recursive: true,
596
+ force: true
597
+ });
598
+ } catch {}
599
+ const autoCleanup = this.options.autoCleanup ?? true;
600
+ if (this.isAutoCloned && autoCleanup && this.clonedPath) try {
601
+ await (0, node_fs_promises.rm)(this.clonedPath, {
602
+ recursive: true,
603
+ force: true
604
+ });
605
+ } catch {}
606
+ }
607
+ };
608
+
609
+ //#endregion
610
+ exports.AFSGit = AFSGit;