@aigne/afs-git 1.11.0-beta → 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,19 +1,24 @@
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");
10
14
 
11
15
  //#region src/index.ts
12
- const execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
13
16
  const LIST_MAX_LIMIT = 1e3;
17
+ const execFileAsync = (0, node_util.promisify)(node_child_process.execFile);
14
18
  const afsGitOptionsSchema = (0, _aigne_afs_utils_zod.camelize)(zod.z.object({
15
19
  name: (0, _aigne_afs_utils_zod.optionalize)(zod.z.string()),
16
- repoPath: zod.z.string().describe("The path to the git repository"),
20
+ repoPath: (0, _aigne_afs_utils_zod.optionalize)(zod.z.string().describe("The path to the git repository")),
21
+ remoteUrl: (0, _aigne_afs_utils_zod.optionalize)(zod.z.string().describe("Remote repository URL (https or git protocol)")),
17
22
  description: (0, _aigne_afs_utils_zod.optionalize)(zod.z.string().describe("A description of the repository")),
18
23
  branches: (0, _aigne_afs_utils_zod.optionalize)(zod.z.array(zod.z.string()).describe("List of branches to expose")),
19
24
  accessMode: (0, _aigne_afs_utils_zod.optionalize)(zod.z.enum(["readonly", "readwrite"]).describe("Access mode for this module")),
@@ -21,317 +26,377 @@ const afsGitOptionsSchema = (0, _aigne_afs_utils_zod.camelize)(zod.z.object({
21
26
  commitAuthor: (0, _aigne_afs_utils_zod.optionalize)(zod.z.object({
22
27
  name: zod.z.string(),
23
28
  email: zod.z.string()
24
- }))
25
- }));
26
- var AFSGit = class AFSGit {
29
+ })),
30
+ depth: (0, _aigne_afs_utils_zod.optionalize)(zod.z.number().describe("Clone depth for shallow clone")),
31
+ autoCleanup: (0, _aigne_afs_utils_zod.optionalize)(zod.z.boolean().describe("Automatically clean up cloned repository on cleanup()")),
32
+ cloneOptions: (0, _aigne_afs_utils_zod.optionalize)(zod.z.object({ auth: (0, _aigne_afs_utils_zod.optionalize)(zod.z.object({
33
+ username: (0, _aigne_afs_utils_zod.optionalize)(zod.z.string()),
34
+ password: (0, _aigne_afs_utils_zod.optionalize)(zod.z.string())
35
+ })) }))
36
+ }).refine((data) => data.repoPath || data.remoteUrl, { message: "Either repoPath or remoteUrl must be provided" }));
37
+ var AFSGit = class AFSGit extends _aigne_afs_provider.AFSBaseProvider {
27
38
  static schema() {
28
39
  return afsGitOptionsSchema;
29
40
  }
30
- static async load({ filepath, parsed }) {
31
- return new AFSGit({
32
- ...await AFSGit.schema().parseAsync(parsed),
33
- cwd: (0, node_path.dirname)(filepath)
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 } = {}) {
56
+ const instance = new AFSGit({
57
+ ...await AFSGit.schema().parseAsync(config),
58
+ cwd: basePath
34
59
  });
60
+ await instance.ready();
61
+ return instance;
35
62
  }
63
+ name;
64
+ description;
65
+ accessMode;
66
+ initPromise;
36
67
  git;
37
68
  tempBase;
38
69
  worktrees = /* @__PURE__ */ new Map();
39
70
  repoHash;
71
+ isAutoCloned = false;
72
+ clonedPath;
73
+ repoPath;
40
74
  constructor(options) {
75
+ super();
41
76
  this.options = options;
77
+ if (options.localPath && !options.repoPath) options.repoPath = options.localPath;
78
+ if (options.branch && !options.branches) options.branches = [options.branch];
42
79
  (0, _aigne_afs_utils_zod.zodParse)(afsGitOptionsSchema, options);
43
80
  let repoPath;
44
- if ((0, node_path.isAbsolute)(options.repoPath)) repoPath = options.repoPath;
45
- else repoPath = (0, node_path.join)(options.cwd || process.cwd(), options.repoPath);
46
- this.options.repoPath = repoPath;
47
- this.name = options.name || (0, node_path.basename)(repoPath) || "git";
81
+ let repoName;
82
+ if (options.repoPath) {
83
+ repoPath = (0, node_path.isAbsolute)(options.repoPath) ? options.repoPath : (0, node_path.join)(options.cwd || process.cwd(), options.repoPath);
84
+ repoName = (0, node_path.basename)(repoPath);
85
+ } else if (options.remoteUrl) {
86
+ const urlParts = options.remoteUrl.split("/");
87
+ repoName = urlParts[urlParts.length - 1]?.replace(/\.git$/, "") || "git";
88
+ const repoHash = (0, node_crypto.createHash)("md5").update(options.remoteUrl).digest("hex").substring(0, 8);
89
+ repoPath = (0, node_path.join)((0, node_os.tmpdir)(), `afs-git-remote-${repoHash}`);
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
+ }
100
+ this.repoPath = repoPath;
101
+ this.name = options.name || repoName;
48
102
  this.description = options.description;
49
103
  this.accessMode = options.accessMode ?? "readonly";
50
- this.git = (0, simple_git.simpleGit)(repoPath);
51
104
  this.repoHash = (0, node_crypto.createHash)("md5").update(repoPath).digest("hex").substring(0, 8);
52
105
  this.tempBase = (0, node_path.join)((0, node_os.tmpdir)(), `afs-git-${this.repoHash}`);
53
- }
54
- name;
55
- description;
56
- accessMode;
57
- /**
58
- * Parse AFS path into branch and file path
59
- * Branch names may contain slashes and are encoded with ~ in paths
60
- * Examples:
61
- * "/" -> { branch: undefined, filePath: "" }
62
- * "/main" -> { branch: "main", filePath: "" }
63
- * "/feature~new-feature" -> { branch: "feature/new-feature", filePath: "" }
64
- * "/main/src/index.ts" -> { branch: "main", filePath: "src/index.ts" }
65
- */
66
- parsePath(path) {
67
- const segments = (0, node_path.join)("/", path).split("/").filter(Boolean);
68
- if (segments.length === 0) return {
69
- branch: void 0,
70
- filePath: ""
71
- };
72
- return {
73
- branch: segments[0].replace(/~/g, "/"),
74
- filePath: segments.slice(1).join("/")
75
- };
76
- }
77
- /**
78
- * Detect MIME type based on file extension
79
- */
80
- getMimeType(filePath) {
81
- return {
82
- png: "image/png",
83
- jpg: "image/jpeg",
84
- jpeg: "image/jpeg",
85
- gif: "image/gif",
86
- bmp: "image/bmp",
87
- webp: "image/webp",
88
- svg: "image/svg+xml",
89
- ico: "image/x-icon",
90
- pdf: "application/pdf",
91
- txt: "text/plain",
92
- md: "text/markdown",
93
- js: "text/javascript",
94
- ts: "text/typescript",
95
- json: "application/json",
96
- html: "text/html",
97
- css: "text/css",
98
- xml: "text/xml"
99
- }[filePath.split(".").pop()?.toLowerCase() || ""] || "application/octet-stream";
106
+ this.git = null;
107
+ this.initPromise = this.initialize();
100
108
  }
101
109
  /**
102
- * Check if file is likely binary based on extension
110
+ * Wait for async initialization to complete
103
111
  */
104
- isBinaryFile(filePath) {
105
- const ext = filePath.split(".").pop()?.toLowerCase();
106
- return [
107
- "png",
108
- "jpg",
109
- "jpeg",
110
- "gif",
111
- "bmp",
112
- "webp",
113
- "ico",
114
- "pdf",
115
- "zip",
116
- "tar",
117
- "gz",
118
- "exe",
119
- "dll",
120
- "so",
121
- "dylib",
122
- "wasm"
123
- ].includes(ext || "");
112
+ async ready() {
113
+ await this.initPromise;
124
114
  }
125
115
  /**
126
- * Get list of available branches
116
+ * Async initialization logic (runs in constructor)
117
+ * Handles cloning remote repositories if needed
127
118
  */
128
- async getBranches() {
129
- const allBranches = (await this.git.branchLocal()).all;
130
- if (this.options.branches && this.options.branches.length > 0) return allBranches.filter((branch) => this.options.branches.includes(branch));
131
- return allBranches;
119
+ async initialize() {
120
+ const options = this.options;
121
+ if (options.remoteUrl) {
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;
123
+ if (!options.repoPath) this.isAutoCloned = true;
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) {
136
+ const singleBranch = options.branches?.length === 1 ? options.branches[0] : void 0;
137
+ await AFSGit.cloneRepository(options.remoteUrl, targetPath, {
138
+ depth: options.depth ?? 1,
139
+ branch: singleBranch,
140
+ auth: options.cloneOptions?.auth
141
+ });
142
+ }
143
+ if (targetPath !== this.repoPath) {
144
+ this.repoPath = targetPath;
145
+ this.repoHash = (0, node_crypto.createHash)("md5").update(targetPath).digest("hex").substring(0, 8);
146
+ this.tempBase = (0, node_path.join)((0, node_os.tmpdir)(), `afs-git-${this.repoHash}`);
147
+ }
148
+ this.clonedPath = this.isAutoCloned ? targetPath : void 0;
149
+ }
150
+ this.git = (0, simple_git.simpleGit)(this.repoPath);
151
+ if (!await this.git.checkIsRepo()) throw new Error(`Not a git repository: ${this.repoPath}`);
132
152
  }
133
153
  /**
134
- * Ensure worktree exists for a branch (lazy creation)
154
+ * Clone a remote repository to local path
135
155
  */
136
- async ensureWorktree(branch) {
137
- if (this.worktrees.has(branch)) return this.worktrees.get(branch);
138
- if ((await this.git.revparse(["--abbrev-ref", "HEAD"])).trim() === branch) {
139
- this.worktrees.set(branch, this.options.repoPath);
140
- return this.options.repoPath;
141
- }
142
- const worktreePath = (0, node_path.join)(this.tempBase, branch);
143
- if (!await (0, node_fs_promises.stat)(worktreePath).then(() => true).catch(() => false)) {
144
- await (0, node_fs_promises.mkdir)(this.tempBase, { recursive: true });
145
- await this.git.raw([
146
- "worktree",
147
- "add",
148
- worktreePath,
149
- branch
150
- ]);
156
+ static async cloneRepository(remoteUrl, targetPath, options = {}) {
157
+ const git = (0, simple_git.simpleGit)();
158
+ const cloneArgs = [];
159
+ if (options.depth) cloneArgs.push("--depth", options.depth.toString());
160
+ if (options.branch) cloneArgs.push("--branch", options.branch, "--single-branch");
161
+ let cloneUrl = remoteUrl;
162
+ if (options.auth?.username && options.auth?.password) {
163
+ if (remoteUrl.startsWith("https://")) {
164
+ const url = new URL(remoteUrl);
165
+ url.username = encodeURIComponent(options.auth.username);
166
+ url.password = encodeURIComponent(options.auth.password);
167
+ cloneUrl = url.toString();
168
+ }
151
169
  }
152
- this.worktrees.set(branch, worktreePath);
153
- return worktreePath;
170
+ await git.clone(cloneUrl, targetPath, cloneArgs);
154
171
  }
155
172
  /**
156
- * List files using git ls-tree (no worktree needed)
173
+ * List root (branches)
174
+ * Note: list() returns only children (branches), never the path itself (per new semantics)
157
175
  */
158
- async listWithGitLsTree(branch, path, options) {
176
+ async listRootHandler(ctx) {
177
+ await this.ready();
178
+ const options = ctx.options;
159
179
  const maxDepth = options?.maxDepth ?? 1;
160
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();
161
183
  const entries = [];
162
- const targetPath = path || "";
163
- const treeish = targetPath ? `${branch}:${targetPath}` : branch;
164
- try {
165
- const pathType = await this.git.raw([
166
- "cat-file",
167
- "-t",
168
- treeish
169
- ]).then((t) => t.trim()).catch(() => null);
170
- if (pathType === null) return { data: [] };
171
- if (pathType === "blob") {
172
- const size = await this.git.raw([
173
- "cat-file",
174
- "-s",
175
- treeish
176
- ]).then((s) => Number.parseInt(s.trim(), 10));
177
- const afsPath = this.buildPath(branch, path);
178
- entries.push({
179
- id: afsPath,
180
- path: afsPath,
181
- metadata: {
182
- type: "file",
183
- size
184
- }
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
185
196
  });
186
- return { data: entries };
187
- }
188
- const queue = [{
189
- path: targetPath,
190
- depth: 0
191
- }];
192
- while (queue.length > 0) {
193
- const { path: itemPath, depth } = queue.shift();
194
- const itemTreeish = itemPath ? `${branch}:${itemPath}` : branch;
195
- const lines = (await this.git.raw([
196
- "ls-tree",
197
- "-l",
198
- itemTreeish
199
- ])).split("\n").filter((line) => line.trim()).slice(0, limit - entries.length);
200
- for (const line of lines) {
201
- const match = line.match(/^(\d+)\s+(blob|tree)\s+(\w+)\s+(-|\d+)\s+(.+)$/);
202
- if (!match) continue;
203
- const type = match[2];
204
- const sizeStr = match[4];
205
- const name = match[5];
206
- const isDirectory = type === "tree";
207
- const size = sizeStr === "-" ? void 0 : Number.parseInt(sizeStr, 10);
208
- const fullPath = itemPath ? `${itemPath}/${name}` : name;
209
- const afsPath = this.buildPath(branch, fullPath);
210
- entries.push({
211
- id: afsPath,
212
- path: afsPath,
213
- metadata: {
214
- type: isDirectory ? "directory" : "file",
215
- size
216
- }
217
- });
218
- if (isDirectory && depth + 1 < maxDepth) queue.push({
219
- path: fullPath,
220
- depth: depth + 1
221
- });
222
- if (entries.length >= limit) return { data: entries };
223
- }
197
+ entries.push(...branchResult.data);
224
198
  }
225
- return { data: entries };
226
- } catch (error) {
227
- return {
228
- data: [],
229
- message: error.message
230
- };
231
199
  }
200
+ return { data: entries };
232
201
  }
233
202
  /**
234
- * Build AFS path with encoded branch name
235
- * Branch names with slashes are encoded by replacing / with ~
236
- * @param branch Branch name (may contain slashes)
237
- * @param filePath File path within branch
203
+ * List branch root (matches /main, /develop, etc.)
238
204
  */
239
- buildPath(branch, filePath) {
240
- const encodedBranch = branch.replace(/\//g, "~");
241
- if (!filePath) return `/${encodedBranch}`;
242
- return `/${encodedBranch}/${filePath}`;
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);
243
210
  }
244
- async list(path, options) {
245
- const { branch, filePath } = this.parsePath(path);
246
- if (!branch) return { data: (await this.getBranches()).map((name) => {
247
- const encodedPath = this.buildPath(name);
248
- return {
249
- id: encodedPath,
250
- path: encodedPath,
251
- metadata: { type: "directory" }
211
+ /**
212
+ * List files in branch with subpath (matches /main/src, /main/src/foo, etc.)
213
+ */
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);
220
+ }
221
+ /**
222
+ * Read root metadata (introspection only, read-only)
223
+ */
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
+ } });
231
+ }
232
+ /**
233
+ * Read branch root metadata (introspection only, read-only)
234
+ */
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
+ } });
247
+ }
248
+ /**
249
+ * Read file or directory metadata in branch (introspection only, read-only)
250
+ */
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
+ } });
271
+ }
272
+ /**
273
+ * Read root
274
+ */
275
+ async readRootHandler(_ctx) {
276
+ await this.ready();
277
+ const branches = await this.getBranches();
278
+ return this.buildEntry("/", { meta: { childrenCount: branches.length } });
279
+ }
280
+ /**
281
+ * Read branch root
282
+ */
283
+ async readBranchRootHandler(ctx) {
284
+ await this.ready();
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
+ } });
294
+ }
295
+ /**
296
+ * Read file or directory in branch
297
+ */
298
+ async readBranchHandler(ctx) {
299
+ await this.ready();
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 } });
311
+ }
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
252
318
  };
253
- }) };
254
- return this.listWithGitLsTree(branch, filePath, options);
255
- }
256
- async read(path, _options) {
257
- const { branch, filePath } = this.parsePath(path);
258
- if (!branch) return { data: {
259
- id: "/",
260
- path: "/",
261
- metadata: { type: "directory" }
262
- } };
263
- if (!filePath) {
264
- const branchPath = this.buildPath(branch);
265
- return { data: {
266
- id: branchPath,
267
- path: branchPath,
268
- metadata: { type: "directory" }
269
- } };
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 } });
270
341
  }
271
- try {
272
- if (await this.git.raw([
273
- "cat-file",
274
- "-t",
275
- `${branch}:${filePath}`
276
- ]).then((t) => t.trim()) === "tree") {
277
- const afsPath$1 = this.buildPath(branch, filePath);
278
- return { data: {
279
- id: afsPath$1,
280
- path: afsPath$1,
281
- metadata: { type: "directory" }
282
- } };
283
- }
284
- const size = await this.git.raw([
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", [
285
356
  "cat-file",
286
- "-s",
357
+ "-p",
287
358
  `${branch}:${filePath}`
288
- ]).then((s) => Number.parseInt(s.trim(), 10));
289
- const mimeType = this.getMimeType(filePath);
290
- const isBinary = this.isBinaryFile(filePath);
291
- let content;
292
- const metadata = {
293
- type: "file",
294
- size,
295
- mimeType
296
- };
297
- if (isBinary) {
298
- const { stdout } = await execFileAsync("git", [
299
- "cat-file",
300
- "-p",
301
- `${branch}:${filePath}`
302
- ], {
303
- cwd: this.options.repoPath,
304
- encoding: "buffer",
305
- maxBuffer: 10 * 1024 * 1024
306
- });
307
- content = stdout.toString("base64");
308
- metadata.contentType = "base64";
309
- } else content = await this.git.show([`${branch}:${filePath}`]);
310
- const afsPath = this.buildPath(branch, filePath);
311
- return { data: {
312
- id: afsPath,
313
- path: afsPath,
314
- content,
315
- metadata
316
- } };
317
- } catch (error) {
318
- return {
319
- data: void 0,
320
- message: error.message
321
- };
322
- }
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");
323
384
  }
324
- async write(path, entry, options) {
325
- const { branch, filePath } = this.parsePath(path);
326
- if (!branch || !filePath) throw new Error("Cannot write to root or branch root");
385
+ /**
386
+ * Write file in branch
387
+ */
388
+ async writeHandler(ctx, payload) {
389
+ await this.ready();
390
+ const branch = this.decodeBranchName(ctx.params.branch);
391
+ const filePath = ctx.params.path;
392
+ const append = ctx.options?.append ?? false;
327
393
  const worktreePath = await this.ensureWorktree(branch);
328
394
  const fullPath = (0, node_path.join)(worktreePath, filePath);
329
- const append = options?.append ?? false;
330
395
  await (0, node_fs_promises.mkdir)((0, node_path.dirname)(fullPath), { recursive: true });
331
- if (entry.content !== void 0) {
396
+ if (payload.content !== void 0) {
332
397
  let contentToWrite;
333
- if (typeof entry.content === "string") contentToWrite = entry.content;
334
- 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);
335
400
  await (0, node_fs_promises.writeFile)(fullPath, contentToWrite, {
336
401
  encoding: "utf8",
337
402
  flag: append ? "a" : "w"
@@ -347,31 +412,56 @@ var AFSGit = class AFSGit {
347
412
  await gitInstance.commit(`Update ${filePath}`);
348
413
  }
349
414
  const stats = await (0, node_fs_promises.stat)(fullPath);
350
- const afsPath = this.buildPath(branch, filePath);
415
+ const afsPath = this.buildBranchPath(branch, filePath);
351
416
  return { data: {
352
417
  id: afsPath,
353
418
  path: afsPath,
354
- content: entry.content,
355
- summary: entry.summary,
419
+ content: payload.content,
420
+ summary: payload.summary,
356
421
  createdAt: stats.birthtime,
357
422
  updatedAt: stats.mtime,
358
- metadata: {
359
- ...entry.metadata,
360
- type: stats.isDirectory() ? "directory" : "file",
423
+ meta: {
424
+ ...payload.meta,
361
425
  size: stats.size
362
426
  },
363
- userId: entry.userId,
364
- sessionId: entry.sessionId,
365
- linkTo: entry.linkTo
427
+ userId: payload.userId,
428
+ sessionId: payload.sessionId,
429
+ linkTo: payload.linkTo
366
430
  } };
367
431
  }
368
- async delete(path, options) {
369
- const { branch, filePath } = this.parsePath(path);
370
- if (!branch || !filePath) throw new Error("Cannot delete root or branch root");
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) {
451
+ await this.ready();
452
+ const branch = this.decodeBranchName(ctx.params.branch);
453
+ const filePath = ctx.params.path;
454
+ const recursive = ctx.options?.recursive ?? false;
371
455
  const worktreePath = await this.ensureWorktree(branch);
372
456
  const fullPath = (0, node_path.join)(worktreePath, filePath);
373
- const recursive = options?.recursive ?? false;
374
- if ((await (0, node_fs_promises.stat)(fullPath)).isDirectory() && !recursive) throw new Error(`Cannot delete directory '${path}' without recursive option. Set recursive: true to delete directories.`);
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.`);
375
465
  await (0, node_fs_promises.rm)(fullPath, {
376
466
  recursive,
377
467
  force: true
@@ -385,19 +475,28 @@ var AFSGit = class AFSGit {
385
475
  }
386
476
  await gitInstance.commit(`Delete ${filePath}`);
387
477
  }
388
- return { message: `Successfully deleted: ${path}` };
478
+ return { message: `Successfully deleted: /${ctx.params.branch}/${filePath}` };
389
479
  }
390
- async rename(oldPath, newPath, options) {
391
- const { branch: oldBranch, filePath: oldFilePath } = this.parsePath(oldPath);
480
+ /**
481
+ * Rename file in branch
482
+ */
483
+ async renameHandler(ctx, newPath) {
484
+ await this.ready();
485
+ const oldBranch = this.decodeBranchName(ctx.params.branch);
486
+ const oldFilePath = ctx.params.path;
392
487
  const { branch: newBranch, filePath: newFilePath } = this.parsePath(newPath);
393
- if (!oldBranch || !oldFilePath) throw new Error("Cannot rename from root or branch root");
488
+ const overwrite = ctx.options?.overwrite ?? false;
394
489
  if (!newBranch || !newFilePath) throw new Error("Cannot rename to root or branch root");
395
490
  if (oldBranch !== newBranch) throw new Error("Cannot rename across branches");
396
491
  const worktreePath = await this.ensureWorktree(oldBranch);
397
492
  const oldFullPath = (0, node_path.join)(worktreePath, oldFilePath);
398
493
  const newFullPath = (0, node_path.join)(worktreePath, newFilePath);
399
- const overwrite = options?.overwrite ?? false;
400
- 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
+ }
401
500
  try {
402
501
  await (0, node_fs_promises.stat)(newFullPath);
403
502
  if (!overwrite) throw new Error(`Destination '${newPath}' already exists. Set overwrite: true to replace it.`);
@@ -415,14 +514,26 @@ var AFSGit = class AFSGit {
415
514
  }
416
515
  await gitInstance.commit(`Rename ${oldFilePath} to ${newFilePath}`);
417
516
  }
418
- return { message: `Successfully renamed '${oldPath}' to '${newPath}'` };
517
+ return { message: `Successfully renamed '/${ctx.params.branch}/${oldFilePath}' to '${newPath}'` };
419
518
  }
420
- async search(path, query, options) {
421
- const { branch, filePath } = this.parsePath(path);
422
- if (!branch) return {
423
- data: [],
424
- message: "Search requires a branch path"
425
- };
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);
530
+ }
531
+ /**
532
+ * Internal search implementation
533
+ */
534
+ async searchInBranch(encodedBranch, filePath, query, options) {
535
+ await this.ready();
536
+ const branch = this.decodeBranchName(encodedBranch);
426
537
  const limit = Math.min(options?.limit || LIST_MAX_LIMIT, LIST_MAX_LIMIT);
427
538
  try {
428
539
  const args = [
@@ -452,15 +563,12 @@ var AFSGit = class AFSGit {
452
563
  lineNum = matchNoBranch[2];
453
564
  content = matchNoBranch[3];
454
565
  }
455
- const afsPath = this.buildPath(branch, matchPath);
566
+ const afsPath = this.buildBranchPath(branch, matchPath);
456
567
  if (processedFiles.has(afsPath)) continue;
457
568
  processedFiles.add(afsPath);
458
- entries.push({
459
- id: afsPath,
460
- path: afsPath,
461
- summary: `Line ${lineNum}: ${content}`,
462
- metadata: { type: "file" }
463
- });
569
+ const entry = this.buildEntry(afsPath);
570
+ entry.summary = `Line ${lineNum}: ${content}`;
571
+ entries.push(entry);
464
572
  if (entries.length >= limit) break;
465
573
  }
466
574
  return {
@@ -476,26 +584,952 @@ var AFSGit = class AFSGit {
476
584
  }
477
585
  }
478
586
  /**
479
- * Cleanup all worktrees (useful when unmounting)
587
+ * Stat root
480
588
  */
481
- async cleanup() {
482
- for (const [_branch, worktreePath] of this.worktrees) try {
483
- await this.git.raw([
484
- "worktree",
485
- "remove",
486
- worktreePath,
487
- "--force"
488
- ]);
489
- } catch (_error) {}
490
- this.worktrees.clear();
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;
491
622
  try {
492
- await (0, node_fs_promises.rm)(this.tempBase, {
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
+ /**
1448
+ * Fetch latest changes from remote
1449
+ */
1450
+ async fetch() {
1451
+ await this.ready();
1452
+ await this.git.fetch();
1453
+ }
1454
+ /**
1455
+ * Pull latest changes from remote for current branch
1456
+ */
1457
+ async pull() {
1458
+ await this.ready();
1459
+ await this.git.pull();
1460
+ }
1461
+ /**
1462
+ * Push local changes to remote
1463
+ */
1464
+ async push(branch) {
1465
+ await this.ready();
1466
+ if (branch) await this.git.push("origin", branch);
1467
+ else await this.git.push();
1468
+ }
1469
+ /**
1470
+ * Cleanup all worktrees (useful when unmounting)
1471
+ */
1472
+ async cleanup() {
1473
+ await this.ready();
1474
+ for (const [_branch, worktreePath] of this.worktrees) try {
1475
+ await this.git.raw([
1476
+ "worktree",
1477
+ "remove",
1478
+ worktreePath,
1479
+ "--force"
1480
+ ]);
1481
+ } catch (_error) {}
1482
+ this.worktrees.clear();
1483
+ try {
1484
+ await (0, node_fs_promises.rm)(this.tempBase, {
1485
+ recursive: true,
1486
+ force: true
1487
+ });
1488
+ } catch {}
1489
+ const autoCleanup = this.options.autoCleanup ?? true;
1490
+ if (this.isAutoCloned && autoCleanup && this.clonedPath) try {
1491
+ await (0, node_fs_promises.rm)(this.clonedPath, {
493
1492
  recursive: true,
494
1493
  force: true
495
1494
  });
496
1495
  } catch {}
497
1496
  }
498
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;
499
1532
 
500
1533
  //#endregion
501
- exports.AFSGit = AFSGit;
1534
+ exports.AFSGit = AFSGit;
1535
+ exports.default = src_default;