@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/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({ filepath, parsed }) {
41
+ static async load({ basePath, config } = {}) {
38
42
  const instance = new AFSGit({
39
- ...await AFSGit.schema().parseAsync(parsed),
40
- cwd: (0, node_path.dirname)(filepath)
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
- if (!await (0, node_fs_promises.stat)(targetPath).then(() => true).catch(() => false)) {
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
- * 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" }
148
+ * List root (branches)
149
+ * Note: list() returns only children (branches), never the path itself (per new semantics)
138
150
  */
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
- };
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
- * Detect MIME type based on file extension
178
+ * List branch root (matches /main, /develop, etc.)
152
179
  */
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";
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
- * Check if file is likely binary based on extension
187
+ * List files in branch with subpath (matches /main/src, /main/src/foo, etc.)
176
188
  */
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 || "");
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
- * Get list of available branches
197
+ * Read root metadata (introspection only, read-only)
200
198
  */
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;
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
- * Ensure worktree exists for a branch (lazy creation)
208
+ * Read branch root metadata (introspection only, read-only)
208
209
  */
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;
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
- * List files using git ls-tree (no worktree needed)
224
+ * Read file or directory metadata in branch (introspection only, read-only)
230
225
  */
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
- }
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
- * 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
248
+ * Read root
311
249
  */
312
- buildPath(branch, filePath) {
313
- const encodedBranch = branch.replace(/\//g, "~");
314
- if (!filePath) return `/${encodedBranch}`;
315
- return `/${encodedBranch}/${filePath}`;
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
- async list(path, options) {
255
+ /**
256
+ * Read branch root
257
+ */
258
+ async readBranchRootHandler(ctx) {
318
259
  await this.ready();
319
- const { branch, filePath } = this.parsePath(path);
320
- const maxDepth = options?.maxDepth ?? 1;
321
- const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
322
- if (!branch) {
323
- const branches = await this.getBranches();
324
- const entries = [];
325
- for (const name of branches) {
326
- if (entries.length >= limit) break;
327
- const encodedPath = this.buildPath(name);
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
- async read(path, _options) {
270
+ /**
271
+ * Read file or directory in branch
272
+ */
273
+ async readBranchHandler(ctx) {
347
274
  await this.ready();
348
- const { branch, filePath } = this.parsePath(path);
349
- if (!branch) return { data: {
350
- id: "/",
351
- path: "/",
352
- metadata: { type: "directory" }
353
- } };
354
- if (!filePath) {
355
- const branchPath = this.buildPath(branch);
356
- return { data: {
357
- id: branchPath,
358
- path: branchPath,
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 size = await this.git.raw([
376
- "cat-file",
377
- "-s",
378
- `${branch}:${filePath}`
379
- ]).then((s) => Number.parseInt(s.trim(), 10));
380
- const mimeType = this.getMimeType(filePath);
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
- async write(path, entry, options) {
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 { branch, filePath } = this.parsePath(path);
418
- if (!branch || !filePath) throw new Error("Cannot write to root or branch root");
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 (entry.content !== void 0) {
371
+ if (payload.content !== void 0) {
424
372
  let contentToWrite;
425
- if (typeof entry.content === "string") contentToWrite = entry.content;
426
- else contentToWrite = JSON.stringify(entry.content, null, 2);
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.buildPath(branch, filePath);
390
+ const afsPath = this.buildBranchPath(branch, filePath);
443
391
  return { data: {
444
392
  id: afsPath,
445
393
  path: afsPath,
446
- content: entry.content,
447
- summary: entry.summary,
394
+ content: payload.content,
395
+ summary: payload.summary,
448
396
  createdAt: stats.birthtime,
449
397
  updatedAt: stats.mtime,
450
- metadata: {
451
- ...entry.metadata,
452
- type: stats.isDirectory() ? "directory" : "file",
398
+ meta: {
399
+ ...payload.meta,
453
400
  size: stats.size
454
401
  },
455
- userId: entry.userId,
456
- sessionId: entry.sessionId,
457
- linkTo: entry.linkTo
402
+ userId: payload.userId,
403
+ sessionId: payload.sessionId,
404
+ linkTo: payload.linkTo
458
405
  } };
459
406
  }
460
- async delete(path, options) {
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 { branch, filePath } = this.parsePath(path);
463
- if (!branch || !filePath) throw new Error("Cannot delete root or branch root");
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
- const recursive = options?.recursive ?? false;
467
- 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.`);
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: ${path}` };
453
+ return { message: `Successfully deleted: /${ctx.params.branch}/${filePath}` };
482
454
  }
483
- async rename(oldPath, newPath, options) {
455
+ /**
456
+ * Rename file in branch
457
+ */
458
+ async renameHandler(ctx, newPath) {
484
459
  await this.ready();
485
- const { branch: oldBranch, filePath: oldFilePath } = this.parsePath(oldPath);
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
- if (!oldBranch || !oldFilePath) throw new Error("Cannot rename from root or branch root");
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
- const overwrite = options?.overwrite ?? false;
494
- await (0, node_fs_promises.stat)(oldFullPath);
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 '${oldPath}' to '${newPath}'` };
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
- async search(path, query, options) {
506
+ /**
507
+ * Internal search implementation
508
+ */
509
+ async searchInBranch(encodedBranch, filePath, query, options) {
515
510
  await this.ready();
516
- const { branch, filePath } = this.parsePath(path);
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.buildPath(branch, matchPath);
541
+ const afsPath = this.buildBranchPath(branch, matchPath);
551
542
  if (processedFiles.has(afsPath)) continue;
552
543
  processedFiles.add(afsPath);
553
- entries.push({
554
- id: afsPath,
555
- path: afsPath,
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;