mexus-cli 1.0.0 → 1.0.2
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/package.json +1 -1
- package/packages/server/dist/cli.mjs +820 -158
- package/packages/server/dist/cli.mjs.map +1 -1
- package/packages/web/dist/assets/{_basePickBy-B0zzcSaC.js → _basePickBy-Co7zHyMx.js} +1 -1
- package/packages/web/dist/assets/{_baseUniq-B-4_EE7n.js → _baseUniq-Dxx1kUxk.js} +1 -1
- package/packages/web/dist/assets/{arc-DFOgsfTK.js → arc-BvdbVAwY.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-2XIMDMQ5-CNksmKJ-.js → architectureDiagram-2XIMDMQ5-NJEtmrih.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-WCTKOSBZ-B1Tw0xWQ.js → blockDiagram-WCTKOSBZ-NMdKrHRR.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-IC4MRINW-BC3Xs_CM.js → c4Diagram-IC4MRINW-Cid8liHK.js} +1 -1
- package/packages/web/dist/assets/channel-ggzcU6fx.js +1 -0
- package/packages/web/dist/assets/{chunk-4BX2VUAB-Bc_dSZCa.js → chunk-4BX2VUAB-C0Cpcxjt.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-BkpubxQa.js → chunk-55IACEB6-uCQWWVUa.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-DfsMHPhA.js → chunk-FMBD7UC4-j69k_AXR.js} +1 -1
- package/packages/web/dist/assets/{chunk-JSJVCQXG-DQnDNPz7.js → chunk-JSJVCQXG-BRb3U-tF.js} +1 -1
- package/packages/web/dist/assets/{chunk-KX2RTZJC-BoujkdYL.js → chunk-KX2RTZJC-CMLyC_k7.js} +1 -1
- package/packages/web/dist/assets/{chunk-NQ4KR5QH-BI0GEUMc.js → chunk-NQ4KR5QH-DUQxdvVp.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-Dq4w7tDq.js → chunk-QZHKN3VN-I_0dQXDN.js} +1 -1
- package/packages/web/dist/assets/{chunk-WL4C6EOR-DSQ5ZWfk.js → chunk-WL4C6EOR-BR8lxAey.js} +1 -1
- package/packages/web/dist/assets/classDiagram-VBA2DB6C-qeSRjRBc.js +1 -0
- package/packages/web/dist/assets/classDiagram-v2-RAHNMMFH-qeSRjRBc.js +1 -0
- package/packages/web/dist/assets/clone-CGAMRvPs.js +1 -0
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-Caq6VRnC.js → cose-bilkent-S5V4N54A-BF1cADHY.js} +1 -1
- package/packages/web/dist/assets/{dagre-KLK3FWXG-D1rt0jGa.js → dagre-KLK3FWXG-DDt0p4vG.js} +1 -1
- package/packages/web/dist/assets/{diagram-E7M64L7V-BBrZhLH7.js → diagram-E7M64L7V-CTkR6JTl.js} +1 -1
- package/packages/web/dist/assets/{diagram-IFDJBPK2-BFSSudzv.js → diagram-IFDJBPK2-DKPHzwe0.js} +1 -1
- package/packages/web/dist/assets/{diagram-P4PSJMXO-C3oU7Ool.js → diagram-P4PSJMXO-k5sUOXg6.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-INFDFZHY-D0cx2_2Y.js → erDiagram-INFDFZHY-HQ6gUchT.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-PKNHOUZH-BXvZGiWz.js → flowDiagram-PKNHOUZH-zQ6z0FfA.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-A5KZAMGK-C9JOODgY.js → ganttDiagram-A5KZAMGK-7OanXR1G.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-K3NZZRJ6-BiIWik3M.js → gitGraphDiagram-K3NZZRJ6-CWHj7ZAf.js} +1 -1
- package/packages/web/dist/assets/{graph-KIQcHS1V.js → graph-DGZqBZjC.js} +1 -1
- package/packages/web/dist/assets/index-DWMolj1f.css +32 -0
- package/packages/web/dist/assets/{index-CMzYPOPG.js → index-ORmXz3Zu.js} +284 -192
- package/packages/web/dist/assets/{infoDiagram-LFFYTUFH-DV6kISOT.js → infoDiagram-LFFYTUFH-yRz9H7FV.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-PHBUUO56-Dko7qIR3.js → ishikawaDiagram-PHBUUO56-D9KVAEH1.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-4ABVD52K-_FLfsCtQ.js → journeyDiagram-4ABVD52K-xYc2g_2E.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-K7BYSVSG-Bjf_Hkam.js → kanban-definition-K7BYSVSG-C9ctSGeG.js} +1 -1
- package/packages/web/dist/assets/{layout-D09tLn9J.js → layout-dR6Z9QBB.js} +1 -1
- package/packages/web/dist/assets/{linear-BnF-DuHG.js → linear-C41sKDUx.js} +1 -1
- package/packages/web/dist/assets/{mindmap-definition-YRQLILUH-C4ui5Uen.js → mindmap-definition-YRQLILUH-DnEvOt89.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-SKSYHLDU-Chjj2B6Z.js → pieDiagram-SKSYHLDU-Dwygi596.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-337W2JSQ-BgcnBVtA.js → quadrantDiagram-337W2JSQ-CGsDpSZv.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-Z7DCOOCP-D8i7_HQl.js → requirementDiagram-Z7DCOOCP-DrFNwpYL.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-WA2Y5GQK-CE8F22pR.js → sankeyDiagram-WA2Y5GQK-CKWlgXXF.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-2WXFIKYE-BIpm8kwX.js → sequenceDiagram-2WXFIKYE-DpbgNxFF.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-RAJIS63D-lboAMp3Q.js → stateDiagram-RAJIS63D-UQV1zJr3.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-FVOUBMTO-CmZylZ6s.js +1 -0
- package/packages/web/dist/assets/{timeline-definition-YZTLITO2-Dh94GN9W.js → timeline-definition-YZTLITO2-CBNnAMDu.js} +1 -1
- package/packages/web/dist/assets/{treemap-KZPCXAKY-Cnfp1I4Q.js → treemap-KZPCXAKY-DQg0JG5Z.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-LZ73GAT5-oM-IB92W.js → vennDiagram-LZ73GAT5-6EhbiWUV.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-JWTSCODW-rBsEDTWK.js → xychartDiagram-JWTSCODW-DrTSQ6rp.js} +1 -1
- package/packages/web/dist/index.html +2 -2
- package/packages/web/dist/assets/channel-BolzzYH1.js +0 -1
- package/packages/web/dist/assets/classDiagram-VBA2DB6C-DXgRkMh7.js +0 -1
- package/packages/web/dist/assets/classDiagram-v2-RAHNMMFH-DXgRkMh7.js +0 -1
- package/packages/web/dist/assets/clone-BD_2Dvk1.js +0 -1
- package/packages/web/dist/assets/index-SBtQaWyn.css +0 -32
- package/packages/web/dist/assets/stateDiagram-v2-FVOUBMTO-BYDSvNCG.js +0 -1
|
@@ -43,13 +43,43 @@ var init_ConfigManager = __esm({
|
|
|
43
43
|
resume_flag: "--resume",
|
|
44
44
|
yolo_flag: "--dangerously-skip-permissions",
|
|
45
45
|
statusline: true,
|
|
46
|
+
transport: "pty",
|
|
47
|
+
env: {}
|
|
48
|
+
},
|
|
49
|
+
codex: {
|
|
50
|
+
bin: "codex",
|
|
51
|
+
continue_flag: "",
|
|
52
|
+
resume_flag: "",
|
|
53
|
+
yolo_flag: "",
|
|
54
|
+
statusline: false,
|
|
55
|
+
transport: "pty",
|
|
46
56
|
env: {}
|
|
47
57
|
},
|
|
48
58
|
opencode: {
|
|
49
59
|
bin: "opencode",
|
|
50
60
|
continue_flag: "--continue",
|
|
61
|
+
resume_flag: "",
|
|
51
62
|
yolo_flag: "--yolo",
|
|
52
63
|
statusline: false,
|
|
64
|
+
transport: "acp",
|
|
65
|
+
env: {}
|
|
66
|
+
},
|
|
67
|
+
"kimi-cli": {
|
|
68
|
+
bin: "kimi",
|
|
69
|
+
continue_flag: "--continue",
|
|
70
|
+
resume_flag: "",
|
|
71
|
+
yolo_flag: "",
|
|
72
|
+
statusline: false,
|
|
73
|
+
transport: "pty",
|
|
74
|
+
env: {}
|
|
75
|
+
},
|
|
76
|
+
qodercli: {
|
|
77
|
+
bin: "qodercli",
|
|
78
|
+
continue_flag: "-c",
|
|
79
|
+
resume_flag: "-r",
|
|
80
|
+
yolo_flag: "--yolo",
|
|
81
|
+
statusline: false,
|
|
82
|
+
transport: "pty",
|
|
53
83
|
env: {}
|
|
54
84
|
}
|
|
55
85
|
}
|
|
@@ -143,11 +173,11 @@ var init_ConfigManager = __esm({
|
|
|
143
173
|
async detectAgentsAsync() {
|
|
144
174
|
const agents = {};
|
|
145
175
|
const agentBins = [
|
|
146
|
-
{ key: "claudecode", bin: "claude", flag: "--continue", statusline: true },
|
|
147
|
-
{ key: "codex", bin: "codex", flag: "", statusline: false },
|
|
148
|
-
{ key: "opencode", bin: "opencode", flag: "--continue", statusline: false },
|
|
149
|
-
{ key: "kimi-cli", bin: "kimi", flag: "--continue", statusline: false },
|
|
150
|
-
{ key: "
|
|
176
|
+
{ key: "claudecode", bin: "claude", flag: "--continue", statusline: true, transport: "pty" },
|
|
177
|
+
{ key: "codex", bin: "codex", flag: "", statusline: false, transport: "pty" },
|
|
178
|
+
{ key: "opencode", bin: "opencode", flag: "--continue", statusline: false, transport: "acp" },
|
|
179
|
+
{ key: "kimi-cli", bin: "kimi", flag: "--continue", statusline: false, transport: "pty" },
|
|
180
|
+
{ key: "qodercli", bin: "qodercli", flag: "-c", statusline: false, transport: "pty" }
|
|
151
181
|
];
|
|
152
182
|
const results = await Promise.allSettled(
|
|
153
183
|
agentBins.map(async (agent) => {
|
|
@@ -162,6 +192,7 @@ var init_ConfigManager = __esm({
|
|
|
162
192
|
bin: agent.bin,
|
|
163
193
|
continue_flag: agent.flag,
|
|
164
194
|
statusline: agent.statusline,
|
|
195
|
+
transport: agent.transport,
|
|
165
196
|
env: {}
|
|
166
197
|
};
|
|
167
198
|
}
|
|
@@ -210,7 +241,7 @@ var init_ConfigManager = __esm({
|
|
|
210
241
|
{ key: "codex", bin: "codex", installHint: "npm install -g @openai/codex" },
|
|
211
242
|
{ key: "opencode", bin: "opencode", installHint: "go install github.com/opencode-ai/opencode@latest" },
|
|
212
243
|
{ key: "kimi-cli", bin: "kimi", installHint: "pip install kimi-cli" },
|
|
213
|
-
{ key: "
|
|
244
|
+
{ key: "qodercli", bin: "qodercli", installHint: "See https://docs.qoder.com/zh/cli/using-cli" }
|
|
214
245
|
];
|
|
215
246
|
const checks = await Promise.allSettled(
|
|
216
247
|
knownAgents.map(async (agent) => {
|
|
@@ -245,16 +276,16 @@ var init_ConfigManager = __esm({
|
|
|
245
276
|
});
|
|
246
277
|
|
|
247
278
|
// src/cli.ts
|
|
248
|
-
import
|
|
249
|
-
import
|
|
279
|
+
import path11 from "path";
|
|
280
|
+
import fs10 from "fs";
|
|
250
281
|
|
|
251
282
|
// src/index.ts
|
|
252
283
|
init_ConfigManager();
|
|
253
284
|
import Fastify from "fastify";
|
|
254
285
|
import fastifyWebsocket from "@fastify/websocket";
|
|
255
286
|
import fastifyStatic from "@fastify/static";
|
|
256
|
-
import
|
|
257
|
-
import
|
|
287
|
+
import path10 from "path";
|
|
288
|
+
import fs9 from "fs";
|
|
258
289
|
import yaml3 from "js-yaml";
|
|
259
290
|
import { fileURLToPath } from "url";
|
|
260
291
|
|
|
@@ -275,6 +306,7 @@ var KNOWN_FIELDS = {
|
|
|
275
306
|
// Claude Code may add more fields — add them here as discovered
|
|
276
307
|
};
|
|
277
308
|
var MIN_KNOWN_FIELDS = 2;
|
|
309
|
+
var MAX_BUFFER_SIZE = 64 * 1024;
|
|
278
310
|
var StatuslineParser = class {
|
|
279
311
|
buffer = "";
|
|
280
312
|
/**
|
|
@@ -286,6 +318,9 @@ var StatuslineParser = class {
|
|
|
286
318
|
let meta = null;
|
|
287
319
|
if (!data.includes("\n")) {
|
|
288
320
|
this.buffer += data;
|
|
321
|
+
if (this.buffer.length > MAX_BUFFER_SIZE) {
|
|
322
|
+
this.buffer = "";
|
|
323
|
+
}
|
|
289
324
|
return { cleanData: data, meta: null };
|
|
290
325
|
}
|
|
291
326
|
const combined = this.buffer + data;
|
|
@@ -681,7 +716,8 @@ var OutputStateAnalyzer = class {
|
|
|
681
716
|
function stripAnsi2(str) {
|
|
682
717
|
return str.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "").replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "").replace(/\x1b[()][AB012]/g, "").replace(/\x1b[>=<]/g, "").replace(/\x0f|\x0e/g, "").replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "");
|
|
683
718
|
}
|
|
684
|
-
var
|
|
719
|
+
var MAX_BUFFER_SIZE2 = 64 * 1024;
|
|
720
|
+
var ActivityParser = class _ActivityParser {
|
|
685
721
|
buffer = "";
|
|
686
722
|
lastFile = "";
|
|
687
723
|
lastTime = 0;
|
|
@@ -690,6 +726,9 @@ var ActivityParser = class {
|
|
|
690
726
|
parse(data) {
|
|
691
727
|
if (!data.includes("\n")) {
|
|
692
728
|
this.buffer += data;
|
|
729
|
+
if (this.buffer.length > MAX_BUFFER_SIZE2) {
|
|
730
|
+
this.buffer = "";
|
|
731
|
+
}
|
|
693
732
|
return null;
|
|
694
733
|
}
|
|
695
734
|
const lines = (this.buffer + data).split("\n");
|
|
@@ -796,10 +835,15 @@ var ActivityParser = class {
|
|
|
796
835
|
cleanPath(raw) {
|
|
797
836
|
return raw.trim().replace(/^['"`]+|['"`]+$/g, "").replace(/^\.\//, "").replace(/\s+.*$/, "").replace(/[,;:)]+$/, "").replace(/^\(/, "");
|
|
798
837
|
}
|
|
838
|
+
// Paths under these prefixes are Nexus internals or noise — ignore them
|
|
839
|
+
static IGNORED_PREFIXES = [".nexus/", "node_modules/", ".git/"];
|
|
799
840
|
isValidPath(file) {
|
|
800
841
|
if (!file || file.length < 3) return false;
|
|
801
842
|
if (!/\.\w{1,10}$/.test(file)) return false;
|
|
802
843
|
if (file.startsWith("/") || file.includes("://")) return false;
|
|
844
|
+
for (const prefix of _ActivityParser.IGNORED_PREFIXES) {
|
|
845
|
+
if (file.startsWith(prefix)) return false;
|
|
846
|
+
}
|
|
803
847
|
if (/[<>|&$`\\{}[\]]/.test(file)) return false;
|
|
804
848
|
const parts = file.split("/");
|
|
805
849
|
if (parts.length > 15) return false;
|
|
@@ -847,7 +891,19 @@ var PtyManager = class {
|
|
|
847
891
|
}
|
|
848
892
|
}
|
|
849
893
|
if (agentDef?.env) {
|
|
894
|
+
const BLOCKED_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
895
|
+
"PATH",
|
|
896
|
+
"LD_PRELOAD",
|
|
897
|
+
"LD_LIBRARY_PATH",
|
|
898
|
+
"DYLD_INSERT_LIBRARIES",
|
|
899
|
+
"DYLD_LIBRARY_PATH",
|
|
900
|
+
"DYLD_FRAMEWORK_PATH"
|
|
901
|
+
]);
|
|
850
902
|
for (const [key, value] of Object.entries(agentDef.env)) {
|
|
903
|
+
if (BLOCKED_ENV_KEYS.has(key)) {
|
|
904
|
+
console.warn(`[PTY] Ignoring blocked env var from agent config: ${key}`);
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
851
907
|
const resolved = value.replace(/\$\{(\w+)\}/g, (_, varName) => {
|
|
852
908
|
return process.env[varName] || "";
|
|
853
909
|
});
|
|
@@ -913,9 +969,17 @@ var PtyManager = class {
|
|
|
913
969
|
entry.stateAnalyzer.onOutput();
|
|
914
970
|
entry.scrollback.push(cleanData);
|
|
915
971
|
entry.scrollbackBytes += cleanData.length;
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
972
|
+
if (entry.scrollbackBytes > MAX_SCROLLBACK_BYTES) {
|
|
973
|
+
let bytesToRemove = entry.scrollbackBytes - MAX_SCROLLBACK_BYTES;
|
|
974
|
+
let removeCount = 0;
|
|
975
|
+
while (removeCount < entry.scrollback.length - 1 && bytesToRemove > 0) {
|
|
976
|
+
bytesToRemove -= entry.scrollback[removeCount].length;
|
|
977
|
+
entry.scrollbackBytes -= entry.scrollback[removeCount].length;
|
|
978
|
+
removeCount++;
|
|
979
|
+
}
|
|
980
|
+
if (removeCount > 0) {
|
|
981
|
+
entry.scrollback.splice(0, removeCount);
|
|
982
|
+
}
|
|
919
983
|
}
|
|
920
984
|
for (const cb of entry.onDataCallbacks) {
|
|
921
985
|
cb(cleanData);
|
|
@@ -1012,6 +1076,10 @@ var PtyManager = class {
|
|
|
1012
1076
|
entry.shellDetector?.dispose();
|
|
1013
1077
|
entry.agentDetector?.dispose();
|
|
1014
1078
|
entry.parser.reset();
|
|
1079
|
+
entry.onDataCallbacks.length = 0;
|
|
1080
|
+
entry.onStatusCallbacks.length = 0;
|
|
1081
|
+
entry.onMetaCallbacks.length = 0;
|
|
1082
|
+
entry.onActivityCallbacks.length = 0;
|
|
1015
1083
|
try {
|
|
1016
1084
|
entry.pty.kill();
|
|
1017
1085
|
} catch {
|
|
@@ -1067,9 +1135,368 @@ var PtyManager = class {
|
|
|
1067
1135
|
}
|
|
1068
1136
|
};
|
|
1069
1137
|
|
|
1070
|
-
// src/
|
|
1071
|
-
import
|
|
1138
|
+
// src/runtime/AcpRuntime.ts
|
|
1139
|
+
import { spawn as spawn2 } from "child_process";
|
|
1072
1140
|
import fs3 from "fs";
|
|
1141
|
+
import os3 from "os";
|
|
1142
|
+
import path3 from "path";
|
|
1143
|
+
var MAX_SCROLLBACK_BYTES2 = 512 * 1024;
|
|
1144
|
+
function resolveAgentEnv(agentDef) {
|
|
1145
|
+
const env = {};
|
|
1146
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
1147
|
+
if (value !== void 0 && !key.startsWith("CLAUDE") && key !== "CLAUDECODE") {
|
|
1148
|
+
env[key] = value;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
if (agentDef?.env) {
|
|
1152
|
+
const blocked = /* @__PURE__ */ new Set([
|
|
1153
|
+
"PATH",
|
|
1154
|
+
"LD_PRELOAD",
|
|
1155
|
+
"LD_LIBRARY_PATH",
|
|
1156
|
+
"DYLD_INSERT_LIBRARIES",
|
|
1157
|
+
"DYLD_LIBRARY_PATH",
|
|
1158
|
+
"DYLD_FRAMEWORK_PATH"
|
|
1159
|
+
]);
|
|
1160
|
+
for (const [key, value] of Object.entries(agentDef.env)) {
|
|
1161
|
+
if (blocked.has(key)) continue;
|
|
1162
|
+
env[key] = value.replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] || "");
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
if (!env.PATH) {
|
|
1166
|
+
env.PATH = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
|
|
1167
|
+
if (process.platform === "darwin") {
|
|
1168
|
+
env.PATH = `/opt/homebrew/bin:/opt/homebrew/sbin:${env.PATH}`;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return env;
|
|
1172
|
+
}
|
|
1173
|
+
var AcpRuntime = class {
|
|
1174
|
+
entries = /* @__PURE__ */ new Map();
|
|
1175
|
+
configManager;
|
|
1176
|
+
constructor(configManager) {
|
|
1177
|
+
this.configManager = configManager;
|
|
1178
|
+
}
|
|
1179
|
+
spawn(paneId, config) {
|
|
1180
|
+
if (this.entries.has(paneId)) {
|
|
1181
|
+
this.kill(paneId);
|
|
1182
|
+
}
|
|
1183
|
+
const projectDir = this.configManager.getProjectDir();
|
|
1184
|
+
const basePath = config.isolation === "worktree" && config.worktreePath ? config.worktreePath : projectDir;
|
|
1185
|
+
let cwd = config.workdir ? path3.resolve(basePath, config.workdir) : basePath;
|
|
1186
|
+
if (!fs3.existsSync(cwd)) {
|
|
1187
|
+
cwd = fs3.existsSync(projectDir) ? projectDir : os3.homedir();
|
|
1188
|
+
}
|
|
1189
|
+
const agentDef = this.configManager.getAgentDefinition(config.agent);
|
|
1190
|
+
if (!agentDef) {
|
|
1191
|
+
throw new Error(`Missing agent definition for ${config.agent}`);
|
|
1192
|
+
}
|
|
1193
|
+
const env = resolveAgentEnv(agentDef);
|
|
1194
|
+
const proc = spawn2(agentDef.bin, ["acp"], {
|
|
1195
|
+
cwd,
|
|
1196
|
+
env,
|
|
1197
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1198
|
+
});
|
|
1199
|
+
const entry = {
|
|
1200
|
+
proc,
|
|
1201
|
+
config,
|
|
1202
|
+
status: "running",
|
|
1203
|
+
meta: { cwd },
|
|
1204
|
+
nextRequestId: 1,
|
|
1205
|
+
nextMessageId: 1,
|
|
1206
|
+
nextToolId: 1,
|
|
1207
|
+
pending: /* @__PURE__ */ new Map(),
|
|
1208
|
+
stdoutBuffer: "",
|
|
1209
|
+
scrollback: [],
|
|
1210
|
+
scrollbackBytes: 0,
|
|
1211
|
+
onDataCallbacks: [],
|
|
1212
|
+
onStatusCallbacks: [],
|
|
1213
|
+
onMetaCallbacks: [],
|
|
1214
|
+
onConversationCallbacks: [],
|
|
1215
|
+
onActivityCallbacks: []
|
|
1216
|
+
};
|
|
1217
|
+
this.entries.set(paneId, entry);
|
|
1218
|
+
proc.stdout.setEncoding("utf8");
|
|
1219
|
+
proc.stderr.setEncoding("utf8");
|
|
1220
|
+
proc.stdout.on("data", (chunk) => this.handleStdout(paneId, chunk));
|
|
1221
|
+
proc.stderr.on("data", (chunk) => {
|
|
1222
|
+
this.emitTerminal(paneId, `[acp stderr] ${chunk}`);
|
|
1223
|
+
});
|
|
1224
|
+
proc.on("exit", (code, signal) => {
|
|
1225
|
+
const e = this.entries.get(paneId);
|
|
1226
|
+
if (!e) return;
|
|
1227
|
+
for (const pending of e.pending.values()) {
|
|
1228
|
+
pending.reject(new Error(`ACP process exited (${code ?? "null"}${signal ? `, ${signal}` : ""})`));
|
|
1229
|
+
}
|
|
1230
|
+
e.pending.clear();
|
|
1231
|
+
this.setStatus(paneId, code === 0 ? "stopped" : "error");
|
|
1232
|
+
});
|
|
1233
|
+
this.bootstrap(paneId, cwd, config).catch((err) => {
|
|
1234
|
+
this.emitTerminal(paneId, `[acp error] ${err.message}
|
|
1235
|
+
`);
|
|
1236
|
+
this.setStatus(paneId, "error");
|
|
1237
|
+
});
|
|
1238
|
+
return proc.pid;
|
|
1239
|
+
}
|
|
1240
|
+
async bootstrap(paneId, cwd, config) {
|
|
1241
|
+
const initResult = await this.request(paneId, "initialize", {
|
|
1242
|
+
protocolVersion: 1,
|
|
1243
|
+
clientInfo: { name: "nexus", version: "0.1.0" },
|
|
1244
|
+
clientCapabilities: {}
|
|
1245
|
+
});
|
|
1246
|
+
const loadedSessionId = config.restore === "resume" && config.sessionId ? await this.tryLoadSession(paneId, config.sessionId) : null;
|
|
1247
|
+
const sessionResult = loadedSessionId ? { sessionId: loadedSessionId } : await this.request(paneId, "session/new", {
|
|
1248
|
+
cwd
|
|
1249
|
+
});
|
|
1250
|
+
const sessionId = this.extractSessionId(sessionResult) || this.extractSessionId(initResult) || config.sessionId;
|
|
1251
|
+
if (sessionId) {
|
|
1252
|
+
this.updateMeta(paneId, { sessionId, cwd });
|
|
1253
|
+
} else {
|
|
1254
|
+
this.updateMeta(paneId, { cwd });
|
|
1255
|
+
}
|
|
1256
|
+
this.emitConversation(paneId, { type: "status", status: "idle" });
|
|
1257
|
+
this.setStatus(paneId, "idle");
|
|
1258
|
+
if (config.task && config.restore !== "manual") {
|
|
1259
|
+
await this.sendPrompt(paneId, config.task);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
async tryLoadSession(paneId, sessionId) {
|
|
1263
|
+
try {
|
|
1264
|
+
const result = await this.request(paneId, "session/load", { sessionId });
|
|
1265
|
+
return this.extractSessionId(result) || sessionId;
|
|
1266
|
+
} catch {
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
async sendPrompt(paneId, text) {
|
|
1271
|
+
const entry = this.entries.get(paneId);
|
|
1272
|
+
if (!entry) return;
|
|
1273
|
+
const sessionId = entry.meta.sessionId || entry.config.sessionId;
|
|
1274
|
+
const messageId = `user-${entry.nextMessageId++}`;
|
|
1275
|
+
this.emitConversation(paneId, { type: "message", messageId, role: "user", text });
|
|
1276
|
+
this.emitTerminal(paneId, `
|
|
1277
|
+
> ${text}
|
|
1278
|
+
|
|
1279
|
+
`);
|
|
1280
|
+
this.setStatus(paneId, "running");
|
|
1281
|
+
this.emitConversation(paneId, { type: "status", status: "running" });
|
|
1282
|
+
const params = {
|
|
1283
|
+
prompt: [{ type: "text", text }]
|
|
1284
|
+
};
|
|
1285
|
+
if (sessionId) params.sessionId = sessionId;
|
|
1286
|
+
await this.request(paneId, "session/prompt", params);
|
|
1287
|
+
}
|
|
1288
|
+
onData(paneId, cb) {
|
|
1289
|
+
const entry = this.entries.get(paneId);
|
|
1290
|
+
if (entry) entry.onDataCallbacks.push(cb);
|
|
1291
|
+
}
|
|
1292
|
+
onStatus(paneId, cb) {
|
|
1293
|
+
const entry = this.entries.get(paneId);
|
|
1294
|
+
if (entry) entry.onStatusCallbacks.push(cb);
|
|
1295
|
+
}
|
|
1296
|
+
onMeta(paneId, cb) {
|
|
1297
|
+
const entry = this.entries.get(paneId);
|
|
1298
|
+
if (entry) entry.onMetaCallbacks.push(cb);
|
|
1299
|
+
}
|
|
1300
|
+
onConversation(paneId, cb) {
|
|
1301
|
+
const entry = this.entries.get(paneId);
|
|
1302
|
+
if (entry) entry.onConversationCallbacks.push(cb);
|
|
1303
|
+
}
|
|
1304
|
+
onActivity(paneId, cb) {
|
|
1305
|
+
const entry = this.entries.get(paneId);
|
|
1306
|
+
if (entry) entry.onActivityCallbacks.push(cb);
|
|
1307
|
+
}
|
|
1308
|
+
write(_paneId, _data) {
|
|
1309
|
+
}
|
|
1310
|
+
resize(_paneId, _cols, _rows) {
|
|
1311
|
+
}
|
|
1312
|
+
getScrollback(paneId) {
|
|
1313
|
+
const entry = this.entries.get(paneId);
|
|
1314
|
+
return entry ? entry.scrollback.join("") : "";
|
|
1315
|
+
}
|
|
1316
|
+
kill(paneId) {
|
|
1317
|
+
const entry = this.entries.get(paneId);
|
|
1318
|
+
if (!entry) return;
|
|
1319
|
+
entry.proc.kill();
|
|
1320
|
+
this.entries.delete(paneId);
|
|
1321
|
+
}
|
|
1322
|
+
killAll() {
|
|
1323
|
+
for (const paneId of this.entries.keys()) {
|
|
1324
|
+
this.kill(paneId);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
handleStdout(paneId, chunk) {
|
|
1328
|
+
const entry = this.entries.get(paneId);
|
|
1329
|
+
if (!entry) return;
|
|
1330
|
+
entry.stdoutBuffer += chunk;
|
|
1331
|
+
while (true) {
|
|
1332
|
+
const newline = entry.stdoutBuffer.indexOf("\n");
|
|
1333
|
+
if (newline === -1) break;
|
|
1334
|
+
const line = entry.stdoutBuffer.slice(0, newline).trim();
|
|
1335
|
+
entry.stdoutBuffer = entry.stdoutBuffer.slice(newline + 1);
|
|
1336
|
+
if (!line) continue;
|
|
1337
|
+
try {
|
|
1338
|
+
const message = JSON.parse(line);
|
|
1339
|
+
this.handleMessage(paneId, message);
|
|
1340
|
+
} catch {
|
|
1341
|
+
this.emitTerminal(paneId, line + "\n");
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
handleMessage(paneId, message) {
|
|
1346
|
+
const entry = this.entries.get(paneId);
|
|
1347
|
+
if (!entry) return;
|
|
1348
|
+
if (typeof message.id === "number") {
|
|
1349
|
+
const pending = entry.pending.get(message.id);
|
|
1350
|
+
if (pending) {
|
|
1351
|
+
entry.pending.delete(message.id);
|
|
1352
|
+
if ("error" in message && message.error) {
|
|
1353
|
+
const err = message.error;
|
|
1354
|
+
pending.reject(new Error(err?.message || "ACP request failed"));
|
|
1355
|
+
} else {
|
|
1356
|
+
pending.resolve(message.result);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
if (message.method === "session/update") {
|
|
1362
|
+
const params = message.params || {};
|
|
1363
|
+
this.handleSessionUpdate(paneId, params);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
handleSessionUpdate(paneId, params) {
|
|
1368
|
+
const sessionId = this.extractSessionId(params);
|
|
1369
|
+
if (sessionId) {
|
|
1370
|
+
this.updateMeta(paneId, { sessionId });
|
|
1371
|
+
}
|
|
1372
|
+
const rawUpdate = params.update || params.delta || params.event || params;
|
|
1373
|
+
const updates = Array.isArray(rawUpdate) ? rawUpdate : [rawUpdate];
|
|
1374
|
+
for (const update of updates) {
|
|
1375
|
+
if (!update || typeof update !== "object") continue;
|
|
1376
|
+
const record = update;
|
|
1377
|
+
const kind = String(record.type || record.kind || "");
|
|
1378
|
+
const content = this.extractText(record);
|
|
1379
|
+
if (kind.includes("agent_message")) {
|
|
1380
|
+
const messageId = `assistant-${this.entries.get(paneId)?.nextMessageId ?? 1}`;
|
|
1381
|
+
this.emitConversation(paneId, {
|
|
1382
|
+
type: "message",
|
|
1383
|
+
messageId,
|
|
1384
|
+
role: "assistant",
|
|
1385
|
+
text: content,
|
|
1386
|
+
append: true
|
|
1387
|
+
});
|
|
1388
|
+
if (content) {
|
|
1389
|
+
this.emitTerminal(paneId, content);
|
|
1390
|
+
}
|
|
1391
|
+
this.setStatus(paneId, "running");
|
|
1392
|
+
} else if (kind.includes("tool_call")) {
|
|
1393
|
+
const toolCallId = `tool-${this.entries.get(paneId)?.nextToolId ?? 1}`;
|
|
1394
|
+
this.emitConversation(paneId, {
|
|
1395
|
+
type: "tool",
|
|
1396
|
+
toolCallId,
|
|
1397
|
+
title: String(record.title || record.name || "tool"),
|
|
1398
|
+
status: kind.includes("update") ? "in_progress" : "pending",
|
|
1399
|
+
text: content || void 0
|
|
1400
|
+
});
|
|
1401
|
+
if (content) {
|
|
1402
|
+
this.emitTerminal(paneId, `
|
|
1403
|
+
[tool] ${content}
|
|
1404
|
+
`);
|
|
1405
|
+
}
|
|
1406
|
+
} else if (kind.includes("turn") || kind.includes("done") || kind.includes("completed")) {
|
|
1407
|
+
this.setStatus(paneId, "idle");
|
|
1408
|
+
this.emitConversation(paneId, { type: "status", status: "idle" });
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
extractText(record) {
|
|
1413
|
+
const direct = record.text || record.delta || record.content;
|
|
1414
|
+
if (typeof direct === "string") return direct;
|
|
1415
|
+
if (Array.isArray(direct)) {
|
|
1416
|
+
return direct.map((item) => {
|
|
1417
|
+
if (typeof item === "string") return item;
|
|
1418
|
+
if (item && typeof item === "object" && typeof item.text === "string") {
|
|
1419
|
+
return String(item.text);
|
|
1420
|
+
}
|
|
1421
|
+
return "";
|
|
1422
|
+
}).join("");
|
|
1423
|
+
}
|
|
1424
|
+
if (direct && typeof direct === "object" && typeof direct.text === "string") {
|
|
1425
|
+
return String(direct.text);
|
|
1426
|
+
}
|
|
1427
|
+
return "";
|
|
1428
|
+
}
|
|
1429
|
+
request(paneId, method, params) {
|
|
1430
|
+
const entry = this.entries.get(paneId);
|
|
1431
|
+
if (!entry) {
|
|
1432
|
+
return Promise.reject(new Error(`Missing ACP entry for ${paneId}`));
|
|
1433
|
+
}
|
|
1434
|
+
const id = entry.nextRequestId++;
|
|
1435
|
+
const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
|
|
1436
|
+
entry.proc.stdin.write(payload);
|
|
1437
|
+
return new Promise((resolve, reject) => {
|
|
1438
|
+
entry.pending.set(id, { resolve, reject });
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
extractSessionId(result) {
|
|
1442
|
+
if (!result || typeof result !== "object") return void 0;
|
|
1443
|
+
const record = result;
|
|
1444
|
+
if (typeof record.sessionId === "string") return record.sessionId;
|
|
1445
|
+
if (typeof record.session_id === "string") return record.session_id;
|
|
1446
|
+
if (record.session && typeof record.session === "object") {
|
|
1447
|
+
const nested = record.session;
|
|
1448
|
+
if (typeof nested.id === "string") return nested.id;
|
|
1449
|
+
if (typeof nested.sessionId === "string") return nested.sessionId;
|
|
1450
|
+
}
|
|
1451
|
+
return void 0;
|
|
1452
|
+
}
|
|
1453
|
+
emitTerminal(paneId, data) {
|
|
1454
|
+
const entry = this.entries.get(paneId);
|
|
1455
|
+
if (!entry || !data) return;
|
|
1456
|
+
entry.scrollback.push(data);
|
|
1457
|
+
entry.scrollbackBytes += data.length;
|
|
1458
|
+
if (entry.scrollbackBytes > MAX_SCROLLBACK_BYTES2) {
|
|
1459
|
+
let bytesToRemove = entry.scrollbackBytes - MAX_SCROLLBACK_BYTES2;
|
|
1460
|
+
let removeCount = 0;
|
|
1461
|
+
while (removeCount < entry.scrollback.length - 1 && bytesToRemove > 0) {
|
|
1462
|
+
bytesToRemove -= entry.scrollback[removeCount].length;
|
|
1463
|
+
entry.scrollbackBytes -= entry.scrollback[removeCount].length;
|
|
1464
|
+
removeCount++;
|
|
1465
|
+
}
|
|
1466
|
+
if (removeCount > 0) entry.scrollback.splice(0, removeCount);
|
|
1467
|
+
}
|
|
1468
|
+
for (const cb of entry.onDataCallbacks) {
|
|
1469
|
+
cb(data);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
setStatus(paneId, status) {
|
|
1473
|
+
const entry = this.entries.get(paneId);
|
|
1474
|
+
if (!entry || entry.status === status) return;
|
|
1475
|
+
entry.status = status;
|
|
1476
|
+
for (const cb of entry.onStatusCallbacks) {
|
|
1477
|
+
cb(status);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
updateMeta(paneId, meta) {
|
|
1481
|
+
const entry = this.entries.get(paneId);
|
|
1482
|
+
if (!entry) return;
|
|
1483
|
+
entry.meta = { ...entry.meta, ...meta };
|
|
1484
|
+
for (const cb of entry.onMetaCallbacks) {
|
|
1485
|
+
cb(entry.meta);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
emitConversation(paneId, event) {
|
|
1489
|
+
const entry = this.entries.get(paneId);
|
|
1490
|
+
if (!entry) return;
|
|
1491
|
+
for (const cb of entry.onConversationCallbacks) {
|
|
1492
|
+
cb(event);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
// src/git/WorktreeManager.ts
|
|
1498
|
+
import path4 from "path";
|
|
1499
|
+
import fs4 from "fs";
|
|
1073
1500
|
import { simpleGit } from "simple-git";
|
|
1074
1501
|
var WorktreeManager = class {
|
|
1075
1502
|
projectDir;
|
|
@@ -1087,20 +1514,49 @@ var WorktreeManager = class {
|
|
|
1087
1514
|
const baseBranch = await this.getCurrentBranch();
|
|
1088
1515
|
const slug = this.slugify(paneName);
|
|
1089
1516
|
const branch = `nexus/${paneId}-${slug}`;
|
|
1090
|
-
const worktreePath =
|
|
1091
|
-
if (
|
|
1517
|
+
const worktreePath = path4.join(this.projectDir, ".nexus", "worktrees", paneId);
|
|
1518
|
+
if (fs4.existsSync(worktreePath)) {
|
|
1092
1519
|
await this.forceRemoveWorktree(worktreePath);
|
|
1093
1520
|
}
|
|
1094
1521
|
try {
|
|
1095
1522
|
await this.git.raw(["branch", "-D", branch]);
|
|
1096
1523
|
} catch {
|
|
1097
1524
|
}
|
|
1098
|
-
|
|
1525
|
+
fs4.mkdirSync(path4.dirname(worktreePath), { recursive: true });
|
|
1099
1526
|
await this.git.raw(["worktree", "add", "-b", branch, worktreePath, baseBranch]);
|
|
1100
1527
|
const entry = { path: worktreePath, branch, baseBranch };
|
|
1101
1528
|
this.worktrees.set(paneId, entry);
|
|
1102
1529
|
return { worktreePath, branch };
|
|
1103
1530
|
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Restore a worktree from a previous session.
|
|
1533
|
+
* If the worktree directory still exists, re-register it.
|
|
1534
|
+
* If not, recreate it from the existing branch.
|
|
1535
|
+
* Returns false if the branch no longer exists (stale config).
|
|
1536
|
+
*/
|
|
1537
|
+
async restore(paneId, branch, worktreePath) {
|
|
1538
|
+
const baseBranch = await this.getCurrentBranch();
|
|
1539
|
+
try {
|
|
1540
|
+
await this.git.raw(["rev-parse", "--verify", branch]);
|
|
1541
|
+
} catch {
|
|
1542
|
+
return false;
|
|
1543
|
+
}
|
|
1544
|
+
if (fs4.existsSync(worktreePath)) {
|
|
1545
|
+
this.worktrees.set(paneId, { path: worktreePath, branch, baseBranch });
|
|
1546
|
+
return true;
|
|
1547
|
+
}
|
|
1548
|
+
try {
|
|
1549
|
+
await this.git.raw(["worktree", "prune"]).catch(() => {
|
|
1550
|
+
});
|
|
1551
|
+
fs4.mkdirSync(path4.dirname(worktreePath), { recursive: true });
|
|
1552
|
+
await this.git.raw(["worktree", "add", worktreePath, branch]);
|
|
1553
|
+
this.worktrees.set(paneId, { path: worktreePath, branch, baseBranch });
|
|
1554
|
+
return true;
|
|
1555
|
+
} catch (err) {
|
|
1556
|
+
console.warn(`[WorktreeManager] Failed to restore worktree for ${paneId}:`, err.message);
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1104
1560
|
/**
|
|
1105
1561
|
* Remove a worktree. Branch is kept for later merge/PR.
|
|
1106
1562
|
*/
|
|
@@ -1181,6 +1637,60 @@ var WorktreeManager = class {
|
|
|
1181
1637
|
}
|
|
1182
1638
|
return diffs;
|
|
1183
1639
|
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Merge the worktree branch into the base branch (e.g. main).
|
|
1642
|
+
* First commits any uncommitted changes in the worktree, then merges.
|
|
1643
|
+
*/
|
|
1644
|
+
async merge(paneId) {
|
|
1645
|
+
const entry = this.worktrees.get(paneId);
|
|
1646
|
+
if (!entry) {
|
|
1647
|
+
return { success: false, message: "Worktree not found for this pane" };
|
|
1648
|
+
}
|
|
1649
|
+
const wtGit = simpleGit(entry.path);
|
|
1650
|
+
try {
|
|
1651
|
+
const status = await wtGit.status();
|
|
1652
|
+
const hasChanges = status.modified.length > 0 || status.created.length > 0 || status.deleted.length > 0 || status.staged.length > 0 || status.not_added.length > 0;
|
|
1653
|
+
if (hasChanges) {
|
|
1654
|
+
await wtGit.add("-A");
|
|
1655
|
+
await wtGit.commit(`nexus: auto-commit before merge (${entry.branch})`);
|
|
1656
|
+
}
|
|
1657
|
+
const log = await wtGit.log([`${entry.baseBranch}..${entry.branch}`]);
|
|
1658
|
+
if (log.total === 0) {
|
|
1659
|
+
return { success: false, message: "No changes to merge" };
|
|
1660
|
+
}
|
|
1661
|
+
await this.git.merge([entry.branch]);
|
|
1662
|
+
return {
|
|
1663
|
+
success: true,
|
|
1664
|
+
message: `Merged ${log.total} commit${log.total !== 1 ? "s" : ""} from ${entry.branch} into ${entry.baseBranch}`
|
|
1665
|
+
};
|
|
1666
|
+
} catch (err) {
|
|
1667
|
+
try {
|
|
1668
|
+
await this.git.merge(["--abort"]);
|
|
1669
|
+
} catch {
|
|
1670
|
+
}
|
|
1671
|
+
return {
|
|
1672
|
+
success: false,
|
|
1673
|
+
message: `Merge conflict: ${err.message}`
|
|
1674
|
+
};
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1678
|
+
* Discard all changes: remove worktree and delete the branch.
|
|
1679
|
+
*/
|
|
1680
|
+
async discard(paneId) {
|
|
1681
|
+
const entry = this.worktrees.get(paneId);
|
|
1682
|
+
if (!entry) {
|
|
1683
|
+
return { success: false, message: "Worktree not found for this pane" };
|
|
1684
|
+
}
|
|
1685
|
+
const branch = entry.branch;
|
|
1686
|
+
await this.forceRemoveWorktree(entry.path);
|
|
1687
|
+
try {
|
|
1688
|
+
await this.git.raw(["branch", "-D", branch]);
|
|
1689
|
+
} catch {
|
|
1690
|
+
}
|
|
1691
|
+
this.worktrees.delete(paneId);
|
|
1692
|
+
return { success: true, message: `Discarded branch ${branch}` };
|
|
1693
|
+
}
|
|
1184
1694
|
getWorktreePath(paneId) {
|
|
1185
1695
|
return this.worktrees.get(paneId)?.path;
|
|
1186
1696
|
}
|
|
@@ -1217,7 +1727,7 @@ var WorktreeManager = class {
|
|
|
1217
1727
|
await this.git.raw(["worktree", "remove", "--force", wtPath]);
|
|
1218
1728
|
} catch {
|
|
1219
1729
|
try {
|
|
1220
|
-
|
|
1730
|
+
fs4.rmSync(wtPath, { recursive: true, force: true });
|
|
1221
1731
|
await this.git.raw(["worktree", "prune"]);
|
|
1222
1732
|
} catch {
|
|
1223
1733
|
}
|
|
@@ -1237,7 +1747,7 @@ var WorktreeManager = class {
|
|
|
1237
1747
|
};
|
|
1238
1748
|
|
|
1239
1749
|
// src/git/GitService.ts
|
|
1240
|
-
import
|
|
1750
|
+
import path5 from "path";
|
|
1241
1751
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
1242
1752
|
import { watch } from "chokidar";
|
|
1243
1753
|
var GitService = class {
|
|
@@ -1263,11 +1773,11 @@ var GitService = class {
|
|
|
1263
1773
|
this.refresh();
|
|
1264
1774
|
}, 1e3);
|
|
1265
1775
|
};
|
|
1266
|
-
const gitDir =
|
|
1776
|
+
const gitDir = path5.join(this.projectDir, ".git");
|
|
1267
1777
|
this.gitWatcher = watch([
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1778
|
+
path5.join(gitDir, "index"),
|
|
1779
|
+
path5.join(gitDir, "HEAD"),
|
|
1780
|
+
path5.join(gitDir, "refs")
|
|
1271
1781
|
], {
|
|
1272
1782
|
persistent: true,
|
|
1273
1783
|
ignoreInitial: true
|
|
@@ -1281,7 +1791,7 @@ var GitService = class {
|
|
|
1281
1791
|
};
|
|
1282
1792
|
this.workWatcher = watch(this.projectDir, {
|
|
1283
1793
|
ignored: (filePath) => {
|
|
1284
|
-
const basename =
|
|
1794
|
+
const basename = path5.basename(filePath);
|
|
1285
1795
|
return basename === ".git" || basename === "node_modules" || basename === ".nexus" || basename === "dist";
|
|
1286
1796
|
},
|
|
1287
1797
|
persistent: true,
|
|
@@ -1292,10 +1802,18 @@ var GitService = class {
|
|
|
1292
1802
|
}
|
|
1293
1803
|
async refresh() {
|
|
1294
1804
|
try {
|
|
1295
|
-
const result = await
|
|
1805
|
+
const result = await Promise.race([
|
|
1806
|
+
this.getDiffs(),
|
|
1807
|
+
new Promise(
|
|
1808
|
+
(_, reject) => setTimeout(() => reject(new Error("git diff timeout")), 15e3)
|
|
1809
|
+
)
|
|
1810
|
+
]);
|
|
1296
1811
|
this.currentResult = result;
|
|
1297
1812
|
this.notifyListeners();
|
|
1298
|
-
} catch {
|
|
1813
|
+
} catch (err) {
|
|
1814
|
+
if (err.message === "git diff timeout") {
|
|
1815
|
+
console.warn("[GitService] git diff timed out (15s), using cached result");
|
|
1816
|
+
}
|
|
1299
1817
|
}
|
|
1300
1818
|
}
|
|
1301
1819
|
getCurrentDiffs() {
|
|
@@ -1327,10 +1845,10 @@ var GitService = class {
|
|
|
1327
1845
|
const status = await this.git.status();
|
|
1328
1846
|
const isUntracked = status.not_added.includes(file) || status.created.includes(file);
|
|
1329
1847
|
if (isUntracked) {
|
|
1330
|
-
const fullPath =
|
|
1331
|
-
const
|
|
1332
|
-
if (
|
|
1333
|
-
|
|
1848
|
+
const fullPath = path5.join(this.projectDir, file);
|
|
1849
|
+
const fs11 = await import("fs");
|
|
1850
|
+
if (fs11.existsSync(fullPath)) {
|
|
1851
|
+
fs11.unlinkSync(fullPath);
|
|
1334
1852
|
}
|
|
1335
1853
|
} else {
|
|
1336
1854
|
await this.git.checkout(["--", file]);
|
|
@@ -1471,15 +1989,19 @@ function nextPaneId() {
|
|
|
1471
1989
|
var WorkspaceManager = class {
|
|
1472
1990
|
panes = /* @__PURE__ */ new Map();
|
|
1473
1991
|
ptyManager;
|
|
1992
|
+
acpRuntime;
|
|
1474
1993
|
configManager;
|
|
1475
1994
|
worktreeManager;
|
|
1476
1995
|
perPaneGitServices = /* @__PURE__ */ new Map();
|
|
1477
1996
|
wsName = "";
|
|
1478
1997
|
wsDescription = "";
|
|
1998
|
+
// Serialize config writes to prevent race conditions when closing multiple panes
|
|
1999
|
+
configWriteLock = Promise.resolve();
|
|
1479
2000
|
// Multi-client event listener sets
|
|
1480
2001
|
listeners = {
|
|
1481
2002
|
onPaneAdded: /* @__PURE__ */ new Set(),
|
|
1482
2003
|
onPaneRemoved: /* @__PURE__ */ new Set(),
|
|
2004
|
+
onConversationEvent: /* @__PURE__ */ new Set(),
|
|
1483
2005
|
onPaneStatus: /* @__PURE__ */ new Set(),
|
|
1484
2006
|
onPaneMeta: /* @__PURE__ */ new Set(),
|
|
1485
2007
|
onTerminalData: /* @__PURE__ */ new Set(),
|
|
@@ -1492,9 +2014,10 @@ var WorkspaceManager = class {
|
|
|
1492
2014
|
constructor(configManager) {
|
|
1493
2015
|
this.configManager = configManager;
|
|
1494
2016
|
this.ptyManager = new PtyManager(configManager);
|
|
2017
|
+
this.acpRuntime = new AcpRuntime(configManager);
|
|
1495
2018
|
this.worktreeManager = new WorktreeManager(configManager.getProjectDir());
|
|
1496
2019
|
}
|
|
1497
|
-
init() {
|
|
2020
|
+
async init() {
|
|
1498
2021
|
const wsConfig = this.configManager.initWorkspace();
|
|
1499
2022
|
this.wsName = wsConfig.name;
|
|
1500
2023
|
this.wsDescription = wsConfig.description || "";
|
|
@@ -1511,8 +2034,25 @@ var WorkspaceManager = class {
|
|
|
1511
2034
|
if (paneConfig.sessionId && paneConfig.agent !== "__shell__") {
|
|
1512
2035
|
paneConfig.restore = "resume";
|
|
1513
2036
|
}
|
|
2037
|
+
if (paneConfig.isolation === "worktree" && paneConfig.branch && paneConfig.worktreePath) {
|
|
2038
|
+
try {
|
|
2039
|
+
const restored = await this.worktreeManager.restore(paneConfig.id, paneConfig.branch, paneConfig.worktreePath);
|
|
2040
|
+
if (!restored) {
|
|
2041
|
+
console.warn(`Skipping worktree pane ${paneConfig.id} (${paneConfig.name}): branch no longer exists`);
|
|
2042
|
+
failCount++;
|
|
2043
|
+
continue;
|
|
2044
|
+
}
|
|
2045
|
+
} catch (err) {
|
|
2046
|
+
console.warn(`Skipping worktree pane ${paneConfig.id} (${paneConfig.name}): restore failed:`, err.message);
|
|
2047
|
+
failCount++;
|
|
2048
|
+
continue;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
1514
2051
|
try {
|
|
1515
2052
|
this.spawnPane(paneConfig);
|
|
2053
|
+
if (paneConfig.isolation === "worktree" && paneConfig.worktreePath) {
|
|
2054
|
+
await this.startPaneGitService(paneConfig.id, paneConfig.worktreePath);
|
|
2055
|
+
}
|
|
1516
2056
|
} catch (err) {
|
|
1517
2057
|
console.warn(`Skipping stale pane ${paneConfig.id} (${paneConfig.name}):`, err.message);
|
|
1518
2058
|
failCount++;
|
|
@@ -1575,7 +2115,11 @@ var WorkspaceManager = class {
|
|
|
1575
2115
|
}
|
|
1576
2116
|
async closePane(paneId) {
|
|
1577
2117
|
const pane = this.panes.get(paneId);
|
|
1578
|
-
|
|
2118
|
+
if (pane?.runtime === "acp") {
|
|
2119
|
+
this.acpRuntime.kill(paneId);
|
|
2120
|
+
} else {
|
|
2121
|
+
this.ptyManager.kill(paneId);
|
|
2122
|
+
}
|
|
1579
2123
|
if (pane?.isolation === "worktree") {
|
|
1580
2124
|
this.stopPaneGitService(paneId);
|
|
1581
2125
|
await this.worktreeManager.remove(paneId);
|
|
@@ -1587,7 +2131,11 @@ var WorkspaceManager = class {
|
|
|
1587
2131
|
restartPane(paneId, mode, sessionId) {
|
|
1588
2132
|
const existingState = this.panes.get(paneId);
|
|
1589
2133
|
if (!existingState) return;
|
|
1590
|
-
|
|
2134
|
+
if (existingState.runtime === "acp") {
|
|
2135
|
+
this.acpRuntime.kill(paneId);
|
|
2136
|
+
} else {
|
|
2137
|
+
this.ptyManager.kill(paneId);
|
|
2138
|
+
}
|
|
1591
2139
|
const resolvedSessionId = mode === "resume" ? sessionId || existingState.sessionId || existingState.meta.sessionId : void 0;
|
|
1592
2140
|
const config = {
|
|
1593
2141
|
id: paneId,
|
|
@@ -1605,14 +2153,59 @@ var WorkspaceManager = class {
|
|
|
1605
2153
|
this.spawnPane(config);
|
|
1606
2154
|
this.updatePaneConfigSessionId(paneId, resolvedSessionId);
|
|
1607
2155
|
}
|
|
2156
|
+
async mergeWorktree(paneId) {
|
|
2157
|
+
const pane = this.panes.get(paneId);
|
|
2158
|
+
if (!pane || pane.isolation !== "worktree") {
|
|
2159
|
+
return { success: false, message: "Pane is not a worktree pane" };
|
|
2160
|
+
}
|
|
2161
|
+
return this.worktreeManager.merge(paneId);
|
|
2162
|
+
}
|
|
2163
|
+
async discardWorktree(paneId) {
|
|
2164
|
+
const pane = this.panes.get(paneId);
|
|
2165
|
+
if (!pane || pane.isolation !== "worktree") {
|
|
2166
|
+
return { success: false, message: "Pane is not a worktree pane" };
|
|
2167
|
+
}
|
|
2168
|
+
this.stopPaneGitService(paneId);
|
|
2169
|
+
const result = await this.worktreeManager.discard(paneId);
|
|
2170
|
+
if (result.success) {
|
|
2171
|
+
pane.isolation = "shared";
|
|
2172
|
+
pane.worktreePath = void 0;
|
|
2173
|
+
pane.branch = void 0;
|
|
2174
|
+
this.emit("onPaneDiff", paneId, []);
|
|
2175
|
+
}
|
|
2176
|
+
return result;
|
|
2177
|
+
}
|
|
1608
2178
|
writeToPane(paneId, data) {
|
|
2179
|
+
const pane = this.panes.get(paneId);
|
|
2180
|
+
if (!pane) return;
|
|
2181
|
+
if (pane.runtime === "acp") {
|
|
2182
|
+
this.acpRuntime.write(paneId, data);
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
1609
2185
|
this.ptyManager.write(paneId, data);
|
|
1610
2186
|
}
|
|
2187
|
+
sendConversationToPane(paneId, text) {
|
|
2188
|
+
const pane = this.panes.get(paneId);
|
|
2189
|
+
if (!pane) return Promise.resolve();
|
|
2190
|
+
if (pane.runtime === "acp") {
|
|
2191
|
+
return this.acpRuntime.sendPrompt(paneId, text);
|
|
2192
|
+
}
|
|
2193
|
+
this.ptyManager.write(paneId, text + "\r");
|
|
2194
|
+
return Promise.resolve();
|
|
2195
|
+
}
|
|
1611
2196
|
resizePane(paneId, cols, rows) {
|
|
2197
|
+
const pane = this.panes.get(paneId);
|
|
2198
|
+
if (!pane) return;
|
|
2199
|
+
if (pane.runtime === "acp") {
|
|
2200
|
+
this.acpRuntime.resize(paneId, cols, rows);
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
1612
2203
|
this.ptyManager.resize(paneId, cols, rows);
|
|
1613
2204
|
}
|
|
1614
2205
|
getScrollback(paneId) {
|
|
1615
|
-
|
|
2206
|
+
const pane = this.panes.get(paneId);
|
|
2207
|
+
if (!pane) return "";
|
|
2208
|
+
return pane.runtime === "acp" ? this.acpRuntime.getScrollback(paneId) : this.ptyManager.getScrollback(paneId);
|
|
1616
2209
|
}
|
|
1617
2210
|
// ─── Event Registration (multi-client safe) ────────────────
|
|
1618
2211
|
/**
|
|
@@ -1669,7 +2262,8 @@ var WorkspaceManager = class {
|
|
|
1669
2262
|
}
|
|
1670
2263
|
}
|
|
1671
2264
|
spawnPane(config, cols, rows) {
|
|
1672
|
-
const
|
|
2265
|
+
const runtime = this.resolveRuntime(config.agent);
|
|
2266
|
+
const pid = runtime === "acp" ? this.acpRuntime.spawn(config.id, config) : this.ptyManager.spawn(config.id, config, cols || 80, rows || 24);
|
|
1673
2267
|
const pane = {
|
|
1674
2268
|
id: config.id,
|
|
1675
2269
|
name: config.name,
|
|
@@ -1682,6 +2276,7 @@ var WorkspaceManager = class {
|
|
|
1682
2276
|
branch: config.branch,
|
|
1683
2277
|
worktreePath: config.worktreePath,
|
|
1684
2278
|
sessionId: config.sessionId,
|
|
2279
|
+
runtime,
|
|
1685
2280
|
status: "running",
|
|
1686
2281
|
pid,
|
|
1687
2282
|
meta: {},
|
|
@@ -1689,17 +2284,18 @@ var WorkspaceManager = class {
|
|
|
1689
2284
|
};
|
|
1690
2285
|
this.panes.set(config.id, pane);
|
|
1691
2286
|
this.emit("onPaneAdded", pane);
|
|
1692
|
-
this.
|
|
2287
|
+
const runtimeAdapter = runtime === "acp" ? this.acpRuntime : this.ptyManager;
|
|
2288
|
+
runtimeAdapter.onData(config.id, (data) => {
|
|
1693
2289
|
this.emit("onTerminalData", config.id, data);
|
|
1694
2290
|
});
|
|
1695
|
-
|
|
2291
|
+
runtimeAdapter.onStatus(config.id, (status) => {
|
|
1696
2292
|
const p = this.panes.get(config.id);
|
|
1697
2293
|
if (p) {
|
|
1698
2294
|
p.status = status;
|
|
1699
2295
|
this.emit("onPaneStatus", config.id, status);
|
|
1700
2296
|
}
|
|
1701
2297
|
});
|
|
1702
|
-
|
|
2298
|
+
runtimeAdapter.onMeta(config.id, (meta) => {
|
|
1703
2299
|
const p = this.panes.get(config.id);
|
|
1704
2300
|
if (p) {
|
|
1705
2301
|
p.meta = meta;
|
|
@@ -1710,11 +2306,21 @@ var WorkspaceManager = class {
|
|
|
1710
2306
|
this.emit("onPaneMeta", config.id, meta);
|
|
1711
2307
|
}
|
|
1712
2308
|
});
|
|
1713
|
-
|
|
2309
|
+
runtimeAdapter.onActivity(config.id, (activity) => {
|
|
1714
2310
|
this.emit("onPaneActivity", config.id, activity);
|
|
1715
2311
|
});
|
|
2312
|
+
if (runtime === "acp") {
|
|
2313
|
+
this.acpRuntime.onConversation(config.id, (event) => {
|
|
2314
|
+
this.emit("onConversationEvent", config.id, event);
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
1716
2317
|
return pane;
|
|
1717
2318
|
}
|
|
2319
|
+
resolveRuntime(agentType) {
|
|
2320
|
+
if (agentType === "__shell__") return "pty";
|
|
2321
|
+
const def = this.configManager.getAgentDefinition(agentType);
|
|
2322
|
+
return def?.transport || "pty";
|
|
2323
|
+
}
|
|
1718
2324
|
async startPaneGitService(paneId, worktreePath) {
|
|
1719
2325
|
const gitService = new GitService(worktreePath);
|
|
1720
2326
|
gitService.onDiffChange((result) => {
|
|
@@ -1732,21 +2338,25 @@ var WorkspaceManager = class {
|
|
|
1732
2338
|
}
|
|
1733
2339
|
}
|
|
1734
2340
|
persistPaneConfig(config) {
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
wsConfig
|
|
1738
|
-
|
|
1739
|
-
|
|
2341
|
+
this.serializedConfigWrite(() => {
|
|
2342
|
+
const wsConfig = this.configManager.loadWorkspaceConfig();
|
|
2343
|
+
if (wsConfig) {
|
|
2344
|
+
wsConfig.panes.push(config);
|
|
2345
|
+
this.configManager.saveWorkspaceConfig(wsConfig);
|
|
2346
|
+
}
|
|
2347
|
+
});
|
|
1740
2348
|
}
|
|
1741
2349
|
updatePaneConfigSessionId(paneId, sessionId) {
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
paneConfig
|
|
1747
|
-
|
|
2350
|
+
this.serializedConfigWrite(() => {
|
|
2351
|
+
const wsConfig = this.configManager.loadWorkspaceConfig();
|
|
2352
|
+
if (wsConfig) {
|
|
2353
|
+
const paneConfig = wsConfig.panes.find((p) => p.id === paneId);
|
|
2354
|
+
if (paneConfig) {
|
|
2355
|
+
paneConfig.sessionId = sessionId;
|
|
2356
|
+
this.configManager.saveWorkspaceConfig(wsConfig);
|
|
2357
|
+
}
|
|
1748
2358
|
}
|
|
1749
|
-
}
|
|
2359
|
+
});
|
|
1750
2360
|
}
|
|
1751
2361
|
getSessionList(paneId) {
|
|
1752
2362
|
const sessions = [];
|
|
@@ -1784,24 +2394,39 @@ var WorkspaceManager = class {
|
|
|
1784
2394
|
return sessions;
|
|
1785
2395
|
}
|
|
1786
2396
|
removePaneFromConfig(paneId) {
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
2397
|
+
this.serializedConfigWrite(() => {
|
|
2398
|
+
const wsConfig = this.configManager.loadWorkspaceConfig();
|
|
2399
|
+
if (wsConfig) {
|
|
2400
|
+
wsConfig.panes = wsConfig.panes.filter((p) => p.id !== paneId);
|
|
2401
|
+
this.configManager.saveWorkspaceConfig(wsConfig);
|
|
2402
|
+
}
|
|
2403
|
+
});
|
|
2404
|
+
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Serialize config file writes to prevent race conditions
|
|
2407
|
+
* when multiple panes are created/closed simultaneously.
|
|
2408
|
+
*/
|
|
2409
|
+
serializedConfigWrite(fn) {
|
|
2410
|
+
this.configWriteLock = this.configWriteLock.then(() => {
|
|
2411
|
+
try {
|
|
2412
|
+
fn();
|
|
2413
|
+
} catch (err) {
|
|
2414
|
+
console.error("[WorkspaceManager] Config write failed:", err);
|
|
2415
|
+
}
|
|
2416
|
+
});
|
|
1792
2417
|
}
|
|
1793
2418
|
async shutdown() {
|
|
1794
2419
|
this.ptyManager.killAll();
|
|
2420
|
+
this.acpRuntime.killAll();
|
|
1795
2421
|
for (const [paneId] of this.perPaneGitServices) {
|
|
1796
2422
|
this.stopPaneGitService(paneId);
|
|
1797
2423
|
}
|
|
1798
|
-
await this.worktreeManager.removeAll();
|
|
1799
2424
|
}
|
|
1800
2425
|
};
|
|
1801
2426
|
|
|
1802
2427
|
// src/workspace/AgentsYamlWriter.ts
|
|
1803
|
-
import
|
|
1804
|
-
import
|
|
2428
|
+
import fs5 from "fs";
|
|
2429
|
+
import path6 from "path";
|
|
1805
2430
|
import yaml2 from "js-yaml";
|
|
1806
2431
|
var AgentsYamlWriter = class {
|
|
1807
2432
|
projectDir;
|
|
@@ -1826,8 +2451,8 @@ var AgentsYamlWriter = class {
|
|
|
1826
2451
|
this.writeFile(panes);
|
|
1827
2452
|
}
|
|
1828
2453
|
writeFile(panes) {
|
|
1829
|
-
const nexusDir =
|
|
1830
|
-
|
|
2454
|
+
const nexusDir = path6.join(this.projectDir, ".nexus");
|
|
2455
|
+
fs5.mkdirSync(nexusDir, { recursive: true });
|
|
1831
2456
|
const visible = panes.filter((p) => p.agent !== "__shell__");
|
|
1832
2457
|
const data = {
|
|
1833
2458
|
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -1837,11 +2462,12 @@ var AgentsYamlWriter = class {
|
|
|
1837
2462
|
id: p.id,
|
|
1838
2463
|
name: p.name,
|
|
1839
2464
|
agent: p.agent,
|
|
2465
|
+
runtime: p.runtime,
|
|
1840
2466
|
pid: p.pid,
|
|
1841
2467
|
status: p.status,
|
|
1842
2468
|
isolation: p.isolation || "shared",
|
|
1843
2469
|
branch: p.branch || void 0,
|
|
1844
|
-
workdir: p.workdir ?
|
|
2470
|
+
workdir: p.workdir ? path6.resolve(basePath, p.workdir) : basePath,
|
|
1845
2471
|
task: p.task || void 0,
|
|
1846
2472
|
model: p.meta.model || void 0,
|
|
1847
2473
|
context_used_pct: p.meta.contextUsedPct ?? void 0,
|
|
@@ -1851,18 +2477,19 @@ var AgentsYamlWriter = class {
|
|
|
1851
2477
|
};
|
|
1852
2478
|
})
|
|
1853
2479
|
};
|
|
1854
|
-
const filePath =
|
|
1855
|
-
|
|
2480
|
+
const filePath = path6.join(nexusDir, "agents.yaml");
|
|
2481
|
+
fs5.writeFileSync(filePath, yaml2.dump(data, { lineWidth: -1 }));
|
|
1856
2482
|
}
|
|
1857
2483
|
};
|
|
1858
2484
|
|
|
1859
2485
|
// src/fs/FsWatcher.ts
|
|
1860
|
-
import
|
|
1861
|
-
import
|
|
2486
|
+
import fs6 from "fs";
|
|
2487
|
+
import path7 from "path";
|
|
1862
2488
|
import { watch as watch2 } from "chokidar";
|
|
1863
2489
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
1864
2490
|
"node_modules",
|
|
1865
2491
|
".git",
|
|
2492
|
+
".nexus",
|
|
1866
2493
|
"dist",
|
|
1867
2494
|
".cache",
|
|
1868
2495
|
".turbo",
|
|
@@ -1889,7 +2516,7 @@ var FsWatcher = class {
|
|
|
1889
2516
|
this.notifyListeners();
|
|
1890
2517
|
this.watcher = watch2(this.projectDir, {
|
|
1891
2518
|
ignored: (filePath) => {
|
|
1892
|
-
const basename =
|
|
2519
|
+
const basename = path7.basename(filePath);
|
|
1893
2520
|
return IGNORED_DIRS.has(basename) || IGNORED_FILES.has(basename);
|
|
1894
2521
|
},
|
|
1895
2522
|
persistent: true,
|
|
@@ -1906,9 +2533,9 @@ var FsWatcher = class {
|
|
|
1906
2533
|
}, 300);
|
|
1907
2534
|
};
|
|
1908
2535
|
const emitFileChange = (eventType, filePath) => {
|
|
1909
|
-
const relativePath =
|
|
2536
|
+
const relativePath = path7.relative(this.projectDir, filePath);
|
|
1910
2537
|
if (!relativePath || relativePath.startsWith("..")) return;
|
|
1911
|
-
if (!/\.\w{1,10}$/.test(
|
|
2538
|
+
if (!/\.\w{1,10}$/.test(path7.basename(filePath))) return;
|
|
1912
2539
|
const now = Date.now();
|
|
1913
2540
|
const lastChange = this.recentChanges.get(relativePath);
|
|
1914
2541
|
if (lastChange && now - lastChange < 1e3) return;
|
|
@@ -1977,14 +2604,14 @@ var FsWatcher = class {
|
|
|
1977
2604
|
return parts.join("\n");
|
|
1978
2605
|
}
|
|
1979
2606
|
buildTree(dirPath, depth) {
|
|
1980
|
-
if (depth >
|
|
2607
|
+
if (depth > 5) return [];
|
|
1981
2608
|
try {
|
|
1982
|
-
const entries =
|
|
2609
|
+
const entries = fs6.readdirSync(dirPath, { withFileTypes: true });
|
|
1983
2610
|
const nodes = [];
|
|
1984
2611
|
for (const entry of entries) {
|
|
1985
2612
|
if (IGNORED_DIRS.has(entry.name) || IGNORED_FILES.has(entry.name)) continue;
|
|
1986
|
-
const fullPath =
|
|
1987
|
-
const relativePath =
|
|
2613
|
+
const fullPath = path7.join(dirPath, entry.name);
|
|
2614
|
+
const relativePath = path7.relative(this.projectDir, fullPath);
|
|
1988
2615
|
if (entry.isDirectory()) {
|
|
1989
2616
|
nodes.push({
|
|
1990
2617
|
name: entry.name,
|
|
@@ -2023,19 +2650,24 @@ function setupWsHandlers(socket, workspaceManager, gitService) {
|
|
|
2023
2650
|
type: "workspace.state",
|
|
2024
2651
|
state
|
|
2025
2652
|
});
|
|
2026
|
-
const SCROLLBACK_CHUNK_SIZE =
|
|
2027
|
-
|
|
2028
|
-
const
|
|
2029
|
-
|
|
2653
|
+
const SCROLLBACK_CHUNK_SIZE = 512 * 1024;
|
|
2654
|
+
const replayScrollback = async () => {
|
|
2655
|
+
for (const pane of state.panes) {
|
|
2656
|
+
const scrollback = workspaceManager.getScrollback(pane.id);
|
|
2657
|
+
if (!scrollback) continue;
|
|
2030
2658
|
if (scrollback.length <= SCROLLBACK_CHUNK_SIZE) {
|
|
2031
2659
|
send({ type: "terminal.output", paneId: pane.id, data: scrollback });
|
|
2032
2660
|
} else {
|
|
2033
2661
|
for (let i = 0; i < scrollback.length; i += SCROLLBACK_CHUNK_SIZE) {
|
|
2662
|
+
if (socket.readyState !== socket.OPEN) return;
|
|
2034
2663
|
send({ type: "terminal.output", paneId: pane.id, data: scrollback.slice(i, i + SCROLLBACK_CHUNK_SIZE) });
|
|
2664
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
2035
2665
|
}
|
|
2036
2666
|
}
|
|
2037
2667
|
}
|
|
2038
|
-
}
|
|
2668
|
+
};
|
|
2669
|
+
replayScrollback().catch(() => {
|
|
2670
|
+
});
|
|
2039
2671
|
const paneDiffs = workspaceManager.getPaneDiffs();
|
|
2040
2672
|
for (const [paneId, diffs] of paneDiffs) {
|
|
2041
2673
|
if (diffs.length > 0) {
|
|
@@ -2048,6 +2680,9 @@ function setupWsHandlers(socket, workspaceManager, gitService) {
|
|
|
2048
2680
|
onTerminalData: (paneId, data) => {
|
|
2049
2681
|
send({ type: "terminal.output", paneId, data });
|
|
2050
2682
|
},
|
|
2683
|
+
onConversationEvent: (paneId, event) => {
|
|
2684
|
+
send({ type: "conversation.event", paneId, event });
|
|
2685
|
+
},
|
|
2051
2686
|
onPaneStatus: (paneId, status) => {
|
|
2052
2687
|
send({ type: "pane.status", paneId, status });
|
|
2053
2688
|
},
|
|
@@ -2085,10 +2720,21 @@ function setupWsHandlers(socket, workspaceManager, gitService) {
|
|
|
2085
2720
|
}
|
|
2086
2721
|
switch (event.type) {
|
|
2087
2722
|
case "terminal.input":
|
|
2088
|
-
|
|
2723
|
+
try {
|
|
2724
|
+
workspaceManager.writeToPane(event.paneId, event.data);
|
|
2725
|
+
} catch {
|
|
2726
|
+
}
|
|
2089
2727
|
break;
|
|
2090
2728
|
case "terminal.resize":
|
|
2091
|
-
|
|
2729
|
+
try {
|
|
2730
|
+
workspaceManager.resizePane(event.paneId, event.cols, event.rows);
|
|
2731
|
+
} catch {
|
|
2732
|
+
}
|
|
2733
|
+
break;
|
|
2734
|
+
case "conversation.send":
|
|
2735
|
+
workspaceManager.sendConversationToPane(event.paneId, event.text).catch((err) => {
|
|
2736
|
+
console.error("conversation.send failed:", err);
|
|
2737
|
+
});
|
|
2092
2738
|
break;
|
|
2093
2739
|
case "pane.create":
|
|
2094
2740
|
workspaceManager.createPane(event.config).catch((err) => {
|
|
@@ -2163,6 +2809,22 @@ function setupWsHandlers(socket, workspaceManager, gitService) {
|
|
|
2163
2809
|
});
|
|
2164
2810
|
}
|
|
2165
2811
|
break;
|
|
2812
|
+
case "pane.merge":
|
|
2813
|
+
workspaceManager.mergeWorktree(event.paneId).then((result) => {
|
|
2814
|
+
send({ type: "pane.merge.result", paneId: event.paneId, ...result });
|
|
2815
|
+
gitService?.refresh();
|
|
2816
|
+
}).catch((err) => {
|
|
2817
|
+
send({ type: "pane.merge.result", paneId: event.paneId, success: false, message: String(err) });
|
|
2818
|
+
});
|
|
2819
|
+
break;
|
|
2820
|
+
case "pane.discard":
|
|
2821
|
+
workspaceManager.discardWorktree(event.paneId).then((result) => {
|
|
2822
|
+
send({ type: "pane.merge.result", paneId: event.paneId, ...result });
|
|
2823
|
+
gitService?.refresh();
|
|
2824
|
+
}).catch((err) => {
|
|
2825
|
+
send({ type: "pane.merge.result", paneId: event.paneId, success: false, message: String(err) });
|
|
2826
|
+
});
|
|
2827
|
+
break;
|
|
2166
2828
|
case "pane.diff.refresh":
|
|
2167
2829
|
workspaceManager.refreshPaneDiff(event.paneId);
|
|
2168
2830
|
break;
|
|
@@ -2193,8 +2855,8 @@ function setupWsHandlers(socket, workspaceManager, gitService) {
|
|
|
2193
2855
|
}
|
|
2194
2856
|
|
|
2195
2857
|
// src/deps/DependencyAnalyzer.ts
|
|
2196
|
-
import
|
|
2197
|
-
import
|
|
2858
|
+
import fs7 from "fs";
|
|
2859
|
+
import path8 from "path";
|
|
2198
2860
|
var IMPORT_FROM_RE = /import\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
2199
2861
|
var EXPORT_FROM_RE = /export\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
2200
2862
|
var REQUIRE_RE = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
@@ -2215,7 +2877,7 @@ var DependencyAnalyzer = class {
|
|
|
2215
2877
|
const files = this.collectFiles(this.projectDir);
|
|
2216
2878
|
const nodes = [];
|
|
2217
2879
|
for (const absPath of files) {
|
|
2218
|
-
const relPath =
|
|
2880
|
+
const relPath = path8.relative(this.projectDir, absPath);
|
|
2219
2881
|
const imports = this.extractImports(absPath, relPath);
|
|
2220
2882
|
nodes.push({ id: relPath, imports });
|
|
2221
2883
|
}
|
|
@@ -2226,18 +2888,18 @@ var DependencyAnalyzer = class {
|
|
|
2226
2888
|
const files = [];
|
|
2227
2889
|
let entries;
|
|
2228
2890
|
try {
|
|
2229
|
-
entries =
|
|
2891
|
+
entries = fs7.readdirSync(dir, { withFileTypes: true });
|
|
2230
2892
|
} catch {
|
|
2231
2893
|
return files;
|
|
2232
2894
|
}
|
|
2233
2895
|
for (const entry of entries) {
|
|
2234
2896
|
if (entry.name.startsWith(".") && entry.name !== ".") continue;
|
|
2235
|
-
const fullPath =
|
|
2897
|
+
const fullPath = path8.join(dir, entry.name);
|
|
2236
2898
|
if (entry.isDirectory()) {
|
|
2237
2899
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
2238
2900
|
files.push(...this.collectFiles(fullPath, depth + 1));
|
|
2239
2901
|
} else if (entry.isFile()) {
|
|
2240
|
-
const ext =
|
|
2902
|
+
const ext = path8.extname(entry.name);
|
|
2241
2903
|
if (JS_TS_EXTENSIONS.has(ext)) {
|
|
2242
2904
|
files.push(fullPath);
|
|
2243
2905
|
}
|
|
@@ -2248,7 +2910,7 @@ var DependencyAnalyzer = class {
|
|
|
2248
2910
|
extractImports(absPath, relPath) {
|
|
2249
2911
|
let content;
|
|
2250
2912
|
try {
|
|
2251
|
-
content =
|
|
2913
|
+
content = fs7.readFileSync(absPath, "utf-8");
|
|
2252
2914
|
} catch {
|
|
2253
2915
|
return [];
|
|
2254
2916
|
}
|
|
@@ -2265,22 +2927,22 @@ var DependencyAnalyzer = class {
|
|
|
2265
2927
|
}
|
|
2266
2928
|
}
|
|
2267
2929
|
const imports = [];
|
|
2268
|
-
const fileDir =
|
|
2930
|
+
const fileDir = path8.dirname(absPath);
|
|
2269
2931
|
for (const spec of specifiers) {
|
|
2270
2932
|
const resolved = this.resolveSpecifier(fileDir, spec);
|
|
2271
2933
|
if (resolved) {
|
|
2272
|
-
const resolvedRel =
|
|
2934
|
+
const resolvedRel = path8.relative(this.projectDir, resolved);
|
|
2273
2935
|
imports.push(resolvedRel);
|
|
2274
2936
|
}
|
|
2275
2937
|
}
|
|
2276
2938
|
return imports;
|
|
2277
2939
|
}
|
|
2278
2940
|
resolveSpecifier(fromDir, specifier) {
|
|
2279
|
-
const base =
|
|
2941
|
+
const base = path8.resolve(fromDir, specifier);
|
|
2280
2942
|
for (const ext of RESOLVE_EXTENSIONS) {
|
|
2281
2943
|
const candidate = base + ext;
|
|
2282
2944
|
try {
|
|
2283
|
-
if (
|
|
2945
|
+
if (fs7.statSync(candidate).isFile()) {
|
|
2284
2946
|
return candidate;
|
|
2285
2947
|
}
|
|
2286
2948
|
} catch {
|
|
@@ -2291,8 +2953,8 @@ var DependencyAnalyzer = class {
|
|
|
2291
2953
|
};
|
|
2292
2954
|
|
|
2293
2955
|
// src/history/SessionRecorder.ts
|
|
2294
|
-
import
|
|
2295
|
-
import
|
|
2956
|
+
import fs8 from "fs";
|
|
2957
|
+
import path9 from "path";
|
|
2296
2958
|
import { execFile as execFile2 } from "child_process";
|
|
2297
2959
|
var HISTORY_DIR = ".nexus/history";
|
|
2298
2960
|
var SESSIONS_INDEX = "sessions.json";
|
|
@@ -2310,8 +2972,8 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2310
2972
|
constructor(projectDir, projectName, retentionDays = 30) {
|
|
2311
2973
|
this.projectDir = projectDir;
|
|
2312
2974
|
const sessionId = `s-${Date.now()}`;
|
|
2313
|
-
this.sessionDir =
|
|
2314
|
-
|
|
2975
|
+
this.sessionDir = path9.join(projectDir, HISTORY_DIR, sessionId);
|
|
2976
|
+
fs8.mkdirSync(this.sessionDir, { recursive: true });
|
|
2315
2977
|
this.retentionDays = retentionDays;
|
|
2316
2978
|
this.session = {
|
|
2317
2979
|
id: sessionId,
|
|
@@ -2451,47 +3113,47 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2451
3113
|
}
|
|
2452
3114
|
// ─── Query API ────────────────────────────────────────────
|
|
2453
3115
|
static listSessions(projectDir) {
|
|
2454
|
-
const indexPath =
|
|
2455
|
-
if (!
|
|
3116
|
+
const indexPath = path9.join(projectDir, HISTORY_DIR, SESSIONS_INDEX);
|
|
3117
|
+
if (!fs8.existsSync(indexPath)) return [];
|
|
2456
3118
|
try {
|
|
2457
|
-
const data = JSON.parse(
|
|
3119
|
+
const data = JSON.parse(fs8.readFileSync(indexPath, "utf-8"));
|
|
2458
3120
|
return Array.isArray(data) ? data : [];
|
|
2459
3121
|
} catch {
|
|
2460
3122
|
return [];
|
|
2461
3123
|
}
|
|
2462
3124
|
}
|
|
2463
3125
|
static getSession(projectDir, sessionId) {
|
|
2464
|
-
const sessionPath =
|
|
2465
|
-
if (!
|
|
3126
|
+
const sessionPath = path9.join(projectDir, HISTORY_DIR, sessionId, "session.json");
|
|
3127
|
+
if (!fs8.existsSync(sessionPath)) return null;
|
|
2466
3128
|
try {
|
|
2467
|
-
return JSON.parse(
|
|
3129
|
+
return JSON.parse(fs8.readFileSync(sessionPath, "utf-8"));
|
|
2468
3130
|
} catch {
|
|
2469
3131
|
return null;
|
|
2470
3132
|
}
|
|
2471
3133
|
}
|
|
2472
3134
|
static getTurn(projectDir, sessionId, turnId) {
|
|
2473
|
-
const turnPath =
|
|
2474
|
-
if (!
|
|
3135
|
+
const turnPath = path9.join(projectDir, HISTORY_DIR, sessionId, `${turnId}.json`);
|
|
3136
|
+
if (!fs8.existsSync(turnPath)) return null;
|
|
2475
3137
|
try {
|
|
2476
|
-
return JSON.parse(
|
|
3138
|
+
return JSON.parse(fs8.readFileSync(turnPath, "utf-8"));
|
|
2477
3139
|
} catch {
|
|
2478
3140
|
return null;
|
|
2479
3141
|
}
|
|
2480
3142
|
}
|
|
2481
3143
|
/** Delete a single session and its directory. Returns true if deleted. */
|
|
2482
3144
|
static deleteSession(projectDir, sessionId) {
|
|
2483
|
-
const sessionDir =
|
|
2484
|
-
if (
|
|
2485
|
-
|
|
3145
|
+
const sessionDir = path9.join(projectDir, HISTORY_DIR, sessionId);
|
|
3146
|
+
if (fs8.existsSync(sessionDir)) {
|
|
3147
|
+
fs8.rmSync(sessionDir, { recursive: true, force: true });
|
|
2486
3148
|
}
|
|
2487
|
-
const indexPath =
|
|
2488
|
-
if (
|
|
3149
|
+
const indexPath = path9.join(projectDir, HISTORY_DIR, SESSIONS_INDEX);
|
|
3150
|
+
if (fs8.existsSync(indexPath)) {
|
|
2489
3151
|
try {
|
|
2490
|
-
let sessions = JSON.parse(
|
|
3152
|
+
let sessions = JSON.parse(fs8.readFileSync(indexPath, "utf-8"));
|
|
2491
3153
|
const before = sessions.length;
|
|
2492
3154
|
sessions = sessions.filter((s) => s.id !== sessionId);
|
|
2493
3155
|
if (sessions.length < before) {
|
|
2494
|
-
|
|
3156
|
+
fs8.writeFileSync(indexPath, JSON.stringify(sessions, null, 2));
|
|
2495
3157
|
return true;
|
|
2496
3158
|
}
|
|
2497
3159
|
} catch {
|
|
@@ -2504,14 +3166,14 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2504
3166
|
const sessions = _SessionRecorder.listSessions(projectDir);
|
|
2505
3167
|
let count = 0;
|
|
2506
3168
|
for (const session of sessions) {
|
|
2507
|
-
const sessionDir =
|
|
2508
|
-
if (
|
|
2509
|
-
|
|
3169
|
+
const sessionDir = path9.join(projectDir, HISTORY_DIR, session.id);
|
|
3170
|
+
if (fs8.existsSync(sessionDir)) {
|
|
3171
|
+
fs8.rmSync(sessionDir, { recursive: true, force: true });
|
|
2510
3172
|
count++;
|
|
2511
3173
|
}
|
|
2512
3174
|
}
|
|
2513
|
-
const indexPath =
|
|
2514
|
-
|
|
3175
|
+
const indexPath = path9.join(projectDir, HISTORY_DIR, SESSIONS_INDEX);
|
|
3176
|
+
fs8.writeFileSync(indexPath, JSON.stringify([], null, 2));
|
|
2515
3177
|
return count;
|
|
2516
3178
|
}
|
|
2517
3179
|
/**
|
|
@@ -2519,11 +3181,11 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2519
3181
|
* Called automatically when saving a new session.
|
|
2520
3182
|
*/
|
|
2521
3183
|
static pruneOldSessions(projectDir, retentionDays) {
|
|
2522
|
-
const indexPath =
|
|
2523
|
-
if (!
|
|
3184
|
+
const indexPath = path9.join(projectDir, HISTORY_DIR, SESSIONS_INDEX);
|
|
3185
|
+
if (!fs8.existsSync(indexPath)) return 0;
|
|
2524
3186
|
let sessions;
|
|
2525
3187
|
try {
|
|
2526
|
-
sessions = JSON.parse(
|
|
3188
|
+
sessions = JSON.parse(fs8.readFileSync(indexPath, "utf-8"));
|
|
2527
3189
|
} catch {
|
|
2528
3190
|
return 0;
|
|
2529
3191
|
}
|
|
@@ -2539,12 +3201,12 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2539
3201
|
}
|
|
2540
3202
|
if (remove.length === 0) return 0;
|
|
2541
3203
|
for (const s of remove) {
|
|
2542
|
-
const sessionDir =
|
|
2543
|
-
if (
|
|
2544
|
-
|
|
3204
|
+
const sessionDir = path9.join(projectDir, HISTORY_DIR, s.id);
|
|
3205
|
+
if (fs8.existsSync(sessionDir)) {
|
|
3206
|
+
fs8.rmSync(sessionDir, { recursive: true, force: true });
|
|
2545
3207
|
}
|
|
2546
3208
|
}
|
|
2547
|
-
|
|
3209
|
+
fs8.writeFileSync(indexPath, JSON.stringify(keep, null, 2));
|
|
2548
3210
|
return remove.length;
|
|
2549
3211
|
}
|
|
2550
3212
|
// ─── Internal ─────────────────────────────────────────────
|
|
@@ -2651,19 +3313,19 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2651
3313
|
});
|
|
2652
3314
|
}
|
|
2653
3315
|
writeTurnFile(turn) {
|
|
2654
|
-
const turnPath =
|
|
2655
|
-
|
|
3316
|
+
const turnPath = path9.join(this.sessionDir, `${turn.id}.json`);
|
|
3317
|
+
fs8.writeFileSync(turnPath, JSON.stringify(turn));
|
|
2656
3318
|
}
|
|
2657
3319
|
writeSessionFile() {
|
|
2658
|
-
const sessionPath =
|
|
2659
|
-
|
|
3320
|
+
const sessionPath = path9.join(this.sessionDir, "session.json");
|
|
3321
|
+
fs8.writeFileSync(sessionPath, JSON.stringify(this.session, null, 2));
|
|
2660
3322
|
}
|
|
2661
3323
|
updateSessionsIndex() {
|
|
2662
|
-
const indexPath =
|
|
3324
|
+
const indexPath = path9.join(this.projectDir, HISTORY_DIR, SESSIONS_INDEX);
|
|
2663
3325
|
let sessions = [];
|
|
2664
|
-
if (
|
|
3326
|
+
if (fs8.existsSync(indexPath)) {
|
|
2665
3327
|
try {
|
|
2666
|
-
sessions = JSON.parse(
|
|
3328
|
+
sessions = JSON.parse(fs8.readFileSync(indexPath, "utf-8"));
|
|
2667
3329
|
} catch {
|
|
2668
3330
|
}
|
|
2669
3331
|
}
|
|
@@ -2677,7 +3339,7 @@ var SessionRecorder = class _SessionRecorder {
|
|
|
2677
3339
|
paneCount: this.session.panes.length,
|
|
2678
3340
|
totalDurationMs
|
|
2679
3341
|
});
|
|
2680
|
-
|
|
3342
|
+
fs8.writeFileSync(indexPath, JSON.stringify(sessions, null, 2));
|
|
2681
3343
|
const pruned = _SessionRecorder.pruneOldSessions(this.projectDir, this.retentionDays);
|
|
2682
3344
|
if (pruned > 0) {
|
|
2683
3345
|
console.log(`[SessionRecorder] Pruned ${pruned} old session(s)`);
|
|
@@ -2736,13 +3398,13 @@ var SessionDiscovery = class {
|
|
|
2736
3398
|
};
|
|
2737
3399
|
|
|
2738
3400
|
// src/index.ts
|
|
2739
|
-
var __dirname =
|
|
3401
|
+
var __dirname = path10.dirname(fileURLToPath(import.meta.url));
|
|
2740
3402
|
async function startServer(port, projectDir) {
|
|
2741
3403
|
const fastify = Fastify({ logger: false });
|
|
2742
3404
|
const configManager = new ConfigManager(projectDir);
|
|
2743
3405
|
configManager.loadGlobalConfig();
|
|
2744
3406
|
const workspaceManager = new WorkspaceManager(configManager);
|
|
2745
|
-
workspaceManager.init();
|
|
3407
|
+
await workspaceManager.init();
|
|
2746
3408
|
const agentsWriter = new AgentsYamlWriter(projectDir);
|
|
2747
3409
|
workspaceManager.onEvents({
|
|
2748
3410
|
onPaneAdded: () => agentsWriter.update(workspaceManager.getPanes()),
|
|
@@ -2900,17 +3562,17 @@ async function startServer(port, projectDir) {
|
|
|
2900
3562
|
reply.code(400);
|
|
2901
3563
|
return { error: "Missing path parameter" };
|
|
2902
3564
|
}
|
|
2903
|
-
if (filePath.includes("..") ||
|
|
3565
|
+
if (filePath.includes("..") || path10.isAbsolute(filePath)) {
|
|
2904
3566
|
reply.code(403);
|
|
2905
3567
|
return { error: "Invalid path" };
|
|
2906
3568
|
}
|
|
2907
|
-
const fullPath =
|
|
3569
|
+
const fullPath = path10.resolve(projectDir, filePath);
|
|
2908
3570
|
if (!fullPath.startsWith(projectDir)) {
|
|
2909
3571
|
reply.code(403);
|
|
2910
3572
|
return { error: "Path traversal not allowed" };
|
|
2911
3573
|
}
|
|
2912
3574
|
try {
|
|
2913
|
-
const content =
|
|
3575
|
+
const content = fs9.readFileSync(fullPath, "utf-8");
|
|
2914
3576
|
return { content, path: filePath };
|
|
2915
3577
|
} catch {
|
|
2916
3578
|
reply.code(404);
|
|
@@ -2923,20 +3585,20 @@ async function startServer(port, projectDir) {
|
|
|
2923
3585
|
reply.code(400);
|
|
2924
3586
|
return { error: "Missing path parameter" };
|
|
2925
3587
|
}
|
|
2926
|
-
if (filePath.includes("..") ||
|
|
3588
|
+
if (filePath.includes("..") || path10.isAbsolute(filePath)) {
|
|
2927
3589
|
reply.code(403);
|
|
2928
3590
|
return { error: "Invalid path" };
|
|
2929
3591
|
}
|
|
2930
|
-
const fullPath =
|
|
3592
|
+
const fullPath = path10.resolve(projectDir, filePath);
|
|
2931
3593
|
if (!fullPath.startsWith(projectDir)) {
|
|
2932
3594
|
reply.code(403);
|
|
2933
3595
|
return { error: "Path traversal not allowed" };
|
|
2934
3596
|
}
|
|
2935
|
-
if (!
|
|
3597
|
+
if (!fs9.existsSync(fullPath)) {
|
|
2936
3598
|
reply.code(404);
|
|
2937
3599
|
return { error: "File not found" };
|
|
2938
3600
|
}
|
|
2939
|
-
const ext =
|
|
3601
|
+
const ext = path10.extname(fullPath).toLowerCase();
|
|
2940
3602
|
const mimeMap = {
|
|
2941
3603
|
".png": "image/png",
|
|
2942
3604
|
".jpg": "image/jpeg",
|
|
@@ -2950,14 +3612,14 @@ async function startServer(port, projectDir) {
|
|
|
2950
3612
|
".svg": "image/svg+xml"
|
|
2951
3613
|
};
|
|
2952
3614
|
const mime = mimeMap[ext] || "application/octet-stream";
|
|
2953
|
-
const stream =
|
|
3615
|
+
const stream = fs9.createReadStream(fullPath);
|
|
2954
3616
|
reply.type(mime);
|
|
2955
3617
|
return reply.send(stream);
|
|
2956
3618
|
});
|
|
2957
|
-
const notesPath =
|
|
3619
|
+
const notesPath = path10.join(projectDir, ".nexus", "notes.yaml");
|
|
2958
3620
|
fastify.get("/api/notes", async () => {
|
|
2959
3621
|
try {
|
|
2960
|
-
const raw =
|
|
3622
|
+
const raw = fs9.readFileSync(notesPath, "utf-8");
|
|
2961
3623
|
const data = yaml3.load(raw);
|
|
2962
3624
|
return { notes: data?.notes || [] };
|
|
2963
3625
|
} catch {
|
|
@@ -2967,20 +3629,20 @@ async function startServer(port, projectDir) {
|
|
|
2967
3629
|
fastify.put("/api/notes", async (request, reply) => {
|
|
2968
3630
|
try {
|
|
2969
3631
|
const { notes } = request.body;
|
|
2970
|
-
|
|
2971
|
-
|
|
3632
|
+
fs9.mkdirSync(path10.dirname(notesPath), { recursive: true });
|
|
3633
|
+
fs9.writeFileSync(notesPath, yaml3.dump({ notes }, { lineWidth: -1 }), "utf-8");
|
|
2972
3634
|
return { success: true };
|
|
2973
3635
|
} catch (err) {
|
|
2974
3636
|
reply.code(400);
|
|
2975
3637
|
return { error: "Failed to save notes" };
|
|
2976
3638
|
}
|
|
2977
3639
|
});
|
|
2978
|
-
const webDistPath =
|
|
2979
|
-
if (!
|
|
3640
|
+
const webDistPath = path10.resolve(__dirname, "../../web/dist");
|
|
3641
|
+
if (!fs9.existsSync(webDistPath)) {
|
|
2980
3642
|
console.warn(` [Warning] Frontend not found at ${webDistPath}`);
|
|
2981
3643
|
console.warn(` Run 'pnpm run build:web' to build the frontend, or use dev mode.`);
|
|
2982
3644
|
}
|
|
2983
|
-
if (
|
|
3645
|
+
if (fs9.existsSync(webDistPath)) {
|
|
2984
3646
|
await fastify.register(fastifyStatic, {
|
|
2985
3647
|
root: webDistPath,
|
|
2986
3648
|
prefix: "/"
|
|
@@ -3058,14 +3720,14 @@ function findProjectRoot(startDir) {
|
|
|
3058
3720
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
3059
3721
|
let dir = startDir;
|
|
3060
3722
|
let bestMatch = startDir;
|
|
3061
|
-
while (dir !==
|
|
3062
|
-
if (
|
|
3723
|
+
while (dir !== path11.dirname(dir)) {
|
|
3724
|
+
if (fs10.existsSync(path11.join(dir, "pnpm-workspace.yaml"))) {
|
|
3063
3725
|
return dir;
|
|
3064
3726
|
}
|
|
3065
|
-
if (
|
|
3727
|
+
if (fs10.existsSync(path11.join(dir, ".git")) || fs10.existsSync(path11.join(dir, ".nexus"))) {
|
|
3066
3728
|
bestMatch = dir;
|
|
3067
3729
|
}
|
|
3068
|
-
const parent =
|
|
3730
|
+
const parent = path11.dirname(dir);
|
|
3069
3731
|
if (home && parent === home && dir !== startDir) break;
|
|
3070
3732
|
dir = parent;
|
|
3071
3733
|
}
|
|
@@ -3077,15 +3739,15 @@ function findProjectRoot(startDir) {
|
|
|
3077
3739
|
}
|
|
3078
3740
|
function resolveProjectDir(dirArg) {
|
|
3079
3741
|
if (process.env.NEXUS_PROJECT_DIR) {
|
|
3080
|
-
return
|
|
3742
|
+
return path11.resolve(process.env.NEXUS_PROJECT_DIR);
|
|
3081
3743
|
}
|
|
3082
3744
|
if (dirArg) {
|
|
3083
|
-
const resolved =
|
|
3084
|
-
if (!
|
|
3745
|
+
const resolved = path11.resolve(dirArg);
|
|
3746
|
+
if (!fs10.existsSync(resolved)) {
|
|
3085
3747
|
console.error(`Error: directory does not exist: ${resolved}`);
|
|
3086
3748
|
process.exit(1);
|
|
3087
3749
|
}
|
|
3088
|
-
if (!
|
|
3750
|
+
if (!fs10.statSync(resolved).isDirectory()) {
|
|
3089
3751
|
console.error(`Error: not a directory: ${resolved}`);
|
|
3090
3752
|
process.exit(1);
|
|
3091
3753
|
}
|