@aigne/afs-git 1.1.0 → 1.11.0-beta

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 CHANGED
@@ -29,10 +29,12 @@ import { AFSGit } from "@aigne/afs-git";
29
29
  const afs = new AFS();
30
30
 
31
31
  // Mount a git repository in read-only mode
32
- afs.mount(new AFSGit({
33
- repoPath: '/path/to/repo',
34
- accessMode: 'readonly' // default
35
- }));
32
+ afs.mount(
33
+ new AFSGit({
34
+ repoPath: "/path/to/repo",
35
+ accessMode: "readonly", // default
36
+ }),
37
+ );
36
38
  ```
37
39
 
38
40
  ### Path Structure
@@ -54,22 +56,22 @@ afs.mount(new AFSGit({
54
56
 
55
57
  ```typescript
56
58
  // List all branches
57
- const branches = await afs.list('/modules/git');
59
+ const branches = await afs.list("/modules/git");
58
60
  // Returns: ['/main', '/develop', '/feature-auth']
59
61
 
60
62
  // List files in a branch
61
- const files = await afs.list('/modules/git/main');
63
+ const files = await afs.list("/modules/git/main");
62
64
  // Returns files at root of main branch
63
65
 
64
66
  // List files recursively
65
- const allFiles = await afs.list('/modules/git/main', { maxDepth: 10 });
67
+ const allFiles = await afs.list("/modules/git/main", { maxDepth: 10 });
66
68
 
67
69
  // Read file content
68
- const content = await afs.read('/modules/git/main/README.md');
70
+ const content = await afs.read("/modules/git/main/README.md");
69
71
  console.log(content.data?.content);
70
72
 
71
73
  // Search across branch
72
- const results = await afs.search('/modules/git/main', 'TODO');
74
+ const results = await afs.search("/modules/git/main", "TODO");
73
75
  // Uses git grep for fast searching
74
76
  ```
75
77
 
@@ -79,56 +81,56 @@ const results = await afs.search('/modules/git/main', 'TODO');
79
81
  import { AFSGit } from "@aigne/afs-git";
80
82
 
81
83
  const afsGit = new AFSGit({
82
- repoPath: '/path/to/repo',
83
- accessMode: 'readwrite',
84
+ repoPath: "/path/to/repo",
85
+ accessMode: "readwrite",
84
86
  autoCommit: true,
85
87
  commitAuthor: {
86
- name: 'AI Agent',
87
- email: 'agent@example.com'
88
- }
88
+ name: "AI Agent",
89
+ email: "agent@example.com",
90
+ },
89
91
  });
90
92
 
91
93
  afs.mount(afsGit);
92
94
 
93
95
  // Write a new file (creates worktree automatically)
94
- await afs.write('/modules/git/main/newfile.txt', {
95
- content: 'Hello World'
96
+ await afs.write("/modules/git/main/newfile.txt", {
97
+ content: "Hello World",
96
98
  });
97
99
  // Automatically committed with message "Update newfile.txt"
98
100
 
99
101
  // Update existing file
100
- await afs.write('/modules/git/main/src/index.ts', {
101
- content: updatedCode
102
+ await afs.write("/modules/git/main/src/index.ts", {
103
+ content: updatedCode,
102
104
  });
103
105
 
104
106
  // Delete file
105
- await afs.delete('/modules/git/main/oldfile.txt');
107
+ await afs.delete("/modules/git/main/oldfile.txt");
106
108
  // Automatically committed
107
109
 
108
110
  // Rename/move file
109
- await afs.rename('/modules/git/main/old.txt', '/modules/git/main/new.txt');
111
+ await afs.rename("/modules/git/main/old.txt", "/modules/git/main/new.txt");
110
112
  ```
111
113
 
112
114
  ### Advanced Configuration
113
115
 
114
116
  ```typescript
115
117
  const afsGit = new AFSGit({
116
- repoPath: '/path/to/repo',
117
- name: 'my-repo',
118
- description: 'My project repository',
118
+ repoPath: "/path/to/repo",
119
+ name: "my-repo",
120
+ description: "My project repository",
119
121
 
120
122
  // Limit accessible branches
121
- branches: ['main', 'develop'],
123
+ branches: ["main", "develop"],
122
124
 
123
125
  // Access control
124
- accessMode: 'readwrite',
126
+ accessMode: "readwrite",
125
127
 
126
128
  // Auto-commit settings
127
129
  autoCommit: true,
128
130
  commitAuthor: {
129
- name: 'AI Agent',
130
- email: 'agent@example.com'
131
- }
131
+ name: "AI Agent",
132
+ email: "agent@example.com",
133
+ },
132
134
  });
133
135
 
134
136
  // Cleanup worktrees when done
@@ -165,15 +167,16 @@ Worktrees are created in temporary directories and cleaned up when the module is
165
167
  ```typescript
166
168
  interface AFSGitOptions {
167
169
  // Required
168
- repoPath: string; // Path to git repository
170
+ repoPath: string; // Path to git repository
169
171
 
170
172
  // Optional
171
- name?: string; // Module name (default: repo basename)
172
- description?: string; // Module description
173
- branches?: string[]; // Limit accessible branches
174
- accessMode?: 'readonly' | 'readwrite'; // Default: 'readonly'
175
- autoCommit?: boolean; // Auto-commit changes (default: false)
176
- commitAuthor?: { // Author for commits
173
+ name?: string; // Module name (default: repo basename)
174
+ description?: string; // Module description
175
+ branches?: string[]; // Limit accessible branches
176
+ accessMode?: "readonly" | "readwrite"; // Default: 'readonly'
177
+ autoCommit?: boolean; // Auto-commit changes (default: false)
178
+ commitAuthor?: {
179
+ // Author for commits
177
180
  name: string;
178
181
  email: string;
179
182
  };
@@ -202,10 +205,12 @@ import { AFS } from "@aigne/afs";
202
205
  import { AFSGit } from "@aigne/afs-git";
203
206
 
204
207
  const afs = new AFS();
205
- afs.mount(new AFSGit({
206
- repoPath: process.cwd(),
207
- accessMode: 'readonly'
208
- }));
208
+ afs.mount(
209
+ new AFSGit({
210
+ repoPath: process.cwd(),
211
+ accessMode: "readonly",
212
+ }),
213
+ );
209
214
 
210
215
  const aigne = new AIGNE({});
211
216
 
@@ -213,12 +218,12 @@ const aigne = new AIGNE({});
213
218
  const agent = AIAgent.from({
214
219
  name: "CodeReviewer",
215
220
  instructions: "Review code and suggest improvements",
216
- afs
221
+ afs,
217
222
  });
218
223
 
219
224
  // Agent can list files, read code, search for patterns
220
225
  await aigne.invoke(agent, {
221
- message: "Review the authentication code in src/auth/"
226
+ message: "Review the authentication code in src/auth/",
222
227
  });
223
228
  ```
224
229
 
package/dist/index.cjs ADDED
@@ -0,0 +1,501 @@
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 execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
13
+ const LIST_MAX_LIMIT = 1e3;
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: zod.z.string().describe("The path to the git repository"),
17
+ description: (0, _aigne_afs_utils_zod.optionalize)(zod.z.string().describe("A description of the repository")),
18
+ branches: (0, _aigne_afs_utils_zod.optionalize)(zod.z.array(zod.z.string()).describe("List of branches to expose")),
19
+ accessMode: (0, _aigne_afs_utils_zod.optionalize)(zod.z.enum(["readonly", "readwrite"]).describe("Access mode for this module")),
20
+ autoCommit: (0, _aigne_afs_utils_zod.optionalize)(zod.z.boolean().describe("Automatically commit changes after write operations")),
21
+ commitAuthor: (0, _aigne_afs_utils_zod.optionalize)(zod.z.object({
22
+ name: zod.z.string(),
23
+ email: zod.z.string()
24
+ }))
25
+ }));
26
+ var AFSGit = class AFSGit {
27
+ static schema() {
28
+ return afsGitOptionsSchema;
29
+ }
30
+ static async load({ filepath, parsed }) {
31
+ return new AFSGit({
32
+ ...await AFSGit.schema().parseAsync(parsed),
33
+ cwd: (0, node_path.dirname)(filepath)
34
+ });
35
+ }
36
+ git;
37
+ tempBase;
38
+ worktrees = /* @__PURE__ */ new Map();
39
+ repoHash;
40
+ constructor(options) {
41
+ this.options = options;
42
+ (0, _aigne_afs_utils_zod.zodParse)(afsGitOptionsSchema, options);
43
+ let repoPath;
44
+ if ((0, node_path.isAbsolute)(options.repoPath)) repoPath = options.repoPath;
45
+ else repoPath = (0, node_path.join)(options.cwd || process.cwd(), options.repoPath);
46
+ this.options.repoPath = repoPath;
47
+ this.name = options.name || (0, node_path.basename)(repoPath) || "git";
48
+ this.description = options.description;
49
+ this.accessMode = options.accessMode ?? "readonly";
50
+ this.git = (0, simple_git.simpleGit)(repoPath);
51
+ this.repoHash = (0, node_crypto.createHash)("md5").update(repoPath).digest("hex").substring(0, 8);
52
+ this.tempBase = (0, node_path.join)((0, node_os.tmpdir)(), `afs-git-${this.repoHash}`);
53
+ }
54
+ name;
55
+ description;
56
+ accessMode;
57
+ /**
58
+ * Parse AFS path into branch and file path
59
+ * Branch names may contain slashes and are encoded with ~ in paths
60
+ * Examples:
61
+ * "/" -> { branch: undefined, filePath: "" }
62
+ * "/main" -> { branch: "main", filePath: "" }
63
+ * "/feature~new-feature" -> { branch: "feature/new-feature", filePath: "" }
64
+ * "/main/src/index.ts" -> { branch: "main", filePath: "src/index.ts" }
65
+ */
66
+ parsePath(path) {
67
+ const segments = (0, node_path.join)("/", path).split("/").filter(Boolean);
68
+ if (segments.length === 0) return {
69
+ branch: void 0,
70
+ filePath: ""
71
+ };
72
+ return {
73
+ branch: segments[0].replace(/~/g, "/"),
74
+ filePath: segments.slice(1).join("/")
75
+ };
76
+ }
77
+ /**
78
+ * Detect MIME type based on file extension
79
+ */
80
+ getMimeType(filePath) {
81
+ return {
82
+ png: "image/png",
83
+ jpg: "image/jpeg",
84
+ jpeg: "image/jpeg",
85
+ gif: "image/gif",
86
+ bmp: "image/bmp",
87
+ webp: "image/webp",
88
+ svg: "image/svg+xml",
89
+ ico: "image/x-icon",
90
+ pdf: "application/pdf",
91
+ txt: "text/plain",
92
+ md: "text/markdown",
93
+ js: "text/javascript",
94
+ ts: "text/typescript",
95
+ json: "application/json",
96
+ html: "text/html",
97
+ css: "text/css",
98
+ xml: "text/xml"
99
+ }[filePath.split(".").pop()?.toLowerCase() || ""] || "application/octet-stream";
100
+ }
101
+ /**
102
+ * Check if file is likely binary based on extension
103
+ */
104
+ isBinaryFile(filePath) {
105
+ const ext = filePath.split(".").pop()?.toLowerCase();
106
+ return [
107
+ "png",
108
+ "jpg",
109
+ "jpeg",
110
+ "gif",
111
+ "bmp",
112
+ "webp",
113
+ "ico",
114
+ "pdf",
115
+ "zip",
116
+ "tar",
117
+ "gz",
118
+ "exe",
119
+ "dll",
120
+ "so",
121
+ "dylib",
122
+ "wasm"
123
+ ].includes(ext || "");
124
+ }
125
+ /**
126
+ * Get list of available branches
127
+ */
128
+ async getBranches() {
129
+ const allBranches = (await this.git.branchLocal()).all;
130
+ if (this.options.branches && this.options.branches.length > 0) return allBranches.filter((branch) => this.options.branches.includes(branch));
131
+ return allBranches;
132
+ }
133
+ /**
134
+ * Ensure worktree exists for a branch (lazy creation)
135
+ */
136
+ async ensureWorktree(branch) {
137
+ if (this.worktrees.has(branch)) return this.worktrees.get(branch);
138
+ if ((await this.git.revparse(["--abbrev-ref", "HEAD"])).trim() === branch) {
139
+ this.worktrees.set(branch, this.options.repoPath);
140
+ return this.options.repoPath;
141
+ }
142
+ const worktreePath = (0, node_path.join)(this.tempBase, branch);
143
+ if (!await (0, node_fs_promises.stat)(worktreePath).then(() => true).catch(() => false)) {
144
+ await (0, node_fs_promises.mkdir)(this.tempBase, { recursive: true });
145
+ await this.git.raw([
146
+ "worktree",
147
+ "add",
148
+ worktreePath,
149
+ branch
150
+ ]);
151
+ }
152
+ this.worktrees.set(branch, worktreePath);
153
+ return worktreePath;
154
+ }
155
+ /**
156
+ * List files using git ls-tree (no worktree needed)
157
+ */
158
+ async listWithGitLsTree(branch, path, options) {
159
+ const maxDepth = options?.maxDepth ?? 1;
160
+ const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
161
+ const entries = [];
162
+ const targetPath = path || "";
163
+ const treeish = targetPath ? `${branch}:${targetPath}` : branch;
164
+ try {
165
+ const pathType = await this.git.raw([
166
+ "cat-file",
167
+ "-t",
168
+ treeish
169
+ ]).then((t) => t.trim()).catch(() => null);
170
+ if (pathType === null) return { data: [] };
171
+ if (pathType === "blob") {
172
+ const size = await this.git.raw([
173
+ "cat-file",
174
+ "-s",
175
+ treeish
176
+ ]).then((s) => Number.parseInt(s.trim(), 10));
177
+ const afsPath = this.buildPath(branch, path);
178
+ entries.push({
179
+ id: afsPath,
180
+ path: afsPath,
181
+ metadata: {
182
+ type: "file",
183
+ size
184
+ }
185
+ });
186
+ return { data: entries };
187
+ }
188
+ const queue = [{
189
+ path: targetPath,
190
+ depth: 0
191
+ }];
192
+ while (queue.length > 0) {
193
+ const { path: itemPath, depth } = queue.shift();
194
+ const itemTreeish = itemPath ? `${branch}:${itemPath}` : branch;
195
+ const lines = (await this.git.raw([
196
+ "ls-tree",
197
+ "-l",
198
+ itemTreeish
199
+ ])).split("\n").filter((line) => line.trim()).slice(0, limit - entries.length);
200
+ for (const line of lines) {
201
+ const match = line.match(/^(\d+)\s+(blob|tree)\s+(\w+)\s+(-|\d+)\s+(.+)$/);
202
+ if (!match) continue;
203
+ const type = match[2];
204
+ const sizeStr = match[4];
205
+ const name = match[5];
206
+ const isDirectory = type === "tree";
207
+ const size = sizeStr === "-" ? void 0 : Number.parseInt(sizeStr, 10);
208
+ const fullPath = itemPath ? `${itemPath}/${name}` : name;
209
+ const afsPath = this.buildPath(branch, fullPath);
210
+ entries.push({
211
+ id: afsPath,
212
+ path: afsPath,
213
+ metadata: {
214
+ type: isDirectory ? "directory" : "file",
215
+ size
216
+ }
217
+ });
218
+ if (isDirectory && depth + 1 < maxDepth) queue.push({
219
+ path: fullPath,
220
+ depth: depth + 1
221
+ });
222
+ if (entries.length >= limit) return { data: entries };
223
+ }
224
+ }
225
+ return { data: entries };
226
+ } catch (error) {
227
+ return {
228
+ data: [],
229
+ message: error.message
230
+ };
231
+ }
232
+ }
233
+ /**
234
+ * Build AFS path with encoded branch name
235
+ * Branch names with slashes are encoded by replacing / with ~
236
+ * @param branch Branch name (may contain slashes)
237
+ * @param filePath File path within branch
238
+ */
239
+ buildPath(branch, filePath) {
240
+ const encodedBranch = branch.replace(/\//g, "~");
241
+ if (!filePath) return `/${encodedBranch}`;
242
+ return `/${encodedBranch}/${filePath}`;
243
+ }
244
+ async list(path, options) {
245
+ const { branch, filePath } = this.parsePath(path);
246
+ if (!branch) return { data: (await this.getBranches()).map((name) => {
247
+ const encodedPath = this.buildPath(name);
248
+ return {
249
+ id: encodedPath,
250
+ path: encodedPath,
251
+ metadata: { type: "directory" }
252
+ };
253
+ }) };
254
+ return this.listWithGitLsTree(branch, filePath, options);
255
+ }
256
+ async read(path, _options) {
257
+ const { branch, filePath } = this.parsePath(path);
258
+ if (!branch) return { data: {
259
+ id: "/",
260
+ path: "/",
261
+ metadata: { type: "directory" }
262
+ } };
263
+ if (!filePath) {
264
+ const branchPath = this.buildPath(branch);
265
+ return { data: {
266
+ id: branchPath,
267
+ path: branchPath,
268
+ metadata: { type: "directory" }
269
+ } };
270
+ }
271
+ try {
272
+ if (await this.git.raw([
273
+ "cat-file",
274
+ "-t",
275
+ `${branch}:${filePath}`
276
+ ]).then((t) => t.trim()) === "tree") {
277
+ const afsPath$1 = this.buildPath(branch, filePath);
278
+ return { data: {
279
+ id: afsPath$1,
280
+ path: afsPath$1,
281
+ metadata: { type: "directory" }
282
+ } };
283
+ }
284
+ const size = await this.git.raw([
285
+ "cat-file",
286
+ "-s",
287
+ `${branch}:${filePath}`
288
+ ]).then((s) => Number.parseInt(s.trim(), 10));
289
+ const mimeType = this.getMimeType(filePath);
290
+ const isBinary = this.isBinaryFile(filePath);
291
+ let content;
292
+ const metadata = {
293
+ type: "file",
294
+ size,
295
+ mimeType
296
+ };
297
+ if (isBinary) {
298
+ const { stdout } = await execFileAsync("git", [
299
+ "cat-file",
300
+ "-p",
301
+ `${branch}:${filePath}`
302
+ ], {
303
+ cwd: this.options.repoPath,
304
+ encoding: "buffer",
305
+ maxBuffer: 10 * 1024 * 1024
306
+ });
307
+ content = stdout.toString("base64");
308
+ metadata.contentType = "base64";
309
+ } else content = await this.git.show([`${branch}:${filePath}`]);
310
+ const afsPath = this.buildPath(branch, filePath);
311
+ return { data: {
312
+ id: afsPath,
313
+ path: afsPath,
314
+ content,
315
+ metadata
316
+ } };
317
+ } catch (error) {
318
+ return {
319
+ data: void 0,
320
+ message: error.message
321
+ };
322
+ }
323
+ }
324
+ async write(path, entry, options) {
325
+ const { branch, filePath } = this.parsePath(path);
326
+ if (!branch || !filePath) throw new Error("Cannot write to root or branch root");
327
+ const worktreePath = await this.ensureWorktree(branch);
328
+ const fullPath = (0, node_path.join)(worktreePath, filePath);
329
+ const append = options?.append ?? false;
330
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
331
+ if (entry.content !== void 0) {
332
+ let contentToWrite;
333
+ if (typeof entry.content === "string") contentToWrite = entry.content;
334
+ else contentToWrite = JSON.stringify(entry.content, null, 2);
335
+ await (0, node_fs_promises.writeFile)(fullPath, contentToWrite, {
336
+ encoding: "utf8",
337
+ flag: append ? "a" : "w"
338
+ });
339
+ }
340
+ if (this.options.autoCommit) {
341
+ const gitInstance = (0, simple_git.simpleGit)(worktreePath);
342
+ await gitInstance.add(filePath);
343
+ if (this.options.commitAuthor) {
344
+ await gitInstance.addConfig("user.name", this.options.commitAuthor.name, void 0, "local");
345
+ await gitInstance.addConfig("user.email", this.options.commitAuthor.email, void 0, "local");
346
+ }
347
+ await gitInstance.commit(`Update ${filePath}`);
348
+ }
349
+ const stats = await (0, node_fs_promises.stat)(fullPath);
350
+ const afsPath = this.buildPath(branch, filePath);
351
+ return { data: {
352
+ id: afsPath,
353
+ path: afsPath,
354
+ content: entry.content,
355
+ summary: entry.summary,
356
+ createdAt: stats.birthtime,
357
+ updatedAt: stats.mtime,
358
+ metadata: {
359
+ ...entry.metadata,
360
+ type: stats.isDirectory() ? "directory" : "file",
361
+ size: stats.size
362
+ },
363
+ userId: entry.userId,
364
+ sessionId: entry.sessionId,
365
+ linkTo: entry.linkTo
366
+ } };
367
+ }
368
+ async delete(path, options) {
369
+ const { branch, filePath } = this.parsePath(path);
370
+ if (!branch || !filePath) throw new Error("Cannot delete root or branch root");
371
+ const worktreePath = await this.ensureWorktree(branch);
372
+ const fullPath = (0, node_path.join)(worktreePath, filePath);
373
+ const recursive = options?.recursive ?? false;
374
+ 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.`);
375
+ await (0, node_fs_promises.rm)(fullPath, {
376
+ recursive,
377
+ force: true
378
+ });
379
+ if (this.options.autoCommit) {
380
+ const gitInstance = (0, simple_git.simpleGit)(worktreePath);
381
+ await gitInstance.add(filePath);
382
+ if (this.options.commitAuthor) {
383
+ await gitInstance.addConfig("user.name", this.options.commitAuthor.name, void 0, "local");
384
+ await gitInstance.addConfig("user.email", this.options.commitAuthor.email, void 0, "local");
385
+ }
386
+ await gitInstance.commit(`Delete ${filePath}`);
387
+ }
388
+ return { message: `Successfully deleted: ${path}` };
389
+ }
390
+ async rename(oldPath, newPath, options) {
391
+ const { branch: oldBranch, filePath: oldFilePath } = this.parsePath(oldPath);
392
+ const { branch: newBranch, filePath: newFilePath } = this.parsePath(newPath);
393
+ if (!oldBranch || !oldFilePath) throw new Error("Cannot rename from root or branch root");
394
+ if (!newBranch || !newFilePath) throw new Error("Cannot rename to root or branch root");
395
+ if (oldBranch !== newBranch) throw new Error("Cannot rename across branches");
396
+ const worktreePath = await this.ensureWorktree(oldBranch);
397
+ const oldFullPath = (0, node_path.join)(worktreePath, oldFilePath);
398
+ const newFullPath = (0, node_path.join)(worktreePath, newFilePath);
399
+ const overwrite = options?.overwrite ?? false;
400
+ await (0, node_fs_promises.stat)(oldFullPath);
401
+ try {
402
+ await (0, node_fs_promises.stat)(newFullPath);
403
+ if (!overwrite) throw new Error(`Destination '${newPath}' already exists. Set overwrite: true to replace it.`);
404
+ } catch (error) {
405
+ if (error.code !== "ENOENT") throw error;
406
+ }
407
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(newFullPath), { recursive: true });
408
+ await (0, node_fs_promises.rename)(oldFullPath, newFullPath);
409
+ if (this.options.autoCommit) {
410
+ const gitInstance = (0, simple_git.simpleGit)(worktreePath);
411
+ await gitInstance.add([oldFilePath, newFilePath]);
412
+ if (this.options.commitAuthor) {
413
+ await gitInstance.addConfig("user.name", this.options.commitAuthor.name, void 0, "local");
414
+ await gitInstance.addConfig("user.email", this.options.commitAuthor.email, void 0, "local");
415
+ }
416
+ await gitInstance.commit(`Rename ${oldFilePath} to ${newFilePath}`);
417
+ }
418
+ return { message: `Successfully renamed '${oldPath}' to '${newPath}'` };
419
+ }
420
+ async search(path, query, options) {
421
+ const { branch, filePath } = this.parsePath(path);
422
+ if (!branch) return {
423
+ data: [],
424
+ message: "Search requires a branch path"
425
+ };
426
+ const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
427
+ try {
428
+ const args = [
429
+ "grep",
430
+ "-n",
431
+ "-I"
432
+ ];
433
+ if (options?.caseSensitive === false) args.push("-i");
434
+ args.push(query, branch);
435
+ if (filePath) args.push("--", filePath);
436
+ const lines = (await this.git.raw(args)).split("\n").filter((line) => line.trim());
437
+ const entries = [];
438
+ const processedFiles = /* @__PURE__ */ new Set();
439
+ for (const line of lines) {
440
+ let matchPath;
441
+ let lineNum;
442
+ let content;
443
+ const matchWithBranch = line.match(/^[^:]+:([^:]+):(\d+):(.+)$/);
444
+ if (matchWithBranch) {
445
+ matchPath = matchWithBranch[1];
446
+ lineNum = matchWithBranch[2];
447
+ content = matchWithBranch[3];
448
+ } else {
449
+ const matchNoBranch = line.match(/^([^:]+):(\d+):(.+)$/);
450
+ if (!matchNoBranch) continue;
451
+ matchPath = matchNoBranch[1];
452
+ lineNum = matchNoBranch[2];
453
+ content = matchNoBranch[3];
454
+ }
455
+ const afsPath = this.buildPath(branch, matchPath);
456
+ if (processedFiles.has(afsPath)) continue;
457
+ processedFiles.add(afsPath);
458
+ entries.push({
459
+ id: afsPath,
460
+ path: afsPath,
461
+ summary: `Line ${lineNum}: ${content}`,
462
+ metadata: { type: "file" }
463
+ });
464
+ if (entries.length >= limit) break;
465
+ }
466
+ return {
467
+ data: entries,
468
+ message: entries.length >= limit ? `Results truncated to limit ${limit}` : void 0
469
+ };
470
+ } catch (error) {
471
+ if (error.message.includes("did not match any file(s)")) return { data: [] };
472
+ return {
473
+ data: [],
474
+ message: error.message
475
+ };
476
+ }
477
+ }
478
+ /**
479
+ * Cleanup all worktrees (useful when unmounting)
480
+ */
481
+ async cleanup() {
482
+ for (const [_branch, worktreePath] of this.worktrees) try {
483
+ await this.git.raw([
484
+ "worktree",
485
+ "remove",
486
+ worktreePath,
487
+ "--force"
488
+ ]);
489
+ } catch (_error) {}
490
+ this.worktrees.clear();
491
+ try {
492
+ await (0, node_fs_promises.rm)(this.tempBase, {
493
+ recursive: true,
494
+ force: true
495
+ });
496
+ } catch {}
497
+ }
498
+ };
499
+
500
+ //#endregion
501
+ exports.AFSGit = AFSGit;