@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 +46 -41
- package/dist/index.cjs +501 -0
- package/dist/index.d.cts +148 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +148 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +502 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +29 -43
- package/CHANGELOG.md +0 -57
- package/lib/cjs/index.d.ts +0 -129
- package/lib/cjs/index.js +0 -601
- package/lib/cjs/package.json +0 -3
- package/lib/dts/index.d.ts +0 -129
- package/lib/esm/index.d.ts +0 -129
- package/lib/esm/index.js +0 -597
- package/lib/esm/package.json +0 -3
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(
|
|
33
|
-
|
|
34
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
67
|
+
const allFiles = await afs.list("/modules/git/main", { maxDepth: 10 });
|
|
66
68
|
|
|
67
69
|
// Read file content
|
|
68
|
-
const content = await afs.read(
|
|
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(
|
|
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:
|
|
83
|
-
accessMode:
|
|
84
|
+
repoPath: "/path/to/repo",
|
|
85
|
+
accessMode: "readwrite",
|
|
84
86
|
autoCommit: true,
|
|
85
87
|
commitAuthor: {
|
|
86
|
-
name:
|
|
87
|
-
email:
|
|
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(
|
|
95
|
-
content:
|
|
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(
|
|
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(
|
|
107
|
+
await afs.delete("/modules/git/main/oldfile.txt");
|
|
106
108
|
// Automatically committed
|
|
107
109
|
|
|
108
110
|
// Rename/move file
|
|
109
|
-
await afs.rename(
|
|
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:
|
|
117
|
-
name:
|
|
118
|
-
description:
|
|
118
|
+
repoPath: "/path/to/repo",
|
|
119
|
+
name: "my-repo",
|
|
120
|
+
description: "My project repository",
|
|
119
121
|
|
|
120
122
|
// Limit accessible branches
|
|
121
|
-
branches: [
|
|
123
|
+
branches: ["main", "develop"],
|
|
122
124
|
|
|
123
125
|
// Access control
|
|
124
|
-
accessMode:
|
|
126
|
+
accessMode: "readwrite",
|
|
125
127
|
|
|
126
128
|
// Auto-commit settings
|
|
127
129
|
autoCommit: true,
|
|
128
130
|
commitAuthor: {
|
|
129
|
-
name:
|
|
130
|
-
email:
|
|
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;
|
|
170
|
+
repoPath: string; // Path to git repository
|
|
169
171
|
|
|
170
172
|
// Optional
|
|
171
|
-
name?: string;
|
|
172
|
-
description?: string;
|
|
173
|
-
branches?: string[];
|
|
174
|
-
accessMode?:
|
|
175
|
-
autoCommit?: boolean;
|
|
176
|
-
commitAuthor?: {
|
|
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(
|
|
206
|
-
|
|
207
|
-
|
|
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;
|