@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/LICENSE.md +17 -84
- package/README.md +0 -4
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
- package/dist/_virtual/rolldown_runtime.mjs +7 -0
- package/dist/index.cjs +1345 -311
- package/dist/index.d.cts +397 -99
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +397 -99
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1345 -312
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -4
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
|
-
|
|
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
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
110
|
+
* Wait for async initialization to complete
|
|
103
111
|
*/
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
*
|
|
116
|
+
* Async initialization logic (runs in constructor)
|
|
117
|
+
* Handles cloning remote repositories if needed
|
|
127
118
|
*/
|
|
128
|
-
async
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
|
|
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
|
-
*
|
|
154
|
+
* Clone a remote repository to local path
|
|
135
155
|
*/
|
|
136
|
-
async
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
153
|
-
return worktreePath;
|
|
170
|
+
await git.clone(cloneUrl, targetPath, cloneArgs);
|
|
154
171
|
}
|
|
155
172
|
/**
|
|
156
|
-
* List
|
|
173
|
+
* List root (branches)
|
|
174
|
+
* Note: list() returns only children (branches), never the path itself (per new semantics)
|
|
157
175
|
*/
|
|
158
|
-
async
|
|
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
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
"
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
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
|
-
"-
|
|
357
|
+
"-p",
|
|
287
358
|
`${branch}:${filePath}`
|
|
288
|
-
]
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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 (
|
|
396
|
+
if (payload.content !== void 0) {
|
|
332
397
|
let contentToWrite;
|
|
333
|
-
if (typeof
|
|
334
|
-
else contentToWrite = JSON.stringify(
|
|
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.
|
|
415
|
+
const afsPath = this.buildBranchPath(branch, filePath);
|
|
351
416
|
return { data: {
|
|
352
417
|
id: afsPath,
|
|
353
418
|
path: afsPath,
|
|
354
|
-
content:
|
|
355
|
-
summary:
|
|
419
|
+
content: payload.content,
|
|
420
|
+
summary: payload.summary,
|
|
356
421
|
createdAt: stats.birthtime,
|
|
357
422
|
updatedAt: stats.mtime,
|
|
358
|
-
|
|
359
|
-
...
|
|
360
|
-
type: stats.isDirectory() ? "directory" : "file",
|
|
423
|
+
meta: {
|
|
424
|
+
...payload.meta,
|
|
361
425
|
size: stats.size
|
|
362
426
|
},
|
|
363
|
-
userId:
|
|
364
|
-
sessionId:
|
|
365
|
-
linkTo:
|
|
427
|
+
userId: payload.userId,
|
|
428
|
+
sessionId: payload.sessionId,
|
|
429
|
+
linkTo: payload.linkTo
|
|
366
430
|
} };
|
|
367
431
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
374
|
-
|
|
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:
|
|
478
|
+
return { message: `Successfully deleted: /${ctx.params.branch}/${filePath}` };
|
|
389
479
|
}
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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 '
|
|
517
|
+
return { message: `Successfully renamed '/${ctx.params.branch}/${oldFilePath}' to '${newPath}'` };
|
|
419
518
|
}
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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.
|
|
566
|
+
const afsPath = this.buildBranchPath(branch, matchPath);
|
|
456
567
|
if (processedFiles.has(afsPath)) continue;
|
|
457
568
|
processedFiles.add(afsPath);
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
*
|
|
587
|
+
* Stat root
|
|
480
588
|
*/
|
|
481
|
-
async
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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 (
|
|
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;
|