@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
|
@@ -10,6 +10,7 @@ import { showToast } from "./toast.js";
|
|
|
10
10
|
const html = htm.bind(h);
|
|
11
11
|
const kRefreshMs = 10000;
|
|
12
12
|
const kSyncCommitFileNameLimit = 4;
|
|
13
|
+
const kCommitHistoryLimit = 12;
|
|
13
14
|
|
|
14
15
|
const formatCommitTime = (unixSeconds) => {
|
|
15
16
|
if (!unixSeconds) return "";
|
|
@@ -55,11 +56,58 @@ const getChangedFilePresentation = (changedFile) => {
|
|
|
55
56
|
};
|
|
56
57
|
|
|
57
58
|
const formatDelta = (value, prefix) => {
|
|
59
|
+
if (value === null || value === undefined || value === "") return "";
|
|
58
60
|
const numericValue = Number(value);
|
|
59
|
-
if (!Number.isFinite(numericValue) || numericValue
|
|
61
|
+
if (!Number.isFinite(numericValue) || numericValue <= 0) return "";
|
|
60
62
|
return `${prefix}${numericValue}`;
|
|
61
63
|
};
|
|
62
64
|
|
|
65
|
+
const getRemoteSyncPresentation = (summary) => {
|
|
66
|
+
const safeState = String(summary?.syncState || "").trim();
|
|
67
|
+
const aheadCount = Number(summary?.aheadCount) || 0;
|
|
68
|
+
const behindCount = Number(summary?.behindCount) || 0;
|
|
69
|
+
if (safeState === "ahead") {
|
|
70
|
+
return {
|
|
71
|
+
label: "↑",
|
|
72
|
+
title: `Ahead by ${aheadCount}`,
|
|
73
|
+
className: "is-ahead",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (safeState === "behind") {
|
|
77
|
+
return {
|
|
78
|
+
label: "↓",
|
|
79
|
+
title: `Behind by ${behindCount}`,
|
|
80
|
+
className: "is-behind",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (safeState === "diverged") {
|
|
84
|
+
return {
|
|
85
|
+
label: "↕",
|
|
86
|
+
title: `Diverged (${aheadCount} ahead, ${behindCount} behind)`,
|
|
87
|
+
className: "is-diverged",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (safeState === "upstream-gone") {
|
|
91
|
+
return {
|
|
92
|
+
label: "!",
|
|
93
|
+
title: "Upstream missing",
|
|
94
|
+
className: "is-upstream-gone",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (safeState === "no-upstream" || !summary?.hasUpstream) {
|
|
98
|
+
return {
|
|
99
|
+
label: "!",
|
|
100
|
+
title: "Not linked",
|
|
101
|
+
className: "is-no-upstream",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
label: "",
|
|
106
|
+
title: "Up to date",
|
|
107
|
+
className: "is-up-to-date",
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
|
|
63
111
|
const buildSyncCommitMessage = (changedFiles) => {
|
|
64
112
|
const filePaths = Array.isArray(changedFiles)
|
|
65
113
|
? changedFiles
|
|
@@ -138,16 +186,19 @@ export const SidebarGitPanel = ({ onSelectFile = () => {} }) => {
|
|
|
138
186
|
`;
|
|
139
187
|
}
|
|
140
188
|
|
|
141
|
-
const
|
|
189
|
+
const hasUncommittedChanges = (summary.changedFiles || []).length > 0;
|
|
190
|
+
const aheadCount = Number(summary?.aheadCount) || 0;
|
|
191
|
+
const canSyncChanges = hasUncommittedChanges || aheadCount > 0;
|
|
192
|
+
const remoteSync = getRemoteSyncPresentation(summary);
|
|
142
193
|
const handleSyncChanges = async () => {
|
|
143
|
-
if (!
|
|
194
|
+
if (!canSyncChanges || syncing) return;
|
|
144
195
|
try {
|
|
145
196
|
setSyncing(true);
|
|
146
197
|
const commitMessage = buildSyncCommitMessage(summary?.changedFiles || []);
|
|
147
198
|
const syncResult = await syncBrowseChanges(commitMessage);
|
|
148
|
-
if (syncResult?.committed) {
|
|
199
|
+
if (syncResult?.committed || syncResult?.pushed) {
|
|
149
200
|
window.dispatchEvent(new CustomEvent("alphaclaw:browse-git-synced"));
|
|
150
|
-
showToast(syncResult.message || "Changes
|
|
201
|
+
showToast(syncResult.message || "Changes synced", "success");
|
|
151
202
|
} else {
|
|
152
203
|
showToast(syncResult?.message || "No changes to sync", "info");
|
|
153
204
|
}
|
|
@@ -189,14 +240,24 @@ export const SidebarGitPanel = ({ onSelectFile = () => {} }) => {
|
|
|
189
240
|
<${GitBranchLineIcon} className="sidebar-git-bar-icon" />
|
|
190
241
|
<span class="sidebar-git-branch">${summary.branch || "unknown"}</span>
|
|
191
242
|
</span>
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
243
|
+
${remoteSync.label
|
|
244
|
+
? html`
|
|
245
|
+
<span
|
|
246
|
+
class=${`sidebar-git-sync-status ${remoteSync.className}`.trim()}
|
|
247
|
+
title=${remoteSync.title || ""}
|
|
248
|
+
aria-label=${remoteSync.title || ""}
|
|
249
|
+
>
|
|
250
|
+
${remoteSync.label}
|
|
251
|
+
</span>
|
|
252
|
+
`
|
|
253
|
+
: null}
|
|
195
254
|
</div>
|
|
196
255
|
<div class="sidebar-git-scroll">
|
|
197
256
|
${(summary.changedFiles || []).length > 0
|
|
198
257
|
? html`
|
|
199
|
-
<div class="sidebar-git-changes-label">
|
|
258
|
+
<div class="sidebar-git-changes-label">
|
|
259
|
+
${`Unsynced Changes (${summary.changedFilesCount || (summary.changedFiles || []).length})`}
|
|
260
|
+
</div>
|
|
200
261
|
<ul class="sidebar-git-changes-list">
|
|
201
262
|
${(summary.changedFiles || []).map((changedFile) => {
|
|
202
263
|
const presentation = getChangedFilePresentation(changedFile);
|
|
@@ -229,7 +290,7 @@ export const SidebarGitPanel = ({ onSelectFile = () => {} }) => {
|
|
|
229
290
|
<div class="sidebar-git-actions">
|
|
230
291
|
<${ActionButton}
|
|
231
292
|
onClick=${handleSyncChanges}
|
|
232
|
-
disabled=${!
|
|
293
|
+
disabled=${!canSyncChanges}
|
|
233
294
|
loading=${syncing}
|
|
234
295
|
loadingMode="inline"
|
|
235
296
|
idleLabel="Sync Changes"
|
|
@@ -245,7 +306,7 @@ export const SidebarGitPanel = ({ onSelectFile = () => {} }) => {
|
|
|
245
306
|
? html`
|
|
246
307
|
<div class="sidebar-git-changes-label">commit history</div>
|
|
247
308
|
<ul class="sidebar-git-list">
|
|
248
|
-
${(summary.commits || []).slice(0,
|
|
309
|
+
${(summary.commits || []).slice(0, kCommitHistoryLimit).map(
|
|
249
310
|
(commit) => html`
|
|
250
311
|
<li title=${formatCommitTime(commit.timestamp)}>
|
|
251
312
|
${commit.url
|
|
@@ -122,7 +122,10 @@ const kUsageMetricUiSettingKey = "usageMetric";
|
|
|
122
122
|
const SummaryCard = ({ title, tokens, cost }) => html`
|
|
123
123
|
<div class="bg-surface border border-border rounded-xl p-4">
|
|
124
124
|
<h3 class="card-label text-xs">${title}</h3>
|
|
125
|
-
<div class="text-lg font-semibold mt-1"
|
|
125
|
+
<div class="text-lg font-semibold mt-1">
|
|
126
|
+
${formatTokens(tokens)}
|
|
127
|
+
<span class="text-xs text-[var(--text-muted)] ml-1">tokens</span>
|
|
128
|
+
</div>
|
|
126
129
|
<div class="text-xs text-[var(--text-muted)] mt-1">${formatUsd(cost)}</div>
|
|
127
130
|
</div>
|
|
128
131
|
`;
|
|
@@ -137,6 +137,9 @@ export const WatchdogTab = ({
|
|
|
137
137
|
restartingGateway = false,
|
|
138
138
|
onRestartGateway,
|
|
139
139
|
restartSignal = 0,
|
|
140
|
+
openclawUpdateInProgress = false,
|
|
141
|
+
onOpenclawVersionActionComplete = () => {},
|
|
142
|
+
onOpenclawUpdate,
|
|
140
143
|
}) => {
|
|
141
144
|
const eventsPoll = usePolling(() => fetchWatchdogEvents(20), 15000);
|
|
142
145
|
const resourcesPoll = usePolling(() => fetchWatchdogResources(), 5000);
|
|
@@ -300,6 +303,9 @@ export const WatchdogTab = ({
|
|
|
300
303
|
watchdogStatus=${currentWatchdogStatus}
|
|
301
304
|
onRepair=${onRepair}
|
|
302
305
|
repairing=${isRepairInProgress}
|
|
306
|
+
openclawUpdateInProgress=${openclawUpdateInProgress}
|
|
307
|
+
onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
|
|
308
|
+
onOpenclawUpdate=${onOpenclawUpdate}
|
|
303
309
|
/>
|
|
304
310
|
|
|
305
311
|
${(() => {
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -400,6 +400,22 @@ export const fetchBrowseFileDiff = async (filePath) => {
|
|
|
400
400
|
return parseJsonOrThrow(res, 'Could not load file diff');
|
|
401
401
|
};
|
|
402
402
|
|
|
403
|
+
export const fetchBrowseSqliteTable = async ({
|
|
404
|
+
filePath,
|
|
405
|
+
table,
|
|
406
|
+
limit = 50,
|
|
407
|
+
offset = 0,
|
|
408
|
+
}) => {
|
|
409
|
+
const params = new URLSearchParams({
|
|
410
|
+
path: String(filePath || ""),
|
|
411
|
+
table: String(table || ""),
|
|
412
|
+
limit: String(limit),
|
|
413
|
+
offset: String(offset),
|
|
414
|
+
});
|
|
415
|
+
const res = await authFetch(`/api/browse/sqlite-table?${params.toString()}`);
|
|
416
|
+
return parseJsonOrThrow(res, "Could not load sqlite table data");
|
|
417
|
+
};
|
|
418
|
+
|
|
403
419
|
export const syncBrowseChanges = async (message = "") => {
|
|
404
420
|
const res = await authFetch('/api/browse/git-sync', {
|
|
405
421
|
method: 'POST',
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const kProtectedBrowsePaths = new Set([
|
|
2
|
+
"openclaw.json",
|
|
3
|
+
"devices/paired.json",
|
|
4
|
+
]);
|
|
5
|
+
|
|
6
|
+
export const kLockedBrowsePaths = new Set([
|
|
7
|
+
"hooks/bootstrap/agents.md",
|
|
8
|
+
"hooks/bootstrap/tools.md",
|
|
9
|
+
"skills/control-ui/skill.md",
|
|
10
|
+
".alphaclaw/hourly-git-sync.sh",
|
|
11
|
+
".alphaclaw/.cli-device-auto-approved",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export const normalizeBrowsePolicyPath = (inputPath) =>
|
|
15
|
+
String(inputPath || "")
|
|
16
|
+
.replaceAll("\\", "/")
|
|
17
|
+
.replace(/^\.\/+/, "")
|
|
18
|
+
.replace(/^\/+/, "")
|
|
19
|
+
.trim()
|
|
20
|
+
.toLowerCase();
|
|
21
|
+
|
|
22
|
+
export const matchesBrowsePolicyPath = (policyPathSet, normalizedPath) => {
|
|
23
|
+
const safeNormalizedPath = String(normalizedPath || "").trim();
|
|
24
|
+
if (!safeNormalizedPath) return false;
|
|
25
|
+
for (const policyPath of policyPathSet) {
|
|
26
|
+
if (
|
|
27
|
+
safeNormalizedPath === policyPath ||
|
|
28
|
+
safeNormalizedPath.endsWith(`/${policyPath}`)
|
|
29
|
+
) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
};
|
package/lib/scripts/git
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Git auth shim -- injects GITHUB_TOKEN credentials for network operations
|
|
3
|
+
# inside the configured OpenClaw repo so the agent can use plain git commands.
|
|
4
|
+
|
|
5
|
+
REAL_GIT="@@REAL_GIT@@"
|
|
6
|
+
OPENCLAW_REPO_ROOT="@@OPENCLAW_REPO_ROOT@@"
|
|
7
|
+
ASKPASS_PATH="/tmp/alphaclaw-git-askpass.sh"
|
|
8
|
+
|
|
9
|
+
SUBCMD=""
|
|
10
|
+
for arg in "$@"; do
|
|
11
|
+
case "$arg" in
|
|
12
|
+
-*) ;;
|
|
13
|
+
*) SUBCMD="$arg"; break ;;
|
|
14
|
+
esac
|
|
15
|
+
done
|
|
16
|
+
|
|
17
|
+
needs_auth() {
|
|
18
|
+
case "$SUBCMD" in
|
|
19
|
+
push|pull|fetch|clone|ls-remote) return 0 ;;
|
|
20
|
+
*) return 1 ;;
|
|
21
|
+
esac
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
in_openclaw_root() {
|
|
25
|
+
if [ -z "$OPENCLAW_REPO_ROOT" ]; then
|
|
26
|
+
return 1
|
|
27
|
+
fi
|
|
28
|
+
case "$(pwd)" in
|
|
29
|
+
"$OPENCLAW_REPO_ROOT"|"${OPENCLAW_REPO_ROOT}"/*) return 0 ;;
|
|
30
|
+
*) return 1 ;;
|
|
31
|
+
esac
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if [ "${ALPHACLAW_GIT_NO_AUTH:-}" = "1" ] || [ -z "${GITHUB_TOKEN:-}" ] || ! needs_auth || ! in_openclaw_root; then
|
|
35
|
+
exec "$REAL_GIT" "$@"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
export GIT_TERMINAL_PROMPT=0
|
|
39
|
+
export GIT_ASKPASS="$ASKPASS_PATH"
|
|
40
|
+
exec "$REAL_GIT" "$@"
|
package/lib/server/constants.js
CHANGED
|
@@ -274,6 +274,13 @@ const kChannelDefs = {
|
|
|
274
274
|
telegram: { envKey: "TELEGRAM_BOT_TOKEN" },
|
|
275
275
|
discord: { envKey: "DISCORD_BOT_TOKEN" },
|
|
276
276
|
};
|
|
277
|
+
const kLockedBrowsePaths = new Set([
|
|
278
|
+
"hooks/bootstrap/agents.md",
|
|
279
|
+
"hooks/bootstrap/tools.md",
|
|
280
|
+
"skills/control-ui/skill.md",
|
|
281
|
+
".alphaclaw/hourly-git-sync.sh",
|
|
282
|
+
".alphaclaw/.cli-device-auto-approved",
|
|
283
|
+
]);
|
|
277
284
|
|
|
278
285
|
const SETUP_API_PREFIXES = [
|
|
279
286
|
"/api/status",
|
|
@@ -343,6 +350,7 @@ module.exports = {
|
|
|
343
350
|
kSystemVars,
|
|
344
351
|
kKnownVars,
|
|
345
352
|
kKnownKeys,
|
|
353
|
+
kLockedBrowsePaths,
|
|
346
354
|
SCOPE_MAP,
|
|
347
355
|
REVERSE_SCOPE_MAP,
|
|
348
356
|
BASE_SCOPES,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const kDefaultTreeDepth = 10;
|
|
2
|
+
const kIgnoredDirectoryNames = new Set([
|
|
3
|
+
".git",
|
|
4
|
+
".alphaclaw",
|
|
5
|
+
"node_modules",
|
|
6
|
+
".cache",
|
|
7
|
+
"dist",
|
|
8
|
+
"build",
|
|
9
|
+
]);
|
|
10
|
+
const kImageMimeTypeByExtension = new Map([
|
|
11
|
+
[".png", "image/png"],
|
|
12
|
+
[".jpg", "image/jpeg"],
|
|
13
|
+
[".jpeg", "image/jpeg"],
|
|
14
|
+
[".gif", "image/gif"],
|
|
15
|
+
[".webp", "image/webp"],
|
|
16
|
+
[".svg", "image/svg+xml"],
|
|
17
|
+
[".bmp", "image/bmp"],
|
|
18
|
+
[".ico", "image/x-icon"],
|
|
19
|
+
[".avif", "image/avif"],
|
|
20
|
+
]);
|
|
21
|
+
const kCommitHistoryLimit = 12;
|
|
22
|
+
const kAudioMimeTypeByExtension = new Map([
|
|
23
|
+
[".mp3", "audio/mpeg"],
|
|
24
|
+
[".wav", "audio/wav"],
|
|
25
|
+
[".ogg", "audio/ogg"],
|
|
26
|
+
[".oga", "audio/ogg"],
|
|
27
|
+
[".m4a", "audio/mp4"],
|
|
28
|
+
[".aac", "audio/aac"],
|
|
29
|
+
[".flac", "audio/flac"],
|
|
30
|
+
[".opus", "audio/opus"],
|
|
31
|
+
[".weba", "audio/webm"],
|
|
32
|
+
]);
|
|
33
|
+
const kSqliteFileExtensions = new Set([
|
|
34
|
+
".sqlite",
|
|
35
|
+
".sqlite3",
|
|
36
|
+
".db",
|
|
37
|
+
".db3",
|
|
38
|
+
".sdb",
|
|
39
|
+
".sqlitedb",
|
|
40
|
+
]);
|
|
41
|
+
const kSqliteTablePageSize = 50;
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
kDefaultTreeDepth,
|
|
45
|
+
kIgnoredDirectoryNames,
|
|
46
|
+
kImageMimeTypeByExtension,
|
|
47
|
+
kCommitHistoryLimit,
|
|
48
|
+
kAudioMimeTypeByExtension,
|
|
49
|
+
kSqliteFileExtensions,
|
|
50
|
+
kSqliteTablePageSize,
|
|
51
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const {
|
|
3
|
+
kImageMimeTypeByExtension,
|
|
4
|
+
kAudioMimeTypeByExtension,
|
|
5
|
+
kSqliteFileExtensions,
|
|
6
|
+
} = require("./constants");
|
|
7
|
+
|
|
8
|
+
const isLikelyBinaryFile = (fs, targetPath) => {
|
|
9
|
+
let fileHandle = null;
|
|
10
|
+
try {
|
|
11
|
+
fileHandle = fs.openSync(targetPath, "r");
|
|
12
|
+
const sample = Buffer.alloc(512);
|
|
13
|
+
const bytesRead = fs.readSync(fileHandle, sample, 0, sample.length, 0);
|
|
14
|
+
for (let index = 0; index < bytesRead; index += 1) {
|
|
15
|
+
if (sample[index] === 0) return true;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
} finally {
|
|
19
|
+
if (fileHandle !== null) fs.closeSync(fileHandle);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const getImageMimeType = (targetPath) => {
|
|
24
|
+
const extension = String(path.extname(targetPath || "") || "").toLowerCase();
|
|
25
|
+
return kImageMimeTypeByExtension.get(extension) || "";
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const getAudioMimeType = (targetPath) => {
|
|
29
|
+
const extension = String(path.extname(targetPath || "") || "").toLowerCase();
|
|
30
|
+
return kAudioMimeTypeByExtension.get(extension) || "";
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const isSqliteFilePath = (targetPath) => {
|
|
34
|
+
const extension = String(path.extname(targetPath || "") || "").toLowerCase();
|
|
35
|
+
return kSqliteFileExtensions.has(extension);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
isLikelyBinaryFile,
|
|
40
|
+
getImageMimeType,
|
|
41
|
+
getAudioMimeType,
|
|
42
|
+
isSqliteFilePath,
|
|
43
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const { execFile } = require("child_process");
|
|
2
|
+
|
|
3
|
+
const runGitCommand = (args, kRootResolved) =>
|
|
4
|
+
new Promise((resolve) => {
|
|
5
|
+
execFile(
|
|
6
|
+
"git",
|
|
7
|
+
args,
|
|
8
|
+
{ timeout: 10000, cwd: kRootResolved },
|
|
9
|
+
(error, stdout, stderr) => {
|
|
10
|
+
if (error) {
|
|
11
|
+
return resolve({
|
|
12
|
+
ok: false,
|
|
13
|
+
error: String(
|
|
14
|
+
stderr || stdout || error.message || "git command failed",
|
|
15
|
+
).trim(),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return resolve({ ok: true, stdout: String(stdout || "") });
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const runGitCommandWithExitCode = (args, kRootResolved) =>
|
|
24
|
+
new Promise((resolve) => {
|
|
25
|
+
execFile(
|
|
26
|
+
"git",
|
|
27
|
+
args,
|
|
28
|
+
{ timeout: 10000, cwd: kRootResolved },
|
|
29
|
+
(error, stdout, stderr) => {
|
|
30
|
+
const safeStdout = String(stdout || "");
|
|
31
|
+
const safeStderr = String(stderr || "");
|
|
32
|
+
if (!error) {
|
|
33
|
+
return resolve({
|
|
34
|
+
ok: true,
|
|
35
|
+
stdout: safeStdout,
|
|
36
|
+
stderr: safeStderr,
|
|
37
|
+
exitCode: 0,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return resolve({
|
|
41
|
+
ok: false,
|
|
42
|
+
stdout: safeStdout,
|
|
43
|
+
stderr: safeStderr,
|
|
44
|
+
exitCode: Number.isInteger(error.code) ? error.code : 1,
|
|
45
|
+
error: String(error.message || "git command failed"),
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const parseGithubRepoSlug = (value) => {
|
|
52
|
+
const raw = String(value || "").trim();
|
|
53
|
+
if (!raw) return "";
|
|
54
|
+
return raw
|
|
55
|
+
.replace(/^git@github\.com:/i, "")
|
|
56
|
+
.replace(/^https:\/\/github\.com\//i, "")
|
|
57
|
+
.replace(/\.git$/i, "")
|
|
58
|
+
.trim();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const normalizeChangedPath = (rawPath) => {
|
|
62
|
+
const value = String(rawPath || "").trim();
|
|
63
|
+
if (!value) return "";
|
|
64
|
+
if (value.includes(" -> ")) {
|
|
65
|
+
const segments = value.split(" -> ");
|
|
66
|
+
return String(segments[segments.length - 1] || "").trim();
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const parseBranchTracking = (branchLine) => {
|
|
72
|
+
const safeBranchLine = String(branchLine || "").trim();
|
|
73
|
+
const withoutPrefix = safeBranchLine.replace(/^##\s*/, "");
|
|
74
|
+
const [localBranchRaw = "", trackingRaw = ""] = withoutPrefix.split("...");
|
|
75
|
+
const localBranch = localBranchRaw || "unknown";
|
|
76
|
+
const trackingSegment = String(trackingRaw || "").trim();
|
|
77
|
+
const upstreamBranch = trackingSegment.split(" [")[0]?.trim() || "";
|
|
78
|
+
const hasUpstream = upstreamBranch.length > 0;
|
|
79
|
+
const countsMatch = trackingSegment.match(/\[([^\]]+)\]/);
|
|
80
|
+
const countsRaw = countsMatch?.[1] || "";
|
|
81
|
+
const countParts = countsRaw
|
|
82
|
+
.split(",")
|
|
83
|
+
.map((part) => String(part || "").trim())
|
|
84
|
+
.filter(Boolean);
|
|
85
|
+
let aheadCount = 0;
|
|
86
|
+
let behindCount = 0;
|
|
87
|
+
let upstreamGone = false;
|
|
88
|
+
countParts.forEach((part) => {
|
|
89
|
+
const aheadMatch = part.match(/^ahead\s+(\d+)$/i);
|
|
90
|
+
if (aheadMatch?.[1]) {
|
|
91
|
+
aheadCount = Number.parseInt(aheadMatch[1], 10) || 0;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const behindMatch = part.match(/^behind\s+(\d+)$/i);
|
|
95
|
+
if (behindMatch?.[1]) {
|
|
96
|
+
behindCount = Number.parseInt(behindMatch[1], 10) || 0;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (part.toLowerCase() === "gone") {
|
|
100
|
+
upstreamGone = true;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
const syncState = !hasUpstream
|
|
104
|
+
? "no-upstream"
|
|
105
|
+
: upstreamGone
|
|
106
|
+
? "upstream-gone"
|
|
107
|
+
: aheadCount > 0 && behindCount > 0
|
|
108
|
+
? "diverged"
|
|
109
|
+
: aheadCount > 0
|
|
110
|
+
? "ahead"
|
|
111
|
+
: behindCount > 0
|
|
112
|
+
? "behind"
|
|
113
|
+
: "up-to-date";
|
|
114
|
+
return {
|
|
115
|
+
branch: localBranch,
|
|
116
|
+
upstreamBranch,
|
|
117
|
+
hasUpstream,
|
|
118
|
+
upstreamGone,
|
|
119
|
+
aheadCount,
|
|
120
|
+
behindCount,
|
|
121
|
+
syncState,
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
runGitCommand,
|
|
127
|
+
runGitCommandWithExitCode,
|
|
128
|
+
parseGithubRepoSlug,
|
|
129
|
+
normalizeChangedPath,
|
|
130
|
+
parseBranchTracking,
|
|
131
|
+
};
|