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

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