@aigne/afs-git 1.11.0-beta.5 → 1.11.0-beta.7
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/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
- package/dist/index.cjs +1191 -307
- package/dist/index.d.cts +323 -97
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +323 -97
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1190 -308
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
|
+
const require_decorate = require('./_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs');
|
|
1
3
|
let node_child_process = require("node:child_process");
|
|
2
4
|
let node_crypto = require("node:crypto");
|
|
3
5
|
let node_fs_promises = require("node:fs/promises");
|
|
4
6
|
let node_os = require("node:os");
|
|
5
7
|
let node_path = require("node:path");
|
|
6
8
|
let node_util = require("node:util");
|
|
9
|
+
let _aigne_afs = require("@aigne/afs");
|
|
10
|
+
let _aigne_afs_provider = require("@aigne/afs/provider");
|
|
7
11
|
let _aigne_afs_utils_zod = require("@aigne/afs/utils/zod");
|
|
8
12
|
let simple_git = require("simple-git");
|
|
9
13
|
let zod = require("zod");
|
|
@@ -30,18 +34,21 @@ const afsGitOptionsSchema = (0, _aigne_afs_utils_zod.camelize)(zod.z.object({
|
|
|
30
34
|
password: (0, _aigne_afs_utils_zod.optionalize)(zod.z.string())
|
|
31
35
|
})) }))
|
|
32
36
|
}).refine((data) => data.repoPath || data.remoteUrl, { message: "Either repoPath or remoteUrl must be provided" }));
|
|
33
|
-
var AFSGit = class AFSGit {
|
|
37
|
+
var AFSGit = class AFSGit extends _aigne_afs_provider.AFSBaseProvider {
|
|
34
38
|
static schema() {
|
|
35
39
|
return afsGitOptionsSchema;
|
|
36
40
|
}
|
|
37
|
-
static async load({
|
|
41
|
+
static async load({ basePath, config } = {}) {
|
|
38
42
|
const instance = new AFSGit({
|
|
39
|
-
...await AFSGit.schema().parseAsync(
|
|
40
|
-
cwd:
|
|
43
|
+
...await AFSGit.schema().parseAsync(config),
|
|
44
|
+
cwd: basePath
|
|
41
45
|
});
|
|
42
46
|
await instance.ready();
|
|
43
47
|
return instance;
|
|
44
48
|
}
|
|
49
|
+
name;
|
|
50
|
+
description;
|
|
51
|
+
accessMode;
|
|
45
52
|
initPromise;
|
|
46
53
|
git;
|
|
47
54
|
tempBase;
|
|
@@ -51,6 +58,7 @@ var AFSGit = class AFSGit {
|
|
|
51
58
|
clonedPath;
|
|
52
59
|
repoPath;
|
|
53
60
|
constructor(options) {
|
|
61
|
+
super();
|
|
54
62
|
this.options = options;
|
|
55
63
|
(0, _aigne_afs_utils_zod.zodParse)(afsGitOptionsSchema, options);
|
|
56
64
|
let repoPath;
|
|
@@ -88,7 +96,18 @@ var AFSGit = class AFSGit {
|
|
|
88
96
|
if (options.remoteUrl) {
|
|
89
97
|
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
98
|
if (!options.repoPath) this.isAutoCloned = true;
|
|
91
|
-
|
|
99
|
+
const exists = await (0, node_fs_promises.stat)(targetPath).then(() => true).catch(() => false);
|
|
100
|
+
let needsClone = !exists;
|
|
101
|
+
if (exists) {
|
|
102
|
+
if (!await (0, simple_git.simpleGit)(targetPath).checkIsRepo().catch(() => false)) {
|
|
103
|
+
await (0, node_fs_promises.rm)(targetPath, {
|
|
104
|
+
recursive: true,
|
|
105
|
+
force: true
|
|
106
|
+
});
|
|
107
|
+
needsClone = true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (needsClone) {
|
|
92
111
|
const singleBranch = options.branches?.length === 1 ? options.branches[0] : void 0;
|
|
93
112
|
await AFSGit.cloneRepository(options.remoteUrl, targetPath, {
|
|
94
113
|
depth: options.depth ?? 1,
|
|
@@ -104,6 +123,7 @@ var AFSGit = class AFSGit {
|
|
|
104
123
|
this.clonedPath = this.isAutoCloned ? targetPath : void 0;
|
|
105
124
|
}
|
|
106
125
|
this.git = (0, simple_git.simpleGit)(this.repoPath);
|
|
126
|
+
if (!await this.git.checkIsRepo()) throw new Error(`Not a git repository: ${this.repoPath}`);
|
|
107
127
|
}
|
|
108
128
|
/**
|
|
109
129
|
* Clone a remote repository to local path
|
|
@@ -124,306 +144,234 @@ var AFSGit = class AFSGit {
|
|
|
124
144
|
}
|
|
125
145
|
await git.clone(cloneUrl, targetPath, cloneArgs);
|
|
126
146
|
}
|
|
127
|
-
name;
|
|
128
|
-
description;
|
|
129
|
-
accessMode;
|
|
130
147
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
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" }
|
|
148
|
+
* List root (branches)
|
|
149
|
+
* Note: list() returns only children (branches), never the path itself (per new semantics)
|
|
138
150
|
*/
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
async listRootHandler(ctx) {
|
|
152
|
+
await this.ready();
|
|
153
|
+
const options = ctx.options;
|
|
154
|
+
const maxDepth = options?.maxDepth ?? 1;
|
|
155
|
+
const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
|
|
156
|
+
if (maxDepth === 0) return { data: [] };
|
|
157
|
+
const branches = await this.getBranches();
|
|
158
|
+
const entries = [];
|
|
159
|
+
for (const name of branches) {
|
|
160
|
+
if (entries.length >= limit) break;
|
|
161
|
+
const encodedPath = this.buildBranchPath(name);
|
|
162
|
+
const branchChildrenCount = await this.getChildrenCount(name, "");
|
|
163
|
+
entries.push(this.buildEntry(encodedPath, { meta: {
|
|
164
|
+
kind: "git:branch",
|
|
165
|
+
childrenCount: branchChildrenCount
|
|
166
|
+
} }));
|
|
167
|
+
if (maxDepth > 1) {
|
|
168
|
+
const branchResult = await this.listWithGitLsTree(name, "", {
|
|
169
|
+
maxDepth: maxDepth - 1,
|
|
170
|
+
limit: limit - entries.length
|
|
171
|
+
});
|
|
172
|
+
entries.push(...branchResult.data);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { data: entries };
|
|
149
176
|
}
|
|
150
177
|
/**
|
|
151
|
-
*
|
|
178
|
+
* List branch root (matches /main, /develop, etc.)
|
|
152
179
|
*/
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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";
|
|
180
|
+
async listBranchRootHandler(ctx) {
|
|
181
|
+
await this.ready();
|
|
182
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
183
|
+
await this.ensureBranchExists(branch);
|
|
184
|
+
return this.listWithGitLsTree(branch, "", ctx.options);
|
|
173
185
|
}
|
|
174
186
|
/**
|
|
175
|
-
*
|
|
187
|
+
* List files in branch with subpath (matches /main/src, /main/src/foo, etc.)
|
|
176
188
|
*/
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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 || "");
|
|
189
|
+
async listBranchHandler(ctx) {
|
|
190
|
+
await this.ready();
|
|
191
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
192
|
+
await this.ensureBranchExists(branch);
|
|
193
|
+
const filePath = ctx.params.path;
|
|
194
|
+
return this.listWithGitLsTree(branch, filePath, ctx.options);
|
|
197
195
|
}
|
|
198
196
|
/**
|
|
199
|
-
*
|
|
197
|
+
* Read root metadata (introspection only, read-only)
|
|
200
198
|
*/
|
|
201
|
-
async
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return
|
|
199
|
+
async readRootMetaHandler(_ctx) {
|
|
200
|
+
await this.ready();
|
|
201
|
+
const branches = await this.getBranches();
|
|
202
|
+
return this.buildEntry("/.meta", { meta: {
|
|
203
|
+
childrenCount: branches.length,
|
|
204
|
+
type: "root"
|
|
205
|
+
} });
|
|
205
206
|
}
|
|
206
207
|
/**
|
|
207
|
-
*
|
|
208
|
+
* Read branch root metadata (introspection only, read-only)
|
|
208
209
|
*/
|
|
209
|
-
async
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
worktreePath,
|
|
222
|
-
branch
|
|
223
|
-
]);
|
|
224
|
-
}
|
|
225
|
-
this.worktrees.set(branch, worktreePath);
|
|
226
|
-
return worktreePath;
|
|
210
|
+
async readBranchMetaHandler(ctx) {
|
|
211
|
+
await this.ready();
|
|
212
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
213
|
+
await this.ensureBranchExists(branch);
|
|
214
|
+
const childrenCount = await this.getChildrenCount(branch, "");
|
|
215
|
+
const metaPath = `${`/${this.encodeBranchName(branch)}`}/.meta`;
|
|
216
|
+
const lastCommit = await this.getLastCommit(branch);
|
|
217
|
+
return this.buildEntry(metaPath, { meta: {
|
|
218
|
+
childrenCount,
|
|
219
|
+
type: "branch",
|
|
220
|
+
lastCommit
|
|
221
|
+
} });
|
|
227
222
|
}
|
|
228
223
|
/**
|
|
229
|
-
*
|
|
224
|
+
* Read file or directory metadata in branch (introspection only, read-only)
|
|
230
225
|
*/
|
|
231
|
-
async
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
}
|
|
226
|
+
async readPathMetaHandler(ctx) {
|
|
227
|
+
await this.ready();
|
|
228
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
229
|
+
await this.ensureBranchExists(branch);
|
|
230
|
+
const filePath = ctx.params.path;
|
|
231
|
+
const objectType = await this.git.raw([
|
|
232
|
+
"cat-file",
|
|
233
|
+
"-t",
|
|
234
|
+
`${branch}:${filePath}`
|
|
235
|
+
]).then((t) => t.trim()).catch(() => null);
|
|
236
|
+
if (objectType === null) throw new _aigne_afs.AFSNotFoundError(this.buildBranchPath(branch, filePath));
|
|
237
|
+
const isDir = objectType === "tree";
|
|
238
|
+
const metaPath = `/${this.encodeBranchName(branch)}/${filePath}/.meta`;
|
|
239
|
+
let childrenCount;
|
|
240
|
+
if (isDir) childrenCount = await this.getChildrenCount(branch, filePath);
|
|
241
|
+
return this.buildEntry(metaPath, { meta: {
|
|
242
|
+
childrenCount,
|
|
243
|
+
type: isDir ? "directory" : "file",
|
|
244
|
+
gitObjectType: objectType
|
|
245
|
+
} });
|
|
305
246
|
}
|
|
306
247
|
/**
|
|
307
|
-
*
|
|
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
|
|
248
|
+
* Read root
|
|
311
249
|
*/
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return
|
|
250
|
+
async readRootHandler(_ctx) {
|
|
251
|
+
await this.ready();
|
|
252
|
+
const branches = await this.getBranches();
|
|
253
|
+
return this.buildEntry("/", { meta: { childrenCount: branches.length } });
|
|
316
254
|
}
|
|
317
|
-
|
|
255
|
+
/**
|
|
256
|
+
* Read branch root
|
|
257
|
+
*/
|
|
258
|
+
async readBranchRootHandler(ctx) {
|
|
318
259
|
await this.ready();
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
entries.push({
|
|
329
|
-
id: encodedPath,
|
|
330
|
-
path: encodedPath,
|
|
331
|
-
metadata: { type: "directory" }
|
|
332
|
-
});
|
|
333
|
-
if (maxDepth > 1) {
|
|
334
|
-
const branchResult = await this.listWithGitLsTree(name, "", {
|
|
335
|
-
...options,
|
|
336
|
-
maxDepth: maxDepth - 1,
|
|
337
|
-
limit: limit - entries.length
|
|
338
|
-
});
|
|
339
|
-
entries.push(...branchResult.data);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
return { data: entries };
|
|
343
|
-
}
|
|
344
|
-
return this.listWithGitLsTree(branch, filePath, options);
|
|
260
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
261
|
+
await this.ensureBranchExists(branch);
|
|
262
|
+
const branchPath = this.buildBranchPath(branch);
|
|
263
|
+
const childrenCount = await this.getChildrenCount(branch, "");
|
|
264
|
+
const lastCommit = await this.getLastCommit(branch);
|
|
265
|
+
return this.buildEntry(branchPath, { meta: {
|
|
266
|
+
childrenCount,
|
|
267
|
+
lastCommit
|
|
268
|
+
} });
|
|
345
269
|
}
|
|
346
|
-
|
|
270
|
+
/**
|
|
271
|
+
* Read file or directory in branch
|
|
272
|
+
*/
|
|
273
|
+
async readBranchHandler(ctx) {
|
|
347
274
|
await this.ready();
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
metadata: { type: "directory" }
|
|
360
|
-
} };
|
|
361
|
-
}
|
|
362
|
-
try {
|
|
363
|
-
if (await this.git.raw([
|
|
364
|
-
"cat-file",
|
|
365
|
-
"-t",
|
|
366
|
-
`${branch}:${filePath}`
|
|
367
|
-
]).then((t) => t.trim()) === "tree") {
|
|
368
|
-
const afsPath$1 = this.buildPath(branch, filePath);
|
|
369
|
-
return { data: {
|
|
370
|
-
id: afsPath$1,
|
|
371
|
-
path: afsPath$1,
|
|
372
|
-
metadata: { type: "directory" }
|
|
373
|
-
} };
|
|
275
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
276
|
+
await this.ensureBranchExists(branch);
|
|
277
|
+
const filePath = ctx.params.path;
|
|
278
|
+
const worktreePath = this.worktrees.get(branch);
|
|
279
|
+
if (worktreePath) try {
|
|
280
|
+
const fullPath = (0, node_path.join)(worktreePath, filePath);
|
|
281
|
+
const stats = await (0, node_fs_promises.stat)(fullPath);
|
|
282
|
+
if (stats.isDirectory()) {
|
|
283
|
+
const files = await (0, node_fs_promises.readdir)(fullPath);
|
|
284
|
+
const afsPath$2 = this.buildBranchPath(branch, filePath);
|
|
285
|
+
return this.buildEntry(afsPath$2, { meta: { childrenCount: files.length } });
|
|
374
286
|
}
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const isBinary = this.isBinaryFile(filePath);
|
|
382
|
-
let content;
|
|
383
|
-
const metadata = {
|
|
384
|
-
type: "file",
|
|
385
|
-
size,
|
|
386
|
-
mimeType
|
|
387
|
-
};
|
|
388
|
-
if (isBinary) {
|
|
389
|
-
const { stdout } = await execFileAsync("git", [
|
|
390
|
-
"cat-file",
|
|
391
|
-
"-p",
|
|
392
|
-
`${branch}:${filePath}`
|
|
393
|
-
], {
|
|
394
|
-
cwd: this.options.repoPath,
|
|
395
|
-
encoding: "buffer",
|
|
396
|
-
maxBuffer: 10 * 1024 * 1024
|
|
397
|
-
});
|
|
398
|
-
content = stdout.toString("base64");
|
|
399
|
-
metadata.contentType = "base64";
|
|
400
|
-
} else content = await this.git.show([`${branch}:${filePath}`]);
|
|
401
|
-
const afsPath = this.buildPath(branch, filePath);
|
|
402
|
-
return { data: {
|
|
403
|
-
id: afsPath,
|
|
404
|
-
path: afsPath,
|
|
405
|
-
content,
|
|
406
|
-
metadata
|
|
407
|
-
} };
|
|
408
|
-
} catch (error) {
|
|
409
|
-
return {
|
|
410
|
-
data: void 0,
|
|
411
|
-
message: error.message
|
|
287
|
+
const mimeType$1 = this.getMimeType(filePath);
|
|
288
|
+
const isBinary$1 = this.isBinaryFile(filePath);
|
|
289
|
+
let content$1;
|
|
290
|
+
const meta$1 = {
|
|
291
|
+
size: stats.size,
|
|
292
|
+
mimeType: mimeType$1
|
|
412
293
|
};
|
|
294
|
+
if (isBinary$1) {
|
|
295
|
+
content$1 = (await (0, node_fs_promises.readFile)(fullPath)).toString("base64");
|
|
296
|
+
meta$1.contentType = "base64";
|
|
297
|
+
} else content$1 = await (0, node_fs_promises.readFile)(fullPath, "utf8");
|
|
298
|
+
const afsPath$1 = this.buildBranchPath(branch, filePath);
|
|
299
|
+
return this.buildEntry(afsPath$1, {
|
|
300
|
+
content: content$1,
|
|
301
|
+
meta: meta$1,
|
|
302
|
+
createdAt: stats.birthtime,
|
|
303
|
+
updatedAt: stats.mtime
|
|
304
|
+
});
|
|
305
|
+
} catch {}
|
|
306
|
+
const objectType = await this.git.raw([
|
|
307
|
+
"cat-file",
|
|
308
|
+
"-t",
|
|
309
|
+
`${branch}:${filePath}`
|
|
310
|
+
]).then((t) => t.trim()).catch(() => null);
|
|
311
|
+
if (objectType === null) throw new _aigne_afs.AFSNotFoundError(this.buildBranchPath(branch, filePath));
|
|
312
|
+
if (objectType === "tree") {
|
|
313
|
+
const afsPath$1 = this.buildBranchPath(branch, filePath);
|
|
314
|
+
const childrenCount = await this.getChildrenCount(branch, filePath);
|
|
315
|
+
return this.buildEntry(afsPath$1, { meta: { childrenCount } });
|
|
413
316
|
}
|
|
317
|
+
const size = await this.git.raw([
|
|
318
|
+
"cat-file",
|
|
319
|
+
"-s",
|
|
320
|
+
`${branch}:${filePath}`
|
|
321
|
+
]).then((s) => Number.parseInt(s.trim(), 10));
|
|
322
|
+
const mimeType = this.getMimeType(filePath);
|
|
323
|
+
const isBinary = this.isBinaryFile(filePath);
|
|
324
|
+
let content;
|
|
325
|
+
const meta = {
|
|
326
|
+
size,
|
|
327
|
+
mimeType
|
|
328
|
+
};
|
|
329
|
+
if (isBinary) {
|
|
330
|
+
const { stdout } = await execFileAsync("git", [
|
|
331
|
+
"cat-file",
|
|
332
|
+
"-p",
|
|
333
|
+
`${branch}:${filePath}`
|
|
334
|
+
], {
|
|
335
|
+
cwd: this.options.repoPath,
|
|
336
|
+
encoding: "buffer",
|
|
337
|
+
maxBuffer: 10 * 1024 * 1024
|
|
338
|
+
});
|
|
339
|
+
content = stdout.toString("base64");
|
|
340
|
+
meta.contentType = "base64";
|
|
341
|
+
} else content = await this.git.show([`${branch}:${filePath}`]);
|
|
342
|
+
const afsPath = this.buildBranchPath(branch, filePath);
|
|
343
|
+
return this.buildEntry(afsPath, {
|
|
344
|
+
content,
|
|
345
|
+
meta
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Write to root is not allowed
|
|
350
|
+
*/
|
|
351
|
+
async writeRootHandler() {
|
|
352
|
+
throw new Error("Cannot write to root");
|
|
414
353
|
}
|
|
415
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Write to branch root is not allowed
|
|
356
|
+
*/
|
|
357
|
+
async writeBranchRootHandler() {
|
|
358
|
+
throw new Error("Cannot write to branch root");
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Write file in branch
|
|
362
|
+
*/
|
|
363
|
+
async writeHandler(ctx, payload) {
|
|
416
364
|
await this.ready();
|
|
417
|
-
const
|
|
418
|
-
|
|
365
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
366
|
+
const filePath = ctx.params.path;
|
|
367
|
+
const append = ctx.options?.append ?? false;
|
|
419
368
|
const worktreePath = await this.ensureWorktree(branch);
|
|
420
369
|
const fullPath = (0, node_path.join)(worktreePath, filePath);
|
|
421
|
-
const append = options?.append ?? false;
|
|
422
370
|
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
|
|
423
|
-
if (
|
|
371
|
+
if (payload.content !== void 0) {
|
|
424
372
|
let contentToWrite;
|
|
425
|
-
if (typeof
|
|
426
|
-
else contentToWrite = JSON.stringify(
|
|
373
|
+
if (typeof payload.content === "string") contentToWrite = payload.content;
|
|
374
|
+
else contentToWrite = JSON.stringify(payload.content, null, 2);
|
|
427
375
|
await (0, node_fs_promises.writeFile)(fullPath, contentToWrite, {
|
|
428
376
|
encoding: "utf8",
|
|
429
377
|
flag: append ? "a" : "w"
|
|
@@ -439,32 +387,56 @@ var AFSGit = class AFSGit {
|
|
|
439
387
|
await gitInstance.commit(`Update ${filePath}`);
|
|
440
388
|
}
|
|
441
389
|
const stats = await (0, node_fs_promises.stat)(fullPath);
|
|
442
|
-
const afsPath = this.
|
|
390
|
+
const afsPath = this.buildBranchPath(branch, filePath);
|
|
443
391
|
return { data: {
|
|
444
392
|
id: afsPath,
|
|
445
393
|
path: afsPath,
|
|
446
|
-
content:
|
|
447
|
-
summary:
|
|
394
|
+
content: payload.content,
|
|
395
|
+
summary: payload.summary,
|
|
448
396
|
createdAt: stats.birthtime,
|
|
449
397
|
updatedAt: stats.mtime,
|
|
450
|
-
|
|
451
|
-
...
|
|
452
|
-
type: stats.isDirectory() ? "directory" : "file",
|
|
398
|
+
meta: {
|
|
399
|
+
...payload.meta,
|
|
453
400
|
size: stats.size
|
|
454
401
|
},
|
|
455
|
-
userId:
|
|
456
|
-
sessionId:
|
|
457
|
-
linkTo:
|
|
402
|
+
userId: payload.userId,
|
|
403
|
+
sessionId: payload.sessionId,
|
|
404
|
+
linkTo: payload.linkTo
|
|
458
405
|
} };
|
|
459
406
|
}
|
|
460
|
-
|
|
407
|
+
/**
|
|
408
|
+
* Delete root is not allowed
|
|
409
|
+
*/
|
|
410
|
+
async deleteRootHandler() {
|
|
411
|
+
throw new Error("Cannot delete root");
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Delete branch root is not allowed
|
|
415
|
+
*/
|
|
416
|
+
async deleteBranchRootHandler(ctx) {
|
|
417
|
+
await this.ready();
|
|
418
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
419
|
+
await this.ensureBranchExists(branch);
|
|
420
|
+
throw new Error("Cannot delete branch root");
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Delete file in branch
|
|
424
|
+
*/
|
|
425
|
+
async deleteHandler(ctx) {
|
|
461
426
|
await this.ready();
|
|
462
|
-
const
|
|
463
|
-
|
|
427
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
428
|
+
const filePath = ctx.params.path;
|
|
429
|
+
const recursive = ctx.options?.recursive ?? false;
|
|
464
430
|
const worktreePath = await this.ensureWorktree(branch);
|
|
465
431
|
const fullPath = (0, node_path.join)(worktreePath, filePath);
|
|
466
|
-
|
|
467
|
-
|
|
432
|
+
let stats;
|
|
433
|
+
try {
|
|
434
|
+
stats = await (0, node_fs_promises.stat)(fullPath);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
if (error.code === "ENOENT") throw new _aigne_afs.AFSNotFoundError(this.buildBranchPath(branch, filePath));
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
if (stats.isDirectory() && !recursive) throw new Error(`Cannot delete directory '/${ctx.params.branch}/${filePath}' without recursive option. Set recursive: true to delete directories.`);
|
|
468
440
|
await (0, node_fs_promises.rm)(fullPath, {
|
|
469
441
|
recursive,
|
|
470
442
|
force: true
|
|
@@ -478,20 +450,28 @@ var AFSGit = class AFSGit {
|
|
|
478
450
|
}
|
|
479
451
|
await gitInstance.commit(`Delete ${filePath}`);
|
|
480
452
|
}
|
|
481
|
-
return { message: `Successfully deleted:
|
|
453
|
+
return { message: `Successfully deleted: /${ctx.params.branch}/${filePath}` };
|
|
482
454
|
}
|
|
483
|
-
|
|
455
|
+
/**
|
|
456
|
+
* Rename file in branch
|
|
457
|
+
*/
|
|
458
|
+
async renameHandler(ctx, newPath) {
|
|
484
459
|
await this.ready();
|
|
485
|
-
const
|
|
460
|
+
const oldBranch = this.decodeBranchName(ctx.params.branch);
|
|
461
|
+
const oldFilePath = ctx.params.path;
|
|
486
462
|
const { branch: newBranch, filePath: newFilePath } = this.parsePath(newPath);
|
|
487
|
-
|
|
463
|
+
const overwrite = ctx.options?.overwrite ?? false;
|
|
488
464
|
if (!newBranch || !newFilePath) throw new Error("Cannot rename to root or branch root");
|
|
489
465
|
if (oldBranch !== newBranch) throw new Error("Cannot rename across branches");
|
|
490
466
|
const worktreePath = await this.ensureWorktree(oldBranch);
|
|
491
467
|
const oldFullPath = (0, node_path.join)(worktreePath, oldFilePath);
|
|
492
468
|
const newFullPath = (0, node_path.join)(worktreePath, newFilePath);
|
|
493
|
-
|
|
494
|
-
|
|
469
|
+
try {
|
|
470
|
+
await (0, node_fs_promises.stat)(oldFullPath);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
if (error.code === "ENOENT") throw new _aigne_afs.AFSNotFoundError(this.buildBranchPath(oldBranch, oldFilePath));
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
495
475
|
try {
|
|
496
476
|
await (0, node_fs_promises.stat)(newFullPath);
|
|
497
477
|
if (!overwrite) throw new Error(`Destination '${newPath}' already exists. Set overwrite: true to replace it.`);
|
|
@@ -509,15 +489,26 @@ var AFSGit = class AFSGit {
|
|
|
509
489
|
}
|
|
510
490
|
await gitInstance.commit(`Rename ${oldFilePath} to ${newFilePath}`);
|
|
511
491
|
}
|
|
512
|
-
return { message: `Successfully renamed '
|
|
492
|
+
return { message: `Successfully renamed '/${ctx.params.branch}/${oldFilePath}' to '${newPath}'` };
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Search files in branch root
|
|
496
|
+
*/
|
|
497
|
+
async searchBranchRootHandler(ctx, query, options) {
|
|
498
|
+
return this.searchInBranch(ctx.params.branch, "", query, options);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Search files in branch path
|
|
502
|
+
*/
|
|
503
|
+
async searchHandler(ctx, query, options) {
|
|
504
|
+
return this.searchInBranch(ctx.params.branch, ctx.params.path, query, options);
|
|
513
505
|
}
|
|
514
|
-
|
|
506
|
+
/**
|
|
507
|
+
* Internal search implementation
|
|
508
|
+
*/
|
|
509
|
+
async searchInBranch(encodedBranch, filePath, query, options) {
|
|
515
510
|
await this.ready();
|
|
516
|
-
const
|
|
517
|
-
if (!branch) return {
|
|
518
|
-
data: [],
|
|
519
|
-
message: "Search requires a branch path"
|
|
520
|
-
};
|
|
511
|
+
const branch = this.decodeBranchName(encodedBranch);
|
|
521
512
|
const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
|
|
522
513
|
try {
|
|
523
514
|
const args = [
|
|
@@ -547,15 +538,12 @@ var AFSGit = class AFSGit {
|
|
|
547
538
|
lineNum = matchNoBranch[2];
|
|
548
539
|
content = matchNoBranch[3];
|
|
549
540
|
}
|
|
550
|
-
const afsPath = this.
|
|
541
|
+
const afsPath = this.buildBranchPath(branch, matchPath);
|
|
551
542
|
if (processedFiles.has(afsPath)) continue;
|
|
552
543
|
processedFiles.add(afsPath);
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
summary: `Line ${lineNum}: ${content}`,
|
|
557
|
-
metadata: { type: "file" }
|
|
558
|
-
});
|
|
544
|
+
const entry = this.buildEntry(afsPath);
|
|
545
|
+
entry.summary = `Line ${lineNum}: ${content}`;
|
|
546
|
+
entries.push(entry);
|
|
559
547
|
if (entries.length >= limit) break;
|
|
560
548
|
}
|
|
561
549
|
return {
|
|
@@ -571,6 +559,867 @@ var AFSGit = class AFSGit {
|
|
|
571
559
|
}
|
|
572
560
|
}
|
|
573
561
|
/**
|
|
562
|
+
* Stat root
|
|
563
|
+
*/
|
|
564
|
+
async statRootHandler(_ctx) {
|
|
565
|
+
const entry = await this.readRootHandler(_ctx);
|
|
566
|
+
if (!entry) return { data: void 0 };
|
|
567
|
+
const { content: _content, ...rest } = entry;
|
|
568
|
+
return { data: rest };
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Stat branch root
|
|
572
|
+
*/
|
|
573
|
+
async statBranchRootHandler(ctx) {
|
|
574
|
+
const entry = await this.readBranchRootHandler(ctx);
|
|
575
|
+
if (!entry) return { data: void 0 };
|
|
576
|
+
const { content: _content, ...rest } = entry;
|
|
577
|
+
return { data: rest };
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Stat file or directory in branch
|
|
581
|
+
*/
|
|
582
|
+
async statHandler(ctx) {
|
|
583
|
+
const entry = await this.readBranchHandler(ctx);
|
|
584
|
+
if (!entry) return { data: void 0 };
|
|
585
|
+
const { content: _content, ...rest } = entry;
|
|
586
|
+
return { data: rest };
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Explain root → repo info, branch list, default branch
|
|
590
|
+
*/
|
|
591
|
+
async explainRootHandler(_ctx) {
|
|
592
|
+
await this.ready();
|
|
593
|
+
const format = _ctx.options?.format || "markdown";
|
|
594
|
+
const branches = await this.getBranches();
|
|
595
|
+
const currentBranch = await this.git.revparse(["--abbrev-ref", "HEAD"]).then((b) => b.trim());
|
|
596
|
+
let remoteUrl;
|
|
597
|
+
try {
|
|
598
|
+
remoteUrl = await this.git.remote(["get-url", "origin"]).then((u) => u?.trim());
|
|
599
|
+
} catch {}
|
|
600
|
+
const lines = [];
|
|
601
|
+
lines.push("# Git Repository");
|
|
602
|
+
lines.push("");
|
|
603
|
+
lines.push(`**Provider:** ${this.name}`);
|
|
604
|
+
if (this.description) lines.push(`**Description:** ${this.description}`);
|
|
605
|
+
lines.push(`**Default Branch:** ${currentBranch}`);
|
|
606
|
+
if (remoteUrl) lines.push(`**Remote:** ${remoteUrl}`);
|
|
607
|
+
lines.push(`**Branches:** ${branches.length}`);
|
|
608
|
+
lines.push("");
|
|
609
|
+
lines.push("## Branches");
|
|
610
|
+
lines.push("");
|
|
611
|
+
for (const branch of branches) lines.push(`- ${branch}`);
|
|
612
|
+
return {
|
|
613
|
+
content: lines.join("\n"),
|
|
614
|
+
format
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Explain branch → branch name, HEAD commit, file count
|
|
619
|
+
*/
|
|
620
|
+
async explainBranchHandler(ctx) {
|
|
621
|
+
await this.ready();
|
|
622
|
+
const format = ctx.options?.format || "markdown";
|
|
623
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
624
|
+
await this.ensureBranchExists(branch);
|
|
625
|
+
const lastCommit = await this.getLastCommit(branch);
|
|
626
|
+
const fileCount = await this.getTreeFileCount(branch, "");
|
|
627
|
+
const lines = [];
|
|
628
|
+
lines.push(`# Branch: ${branch}`);
|
|
629
|
+
lines.push("");
|
|
630
|
+
lines.push(`**HEAD Commit:** ${lastCommit.shortHash} - ${lastCommit.message}`);
|
|
631
|
+
lines.push(`**Author:** ${lastCommit.author}`);
|
|
632
|
+
lines.push(`**Date:** ${lastCommit.date}`);
|
|
633
|
+
lines.push(`**Files:** ${fileCount} entries in tree`);
|
|
634
|
+
return {
|
|
635
|
+
content: lines.join("\n"),
|
|
636
|
+
format
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Explain file or directory → path, size, last modified commit
|
|
641
|
+
*/
|
|
642
|
+
async explainPathHandler(ctx) {
|
|
643
|
+
await this.ready();
|
|
644
|
+
const format = ctx.options?.format || "markdown";
|
|
645
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
646
|
+
await this.ensureBranchExists(branch);
|
|
647
|
+
const filePath = ctx.params.path;
|
|
648
|
+
const objectType = await this.git.raw([
|
|
649
|
+
"cat-file",
|
|
650
|
+
"-t",
|
|
651
|
+
`${branch}:${filePath}`
|
|
652
|
+
]).then((t) => t.trim()).catch(() => null);
|
|
653
|
+
if (objectType === null) throw new _aigne_afs.AFSNotFoundError(this.buildBranchPath(branch, filePath));
|
|
654
|
+
const isDir = objectType === "tree";
|
|
655
|
+
const lines = [];
|
|
656
|
+
lines.push(`# ${(0, node_path.basename)(filePath)}`);
|
|
657
|
+
lines.push("");
|
|
658
|
+
lines.push(`**Path:** ${filePath}`);
|
|
659
|
+
lines.push(`**Type:** ${isDir ? "directory" : "file"}`);
|
|
660
|
+
if (!isDir) {
|
|
661
|
+
const size = await this.git.raw([
|
|
662
|
+
"cat-file",
|
|
663
|
+
"-s",
|
|
664
|
+
`${branch}:${filePath}`
|
|
665
|
+
]).then((s) => Number.parseInt(s.trim(), 10));
|
|
666
|
+
lines.push(`**Size:** ${size} bytes`);
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
669
|
+
const logLines = (await this.git.raw([
|
|
670
|
+
"log",
|
|
671
|
+
"-1",
|
|
672
|
+
"--format=%H%n%h%n%an%n%aI%n%s",
|
|
673
|
+
branch,
|
|
674
|
+
"--",
|
|
675
|
+
filePath
|
|
676
|
+
])).trim().split("\n");
|
|
677
|
+
if (logLines.length >= 5) {
|
|
678
|
+
lines.push("");
|
|
679
|
+
lines.push("## Last Modified");
|
|
680
|
+
lines.push(`**Commit:** ${logLines[1]} - ${logLines[4]}`);
|
|
681
|
+
lines.push(`**Author:** ${logLines[2]}`);
|
|
682
|
+
lines.push(`**Date:** ${logLines[3]}`);
|
|
683
|
+
}
|
|
684
|
+
} catch {}
|
|
685
|
+
return {
|
|
686
|
+
content: lines.join("\n"),
|
|
687
|
+
format
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
async readCapabilitiesHandler(_ctx) {
|
|
691
|
+
const operations = [
|
|
692
|
+
"list",
|
|
693
|
+
"read",
|
|
694
|
+
"stat",
|
|
695
|
+
"explain",
|
|
696
|
+
"search"
|
|
697
|
+
];
|
|
698
|
+
if (this.accessMode === "readwrite") operations.push("write", "delete", "rename");
|
|
699
|
+
const actionCatalogs = [];
|
|
700
|
+
if (this.accessMode === "readwrite") actionCatalogs.push({
|
|
701
|
+
description: "Git workflow actions",
|
|
702
|
+
catalog: [
|
|
703
|
+
{
|
|
704
|
+
name: "diff",
|
|
705
|
+
description: "Compare two branches or refs",
|
|
706
|
+
inputSchema: {
|
|
707
|
+
type: "object",
|
|
708
|
+
properties: {
|
|
709
|
+
from: {
|
|
710
|
+
type: "string",
|
|
711
|
+
description: "Source ref"
|
|
712
|
+
},
|
|
713
|
+
to: {
|
|
714
|
+
type: "string",
|
|
715
|
+
description: "Target ref"
|
|
716
|
+
},
|
|
717
|
+
path: {
|
|
718
|
+
type: "string",
|
|
719
|
+
description: "Optional path filter"
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
required: ["from", "to"]
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
name: "create-branch",
|
|
727
|
+
description: "Create a new branch",
|
|
728
|
+
inputSchema: {
|
|
729
|
+
type: "object",
|
|
730
|
+
properties: {
|
|
731
|
+
name: {
|
|
732
|
+
type: "string",
|
|
733
|
+
description: "New branch name"
|
|
734
|
+
},
|
|
735
|
+
from: {
|
|
736
|
+
type: "string",
|
|
737
|
+
description: "Source ref (defaults to current HEAD)"
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
required: ["name"]
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
name: "commit",
|
|
745
|
+
description: "Commit staged changes",
|
|
746
|
+
inputSchema: {
|
|
747
|
+
type: "object",
|
|
748
|
+
properties: {
|
|
749
|
+
message: {
|
|
750
|
+
type: "string",
|
|
751
|
+
description: "Commit message"
|
|
752
|
+
},
|
|
753
|
+
author: {
|
|
754
|
+
type: "object",
|
|
755
|
+
properties: {
|
|
756
|
+
name: { type: "string" },
|
|
757
|
+
email: { type: "string" }
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
},
|
|
761
|
+
required: ["message"]
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
name: "merge",
|
|
766
|
+
description: "Merge a branch into the current branch",
|
|
767
|
+
inputSchema: {
|
|
768
|
+
type: "object",
|
|
769
|
+
properties: {
|
|
770
|
+
branch: {
|
|
771
|
+
type: "string",
|
|
772
|
+
description: "Branch to merge"
|
|
773
|
+
},
|
|
774
|
+
message: {
|
|
775
|
+
type: "string",
|
|
776
|
+
description: "Custom merge message"
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
required: ["branch"]
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
],
|
|
783
|
+
discovery: {
|
|
784
|
+
pathTemplate: "/:branch/.actions",
|
|
785
|
+
note: "Git workflow actions (readwrite mode only)"
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
return {
|
|
789
|
+
id: "/.meta/.capabilities",
|
|
790
|
+
path: "/.meta/.capabilities",
|
|
791
|
+
content: {
|
|
792
|
+
schemaVersion: 1,
|
|
793
|
+
provider: this.name,
|
|
794
|
+
description: this.description || "Git repository provider",
|
|
795
|
+
tools: [],
|
|
796
|
+
actions: actionCatalogs,
|
|
797
|
+
operations: this.getOperationsDeclaration()
|
|
798
|
+
},
|
|
799
|
+
meta: {
|
|
800
|
+
kind: "afs:capabilities",
|
|
801
|
+
operations
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* List available actions for a branch
|
|
807
|
+
*/
|
|
808
|
+
async listBranchActions(ctx) {
|
|
809
|
+
if (this.accessMode !== "readwrite") return { data: [] };
|
|
810
|
+
const basePath = `/${ctx.params.branch}/.actions`;
|
|
811
|
+
return { data: [
|
|
812
|
+
{
|
|
813
|
+
id: "diff",
|
|
814
|
+
path: `${basePath}/diff`,
|
|
815
|
+
summary: "Compare two branches or refs",
|
|
816
|
+
meta: {
|
|
817
|
+
kind: "afs:executable",
|
|
818
|
+
kinds: ["afs:executable", "afs:node"],
|
|
819
|
+
inputSchema: {
|
|
820
|
+
type: "object",
|
|
821
|
+
properties: {
|
|
822
|
+
from: {
|
|
823
|
+
type: "string",
|
|
824
|
+
description: "Source ref"
|
|
825
|
+
},
|
|
826
|
+
to: {
|
|
827
|
+
type: "string",
|
|
828
|
+
description: "Target ref"
|
|
829
|
+
},
|
|
830
|
+
path: {
|
|
831
|
+
type: "string",
|
|
832
|
+
description: "Optional path filter"
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
required: ["from", "to"]
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
id: "create-branch",
|
|
841
|
+
path: `${basePath}/create-branch`,
|
|
842
|
+
summary: "Create a new branch from this ref",
|
|
843
|
+
meta: {
|
|
844
|
+
kind: "afs:executable",
|
|
845
|
+
kinds: ["afs:executable", "afs:node"],
|
|
846
|
+
inputSchema: {
|
|
847
|
+
type: "object",
|
|
848
|
+
properties: {
|
|
849
|
+
name: {
|
|
850
|
+
type: "string",
|
|
851
|
+
description: "New branch name"
|
|
852
|
+
},
|
|
853
|
+
from: {
|
|
854
|
+
type: "string",
|
|
855
|
+
description: "Source ref (defaults to current HEAD)"
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
required: ["name"]
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
id: "commit",
|
|
864
|
+
path: `${basePath}/commit`,
|
|
865
|
+
summary: "Commit staged changes",
|
|
866
|
+
meta: {
|
|
867
|
+
kind: "afs:executable",
|
|
868
|
+
kinds: ["afs:executable", "afs:node"],
|
|
869
|
+
inputSchema: {
|
|
870
|
+
type: "object",
|
|
871
|
+
properties: {
|
|
872
|
+
message: {
|
|
873
|
+
type: "string",
|
|
874
|
+
description: "Commit message"
|
|
875
|
+
},
|
|
876
|
+
author: {
|
|
877
|
+
type: "object",
|
|
878
|
+
properties: {
|
|
879
|
+
name: { type: "string" },
|
|
880
|
+
email: { type: "string" }
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
},
|
|
884
|
+
required: ["message"]
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
id: "merge",
|
|
890
|
+
path: `${basePath}/merge`,
|
|
891
|
+
summary: "Merge another branch into this branch",
|
|
892
|
+
meta: {
|
|
893
|
+
kind: "afs:executable",
|
|
894
|
+
kinds: ["afs:executable", "afs:node"],
|
|
895
|
+
inputSchema: {
|
|
896
|
+
type: "object",
|
|
897
|
+
properties: {
|
|
898
|
+
branch: {
|
|
899
|
+
type: "string",
|
|
900
|
+
description: "Branch to merge"
|
|
901
|
+
},
|
|
902
|
+
message: {
|
|
903
|
+
type: "string",
|
|
904
|
+
description: "Custom merge message"
|
|
905
|
+
}
|
|
906
|
+
},
|
|
907
|
+
required: ["branch"]
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
] };
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* diff action — compare two branches or refs
|
|
915
|
+
*/
|
|
916
|
+
async diffAction(_ctx, args) {
|
|
917
|
+
await this.ready();
|
|
918
|
+
const from = args.from;
|
|
919
|
+
const to = args.to;
|
|
920
|
+
const pathFilter = args.path;
|
|
921
|
+
if (!from || !to) return {
|
|
922
|
+
success: false,
|
|
923
|
+
error: {
|
|
924
|
+
code: "INVALID_ARGS",
|
|
925
|
+
message: "from and to are required"
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
try {
|
|
929
|
+
const diffArgs = [
|
|
930
|
+
"diff",
|
|
931
|
+
"--stat",
|
|
932
|
+
"--name-only",
|
|
933
|
+
`${from}...${to}`
|
|
934
|
+
];
|
|
935
|
+
if (pathFilter) diffArgs.push("--", pathFilter);
|
|
936
|
+
const files = (await this.git.raw(diffArgs)).trim().split("\n").filter((l) => l.trim()).map((path) => ({ path }));
|
|
937
|
+
const patchArgs = ["diff", `${from}...${to}`];
|
|
938
|
+
if (pathFilter) patchArgs.push("--", pathFilter);
|
|
939
|
+
return {
|
|
940
|
+
success: true,
|
|
941
|
+
data: {
|
|
942
|
+
from,
|
|
943
|
+
to,
|
|
944
|
+
files,
|
|
945
|
+
patch: await this.git.raw(patchArgs),
|
|
946
|
+
filesChanged: files.length
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
} catch (error) {
|
|
950
|
+
return {
|
|
951
|
+
success: false,
|
|
952
|
+
error: {
|
|
953
|
+
code: "DIFF_FAILED",
|
|
954
|
+
message: error.message.replace(this.repoPath, "<repo>")
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* create-branch action — create a new branch
|
|
961
|
+
*/
|
|
962
|
+
async createBranchAction(_ctx, args) {
|
|
963
|
+
await this.ready();
|
|
964
|
+
const name = args.name;
|
|
965
|
+
const from = args.from;
|
|
966
|
+
if (!name) return {
|
|
967
|
+
success: false,
|
|
968
|
+
error: {
|
|
969
|
+
code: "INVALID_ARGS",
|
|
970
|
+
message: "name is required"
|
|
971
|
+
}
|
|
972
|
+
};
|
|
973
|
+
if (name.includes("..")) return {
|
|
974
|
+
success: false,
|
|
975
|
+
error: {
|
|
976
|
+
code: "INVALID_NAME",
|
|
977
|
+
message: "Branch name contains invalid characters"
|
|
978
|
+
}
|
|
979
|
+
};
|
|
980
|
+
try {
|
|
981
|
+
if (from) await this.git.raw([
|
|
982
|
+
"branch",
|
|
983
|
+
name,
|
|
984
|
+
from
|
|
985
|
+
]);
|
|
986
|
+
else await this.git.raw(["branch", name]);
|
|
987
|
+
return {
|
|
988
|
+
success: true,
|
|
989
|
+
data: {
|
|
990
|
+
branch: name,
|
|
991
|
+
hash: await this.git.revparse([name]).then((h) => h.trim())
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
} catch (error) {
|
|
995
|
+
return {
|
|
996
|
+
success: false,
|
|
997
|
+
error: {
|
|
998
|
+
code: "CREATE_BRANCH_FAILED",
|
|
999
|
+
message: error.message.replace(this.repoPath, "<repo>")
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* commit action — commit staged changes
|
|
1006
|
+
*/
|
|
1007
|
+
async commitAction(_ctx, args) {
|
|
1008
|
+
await this.ready();
|
|
1009
|
+
const message = args.message;
|
|
1010
|
+
if (!message) return {
|
|
1011
|
+
success: false,
|
|
1012
|
+
error: {
|
|
1013
|
+
code: "INVALID_ARGS",
|
|
1014
|
+
message: "message is required"
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
const author = args.author;
|
|
1018
|
+
try {
|
|
1019
|
+
const git = (0, simple_git.simpleGit)(this.repoPath);
|
|
1020
|
+
const status = await git.status();
|
|
1021
|
+
if (status.staged.length === 0 && status.files.filter((f) => f.index !== " " && f.index !== "?").length === 0) return {
|
|
1022
|
+
success: false,
|
|
1023
|
+
error: {
|
|
1024
|
+
code: "NO_CHANGES",
|
|
1025
|
+
message: "No staged changes to commit"
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
if (author?.name) await git.addConfig("user.name", author.name, void 0, "local");
|
|
1029
|
+
if (author?.email) await git.addConfig("user.email", author.email, void 0, "local");
|
|
1030
|
+
const result = await git.commit(message);
|
|
1031
|
+
return {
|
|
1032
|
+
success: true,
|
|
1033
|
+
data: {
|
|
1034
|
+
hash: result.commit || "",
|
|
1035
|
+
message,
|
|
1036
|
+
filesChanged: result.summary.changes
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
return {
|
|
1041
|
+
success: false,
|
|
1042
|
+
error: {
|
|
1043
|
+
code: "COMMIT_FAILED",
|
|
1044
|
+
message: error.message.replace(this.repoPath, "<repo>")
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* merge action — merge a branch into current branch
|
|
1051
|
+
*/
|
|
1052
|
+
async mergeAction(_ctx, args) {
|
|
1053
|
+
await this.ready();
|
|
1054
|
+
const branch = args.branch;
|
|
1055
|
+
if (!branch) return {
|
|
1056
|
+
success: false,
|
|
1057
|
+
error: {
|
|
1058
|
+
code: "INVALID_ARGS",
|
|
1059
|
+
message: "branch is required"
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
const customMessage = args.message;
|
|
1063
|
+
try {
|
|
1064
|
+
const git = (0, simple_git.simpleGit)(this.repoPath);
|
|
1065
|
+
if (!(await git.branchLocal()).all.includes(branch)) return {
|
|
1066
|
+
success: false,
|
|
1067
|
+
error: {
|
|
1068
|
+
code: "BRANCH_NOT_FOUND",
|
|
1069
|
+
message: `Branch '${branch}' not found`
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
const mergeArgs = [branch];
|
|
1073
|
+
if (customMessage) mergeArgs.push("-m", customMessage);
|
|
1074
|
+
const result = await git.merge(mergeArgs);
|
|
1075
|
+
return {
|
|
1076
|
+
success: true,
|
|
1077
|
+
data: {
|
|
1078
|
+
hash: await git.revparse(["HEAD"]).then((h) => h.trim()),
|
|
1079
|
+
merged: branch,
|
|
1080
|
+
conflicts: result.conflicts || []
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
try {
|
|
1085
|
+
await (0, simple_git.simpleGit)(this.repoPath).merge(["--abort"]);
|
|
1086
|
+
} catch {}
|
|
1087
|
+
return {
|
|
1088
|
+
success: false,
|
|
1089
|
+
error: {
|
|
1090
|
+
code: "MERGE_FAILED",
|
|
1091
|
+
message: error.message.replace(this.repoPath, "<repo>")
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* List .log/ → commit list with pagination
|
|
1098
|
+
*/
|
|
1099
|
+
async listLogHandler(ctx) {
|
|
1100
|
+
await this.ready();
|
|
1101
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
1102
|
+
await this.ensureBranchExists(branch);
|
|
1103
|
+
const options = ctx.options;
|
|
1104
|
+
const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
|
|
1105
|
+
const offset = options?.offset || 0;
|
|
1106
|
+
const commits = await this.getCommitList(branch, limit, offset);
|
|
1107
|
+
const branchEncoded = this.encodeBranchName(branch);
|
|
1108
|
+
return { data: commits.map((commit, i) => this.buildEntry(`/${branchEncoded}/.log/${offset + i}`, { meta: {
|
|
1109
|
+
hash: commit.hash,
|
|
1110
|
+
shortHash: commit.shortHash,
|
|
1111
|
+
author: commit.author,
|
|
1112
|
+
date: commit.date,
|
|
1113
|
+
message: commit.message
|
|
1114
|
+
} })) };
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Read .log/{index} → commit diff/patch content
|
|
1118
|
+
*/
|
|
1119
|
+
async readLogEntryHandler(ctx) {
|
|
1120
|
+
await this.ready();
|
|
1121
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
1122
|
+
await this.ensureBranchExists(branch);
|
|
1123
|
+
const index = Number.parseInt(ctx.params.index, 10);
|
|
1124
|
+
if (Number.isNaN(index) || index < 0) throw new _aigne_afs.AFSNotFoundError(`/${this.encodeBranchName(branch)}/.log/${ctx.params.index}`);
|
|
1125
|
+
const commits = await this.getCommitList(branch, 1, index);
|
|
1126
|
+
if (commits.length === 0) throw new _aigne_afs.AFSNotFoundError(`/${this.encodeBranchName(branch)}/.log/${index}`);
|
|
1127
|
+
const commit = commits[0];
|
|
1128
|
+
let diff;
|
|
1129
|
+
try {
|
|
1130
|
+
diff = await this.git.raw([
|
|
1131
|
+
"show",
|
|
1132
|
+
"--stat",
|
|
1133
|
+
"--patch",
|
|
1134
|
+
commit.hash
|
|
1135
|
+
]);
|
|
1136
|
+
} catch {
|
|
1137
|
+
diff = "";
|
|
1138
|
+
}
|
|
1139
|
+
const branchEncoded = this.encodeBranchName(branch);
|
|
1140
|
+
return this.buildEntry(`/${branchEncoded}/.log/${index}`, {
|
|
1141
|
+
content: diff,
|
|
1142
|
+
meta: {
|
|
1143
|
+
hash: commit.hash,
|
|
1144
|
+
shortHash: commit.shortHash,
|
|
1145
|
+
author: commit.author,
|
|
1146
|
+
date: commit.date,
|
|
1147
|
+
message: commit.message
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* Read .log/{index}/.meta → commit metadata only (no diff)
|
|
1153
|
+
*/
|
|
1154
|
+
async readLogEntryMetaHandler(ctx) {
|
|
1155
|
+
await this.ready();
|
|
1156
|
+
const branch = this.decodeBranchName(ctx.params.branch);
|
|
1157
|
+
await this.ensureBranchExists(branch);
|
|
1158
|
+
const index = Number.parseInt(ctx.params.index, 10);
|
|
1159
|
+
if (Number.isNaN(index) || index < 0) throw new _aigne_afs.AFSNotFoundError(`/${this.encodeBranchName(branch)}/.log/${ctx.params.index}/.meta`);
|
|
1160
|
+
const commits = await this.getCommitList(branch, 1, index);
|
|
1161
|
+
if (commits.length === 0) throw new _aigne_afs.AFSNotFoundError(`/${this.encodeBranchName(branch)}/.log/${index}/.meta`);
|
|
1162
|
+
const commit = commits[0];
|
|
1163
|
+
const branchEncoded = this.encodeBranchName(branch);
|
|
1164
|
+
return this.buildEntry(`/${branchEncoded}/.log/${index}/.meta`, { meta: {
|
|
1165
|
+
hash: commit.hash,
|
|
1166
|
+
shortHash: commit.shortHash,
|
|
1167
|
+
author: commit.author,
|
|
1168
|
+
date: commit.date,
|
|
1169
|
+
message: commit.message
|
|
1170
|
+
} });
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Decode branch name (replace ~ with /)
|
|
1174
|
+
*/
|
|
1175
|
+
decodeBranchName(encoded) {
|
|
1176
|
+
return encoded.replace(/~/g, "/");
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Encode branch name (replace / with ~)
|
|
1180
|
+
*/
|
|
1181
|
+
encodeBranchName(branch) {
|
|
1182
|
+
return branch.replace(/\//g, "~");
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Parse AFS path into branch and file path
|
|
1186
|
+
* Branch names may contain slashes and are encoded with ~ in paths
|
|
1187
|
+
*/
|
|
1188
|
+
parsePath(path) {
|
|
1189
|
+
const segments = (0, node_path.join)("/", path).split("/").filter(Boolean);
|
|
1190
|
+
if (segments.length === 0) return {
|
|
1191
|
+
branch: void 0,
|
|
1192
|
+
filePath: ""
|
|
1193
|
+
};
|
|
1194
|
+
return {
|
|
1195
|
+
branch: segments[0].replace(/~/g, "/"),
|
|
1196
|
+
filePath: segments.slice(1).join("/")
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Build AFS path with encoded branch name
|
|
1201
|
+
* Branch names with slashes are encoded by replacing / with ~
|
|
1202
|
+
*/
|
|
1203
|
+
buildBranchPath(branch, filePath) {
|
|
1204
|
+
const encodedBranch = this.encodeBranchName(branch);
|
|
1205
|
+
if (!filePath) return `/${encodedBranch}`;
|
|
1206
|
+
return `/${encodedBranch}/${filePath}`;
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Get list of available branches
|
|
1210
|
+
*/
|
|
1211
|
+
async getBranches() {
|
|
1212
|
+
const allBranches = (await this.git.branchLocal()).all;
|
|
1213
|
+
if (this.options.branches && this.options.branches.length > 0) return allBranches.filter((branch) => this.options.branches.includes(branch));
|
|
1214
|
+
return allBranches;
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Check if a branch exists, throw AFSNotFoundError if not
|
|
1218
|
+
*/
|
|
1219
|
+
async ensureBranchExists(branch) {
|
|
1220
|
+
if (!(await this.getBranches()).includes(branch)) throw new _aigne_afs.AFSNotFoundError(this.buildBranchPath(branch));
|
|
1221
|
+
}
|
|
1222
|
+
/**
|
|
1223
|
+
* Get the number of children for a tree (directory) in git
|
|
1224
|
+
*/
|
|
1225
|
+
async getChildrenCount(branch, path) {
|
|
1226
|
+
try {
|
|
1227
|
+
const treeish = path ? `${branch}:${path}` : branch;
|
|
1228
|
+
return (await this.git.raw(["ls-tree", treeish])).split("\n").filter((line) => line.trim()).length;
|
|
1229
|
+
} catch {
|
|
1230
|
+
return 0;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Get the last commit on a branch
|
|
1235
|
+
*/
|
|
1236
|
+
async getLastCommit(branch) {
|
|
1237
|
+
const lines = (await this.git.raw([
|
|
1238
|
+
"log",
|
|
1239
|
+
"-1",
|
|
1240
|
+
"--format=%H%n%h%n%an%n%aI%n%s",
|
|
1241
|
+
branch
|
|
1242
|
+
])).trim().split("\n");
|
|
1243
|
+
return {
|
|
1244
|
+
hash: lines[0] || "",
|
|
1245
|
+
shortHash: lines[1] || "",
|
|
1246
|
+
author: lines[2] || "",
|
|
1247
|
+
date: lines[3] || "",
|
|
1248
|
+
message: lines[4] || ""
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Count total files in a tree (recursively)
|
|
1253
|
+
*/
|
|
1254
|
+
async getTreeFileCount(branch, path) {
|
|
1255
|
+
try {
|
|
1256
|
+
const treeish = path ? `${branch}:${path}` : branch;
|
|
1257
|
+
return (await this.git.raw([
|
|
1258
|
+
"ls-tree",
|
|
1259
|
+
"-r",
|
|
1260
|
+
treeish
|
|
1261
|
+
])).split("\n").filter((line) => line.trim()).length;
|
|
1262
|
+
} catch {
|
|
1263
|
+
return 0;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Get a list of commits on a branch with limit/offset
|
|
1268
|
+
*/
|
|
1269
|
+
async getCommitList(branch, limit, offset) {
|
|
1270
|
+
try {
|
|
1271
|
+
const args = [
|
|
1272
|
+
"log",
|
|
1273
|
+
`--skip=${offset}`,
|
|
1274
|
+
`-${limit}`,
|
|
1275
|
+
"--format=%H%n%h%n%an%n%aI%n%s%n---COMMIT_SEP---",
|
|
1276
|
+
branch
|
|
1277
|
+
];
|
|
1278
|
+
return (await this.git.raw(args)).split("---COMMIT_SEP---").filter((b) => b.trim()).map((block) => {
|
|
1279
|
+
const lines = block.trim().split("\n");
|
|
1280
|
+
return {
|
|
1281
|
+
hash: lines[0] || "",
|
|
1282
|
+
shortHash: lines[1] || "",
|
|
1283
|
+
author: lines[2] || "",
|
|
1284
|
+
date: lines[3] || "",
|
|
1285
|
+
message: lines[4] || ""
|
|
1286
|
+
};
|
|
1287
|
+
});
|
|
1288
|
+
} catch {
|
|
1289
|
+
return [];
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Ensure worktree exists for a branch (lazy creation)
|
|
1294
|
+
*/
|
|
1295
|
+
async ensureWorktree(branch) {
|
|
1296
|
+
if (this.worktrees.has(branch)) return this.worktrees.get(branch);
|
|
1297
|
+
if ((await this.git.revparse(["--abbrev-ref", "HEAD"])).trim() === branch) {
|
|
1298
|
+
this.worktrees.set(branch, this.repoPath);
|
|
1299
|
+
return this.repoPath;
|
|
1300
|
+
}
|
|
1301
|
+
const worktreePath = (0, node_path.join)(this.tempBase, branch);
|
|
1302
|
+
if (!await (0, node_fs_promises.stat)(worktreePath).then(() => true).catch(() => false)) {
|
|
1303
|
+
await (0, node_fs_promises.mkdir)(this.tempBase, { recursive: true });
|
|
1304
|
+
await this.git.raw([
|
|
1305
|
+
"worktree",
|
|
1306
|
+
"add",
|
|
1307
|
+
worktreePath,
|
|
1308
|
+
branch
|
|
1309
|
+
]);
|
|
1310
|
+
}
|
|
1311
|
+
this.worktrees.set(branch, worktreePath);
|
|
1312
|
+
return worktreePath;
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* List files using git ls-tree (no worktree needed)
|
|
1316
|
+
* Note: list() returns only children, never the path itself (per new semantics)
|
|
1317
|
+
*/
|
|
1318
|
+
async listWithGitLsTree(branch, path, options) {
|
|
1319
|
+
const maxDepth = options?.maxDepth ?? 1;
|
|
1320
|
+
const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
|
|
1321
|
+
const entries = [];
|
|
1322
|
+
const targetPath = path || "";
|
|
1323
|
+
const treeish = targetPath ? `${branch}:${targetPath}` : branch;
|
|
1324
|
+
try {
|
|
1325
|
+
const pathType = await this.git.raw([
|
|
1326
|
+
"cat-file",
|
|
1327
|
+
"-t",
|
|
1328
|
+
treeish
|
|
1329
|
+
]).then((t) => t.trim()).catch(() => null);
|
|
1330
|
+
if (pathType === null) throw new _aigne_afs.AFSNotFoundError(this.buildBranchPath(branch, path));
|
|
1331
|
+
if (pathType === "blob") return { data: [] };
|
|
1332
|
+
if (maxDepth === 0) return { data: [] };
|
|
1333
|
+
const queue = [{
|
|
1334
|
+
path: targetPath,
|
|
1335
|
+
depth: 0
|
|
1336
|
+
}];
|
|
1337
|
+
while (queue.length > 0) {
|
|
1338
|
+
const { path: itemPath, depth } = queue.shift();
|
|
1339
|
+
const itemTreeish = itemPath ? `${branch}:${itemPath}` : branch;
|
|
1340
|
+
const lines = (await this.git.raw([
|
|
1341
|
+
"ls-tree",
|
|
1342
|
+
"-l",
|
|
1343
|
+
itemTreeish
|
|
1344
|
+
])).split("\n").filter((line) => line.trim()).slice(0, limit - entries.length);
|
|
1345
|
+
for (const line of lines) {
|
|
1346
|
+
const match = line.match(/^(\d+)\s+(blob|tree)\s+(\w+)\s+(-|\d+)\s+(.+)$/);
|
|
1347
|
+
if (!match) continue;
|
|
1348
|
+
const type = match[2];
|
|
1349
|
+
const sizeStr = match[4];
|
|
1350
|
+
const name = match[5];
|
|
1351
|
+
const isDirectory = type === "tree";
|
|
1352
|
+
const size = sizeStr === "-" ? void 0 : Number.parseInt(sizeStr, 10);
|
|
1353
|
+
const fullPath = itemPath ? `${itemPath}/${name}` : name;
|
|
1354
|
+
const afsPath = this.buildBranchPath(branch, fullPath);
|
|
1355
|
+
const childrenCount = isDirectory ? await this.getChildrenCount(branch, fullPath) : void 0;
|
|
1356
|
+
entries.push(this.buildEntry(afsPath, { meta: {
|
|
1357
|
+
kind: isDirectory ? "git:directory" : "git:file",
|
|
1358
|
+
size,
|
|
1359
|
+
childrenCount
|
|
1360
|
+
} }));
|
|
1361
|
+
if (isDirectory && depth + 1 < maxDepth) queue.push({
|
|
1362
|
+
path: fullPath,
|
|
1363
|
+
depth: depth + 1
|
|
1364
|
+
});
|
|
1365
|
+
if (entries.length >= limit) return { data: entries };
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return { data: entries };
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
if (error instanceof _aigne_afs.AFSNotFoundError) throw error;
|
|
1371
|
+
throw new Error(`Failed to list: ${error.message}`);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Detect MIME type based on file extension
|
|
1376
|
+
*/
|
|
1377
|
+
getMimeType(filePath) {
|
|
1378
|
+
return {
|
|
1379
|
+
png: "image/png",
|
|
1380
|
+
jpg: "image/jpeg",
|
|
1381
|
+
jpeg: "image/jpeg",
|
|
1382
|
+
gif: "image/gif",
|
|
1383
|
+
bmp: "image/bmp",
|
|
1384
|
+
webp: "image/webp",
|
|
1385
|
+
svg: "image/svg+xml",
|
|
1386
|
+
ico: "image/x-icon",
|
|
1387
|
+
pdf: "application/pdf",
|
|
1388
|
+
txt: "text/plain",
|
|
1389
|
+
md: "text/markdown",
|
|
1390
|
+
js: "text/javascript",
|
|
1391
|
+
ts: "text/typescript",
|
|
1392
|
+
json: "application/json",
|
|
1393
|
+
html: "text/html",
|
|
1394
|
+
css: "text/css",
|
|
1395
|
+
xml: "text/xml"
|
|
1396
|
+
}[filePath.split(".").pop()?.toLowerCase() || ""] || "application/octet-stream";
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Check if file is likely binary based on extension
|
|
1400
|
+
*/
|
|
1401
|
+
isBinaryFile(filePath) {
|
|
1402
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
1403
|
+
return [
|
|
1404
|
+
"png",
|
|
1405
|
+
"jpg",
|
|
1406
|
+
"jpeg",
|
|
1407
|
+
"gif",
|
|
1408
|
+
"bmp",
|
|
1409
|
+
"webp",
|
|
1410
|
+
"ico",
|
|
1411
|
+
"pdf",
|
|
1412
|
+
"zip",
|
|
1413
|
+
"tar",
|
|
1414
|
+
"gz",
|
|
1415
|
+
"exe",
|
|
1416
|
+
"dll",
|
|
1417
|
+
"so",
|
|
1418
|
+
"dylib",
|
|
1419
|
+
"wasm"
|
|
1420
|
+
].includes(ext || "");
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
574
1423
|
* Fetch latest changes from remote
|
|
575
1424
|
*/
|
|
576
1425
|
async fetch() {
|
|
@@ -621,6 +1470,41 @@ var AFSGit = class AFSGit {
|
|
|
621
1470
|
} catch {}
|
|
622
1471
|
}
|
|
623
1472
|
};
|
|
1473
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.List)("/", { handleDepth: true })], AFSGit.prototype, "listRootHandler", null);
|
|
1474
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.List)("/:branch", { handleDepth: true })], AFSGit.prototype, "listBranchRootHandler", null);
|
|
1475
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.List)("/:branch/:path+", { handleDepth: true })], AFSGit.prototype, "listBranchHandler", null);
|
|
1476
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Meta)("/")], AFSGit.prototype, "readRootMetaHandler", null);
|
|
1477
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Meta)("/:branch")], AFSGit.prototype, "readBranchMetaHandler", null);
|
|
1478
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Meta)("/:branch/:path+")], AFSGit.prototype, "readPathMetaHandler", null);
|
|
1479
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Read)("/")], AFSGit.prototype, "readRootHandler", null);
|
|
1480
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Read)("/:branch")], AFSGit.prototype, "readBranchRootHandler", null);
|
|
1481
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Read)("/:branch/:path+")], AFSGit.prototype, "readBranchHandler", null);
|
|
1482
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Write)("/")], AFSGit.prototype, "writeRootHandler", null);
|
|
1483
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Write)("/:branch")], AFSGit.prototype, "writeBranchRootHandler", null);
|
|
1484
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Write)("/:branch/:path+")], AFSGit.prototype, "writeHandler", null);
|
|
1485
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Delete)("/")], AFSGit.prototype, "deleteRootHandler", null);
|
|
1486
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Delete)("/:branch")], AFSGit.prototype, "deleteBranchRootHandler", null);
|
|
1487
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Delete)("/:branch/:path+")], AFSGit.prototype, "deleteHandler", null);
|
|
1488
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Rename)("/:branch/:path+")], AFSGit.prototype, "renameHandler", null);
|
|
1489
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Search)("/:branch")], AFSGit.prototype, "searchBranchRootHandler", null);
|
|
1490
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Search)("/:branch/:path+")], AFSGit.prototype, "searchHandler", null);
|
|
1491
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Stat)("/")], AFSGit.prototype, "statRootHandler", null);
|
|
1492
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Stat)("/:branch")], AFSGit.prototype, "statBranchRootHandler", null);
|
|
1493
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Stat)("/:branch/:path+")], AFSGit.prototype, "statHandler", null);
|
|
1494
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Explain)("/")], AFSGit.prototype, "explainRootHandler", null);
|
|
1495
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Explain)("/:branch")], AFSGit.prototype, "explainBranchHandler", null);
|
|
1496
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Explain)("/:branch/:path+")], AFSGit.prototype, "explainPathHandler", null);
|
|
1497
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Read)("/.meta/.capabilities")], AFSGit.prototype, "readCapabilitiesHandler", null);
|
|
1498
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Actions)("/:branch")], AFSGit.prototype, "listBranchActions", null);
|
|
1499
|
+
require_decorate.__decorate([_aigne_afs_provider.Actions.Exec("/:branch", "diff")], AFSGit.prototype, "diffAction", null);
|
|
1500
|
+
require_decorate.__decorate([_aigne_afs_provider.Actions.Exec("/:branch", "create-branch")], AFSGit.prototype, "createBranchAction", null);
|
|
1501
|
+
require_decorate.__decorate([_aigne_afs_provider.Actions.Exec("/:branch", "commit")], AFSGit.prototype, "commitAction", null);
|
|
1502
|
+
require_decorate.__decorate([_aigne_afs_provider.Actions.Exec("/:branch", "merge")], AFSGit.prototype, "mergeAction", null);
|
|
1503
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.List)("/:branch/.log")], AFSGit.prototype, "listLogHandler", null);
|
|
1504
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Read)("/:branch/.log/:index")], AFSGit.prototype, "readLogEntryHandler", null);
|
|
1505
|
+
require_decorate.__decorate([(0, _aigne_afs_provider.Read)("/:branch/.log/:index/.meta")], AFSGit.prototype, "readLogEntryMetaHandler", null);
|
|
1506
|
+
var src_default = AFSGit;
|
|
624
1507
|
|
|
625
1508
|
//#endregion
|
|
626
|
-
exports.AFSGit = AFSGit;
|
|
1509
|
+
exports.AFSGit = AFSGit;
|
|
1510
|
+
exports.default = src_default;
|