@chrysb/alphaclaw 0.3.5-beta.0 → 0.3.5-beta.1
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/bin/alphaclaw.js +65 -1
- package/lib/public/css/explorer.css +201 -6
- package/lib/public/js/app.js +45 -1
- package/lib/public/js/components/channels.js +1 -0
- package/lib/public/js/components/file-tree.js +56 -67
- package/lib/public/js/components/file-viewer/constants.js +6 -0
- package/lib/public/js/components/file-viewer/diff-viewer.js +46 -0
- package/lib/public/js/components/file-viewer/editor-surface.js +120 -0
- package/lib/public/js/components/file-viewer/frontmatter-panel.js +56 -0
- package/lib/public/js/components/file-viewer/index.js +164 -0
- package/lib/public/js/components/file-viewer/markdown-split-view.js +51 -0
- package/lib/public/js/components/file-viewer/media-preview.js +44 -0
- package/lib/public/js/components/file-viewer/scroll-sync.js +95 -0
- package/lib/public/js/components/file-viewer/sqlite-viewer.js +167 -0
- package/lib/public/js/components/file-viewer/status-banners.js +59 -0
- package/lib/public/js/components/file-viewer/storage.js +58 -0
- package/lib/public/js/components/file-viewer/toolbar.js +77 -0
- package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +87 -0
- package/lib/public/js/components/file-viewer/use-file-diff.js +49 -0
- package/lib/public/js/components/file-viewer/use-file-loader.js +302 -0
- package/lib/public/js/components/file-viewer/use-file-viewer-draft-sync.js +32 -0
- package/lib/public/js/components/file-viewer/use-file-viewer-hotkeys.js +25 -0
- package/lib/public/js/components/file-viewer/use-file-viewer.js +379 -0
- package/lib/public/js/components/file-viewer/utils.js +11 -0
- package/lib/public/js/components/gateway.js +83 -30
- package/lib/public/js/components/icons.js +13 -0
- package/lib/public/js/components/sidebar-git-panel.js +72 -11
- package/lib/public/js/components/usage-tab.js +4 -1
- package/lib/public/js/components/watchdog-tab.js +6 -0
- package/lib/public/js/lib/api.js +16 -0
- package/lib/public/js/lib/browse-file-policies.js +34 -0
- package/lib/scripts/git +40 -0
- package/lib/scripts/git-askpass +6 -0
- package/lib/server/constants.js +8 -0
- package/lib/server/routes/browse/constants.js +51 -0
- package/lib/server/routes/browse/file-helpers.js +43 -0
- package/lib/server/routes/browse/git.js +131 -0
- package/lib/server/routes/{browse.js → browse/index.js} +290 -218
- package/lib/server/routes/browse/path-utils.js +53 -0
- package/lib/server/routes/browse/sqlite.js +140 -0
- package/lib/server/routes/proxy.js +11 -5
- package/lib/setup/core-prompts/TOOLS.md +0 -4
- package/package.json +1 -1
- package/lib/public/js/components/file-viewer.js +0 -1095
|
@@ -1,35 +1,30 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
|
-
const {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return false;
|
|
32
|
-
};
|
|
2
|
+
const { kLockedBrowsePaths } = require("../../constants");
|
|
3
|
+
const {
|
|
4
|
+
kDefaultTreeDepth,
|
|
5
|
+
kIgnoredDirectoryNames,
|
|
6
|
+
kCommitHistoryLimit,
|
|
7
|
+
} = require("./constants");
|
|
8
|
+
const {
|
|
9
|
+
normalizePolicyPath,
|
|
10
|
+
resolveSafePath,
|
|
11
|
+
toRelativePath,
|
|
12
|
+
matchesPolicyPath,
|
|
13
|
+
} = require("./path-utils");
|
|
14
|
+
const {
|
|
15
|
+
isLikelyBinaryFile,
|
|
16
|
+
getImageMimeType,
|
|
17
|
+
getAudioMimeType,
|
|
18
|
+
isSqliteFilePath,
|
|
19
|
+
} = require("./file-helpers");
|
|
20
|
+
const { readSqliteSummary, readSqliteTableData } = require("./sqlite");
|
|
21
|
+
const {
|
|
22
|
+
runGitCommand,
|
|
23
|
+
runGitCommandWithExitCode,
|
|
24
|
+
parseGithubRepoSlug,
|
|
25
|
+
normalizeChangedPath,
|
|
26
|
+
parseBranchTracking,
|
|
27
|
+
} = require("./git");
|
|
33
28
|
|
|
34
29
|
const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
35
30
|
const kRootResolved = path.resolve(kRootDir);
|
|
@@ -40,55 +35,10 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
40
35
|
fs.mkdirSync(kRootResolved, { recursive: true });
|
|
41
36
|
}
|
|
42
37
|
|
|
43
|
-
const normalizeRelativePath = (inputPath) => {
|
|
44
|
-
const rawPath = String(inputPath || "").trim();
|
|
45
|
-
if (!rawPath) return "";
|
|
46
|
-
return rawPath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const normalizePolicyPath = (inputPath) =>
|
|
50
|
-
String(inputPath || "")
|
|
51
|
-
.replace(/\\/g, "/")
|
|
52
|
-
.replace(/^\.\/+/, "")
|
|
53
|
-
.replace(/^\/+/, "")
|
|
54
|
-
.trim()
|
|
55
|
-
.toLowerCase();
|
|
56
|
-
|
|
57
|
-
const resolveSafePath = (inputPath) => {
|
|
58
|
-
const relativePath = normalizeRelativePath(inputPath);
|
|
59
|
-
const absolutePath = path.resolve(kRootResolved, relativePath);
|
|
60
|
-
const isInsideRoot =
|
|
61
|
-
absolutePath === kRootResolved || absolutePath.startsWith(kRootWithSep);
|
|
62
|
-
if (!isInsideRoot) {
|
|
63
|
-
return { ok: false, error: `Path must stay within ${kRootDisplayName}` };
|
|
64
|
-
}
|
|
65
|
-
return { ok: true, relativePath, absolutePath };
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const isLikelyBinaryFile = (targetPath) => {
|
|
69
|
-
let fileHandle = null;
|
|
70
|
-
try {
|
|
71
|
-
fileHandle = fs.openSync(targetPath, "r");
|
|
72
|
-
const sample = Buffer.alloc(512);
|
|
73
|
-
const bytesRead = fs.readSync(fileHandle, sample, 0, sample.length, 0);
|
|
74
|
-
for (let index = 0; index < bytesRead; index += 1) {
|
|
75
|
-
if (sample[index] === 0) return true;
|
|
76
|
-
}
|
|
77
|
-
return false;
|
|
78
|
-
} finally {
|
|
79
|
-
if (fileHandle !== null) fs.closeSync(fileHandle);
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const toRelativePath = (absolutePath) => {
|
|
84
|
-
const relative = path.relative(kRootResolved, absolutePath);
|
|
85
|
-
return relative === "" ? "" : relative.split(path.sep).join("/");
|
|
86
|
-
};
|
|
87
|
-
|
|
88
38
|
const buildTreeNode = (absolutePath, depthRemaining) => {
|
|
89
39
|
const stats = fs.statSync(absolutePath);
|
|
90
40
|
const nodeName = path.basename(absolutePath);
|
|
91
|
-
const nodePath = toRelativePath(absolutePath);
|
|
41
|
+
const nodePath = toRelativePath(absolutePath, kRootResolved);
|
|
92
42
|
|
|
93
43
|
if (!stats.isDirectory()) {
|
|
94
44
|
return { type: "file", name: nodeName, path: nodePath };
|
|
@@ -106,7 +56,9 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
106
56
|
}
|
|
107
57
|
return entry.isDirectory() || entry.isFile();
|
|
108
58
|
})
|
|
109
|
-
.map((entry) =>
|
|
59
|
+
.map((entry) =>
|
|
60
|
+
buildTreeNode(path.join(absolutePath, entry.name), depthRemaining - 1),
|
|
61
|
+
)
|
|
110
62
|
.sort((leftNode, rightNode) => {
|
|
111
63
|
if (leftNode.type !== rightNode.type) {
|
|
112
64
|
return leftNode.type === "folder" ? -1 : 1;
|
|
@@ -117,87 +69,30 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
117
69
|
return { type: "folder", name: nodeName, path: nodePath, children };
|
|
118
70
|
};
|
|
119
71
|
|
|
120
|
-
const runGitCommand = (args) =>
|
|
121
|
-
new Promise((resolve) => {
|
|
122
|
-
execFile(
|
|
123
|
-
"git",
|
|
124
|
-
args,
|
|
125
|
-
{ timeout: 10000, cwd: kRootResolved },
|
|
126
|
-
(error, stdout, stderr) => {
|
|
127
|
-
if (error) {
|
|
128
|
-
return resolve({
|
|
129
|
-
ok: false,
|
|
130
|
-
error: String(stderr || stdout || error.message || "git command failed").trim(),
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
return resolve({ ok: true, stdout: String(stdout || "") });
|
|
134
|
-
},
|
|
135
|
-
);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const runGitCommandWithExitCode = (args) =>
|
|
139
|
-
new Promise((resolve) => {
|
|
140
|
-
execFile(
|
|
141
|
-
"git",
|
|
142
|
-
args,
|
|
143
|
-
{ timeout: 10000, cwd: kRootResolved },
|
|
144
|
-
(error, stdout, stderr) => {
|
|
145
|
-
const safeStdout = String(stdout || "");
|
|
146
|
-
const safeStderr = String(stderr || "");
|
|
147
|
-
if (!error) {
|
|
148
|
-
return resolve({
|
|
149
|
-
ok: true,
|
|
150
|
-
stdout: safeStdout,
|
|
151
|
-
stderr: safeStderr,
|
|
152
|
-
exitCode: 0,
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
return resolve({
|
|
156
|
-
ok: false,
|
|
157
|
-
stdout: safeStdout,
|
|
158
|
-
stderr: safeStderr,
|
|
159
|
-
exitCode: Number.isInteger(error.code) ? error.code : 1,
|
|
160
|
-
error: String(error.message || "git command failed"),
|
|
161
|
-
});
|
|
162
|
-
},
|
|
163
|
-
);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
const parseGithubRepoSlug = (value) => {
|
|
167
|
-
const raw = String(value || "").trim();
|
|
168
|
-
if (!raw) return "";
|
|
169
|
-
return raw
|
|
170
|
-
.replace(/^git@github\.com:/i, "")
|
|
171
|
-
.replace(/^https:\/\/github\.com\//i, "")
|
|
172
|
-
.replace(/\.git$/i, "")
|
|
173
|
-
.trim();
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
const normalizeChangedPath = (rawPath) => {
|
|
177
|
-
const value = String(rawPath || "").trim();
|
|
178
|
-
if (!value) return "";
|
|
179
|
-
if (value.includes(" -> ")) {
|
|
180
|
-
const segments = value.split(" -> ");
|
|
181
|
-
return String(segments[segments.length - 1] || "").trim();
|
|
182
|
-
}
|
|
183
|
-
return value;
|
|
184
|
-
};
|
|
185
|
-
|
|
186
72
|
app.get("/api/browse/tree", (req, res) => {
|
|
187
73
|
const depthValue = Number.parseInt(String(req.query.depth || ""), 10);
|
|
188
|
-
const depth =
|
|
74
|
+
const depth =
|
|
75
|
+
Number.isFinite(depthValue) && depthValue > 0
|
|
76
|
+
? depthValue
|
|
77
|
+
: kDefaultTreeDepth;
|
|
189
78
|
try {
|
|
190
79
|
const tree = buildTreeNode(kRootResolved, depth);
|
|
191
80
|
return res.json({ ok: true, root: tree });
|
|
192
81
|
} catch (error) {
|
|
193
|
-
return res
|
|
194
|
-
|
|
195
|
-
|
|
82
|
+
return res.status(500).json({
|
|
83
|
+
ok: false,
|
|
84
|
+
error: error.message || "Could not build file tree",
|
|
85
|
+
});
|
|
196
86
|
}
|
|
197
87
|
});
|
|
198
88
|
|
|
199
89
|
app.get("/api/browse/read", (req, res) => {
|
|
200
|
-
const resolvedPath = resolveSafePath(
|
|
90
|
+
const resolvedPath = resolveSafePath(
|
|
91
|
+
req.query.path,
|
|
92
|
+
kRootResolved,
|
|
93
|
+
kRootWithSep,
|
|
94
|
+
kRootDisplayName,
|
|
95
|
+
);
|
|
201
96
|
if (!resolvedPath.ok) {
|
|
202
97
|
return res.status(400).json({ ok: false, error: resolvedPath.error });
|
|
203
98
|
}
|
|
@@ -207,24 +102,70 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
207
102
|
if (!stats.isFile()) {
|
|
208
103
|
return res.status(400).json({ ok: false, error: "Path is not a file" });
|
|
209
104
|
}
|
|
210
|
-
if (
|
|
211
|
-
|
|
105
|
+
if (isSqliteFilePath(resolvedPath.absolutePath)) {
|
|
106
|
+
const sqliteSummary = readSqliteSummary(resolvedPath.absolutePath);
|
|
107
|
+
return res.json({
|
|
108
|
+
ok: true,
|
|
109
|
+
path: resolvedPath.relativePath,
|
|
110
|
+
kind: "sqlite",
|
|
111
|
+
sqliteSummary,
|
|
112
|
+
content: "",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const audioMimeType = getAudioMimeType(resolvedPath.absolutePath);
|
|
116
|
+
if (audioMimeType) {
|
|
117
|
+
const audioBytes = fs.readFileSync(resolvedPath.absolutePath);
|
|
118
|
+
const audioDataUrl = `data:${audioMimeType};base64,${audioBytes.toString("base64")}`;
|
|
119
|
+
return res.json({
|
|
120
|
+
ok: true,
|
|
121
|
+
path: resolvedPath.relativePath,
|
|
122
|
+
kind: "audio",
|
|
123
|
+
mimeType: audioMimeType,
|
|
124
|
+
audioDataUrl,
|
|
125
|
+
content: "",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (isLikelyBinaryFile(fs, resolvedPath.absolutePath)) {
|
|
129
|
+
const imageMimeType = getImageMimeType(resolvedPath.absolutePath);
|
|
130
|
+
if (!imageMimeType) {
|
|
131
|
+
return res
|
|
132
|
+
.status(400)
|
|
133
|
+
.json({ ok: false, error: "Binary files are not editable" });
|
|
134
|
+
}
|
|
135
|
+
const imageBytes = fs.readFileSync(resolvedPath.absolutePath);
|
|
136
|
+
const imageDataUrl = `data:${imageMimeType};base64,${imageBytes.toString("base64")}`;
|
|
137
|
+
return res.json({
|
|
138
|
+
ok: true,
|
|
139
|
+
path: resolvedPath.relativePath,
|
|
140
|
+
kind: "image",
|
|
141
|
+
mimeType: imageMimeType,
|
|
142
|
+
imageDataUrl,
|
|
143
|
+
content: "",
|
|
144
|
+
});
|
|
212
145
|
}
|
|
213
146
|
const content = fs.readFileSync(resolvedPath.absolutePath, "utf8");
|
|
214
147
|
return res.json({
|
|
215
148
|
ok: true,
|
|
216
149
|
path: resolvedPath.relativePath,
|
|
150
|
+
kind: "text",
|
|
217
151
|
content,
|
|
218
152
|
});
|
|
219
153
|
} catch (error) {
|
|
220
|
-
return res
|
|
154
|
+
return res
|
|
155
|
+
.status(500)
|
|
156
|
+
.json({ ok: false, error: error.message || "Could not read file" });
|
|
221
157
|
}
|
|
222
158
|
});
|
|
223
159
|
|
|
224
160
|
app.get("/api/browse/git-summary", async (req, res) => {
|
|
225
161
|
try {
|
|
226
|
-
const envRepoSlug = parseGithubRepoSlug(
|
|
227
|
-
|
|
162
|
+
const envRepoSlug = parseGithubRepoSlug(
|
|
163
|
+
process.env.GITHUB_WORKSPACE_REPO || "",
|
|
164
|
+
);
|
|
165
|
+
const statusResult = await runGitCommand(
|
|
166
|
+
["status", "--porcelain", "--branch"],
|
|
167
|
+
kRootResolved,
|
|
168
|
+
);
|
|
228
169
|
if (!statusResult.ok) {
|
|
229
170
|
if (/not a git repository/i.test(statusResult.error || "")) {
|
|
230
171
|
return res.json({
|
|
@@ -244,8 +185,12 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
244
185
|
.map((line) => line.trimEnd())
|
|
245
186
|
.filter(Boolean);
|
|
246
187
|
const branchLine = statusLines.find((line) => line.startsWith("##")) || "";
|
|
247
|
-
const
|
|
248
|
-
const
|
|
188
|
+
const branchTracking = parseBranchTracking(branchLine);
|
|
189
|
+
const branch = branchTracking.branch;
|
|
190
|
+
const diffNumstatResult = await runGitCommand(
|
|
191
|
+
["diff", "--numstat", "HEAD"],
|
|
192
|
+
kRootResolved,
|
|
193
|
+
);
|
|
249
194
|
const diffStatsByPath = new Map();
|
|
250
195
|
if (diffNumstatResult.ok) {
|
|
251
196
|
diffNumstatResult.stdout
|
|
@@ -253,7 +198,8 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
253
198
|
.map((line) => line.trim())
|
|
254
199
|
.filter(Boolean)
|
|
255
200
|
.forEach((line) => {
|
|
256
|
-
const [addedRaw = "", deletedRaw = "", rawPath = ""] =
|
|
201
|
+
const [addedRaw = "", deletedRaw = "", rawPath = ""] =
|
|
202
|
+
line.split("\t");
|
|
257
203
|
const normalizedPath = normalizeChangedPath(rawPath);
|
|
258
204
|
if (!normalizedPath) return;
|
|
259
205
|
const addedLines = Number.parseInt(addedRaw, 10);
|
|
@@ -290,26 +236,33 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
290
236
|
|
|
291
237
|
let repoSlug = envRepoSlug;
|
|
292
238
|
if (!repoSlug) {
|
|
293
|
-
const remoteResult = await runGitCommand(
|
|
239
|
+
const remoteResult = await runGitCommand(
|
|
240
|
+
["remote", "get-url", "origin"],
|
|
241
|
+
kRootResolved,
|
|
242
|
+
);
|
|
294
243
|
if (remoteResult.ok) {
|
|
295
244
|
repoSlug = parseGithubRepoSlug(remoteResult.stdout || "");
|
|
296
245
|
}
|
|
297
246
|
}
|
|
298
247
|
const repoUrl = repoSlug ? `https://github.com/${repoSlug}` : "";
|
|
299
248
|
|
|
300
|
-
const logResult = await runGitCommand(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
249
|
+
const logResult = await runGitCommand(
|
|
250
|
+
[
|
|
251
|
+
"log",
|
|
252
|
+
"--pretty=format:%H%x09%h%x09%s%x09%ct",
|
|
253
|
+
"-n",
|
|
254
|
+
String(kCommitHistoryLimit),
|
|
255
|
+
],
|
|
256
|
+
kRootResolved,
|
|
257
|
+
);
|
|
306
258
|
const commits = logResult.ok
|
|
307
259
|
? logResult.stdout
|
|
308
260
|
.split("\n")
|
|
309
261
|
.map((line) => line.trim())
|
|
310
262
|
.filter(Boolean)
|
|
311
263
|
.map((line) => {
|
|
312
|
-
const [hash = "", shortHash = "", message = "", unixTs = "0"] =
|
|
264
|
+
const [hash = "", shortHash = "", message = "", unixTs = "0"] =
|
|
265
|
+
line.split("\t");
|
|
313
266
|
return {
|
|
314
267
|
hash,
|
|
315
268
|
shortHash,
|
|
@@ -327,6 +280,12 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
327
280
|
repoSlug,
|
|
328
281
|
repoUrl,
|
|
329
282
|
branch,
|
|
283
|
+
upstreamBranch: branchTracking.upstreamBranch,
|
|
284
|
+
hasUpstream: branchTracking.hasUpstream,
|
|
285
|
+
upstreamGone: branchTracking.upstreamGone,
|
|
286
|
+
aheadCount: branchTracking.aheadCount,
|
|
287
|
+
behindCount: branchTracking.behindCount,
|
|
288
|
+
syncState: branchTracking.syncState,
|
|
330
289
|
isDirty: changedFiles.length > 0,
|
|
331
290
|
changedFilesCount: changedFiles.length,
|
|
332
291
|
changedFiles: changedFiles.slice(0, 16),
|
|
@@ -340,8 +299,53 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
340
299
|
}
|
|
341
300
|
});
|
|
342
301
|
|
|
302
|
+
app.get("/api/browse/sqlite-table", (req, res) => {
|
|
303
|
+
const resolvedPath = resolveSafePath(
|
|
304
|
+
req.query.path,
|
|
305
|
+
kRootResolved,
|
|
306
|
+
kRootWithSep,
|
|
307
|
+
kRootDisplayName,
|
|
308
|
+
);
|
|
309
|
+
if (!resolvedPath.ok) {
|
|
310
|
+
return res.status(400).json({ ok: false, error: resolvedPath.error });
|
|
311
|
+
}
|
|
312
|
+
if (!isSqliteFilePath(resolvedPath.absolutePath)) {
|
|
313
|
+
return res.status(400).json({ ok: false, error: "Path is not a sqlite file" });
|
|
314
|
+
}
|
|
315
|
+
const tableName = String(req.query.table || "").trim();
|
|
316
|
+
const limit = req.query.limit;
|
|
317
|
+
const offset = req.query.offset;
|
|
318
|
+
const sqliteResult = readSqliteTableData(
|
|
319
|
+
resolvedPath.absolutePath,
|
|
320
|
+
tableName,
|
|
321
|
+
limit,
|
|
322
|
+
offset,
|
|
323
|
+
);
|
|
324
|
+
if (!sqliteResult.ok) {
|
|
325
|
+
return res.status(400).json({
|
|
326
|
+
ok: false,
|
|
327
|
+
error: sqliteResult.error || "Could not read sqlite table",
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return res.json({
|
|
331
|
+
ok: true,
|
|
332
|
+
path: resolvedPath.relativePath,
|
|
333
|
+
table: sqliteResult.table,
|
|
334
|
+
columns: sqliteResult.columns,
|
|
335
|
+
rows: sqliteResult.rows,
|
|
336
|
+
limit: sqliteResult.limit,
|
|
337
|
+
offset: sqliteResult.offset,
|
|
338
|
+
totalRows: sqliteResult.totalRows,
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
343
342
|
app.get("/api/browse/git-diff", async (req, res) => {
|
|
344
|
-
const resolvedPath = resolveSafePath(
|
|
343
|
+
const resolvedPath = resolveSafePath(
|
|
344
|
+
req.query.path,
|
|
345
|
+
kRootResolved,
|
|
346
|
+
kRootWithSep,
|
|
347
|
+
kRootDisplayName,
|
|
348
|
+
);
|
|
345
349
|
if (!resolvedPath.ok) {
|
|
346
350
|
return res.status(400).json({ ok: false, error: resolvedPath.error });
|
|
347
351
|
}
|
|
@@ -351,13 +355,14 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
351
355
|
}
|
|
352
356
|
|
|
353
357
|
try {
|
|
354
|
-
const statusResult = await runGitCommandWithExitCode(
|
|
355
|
-
"status",
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
358
|
+
const statusResult = await runGitCommandWithExitCode(
|
|
359
|
+
["status", "--porcelain", "--", relativePath],
|
|
360
|
+
kRootResolved,
|
|
361
|
+
);
|
|
362
|
+
if (
|
|
363
|
+
!statusResult.ok &&
|
|
364
|
+
/not a git repository/i.test(statusResult.stderr || "")
|
|
365
|
+
) {
|
|
361
366
|
return res.status(400).json({ ok: false, error: "No git repo at this root" });
|
|
362
367
|
}
|
|
363
368
|
const statusLines = statusResult.stdout
|
|
@@ -367,14 +372,14 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
367
372
|
const isUntracked = statusLines.some((line) => line.startsWith("??"));
|
|
368
373
|
|
|
369
374
|
const diffResult = isUntracked
|
|
370
|
-
? await runGitCommandWithExitCode(
|
|
371
|
-
"diff",
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
375
|
+
? await runGitCommandWithExitCode(
|
|
376
|
+
["diff", "--no-index", "--", "/dev/null", resolvedPath.absolutePath],
|
|
377
|
+
kRootResolved,
|
|
378
|
+
)
|
|
379
|
+
: await runGitCommandWithExitCode(
|
|
380
|
+
["diff", "HEAD", "--", relativePath],
|
|
381
|
+
kRootResolved,
|
|
382
|
+
);
|
|
378
383
|
|
|
379
384
|
const untrackedAllowedFailure =
|
|
380
385
|
isUntracked && diffResult.exitCode === 1 && diffResult.stdout;
|
|
@@ -394,9 +399,10 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
394
399
|
content,
|
|
395
400
|
});
|
|
396
401
|
} catch (error) {
|
|
397
|
-
return res
|
|
398
|
-
|
|
399
|
-
|
|
402
|
+
return res.status(500).json({
|
|
403
|
+
ok: false,
|
|
404
|
+
error: error.message || "Could not load file diff",
|
|
405
|
+
});
|
|
400
406
|
}
|
|
401
407
|
});
|
|
402
408
|
|
|
@@ -404,57 +410,116 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
404
410
|
try {
|
|
405
411
|
const commitMessageRaw = String(req.body?.message || "").trim();
|
|
406
412
|
const commitMessage = commitMessageRaw || "sync changes";
|
|
407
|
-
const statusResult = await runGitCommand(
|
|
413
|
+
const statusResult = await runGitCommand(
|
|
414
|
+
["status", "--porcelain", "--branch"],
|
|
415
|
+
kRootResolved,
|
|
416
|
+
);
|
|
408
417
|
if (!statusResult.ok) {
|
|
409
418
|
if (/not a git repository/i.test(statusResult.error || "")) {
|
|
410
|
-
return res
|
|
411
|
-
.status(400)
|
|
412
|
-
.json({ ok: false, error: "No git repo at this root" });
|
|
419
|
+
return res.status(400).json({ ok: false, error: "No git repo at this root" });
|
|
413
420
|
}
|
|
414
421
|
return res.status(500).json({
|
|
415
422
|
ok: false,
|
|
416
423
|
error: statusResult.error || "Could not read git status",
|
|
417
424
|
});
|
|
418
425
|
}
|
|
419
|
-
const
|
|
426
|
+
const statusLines = statusResult.stdout
|
|
420
427
|
.split("\n")
|
|
421
|
-
.map((line) => line.
|
|
422
|
-
.filter(Boolean)
|
|
428
|
+
.map((line) => line.trimEnd())
|
|
429
|
+
.filter(Boolean);
|
|
430
|
+
const branchLine = statusLines.find((line) => line.startsWith("##")) || "";
|
|
431
|
+
const branchTracking = parseBranchTracking(branchLine);
|
|
432
|
+
const hasChanges =
|
|
433
|
+
statusLines
|
|
434
|
+
.filter((line) => !line.startsWith("##"))
|
|
435
|
+
.map((line) => line.trim())
|
|
436
|
+
.filter(Boolean).length > 0;
|
|
437
|
+
let committed = false;
|
|
438
|
+
let pushed = false;
|
|
439
|
+
let shortHash = "";
|
|
423
440
|
if (!hasChanges) {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
message: "No changes to sync",
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
const addResult = await runGitCommand(["add", "-A"]);
|
|
431
|
-
if (!addResult.ok) {
|
|
432
|
-
return res.status(500).json({
|
|
433
|
-
ok: false,
|
|
434
|
-
error: addResult.error || "Could not stage changes",
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
const commitResult = await runGitCommand(["commit", "-m", commitMessage]);
|
|
438
|
-
if (!commitResult.ok) {
|
|
439
|
-
if (/nothing to commit/i.test(commitResult.error || "")) {
|
|
441
|
+
const hasAheadCommits =
|
|
442
|
+
branchTracking.hasUpstream && branchTracking.aheadCount > 0;
|
|
443
|
+
if (!hasAheadCommits) {
|
|
440
444
|
return res.json({
|
|
441
445
|
ok: true,
|
|
442
446
|
committed: false,
|
|
447
|
+
pushed: false,
|
|
443
448
|
message: "No changes to sync",
|
|
444
449
|
});
|
|
445
450
|
}
|
|
446
|
-
return res.status(500).json({
|
|
447
|
-
ok: false,
|
|
448
|
-
error: commitResult.error || "Could not commit changes",
|
|
449
|
-
});
|
|
450
451
|
}
|
|
451
|
-
|
|
452
|
-
|
|
452
|
+
if (hasChanges) {
|
|
453
|
+
const addResult = await runGitCommand(["add", "-A"], kRootResolved);
|
|
454
|
+
if (!addResult.ok) {
|
|
455
|
+
return res.status(500).json({
|
|
456
|
+
ok: false,
|
|
457
|
+
error: addResult.error || "Could not stage changes",
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
const commitResult = await runGitCommand(
|
|
461
|
+
["commit", "-m", commitMessage],
|
|
462
|
+
kRootResolved,
|
|
463
|
+
);
|
|
464
|
+
if (!commitResult.ok) {
|
|
465
|
+
if (/nothing to commit/i.test(commitResult.error || "")) {
|
|
466
|
+
return res.json({
|
|
467
|
+
ok: true,
|
|
468
|
+
committed: false,
|
|
469
|
+
pushed: false,
|
|
470
|
+
message: "No changes to sync",
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
return res.status(500).json({
|
|
474
|
+
ok: false,
|
|
475
|
+
error: commitResult.error || "Could not commit changes",
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
committed = true;
|
|
479
|
+
const shortHashResult = await runGitCommand(
|
|
480
|
+
["rev-parse", "--short", "HEAD"],
|
|
481
|
+
kRootResolved,
|
|
482
|
+
);
|
|
483
|
+
shortHash = shortHashResult.ok
|
|
484
|
+
? String(shortHashResult.stdout || "").trim()
|
|
485
|
+
: "";
|
|
486
|
+
}
|
|
487
|
+
const shouldPush = branchTracking.hasUpstream
|
|
488
|
+
? branchTracking.aheadCount > 0 || committed
|
|
489
|
+
: committed;
|
|
490
|
+
if (shouldPush) {
|
|
491
|
+
const pushArgs = branchTracking.hasUpstream
|
|
492
|
+
? ["push"]
|
|
493
|
+
: ["push", "-u", "origin", "HEAD"];
|
|
494
|
+
const pushResult = await runGitCommand(pushArgs, kRootResolved);
|
|
495
|
+
if (pushResult.ok) {
|
|
496
|
+
pushed = true;
|
|
497
|
+
} else {
|
|
498
|
+
return res.json({
|
|
499
|
+
ok: true,
|
|
500
|
+
committed,
|
|
501
|
+
pushed: false,
|
|
502
|
+
shortHash,
|
|
503
|
+
message: committed
|
|
504
|
+
? `Committed ${shortHash || "changes"} locally; push failed`
|
|
505
|
+
: "Could not push commits",
|
|
506
|
+
pushError: pushResult.error || "Could not push commits",
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const syncMessage = pushed
|
|
511
|
+
? committed
|
|
512
|
+
? `Committed and pushed ${shortHash || "changes"}`
|
|
513
|
+
: "Pushed local commits"
|
|
514
|
+
: committed
|
|
515
|
+
? `Committed ${shortHash || "changes"}`
|
|
516
|
+
: "No changes to sync";
|
|
453
517
|
return res.json({
|
|
454
518
|
ok: true,
|
|
455
|
-
committed
|
|
519
|
+
committed,
|
|
520
|
+
pushed,
|
|
456
521
|
shortHash,
|
|
457
|
-
message:
|
|
522
|
+
message: syncMessage,
|
|
458
523
|
});
|
|
459
524
|
} catch (error) {
|
|
460
525
|
return res.status(500).json({
|
|
@@ -466,7 +531,12 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
466
531
|
|
|
467
532
|
app.put("/api/browse/write", async (req, res) => {
|
|
468
533
|
const { path: targetPath, content } = req.body || {};
|
|
469
|
-
const resolvedPath = resolveSafePath(
|
|
534
|
+
const resolvedPath = resolveSafePath(
|
|
535
|
+
targetPath,
|
|
536
|
+
kRootResolved,
|
|
537
|
+
kRootWithSep,
|
|
538
|
+
kRootDisplayName,
|
|
539
|
+
);
|
|
470
540
|
if (!resolvedPath.ok) {
|
|
471
541
|
return res.status(400).json({ ok: false, error: resolvedPath.error });
|
|
472
542
|
}
|
|
@@ -474,7 +544,7 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
474
544
|
if (matchesPolicyPath(kLockedBrowsePaths, normalizedPolicyPath)) {
|
|
475
545
|
return res.status(403).json({
|
|
476
546
|
ok: false,
|
|
477
|
-
error: "This file is managed by
|
|
547
|
+
error: "This file is managed by AlphaClaw and cannot be edited.",
|
|
478
548
|
});
|
|
479
549
|
}
|
|
480
550
|
if (typeof content !== "string") {
|
|
@@ -492,7 +562,9 @@ const registerBrowseRoutes = ({ app, fs, kRootDir }) => {
|
|
|
492
562
|
path: resolvedPath.relativePath,
|
|
493
563
|
});
|
|
494
564
|
} catch (error) {
|
|
495
|
-
return res
|
|
565
|
+
return res
|
|
566
|
+
.status(500)
|
|
567
|
+
.json({ ok: false, error: error.message || "Could not save file" });
|
|
496
568
|
}
|
|
497
569
|
});
|
|
498
570
|
};
|