clay-server 2.11.0 → 2.12.0-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/cli.js +16 -4
- package/lib/daemon.js +167 -0
- package/lib/project.js +83 -1
- package/lib/public/app.js +567 -20
- package/lib/public/css/icon-strip.css +308 -5
- package/lib/public/css/menus.css +1 -16
- package/lib/public/css/messages.css +7 -0
- package/lib/public/css/session-search.css +150 -0
- package/lib/public/css/sidebar.css +30 -0
- package/lib/public/css/tooltip.css +20 -0
- package/lib/public/index.html +2 -1
- package/lib/public/modules/notifications.js +1 -58
- package/lib/public/modules/session-search.js +440 -0
- package/lib/public/modules/sidebar.js +576 -148
- package/lib/public/modules/tooltip.js +123 -0
- package/lib/public/style.css +2 -0
- package/lib/server.js +46 -3
- package/lib/sessions.js +37 -0
- package/lib/worktree.js +134 -0
- package/package.json +1 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// --- Global JS tooltip system ---
|
|
2
|
+
// Unified tooltip using [data-tip] attribute.
|
|
3
|
+
// Usage: initTooltips() auto-binds, converts [title] to [data-tip].
|
|
4
|
+
// Call registerTooltip(el, text) for dynamic elements.
|
|
5
|
+
|
|
6
|
+
var tooltipEl = null;
|
|
7
|
+
var showTimer = null;
|
|
8
|
+
var SHOW_DELAY = 120;
|
|
9
|
+
|
|
10
|
+
function initTooltips() {
|
|
11
|
+
// Create singleton tooltip element
|
|
12
|
+
tooltipEl = document.createElement("div");
|
|
13
|
+
tooltipEl.className = "tooltip";
|
|
14
|
+
document.body.appendChild(tooltipEl);
|
|
15
|
+
|
|
16
|
+
// Convert existing title attributes to data-tip in target areas
|
|
17
|
+
convertTitles();
|
|
18
|
+
|
|
19
|
+
// Delegate hover events on document for [data-tip]
|
|
20
|
+
document.addEventListener("mouseover", function (e) {
|
|
21
|
+
var target = e.target.closest("[data-tip]");
|
|
22
|
+
if (!target) return;
|
|
23
|
+
scheduleShow(target);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
document.addEventListener("mouseout", function (e) {
|
|
27
|
+
var target = e.target.closest("[data-tip]");
|
|
28
|
+
if (!target) return;
|
|
29
|
+
cancelShow();
|
|
30
|
+
hideTooltip();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
document.addEventListener("pointerdown", function () {
|
|
34
|
+
cancelShow();
|
|
35
|
+
hideTooltip();
|
|
36
|
+
}, true);
|
|
37
|
+
|
|
38
|
+
document.addEventListener("scroll", function () {
|
|
39
|
+
cancelShow();
|
|
40
|
+
hideTooltip();
|
|
41
|
+
}, true);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function convertTitles() {
|
|
45
|
+
var selectors = [
|
|
46
|
+
"#top-bar [title]",
|
|
47
|
+
".title-bar-content [title]",
|
|
48
|
+
"#input-area [title]",
|
|
49
|
+
];
|
|
50
|
+
var els = document.querySelectorAll(selectors.join(", "));
|
|
51
|
+
for (var i = 0; i < els.length; i++) {
|
|
52
|
+
var el = els[i];
|
|
53
|
+
if (!el.getAttribute("data-tip")) {
|
|
54
|
+
el.setAttribute("data-tip", el.getAttribute("title"));
|
|
55
|
+
el.removeAttribute("title");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function registerTooltip(el, text) {
|
|
61
|
+
el.setAttribute("data-tip", text);
|
|
62
|
+
el.removeAttribute("title");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function scheduleShow(el) {
|
|
66
|
+
cancelShow();
|
|
67
|
+
showTimer = setTimeout(function () {
|
|
68
|
+
showTooltipAt(el);
|
|
69
|
+
}, SHOW_DELAY);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function cancelShow() {
|
|
73
|
+
if (showTimer) {
|
|
74
|
+
clearTimeout(showTimer);
|
|
75
|
+
showTimer = null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function showTooltipAt(target) {
|
|
80
|
+
if (!tooltipEl) return;
|
|
81
|
+
var text = target.getAttribute("data-tip");
|
|
82
|
+
if (!text) return;
|
|
83
|
+
|
|
84
|
+
tooltipEl.textContent = text;
|
|
85
|
+
tooltipEl.style.top = "-9999px";
|
|
86
|
+
tooltipEl.style.left = "0";
|
|
87
|
+
tooltipEl.style.right = "";
|
|
88
|
+
tooltipEl.classList.add("visible");
|
|
89
|
+
|
|
90
|
+
// Position after layout
|
|
91
|
+
var tipW = tooltipEl.offsetWidth;
|
|
92
|
+
var tipH = tooltipEl.offsetHeight;
|
|
93
|
+
var rect = target.getBoundingClientRect();
|
|
94
|
+
var gap = 8;
|
|
95
|
+
var winW = window.innerWidth;
|
|
96
|
+
var winH = window.innerHeight;
|
|
97
|
+
|
|
98
|
+
// Prefer bottom, fallback to top
|
|
99
|
+
var top = rect.bottom + gap;
|
|
100
|
+
if (top + tipH > winH - 8) {
|
|
101
|
+
top = rect.top - tipH - gap;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Center horizontally, clamp to viewport
|
|
105
|
+
var centerX = rect.left + rect.width / 2;
|
|
106
|
+
var left = centerX - tipW / 2;
|
|
107
|
+
if (left + tipW > winW - 8) {
|
|
108
|
+
tooltipEl.style.left = "";
|
|
109
|
+
tooltipEl.style.right = "8px";
|
|
110
|
+
} else {
|
|
111
|
+
tooltipEl.style.left = Math.max(8, left) + "px";
|
|
112
|
+
tooltipEl.style.right = "";
|
|
113
|
+
}
|
|
114
|
+
tooltipEl.style.top = top + "px";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function hideTooltip() {
|
|
118
|
+
if (tooltipEl) {
|
|
119
|
+
tooltipEl.classList.remove("visible");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { initTooltips, registerTooltip };
|
package/lib/public/style.css
CHANGED
package/lib/server.js
CHANGED
|
@@ -220,12 +220,37 @@ function isAuthed(req, authToken) {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
// --- Multi-user auth helpers ---
|
|
223
|
-
// Multi-user auth tokens:
|
|
223
|
+
// Multi-user auth tokens: persisted to disk so they survive restarts
|
|
224
|
+
var TOKENS_FILE = path.join(CONFIG_DIR, "auth-tokens.json");
|
|
224
225
|
var multiUserTokens = {}; // token → userId
|
|
225
226
|
|
|
227
|
+
function loadTokens() {
|
|
228
|
+
try {
|
|
229
|
+
var raw = fs.readFileSync(TOKENS_FILE, "utf8");
|
|
230
|
+
var data = JSON.parse(raw);
|
|
231
|
+
if (data && typeof data === "object") {
|
|
232
|
+
multiUserTokens = data;
|
|
233
|
+
}
|
|
234
|
+
} catch (e) {
|
|
235
|
+
multiUserTokens = {};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function saveTokens() {
|
|
240
|
+
try {
|
|
241
|
+
fs.mkdirSync(path.dirname(TOKENS_FILE), { recursive: true });
|
|
242
|
+
var tmpPath = TOKENS_FILE + ".tmp";
|
|
243
|
+
fs.writeFileSync(tmpPath, JSON.stringify(multiUserTokens));
|
|
244
|
+
fs.renameSync(tmpPath, TOKENS_FILE);
|
|
245
|
+
} catch (e) {}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
loadTokens();
|
|
249
|
+
|
|
226
250
|
function createMultiUserSession(userId, tlsOptions) {
|
|
227
251
|
var token = users.generateUserAuthToken(userId);
|
|
228
252
|
multiUserTokens[token] = userId;
|
|
253
|
+
saveTokens();
|
|
229
254
|
var cookie = "relay_auth_user=" + token + "; Path=/; HttpOnly; SameSite=Strict; Max-Age=31536000" + (tlsOptions ? "; Secure" : "");
|
|
230
255
|
return { token: token, cookie: cookie };
|
|
231
256
|
}
|
|
@@ -244,6 +269,17 @@ function isMultiUserAuthed(req) {
|
|
|
244
269
|
return !!getMultiUserFromReq(req);
|
|
245
270
|
}
|
|
246
271
|
|
|
272
|
+
function revokeUserTokens(userId) {
|
|
273
|
+
var changed = false;
|
|
274
|
+
for (var token in multiUserTokens) {
|
|
275
|
+
if (multiUserTokens[token] === userId) {
|
|
276
|
+
delete multiUserTokens[token];
|
|
277
|
+
changed = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (changed) saveTokens();
|
|
281
|
+
}
|
|
282
|
+
|
|
247
283
|
// --- PIN rate limiting ---
|
|
248
284
|
var pinAttempts = {}; // ip → { count, lastAttempt }
|
|
249
285
|
var PIN_MAX_ATTEMPTS = 5;
|
|
@@ -359,6 +395,7 @@ function createServer(opts) {
|
|
|
359
395
|
var onSetProjectVisibility = opts.onSetProjectVisibility || null;
|
|
360
396
|
var onSetProjectAllowedUsers = opts.onSetProjectAllowedUsers || null;
|
|
361
397
|
var onGetProjectAccess = opts.onGetProjectAccess || null;
|
|
398
|
+
var onCreateWorktree = opts.onCreateWorktree || null;
|
|
362
399
|
var onUserProvisioned = opts.onUserProvisioned || null;
|
|
363
400
|
var onUserDeleted = opts.onUserDeleted || null;
|
|
364
401
|
var getRemovedProjects = opts.getRemovedProjects || function () { return []; };
|
|
@@ -1050,6 +1087,8 @@ function createServer(opts) {
|
|
|
1050
1087
|
res.end(JSON.stringify({ error: result.error }));
|
|
1051
1088
|
return;
|
|
1052
1089
|
}
|
|
1090
|
+
// Remove auth tokens for deleted user
|
|
1091
|
+
revokeUserTokens(targetUserId);
|
|
1053
1092
|
// Deactivate the Linux account if applicable
|
|
1054
1093
|
if (onUserDeleted && targetLinuxUser) {
|
|
1055
1094
|
onUserDeleted(targetUserId, targetLinuxUser);
|
|
@@ -1975,7 +2014,9 @@ function createServer(opts) {
|
|
|
1975
2014
|
wsUser = getMultiUserFromReq(req);
|
|
1976
2015
|
// Check project access for multi-user mode
|
|
1977
2016
|
if (wsUser && onGetProjectAccess) {
|
|
1978
|
-
|
|
2017
|
+
// For worktree projects, inherit access from parent
|
|
2018
|
+
var accessSlug = (wsSlug.indexOf("--") !== -1) ? wsSlug.split("--")[0] : wsSlug;
|
|
2019
|
+
var projectAccess = onGetProjectAccess(accessSlug);
|
|
1979
2020
|
if (debug) console.log("[server] WS access check:", wsSlug, "user:", wsUser.id, "role:", wsUser.role, "visibility:", projectAccess && projectAccess.visibility, "ownerId:", projectAccess && projectAccess.ownerId, "allowed:", projectAccess && projectAccess.allowedUsers);
|
|
1980
2021
|
if (projectAccess && !projectAccess.error) {
|
|
1981
2022
|
if (!users.canAccessProject(wsUser.id, projectAccess)) {
|
|
@@ -2094,7 +2135,7 @@ function createServer(opts) {
|
|
|
2094
2135
|
}
|
|
2095
2136
|
|
|
2096
2137
|
// --- Project management ---
|
|
2097
|
-
function addProject(cwd, slug, title, icon, projectOwnerId) {
|
|
2138
|
+
function addProject(cwd, slug, title, icon, projectOwnerId, worktreeMeta) {
|
|
2098
2139
|
if (projects.has(slug)) return false;
|
|
2099
2140
|
var ctx = createProjectContext({
|
|
2100
2141
|
cwd: cwd,
|
|
@@ -2102,6 +2143,7 @@ function createServer(opts) {
|
|
|
2102
2143
|
title: title || null,
|
|
2103
2144
|
icon: icon || null,
|
|
2104
2145
|
projectOwnerId: projectOwnerId || null,
|
|
2146
|
+
worktreeMeta: worktreeMeta || null,
|
|
2105
2147
|
pushModule: pushModule,
|
|
2106
2148
|
debug: debug,
|
|
2107
2149
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
@@ -2190,6 +2232,7 @@ function createServer(opts) {
|
|
|
2190
2232
|
onCreateProject: onCreateProject,
|
|
2191
2233
|
onCloneProject: onCloneProject,
|
|
2192
2234
|
onRemoveProject: onRemoveProject,
|
|
2235
|
+
onCreateWorktree: onCreateWorktree,
|
|
2193
2236
|
onReorderProjects: onReorderProjects,
|
|
2194
2237
|
onSetProjectTitle: onSetProjectTitle,
|
|
2195
2238
|
onSetProjectIcon: onSetProjectIcon,
|
package/lib/sessions.js
CHANGED
|
@@ -540,6 +540,42 @@ function createSessionManager(opts) {
|
|
|
540
540
|
return results;
|
|
541
541
|
}
|
|
542
542
|
|
|
543
|
+
function searchSessionContent(localId, query) {
|
|
544
|
+
if (!query) return { hits: [], total: 0 };
|
|
545
|
+
var session = sessions.get(localId);
|
|
546
|
+
if (!session) return { hits: [], total: 0 };
|
|
547
|
+
var q = query.toLowerCase();
|
|
548
|
+
var history = session.history;
|
|
549
|
+
var hits = [];
|
|
550
|
+
var lastAssistantHitTurn = -1; // track current assistant turn to deduplicate delta hits
|
|
551
|
+
var currentTurnStart = -1;
|
|
552
|
+
for (var i = 0; i < history.length; i++) {
|
|
553
|
+
var entry = history[i];
|
|
554
|
+
if (entry.type === "user_message") {
|
|
555
|
+
currentTurnStart = i;
|
|
556
|
+
lastAssistantHitTurn = -1;
|
|
557
|
+
}
|
|
558
|
+
if ((entry.type === "delta" || entry.type === "user_message") && entry.text) {
|
|
559
|
+
// Skip duplicate delta hits within the same assistant turn
|
|
560
|
+
if (entry.type === "delta" && currentTurnStart === lastAssistantHitTurn) continue;
|
|
561
|
+
var text = entry.text;
|
|
562
|
+
var lowerText = text.toLowerCase();
|
|
563
|
+
var idx = lowerText.indexOf(q);
|
|
564
|
+
if (idx === -1) continue;
|
|
565
|
+
var start = Math.max(0, idx - 15);
|
|
566
|
+
var end = Math.min(text.length, idx + query.length + 15);
|
|
567
|
+
var snippet = (start > 0 ? "\u2026" : "") + text.substring(start, end) + (end < text.length ? "\u2026" : "");
|
|
568
|
+
if (entry.type === "delta") lastAssistantHitTurn = currentTurnStart;
|
|
569
|
+
hits.push({
|
|
570
|
+
historyIndex: i,
|
|
571
|
+
snippet: snippet,
|
|
572
|
+
role: entry.type === "user_message" ? "user" : "assistant",
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return { hits: hits, total: history.length };
|
|
577
|
+
}
|
|
578
|
+
|
|
543
579
|
function migrateSessionTitles(getSDK, migrateCwd) {
|
|
544
580
|
var toMigrate = [];
|
|
545
581
|
sessions.forEach(function(s) {
|
|
@@ -597,6 +633,7 @@ function createSessionManager(opts) {
|
|
|
597
633
|
findTurnBoundary: findTurnBoundary,
|
|
598
634
|
replayHistory: replayHistory,
|
|
599
635
|
searchSessions: searchSessions,
|
|
636
|
+
searchSessionContent: searchSessionContent,
|
|
600
637
|
setResolveLoopInfo: setResolveLoopInfo,
|
|
601
638
|
migrateSessionTitles: migrateSessionTitles,
|
|
602
639
|
setSessionVisibility: function (localId, visibility) {
|
package/lib/worktree.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
var { execFileSync } = require("child_process");
|
|
2
|
+
var path = require("path");
|
|
3
|
+
|
|
4
|
+
// Parse `git worktree list --porcelain` output into structured objects
|
|
5
|
+
function parseWorktreeOutput(output) {
|
|
6
|
+
var worktrees = [];
|
|
7
|
+
var current = null;
|
|
8
|
+
var lines = output.split("\n");
|
|
9
|
+
for (var i = 0; i < lines.length; i++) {
|
|
10
|
+
var line = lines[i];
|
|
11
|
+
if (line.indexOf("worktree ") === 0) {
|
|
12
|
+
if (current) worktrees.push(current);
|
|
13
|
+
current = { path: line.slice(9), branch: null, bare: false, detached: false };
|
|
14
|
+
} else if (line.indexOf("branch ") === 0 && current) {
|
|
15
|
+
// refs/heads/feat/login -> feat/login
|
|
16
|
+
var ref = line.slice(7);
|
|
17
|
+
var headsIdx = ref.indexOf("refs/heads/");
|
|
18
|
+
current.branch = headsIdx === 0 ? ref.slice(11) : ref;
|
|
19
|
+
} else if (line === "bare" && current) {
|
|
20
|
+
current.bare = true;
|
|
21
|
+
} else if (line === "detached" && current) {
|
|
22
|
+
current.detached = true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (current) worktrees.push(current);
|
|
26
|
+
return worktrees;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check if a given path is itself a worktree (not the main working tree)
|
|
30
|
+
function isWorktree(projectPath) {
|
|
31
|
+
try {
|
|
32
|
+
var gitDir = execFileSync("git", ["rev-parse", "--git-dir"], {
|
|
33
|
+
cwd: projectPath, encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
|
|
34
|
+
}).trim();
|
|
35
|
+
var commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
36
|
+
cwd: projectPath, encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
|
|
37
|
+
}).trim();
|
|
38
|
+
var absGit = path.resolve(projectPath, gitDir);
|
|
39
|
+
var absCommon = path.resolve(projectPath, commonDir);
|
|
40
|
+
return absGit !== absCommon;
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Scan worktrees for a given project path
|
|
47
|
+
// Returns array of { path, branch, bare, detached, accessible }
|
|
48
|
+
// accessible = true if worktree path is inside parentPath
|
|
49
|
+
function scanWorktrees(projectPath) {
|
|
50
|
+
var resolvedParent = path.resolve(projectPath);
|
|
51
|
+
try {
|
|
52
|
+
var output = execFileSync("git", ["worktree", "list", "--porcelain"], {
|
|
53
|
+
cwd: resolvedParent,
|
|
54
|
+
encoding: "utf8",
|
|
55
|
+
timeout: 5000,
|
|
56
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
57
|
+
});
|
|
58
|
+
var all = parseWorktreeOutput(output);
|
|
59
|
+
// Filter out bare worktrees and the main worktree itself
|
|
60
|
+
var results = [];
|
|
61
|
+
for (var i = 0; i < all.length; i++) {
|
|
62
|
+
var wt = all[i];
|
|
63
|
+
if (wt.bare) continue;
|
|
64
|
+
var resolvedWt = path.resolve(wt.path);
|
|
65
|
+
if (resolvedWt === resolvedParent) continue;
|
|
66
|
+
wt.accessible = resolvedWt.indexOf(resolvedParent + path.sep) === 0;
|
|
67
|
+
wt.dirName = path.basename(wt.path);
|
|
68
|
+
results.push(wt);
|
|
69
|
+
}
|
|
70
|
+
return results;
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Create a new worktree inside the parent project directory
|
|
77
|
+
// Returns { ok, path, error }
|
|
78
|
+
function createWorktree(projectPath, branchName, baseBranch) {
|
|
79
|
+
var resolvedParent = path.resolve(projectPath);
|
|
80
|
+
var wtPath = path.join(resolvedParent, branchName);
|
|
81
|
+
var base = baseBranch || "main";
|
|
82
|
+
// Try creating with -b (new branch)
|
|
83
|
+
try {
|
|
84
|
+
execFileSync("git", ["worktree", "add", wtPath, "-b", branchName, base], {
|
|
85
|
+
cwd: resolvedParent,
|
|
86
|
+
encoding: "utf8",
|
|
87
|
+
timeout: 15000,
|
|
88
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
89
|
+
});
|
|
90
|
+
return { ok: true, path: wtPath };
|
|
91
|
+
} catch (e) {
|
|
92
|
+
// Branch may already exist, try without -b
|
|
93
|
+
try {
|
|
94
|
+
execFileSync("git", ["worktree", "add", wtPath, branchName], {
|
|
95
|
+
cwd: resolvedParent,
|
|
96
|
+
encoding: "utf8",
|
|
97
|
+
timeout: 15000,
|
|
98
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
99
|
+
});
|
|
100
|
+
return { ok: true, path: wtPath };
|
|
101
|
+
} catch (e2) {
|
|
102
|
+
return { ok: false, error: e2.message || "Failed to create worktree" };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Remove a worktree
|
|
108
|
+
// Returns { ok, error }
|
|
109
|
+
function removeWorktree(projectPath, worktreeDirName) {
|
|
110
|
+
var resolvedParent = path.resolve(projectPath);
|
|
111
|
+
var wtPath = path.join(resolvedParent, worktreeDirName);
|
|
112
|
+
// Try normal remove first
|
|
113
|
+
try {
|
|
114
|
+
execFileSync("git", ["worktree", "remove", wtPath], {
|
|
115
|
+
cwd: resolvedParent,
|
|
116
|
+
encoding: "utf8",
|
|
117
|
+
timeout: 15000,
|
|
118
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
119
|
+
});
|
|
120
|
+
return { ok: true };
|
|
121
|
+
} catch (e) {
|
|
122
|
+
var errMsg = (e.stderr || e.message || "").toString();
|
|
123
|
+
// If dirty, report to user
|
|
124
|
+
if (errMsg.indexOf("modified") !== -1 || errMsg.indexOf("untracked") !== -1) {
|
|
125
|
+
return { ok: false, error: "Worktree has uncommitted changes. Commit or discard them first." };
|
|
126
|
+
}
|
|
127
|
+
if (errMsg.indexOf("locked") !== -1) {
|
|
128
|
+
return { ok: false, error: "Worktree is locked. Unlock it first with: git worktree unlock" };
|
|
129
|
+
}
|
|
130
|
+
return { ok: false, error: errMsg || "Failed to remove worktree" };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { scanWorktrees: scanWorktrees, createWorktree: createWorktree, removeWorktree: removeWorktree, isWorktree: isWorktree };
|