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
package/bin/cli.js
CHANGED
|
@@ -776,12 +776,12 @@ function promptPin(callback) {
|
|
|
776
776
|
* Enter with empty input returns placeholder value.
|
|
777
777
|
* Tab completes directory paths.
|
|
778
778
|
*/
|
|
779
|
-
function promptText(title, placeholder, callback) {
|
|
779
|
+
function promptText(title, placeholder, callback, opts) {
|
|
780
780
|
var prefix = " " + sym.bar + " ";
|
|
781
781
|
var hintLine = "";
|
|
782
782
|
var lineCount = 2;
|
|
783
|
-
|
|
784
|
-
log(sym.pointer + " " + a.bold + title + a.reset +
|
|
783
|
+
var escHint = (!title || (opts && opts.noEsc)) ? "" : " " + a.dim + "(esc to go back)" + a.reset;
|
|
784
|
+
log(sym.pointer + " " + a.bold + title + a.reset + escHint);
|
|
785
785
|
process.stdout.write(prefix + a.dim + placeholder + a.reset);
|
|
786
786
|
// Move cursor to start of placeholder
|
|
787
787
|
process.stdout.write("\r" + prefix);
|
|
@@ -1527,7 +1527,7 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
|
1527
1527
|
return;
|
|
1528
1528
|
}
|
|
1529
1529
|
|
|
1530
|
-
// Enable multi-user mode
|
|
1530
|
+
// Enable/disable multi-user mode based on startup config
|
|
1531
1531
|
if (config.mode === "multi") {
|
|
1532
1532
|
var muResult = enableMultiUser();
|
|
1533
1533
|
if (muResult.setupCode) {
|
|
@@ -1537,6 +1537,8 @@ async function forkDaemon(mode, keepAwake, extraProjects, addCwd, wantOsUsers) {
|
|
|
1537
1537
|
log(sym.bar + " Open Clay in your browser and enter this code to create the admin account.");
|
|
1538
1538
|
log("");
|
|
1539
1539
|
}
|
|
1540
|
+
} else if (isMultiUser()) {
|
|
1541
|
+
disableMultiUser();
|
|
1540
1542
|
}
|
|
1541
1543
|
|
|
1542
1544
|
// Headless mode — print status and exit immediately
|
|
@@ -1636,6 +1638,16 @@ async function devMode(mode, keepAwake, existingPinHash) {
|
|
|
1636
1638
|
ensureConfigDir();
|
|
1637
1639
|
saveConfig(config);
|
|
1638
1640
|
|
|
1641
|
+
// Enable/disable multi-user mode based on startup config
|
|
1642
|
+
if (config.mode === "multi") {
|
|
1643
|
+
var muResult = enableMultiUser();
|
|
1644
|
+
if (muResult.setupCode) {
|
|
1645
|
+
console.log("\x1b[38;2;0;183;133m[dev]\x1b[0m Multi-user mode enabled. Setup code: " + muResult.setupCode);
|
|
1646
|
+
}
|
|
1647
|
+
} else if (isMultiUser()) {
|
|
1648
|
+
disableMultiUser();
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1639
1651
|
var daemonScript = path.join(__dirname, "..", "lib", "daemon.js");
|
|
1640
1652
|
var libDir = path.join(__dirname, "..", "lib");
|
|
1641
1653
|
var child = null;
|
package/lib/daemon.js
CHANGED
|
@@ -16,6 +16,9 @@ if (nodeMajor < 20) {
|
|
|
16
16
|
if (!Symbol.dispose) Symbol.dispose = Symbol("Symbol.dispose");
|
|
17
17
|
if (!Symbol.asyncDispose) Symbol.asyncDispose = Symbol("Symbol.asyncDispose");
|
|
18
18
|
|
|
19
|
+
// Increase listener limit for projects with many worktrees
|
|
20
|
+
process.setMaxListeners(50);
|
|
21
|
+
|
|
19
22
|
// Remove CLAUDECODE env var so the SDK can spawn Claude Code child processes
|
|
20
23
|
// (prevents "cannot be launched inside another Claude Code session" error)
|
|
21
24
|
delete process.env.CLAUDECODE;
|
|
@@ -27,6 +30,7 @@ var { createIPCServer } = require("./ipc");
|
|
|
27
30
|
var { createServer, generateAuthToken } = require("./server");
|
|
28
31
|
var { grantProjectAccess, revokeProjectAccess, provisionAllUsers, provisionLinuxUser, grantAllUsersAccess, deactivateLinuxUser, ensureProjectsDir } = require("./os-users");
|
|
29
32
|
var usersModule = require("./users");
|
|
33
|
+
var { scanWorktrees, createWorktree, removeWorktree, isWorktree } = require("./worktree");
|
|
30
34
|
|
|
31
35
|
var configFile = process.env.CLAY_CONFIG || process.env.CLAUDE_RELAY_CONFIG || require("./config").configPath();
|
|
32
36
|
var config;
|
|
@@ -155,6 +159,8 @@ var relay = createServer({
|
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
161
|
}
|
|
162
|
+
// Discover and register worktrees for the new project
|
|
163
|
+
scanAndRegisterWorktrees(absPath, slug, null, wsUser && wsUser.id && wsUser.role !== "admin" ? wsUser.id : null);
|
|
158
164
|
// Broadcast updated project list to all clients
|
|
159
165
|
relay.broadcastAll({
|
|
160
166
|
type: "projects_updated",
|
|
@@ -313,11 +319,41 @@ var relay = createServer({
|
|
|
313
319
|
});
|
|
314
320
|
},
|
|
315
321
|
onRemoveProject: function (slug, userId) {
|
|
322
|
+
// Check if this is a worktree project (ephemeral)
|
|
323
|
+
if (isWorktreeSlug(slug)) {
|
|
324
|
+
var wtParent = slug.split("--")[0];
|
|
325
|
+
var wtDirName = slug.split("--").slice(1).join("--");
|
|
326
|
+
// Find parent project path
|
|
327
|
+
var parentProject = null;
|
|
328
|
+
for (var pi = 0; pi < config.projects.length; pi++) {
|
|
329
|
+
if (config.projects[pi].slug === wtParent) { parentProject = config.projects[pi]; break; }
|
|
330
|
+
}
|
|
331
|
+
if (parentProject) {
|
|
332
|
+
var rmResult = removeWorktree(parentProject.path, wtDirName);
|
|
333
|
+
if (!rmResult.ok) {
|
|
334
|
+
console.log("[daemon] Failed to remove worktree:", slug, rmResult.error);
|
|
335
|
+
return { ok: false, error: rmResult.error };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
relay.removeProject(slug);
|
|
339
|
+
if (worktreeRegistry[wtParent]) {
|
|
340
|
+
worktreeRegistry[wtParent] = worktreeRegistry[wtParent].filter(function (s) { return s !== slug; });
|
|
341
|
+
}
|
|
342
|
+
console.log("[daemon] Removed worktree (web):", slug);
|
|
343
|
+
relay.broadcastAll({
|
|
344
|
+
type: "projects_updated",
|
|
345
|
+
projects: relay.getProjects(),
|
|
346
|
+
projectCount: config.projects.length,
|
|
347
|
+
});
|
|
348
|
+
return { ok: true };
|
|
349
|
+
}
|
|
316
350
|
var found = null;
|
|
317
351
|
for (var j = 0; j < config.projects.length; j++) {
|
|
318
352
|
if (config.projects[j].slug === slug) { found = config.projects[j]; break; }
|
|
319
353
|
}
|
|
320
354
|
if (!found) return { ok: false, error: "Project not found" };
|
|
355
|
+
// Cascade remove worktrees belonging to this parent
|
|
356
|
+
cleanupWorktreesForParent(slug);
|
|
321
357
|
// Save to removedProjects for re-add functionality
|
|
322
358
|
if (!config.removedProjects) config.removedProjects = [];
|
|
323
359
|
config.removedProjects.push({
|
|
@@ -756,8 +792,135 @@ var relay = createServer({
|
|
|
756
792
|
if (!config.osUsers || !linuxUser) return;
|
|
757
793
|
deactivateLinuxUser(linuxUser);
|
|
758
794
|
},
|
|
795
|
+
onCreateWorktree: function (parentSlug, branchName, baseBranch) {
|
|
796
|
+
// Find the parent project
|
|
797
|
+
var parent = null;
|
|
798
|
+
for (var j = 0; j < config.projects.length; j++) {
|
|
799
|
+
if (config.projects[j].slug === parentSlug) { parent = config.projects[j]; break; }
|
|
800
|
+
}
|
|
801
|
+
if (!parent) return { ok: false, error: "Parent project not found" };
|
|
802
|
+
if (isWorktree(parent.path)) return { ok: false, error: "Cannot create worktrees from a worktree project" };
|
|
803
|
+
var result = createWorktree(parent.path, branchName, baseBranch);
|
|
804
|
+
if (!result.ok) return result;
|
|
805
|
+
// Register the new worktree as ephemeral project
|
|
806
|
+
var wtSlug = parentSlug + "--" + branchName;
|
|
807
|
+
var wtMeta = { parentSlug: parentSlug, branch: branchName, accessible: true };
|
|
808
|
+
relay.addProject(result.path, wtSlug, branchName, parent.icon, parent.ownerId, wtMeta);
|
|
809
|
+
if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
|
|
810
|
+
worktreeRegistry[parentSlug].push(wtSlug);
|
|
811
|
+
console.log("[daemon] Created worktree:", wtSlug, "->", result.path);
|
|
812
|
+
relay.broadcastAll({
|
|
813
|
+
type: "projects_updated",
|
|
814
|
+
projects: relay.getProjects(),
|
|
815
|
+
projectCount: config.projects.length,
|
|
816
|
+
});
|
|
817
|
+
return { ok: true, slug: wtSlug, path: result.path };
|
|
818
|
+
},
|
|
759
819
|
});
|
|
760
820
|
|
|
821
|
+
// --- Worktree tracking ---
|
|
822
|
+
var worktreeRegistry = {}; // parentSlug -> [wtSlug, ...]
|
|
823
|
+
var worktreeTimers = {}; // parentSlug -> intervalId
|
|
824
|
+
var worktreeScanning = {}; // parentSlug -> boolean (mutex)
|
|
825
|
+
|
|
826
|
+
function isWorktreeSlug(slug) {
|
|
827
|
+
return slug.indexOf("--") !== -1;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function scanAndRegisterWorktrees(parentPath, parentSlug, parentIcon, parentOwnerId) {
|
|
831
|
+
// Skip if this project is itself a worktree (not the main working tree)
|
|
832
|
+
if (isWorktree(parentPath)) return;
|
|
833
|
+
var worktrees = scanWorktrees(parentPath);
|
|
834
|
+
if (worktrees.length === 0) return;
|
|
835
|
+
if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
|
|
836
|
+
for (var i = 0; i < worktrees.length; i++) {
|
|
837
|
+
var wt = worktrees[i];
|
|
838
|
+
var wtSlug = parentSlug + "--" + wt.dirName;
|
|
839
|
+
// Skip if already registered
|
|
840
|
+
var alreadyRegistered = false;
|
|
841
|
+
for (var j = 0; j < worktreeRegistry[parentSlug].length; j++) {
|
|
842
|
+
if (worktreeRegistry[parentSlug][j] === wtSlug) { alreadyRegistered = true; break; }
|
|
843
|
+
}
|
|
844
|
+
if (alreadyRegistered) continue;
|
|
845
|
+
var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName, accessible: wt.accessible };
|
|
846
|
+
// Only add as a full project if accessible, otherwise still track for UI display
|
|
847
|
+
relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, parentIcon, parentOwnerId, wtMeta);
|
|
848
|
+
worktreeRegistry[parentSlug].push(wtSlug);
|
|
849
|
+
console.log("[daemon] Registered worktree:", wtSlug, "->", wt.path, wt.accessible ? "(accessible)" : "(inaccessible)");
|
|
850
|
+
}
|
|
851
|
+
// Start periodic rescan if not already running
|
|
852
|
+
if (!worktreeTimers[parentSlug]) {
|
|
853
|
+
worktreeTimers[parentSlug] = setInterval(function () {
|
|
854
|
+
rescanWorktrees(parentPath, parentSlug, parentIcon, parentOwnerId);
|
|
855
|
+
}, 10000);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function rescanWorktrees(parentPath, parentSlug, parentIcon, parentOwnerId) {
|
|
860
|
+
if (worktreeScanning[parentSlug]) return;
|
|
861
|
+
worktreeScanning[parentSlug] = true;
|
|
862
|
+
try {
|
|
863
|
+
var discovered = scanWorktrees(parentPath);
|
|
864
|
+
var changed = false;
|
|
865
|
+
var existingSlugs = worktreeRegistry[parentSlug] || [];
|
|
866
|
+
// Build set of discovered dirNames
|
|
867
|
+
var discoveredNames = {};
|
|
868
|
+
for (var i = 0; i < discovered.length; i++) {
|
|
869
|
+
discoveredNames[discovered[i].dirName] = discovered[i];
|
|
870
|
+
}
|
|
871
|
+
// Add new worktrees
|
|
872
|
+
for (var di = 0; di < discovered.length; di++) {
|
|
873
|
+
var wt = discovered[di];
|
|
874
|
+
var wtSlug = parentSlug + "--" + wt.dirName;
|
|
875
|
+
var found = false;
|
|
876
|
+
for (var ei = 0; ei < existingSlugs.length; ei++) {
|
|
877
|
+
if (existingSlugs[ei] === wtSlug) { found = true; break; }
|
|
878
|
+
}
|
|
879
|
+
if (!found) {
|
|
880
|
+
var wtMeta = { parentSlug: parentSlug, branch: wt.branch || wt.dirName, accessible: wt.accessible };
|
|
881
|
+
relay.addProject(wt.path, wtSlug, wt.branch || wt.dirName, parentIcon, parentOwnerId, wtMeta);
|
|
882
|
+
if (!worktreeRegistry[parentSlug]) worktreeRegistry[parentSlug] = [];
|
|
883
|
+
worktreeRegistry[parentSlug].push(wtSlug);
|
|
884
|
+
console.log("[daemon] Rescan: added worktree:", wtSlug);
|
|
885
|
+
changed = true;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
// Remove stale worktrees
|
|
889
|
+
for (var si = existingSlugs.length - 1; si >= 0; si--) {
|
|
890
|
+
var sSlug = existingSlugs[si];
|
|
891
|
+
var dirName = sSlug.split("--").slice(1).join("--");
|
|
892
|
+
if (!discoveredNames[dirName]) {
|
|
893
|
+
relay.removeProject(sSlug);
|
|
894
|
+
existingSlugs.splice(si, 1);
|
|
895
|
+
console.log("[daemon] Rescan: removed stale worktree:", sSlug);
|
|
896
|
+
changed = true;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
if (changed) {
|
|
900
|
+
relay.broadcastAll({
|
|
901
|
+
type: "projects_updated",
|
|
902
|
+
projects: relay.getProjects(),
|
|
903
|
+
projectCount: config.projects.length,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
} finally {
|
|
907
|
+
worktreeScanning[parentSlug] = false;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function cleanupWorktreesForParent(parentSlug) {
|
|
912
|
+
var wtSlugs = worktreeRegistry[parentSlug] || [];
|
|
913
|
+
for (var i = 0; i < wtSlugs.length; i++) {
|
|
914
|
+
relay.removeProject(wtSlugs[i]);
|
|
915
|
+
console.log("[daemon] Cascade removed worktree:", wtSlugs[i]);
|
|
916
|
+
}
|
|
917
|
+
delete worktreeRegistry[parentSlug];
|
|
918
|
+
if (worktreeTimers[parentSlug]) {
|
|
919
|
+
clearInterval(worktreeTimers[parentSlug]);
|
|
920
|
+
delete worktreeTimers[parentSlug];
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
761
924
|
// --- Register projects ---
|
|
762
925
|
var projects = config.projects || [];
|
|
763
926
|
for (var i = 0; i < projects.length; i++) {
|
|
@@ -765,6 +928,8 @@ for (var i = 0; i < projects.length; i++) {
|
|
|
765
928
|
if (fs.existsSync(p.path)) {
|
|
766
929
|
console.log("[daemon] Adding project:", p.slug, "→", p.path);
|
|
767
930
|
relay.addProject(p.path, p.slug, p.title, p.icon, p.ownerId);
|
|
931
|
+
// Discover and register worktrees for this project
|
|
932
|
+
scanAndRegisterWorktrees(p.path, p.slug, p.icon, p.ownerId);
|
|
768
933
|
} else {
|
|
769
934
|
console.log("[daemon] Skipping missing project:", p.path);
|
|
770
935
|
}
|
|
@@ -800,6 +965,8 @@ var ipc = createIPCServer(socketPath(), function (msg) {
|
|
|
800
965
|
saveConfig(config);
|
|
801
966
|
try { syncClayrc(config.projects); } catch (e) {}
|
|
802
967
|
console.log("[daemon] Added project:", slug, "→", absPath);
|
|
968
|
+
// Discover and register worktrees for the new project
|
|
969
|
+
scanAndRegisterWorktrees(absPath, slug, null, null);
|
|
803
970
|
relay.broadcastAll({
|
|
804
971
|
type: "projects_updated",
|
|
805
972
|
projects: relay.getProjects(),
|
package/lib/project.js
CHANGED
|
@@ -113,6 +113,8 @@ function createProjectContext(opts) {
|
|
|
113
113
|
var updateChannel = opts.updateChannel || "stable";
|
|
114
114
|
var osUsers = opts.osUsers || false;
|
|
115
115
|
var projectOwnerId = opts.projectOwnerId || null;
|
|
116
|
+
var worktreeMeta = opts.worktreeMeta || null; // { parentSlug, branch, accessible }
|
|
117
|
+
var onCreateWorktree = opts.onCreateWorktree || null;
|
|
116
118
|
var latestVersion = null;
|
|
117
119
|
|
|
118
120
|
// --- OS-level user isolation helper ---
|
|
@@ -1228,7 +1230,8 @@ function createProjectContext(opts) {
|
|
|
1228
1230
|
var session = getSessionForWs(ws);
|
|
1229
1231
|
if (!session || typeof msg.before !== "number") return;
|
|
1230
1232
|
var before = msg.before;
|
|
1231
|
-
var
|
|
1233
|
+
var targetFrom = typeof msg.target === "number" ? msg.target : before - sm.HISTORY_PAGE_SIZE;
|
|
1234
|
+
var from = sm.findTurnBoundary(session.history, Math.max(0, targetFrom));
|
|
1232
1235
|
var to = before;
|
|
1233
1236
|
var items = session.history.slice(from, to);
|
|
1234
1237
|
sendTo(ws, {
|
|
@@ -1410,6 +1413,16 @@ function createProjectContext(opts) {
|
|
|
1410
1413
|
return;
|
|
1411
1414
|
}
|
|
1412
1415
|
|
|
1416
|
+
if (msg.type === "search_session_content") {
|
|
1417
|
+
var targetSession = msg.id ? sm.sessions.get(msg.id) : getSessionForWs(ws);
|
|
1418
|
+
if (!targetSession) return;
|
|
1419
|
+
var contentResults = sm.searchSessionContent(targetSession.localId, msg.query || "");
|
|
1420
|
+
var searchResp = { type: "search_content_results", query: msg.query || "", sessionId: targetSession.localId, hits: contentResults.hits, total: contentResults.total };
|
|
1421
|
+
if (msg.source) searchResp.source = msg.source;
|
|
1422
|
+
sendTo(ws, searchResp);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1413
1426
|
if (msg.type === "set_update_channel") {
|
|
1414
1427
|
if (usersModule.isMultiUser() && (!ws._clayUser || ws._clayUser.role !== "admin")) return;
|
|
1415
1428
|
var newChannel = msg.channel === "beta" ? "beta" : "stable";
|
|
@@ -1771,6 +1784,29 @@ function createProjectContext(opts) {
|
|
|
1771
1784
|
return;
|
|
1772
1785
|
}
|
|
1773
1786
|
|
|
1787
|
+
if (msg.type === "cursor_move" || msg.type === "cursor_leave" || msg.type === "text_select") {
|
|
1788
|
+
if (!usersModule.isMultiUser() || !ws._clayUser) return;
|
|
1789
|
+
var u = ws._clayUser;
|
|
1790
|
+
var p = u.profile || {};
|
|
1791
|
+
var cursorMsg = {
|
|
1792
|
+
type: msg.type,
|
|
1793
|
+
userId: u.id,
|
|
1794
|
+
displayName: p.name || u.displayName || u.username,
|
|
1795
|
+
avatarStyle: p.avatarStyle || "thumbs",
|
|
1796
|
+
avatarSeed: p.avatarSeed || u.username,
|
|
1797
|
+
};
|
|
1798
|
+
if (msg.type === "cursor_move") {
|
|
1799
|
+
cursorMsg.turn = msg.turn;
|
|
1800
|
+
if (msg.rx != null) cursorMsg.rx = msg.rx;
|
|
1801
|
+
if (msg.ry != null) cursorMsg.ry = msg.ry;
|
|
1802
|
+
}
|
|
1803
|
+
if (msg.type === "text_select") {
|
|
1804
|
+
cursorMsg.ranges = msg.ranges || [];
|
|
1805
|
+
}
|
|
1806
|
+
sendToSessionOthers(ws, ws._clayActiveSession, cursorMsg);
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1774
1810
|
if (msg.type === "permission_response") {
|
|
1775
1811
|
var session = getSessionForWs(ws);
|
|
1776
1812
|
if (!session) return;
|
|
@@ -2022,6 +2058,23 @@ function createProjectContext(opts) {
|
|
|
2022
2058
|
return;
|
|
2023
2059
|
}
|
|
2024
2060
|
|
|
2061
|
+
// --- Create worktree from web UI ---
|
|
2062
|
+
if (msg.type === "create_worktree") {
|
|
2063
|
+
var wtBranch = (msg.branch || "").trim();
|
|
2064
|
+
var wtBase = (msg.baseBranch || "").trim() || null;
|
|
2065
|
+
if (!wtBranch || !/^[a-zA-Z0-9_\/.@-]+$/.test(wtBranch)) {
|
|
2066
|
+
sendTo(ws, { type: "create_worktree_result", ok: false, error: "Invalid branch name" });
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
if (typeof onCreateWorktree === "function") {
|
|
2070
|
+
var wtResult = onCreateWorktree(slug, wtBranch, wtBase);
|
|
2071
|
+
sendTo(ws, { type: "create_worktree_result", ok: wtResult.ok, slug: wtResult.slug, error: wtResult.error });
|
|
2072
|
+
} else {
|
|
2073
|
+
sendTo(ws, { type: "create_worktree_result", ok: false, error: "Not supported" });
|
|
2074
|
+
}
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2025
2078
|
// --- Pre-check: does the project have tasks/schedules? ---
|
|
2026
2079
|
if (msg.type === "remove_project_check") {
|
|
2027
2080
|
var checkSlug = msg.slug;
|
|
@@ -3360,6 +3413,29 @@ function createProjectContext(opts) {
|
|
|
3360
3413
|
return true;
|
|
3361
3414
|
}
|
|
3362
3415
|
|
|
3416
|
+
// List branches for worktree modal
|
|
3417
|
+
if (req.method === "GET" && urlPath === "/api/branches") {
|
|
3418
|
+
try {
|
|
3419
|
+
var brRaw = execFileSync("git", ["branch", "-a", "--format=%(refname:short)"], {
|
|
3420
|
+
cwd: cwd, timeout: 5000, encoding: "utf8"
|
|
3421
|
+
});
|
|
3422
|
+
var brList = brRaw.trim().split("\n").filter(Boolean);
|
|
3423
|
+
var defBr = "main";
|
|
3424
|
+
try {
|
|
3425
|
+
var hrRef = execFileSync("git", ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"], {
|
|
3426
|
+
cwd: cwd, timeout: 3000, encoding: "utf8"
|
|
3427
|
+
}).trim();
|
|
3428
|
+
defBr = hrRef.replace(/^origin\//, "");
|
|
3429
|
+
} catch (e) {}
|
|
3430
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3431
|
+
res.end(JSON.stringify({ branches: brList, defaultBranch: defBr }));
|
|
3432
|
+
} catch (e) {
|
|
3433
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3434
|
+
res.end(JSON.stringify({ branches: ["main"], defaultBranch: "main" }));
|
|
3435
|
+
}
|
|
3436
|
+
return true;
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3363
3439
|
// Info endpoint
|
|
3364
3440
|
if (req.method === "GET" && urlPath === "/info") {
|
|
3365
3441
|
res.writeHead(200, {
|
|
@@ -3420,6 +3496,12 @@ function createProjectContext(opts) {
|
|
|
3420
3496
|
isProcessing: hasProcessing,
|
|
3421
3497
|
projectOwnerId: projectOwnerId,
|
|
3422
3498
|
};
|
|
3499
|
+
if (worktreeMeta) {
|
|
3500
|
+
status.isWorktree = true;
|
|
3501
|
+
status.parentSlug = worktreeMeta.parentSlug;
|
|
3502
|
+
status.branch = worktreeMeta.branch;
|
|
3503
|
+
status.worktreeAccessible = worktreeMeta.accessible;
|
|
3504
|
+
}
|
|
3423
3505
|
if (usersModule.isMultiUser()) {
|
|
3424
3506
|
var seen = {};
|
|
3425
3507
|
var onlineUsers = [];
|