lunel-cli 0.1.89 → 0.1.92
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/dist/index.js +280 -1
- package/dist/transport/protocol.d.ts +0 -1
- package/dist/transport/protocol.js +0 -8
- package/dist/transport/v2.js +2 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -70,6 +70,9 @@ let currentPrimaryGateway = DEFAULT_PROXY_URL;
|
|
|
70
70
|
let activeGatewayUrl = DEFAULT_PROXY_URL;
|
|
71
71
|
let shuttingDown = false;
|
|
72
72
|
let activeV2Transport = null;
|
|
73
|
+
const trackedEditorFiles = new Map();
|
|
74
|
+
const trackedEditorDirectories = new Map();
|
|
75
|
+
const pendingTrackedFileChecks = new Set();
|
|
73
76
|
function logWithTimestamp(scope, message, fields) {
|
|
74
77
|
if (!DEBUG_MODE)
|
|
75
78
|
return;
|
|
@@ -493,6 +496,7 @@ async function handleFsWrite(payload) {
|
|
|
493
496
|
const reqPath = payload.path;
|
|
494
497
|
const content = payload.content;
|
|
495
498
|
const encoding = payload.encoding || "utf8";
|
|
499
|
+
const source = typeof payload.source === "string" ? payload.source : null;
|
|
496
500
|
if (!reqPath)
|
|
497
501
|
throw Object.assign(new Error("path is required"), { code: "EINVAL" });
|
|
498
502
|
if (typeof content !== "string")
|
|
@@ -506,6 +510,7 @@ async function handleFsWrite(payload) {
|
|
|
506
510
|
else {
|
|
507
511
|
await fs.writeFile(safePath, content, "utf-8");
|
|
508
512
|
}
|
|
513
|
+
await noteTrackedFileWrite(reqPath, source);
|
|
509
514
|
return { path: reqPath };
|
|
510
515
|
}
|
|
511
516
|
async function handleFsMkdir(payload) {
|
|
@@ -524,6 +529,7 @@ async function handleFsRm(payload) {
|
|
|
524
529
|
throw Object.assign(new Error("path is required"), { code: "EINVAL" });
|
|
525
530
|
const safePath = assertSafePath(reqPath);
|
|
526
531
|
await fs.rm(safePath, { recursive, force: false });
|
|
532
|
+
deleteTrackedEditorFile(reqPath);
|
|
527
533
|
return { path: reqPath };
|
|
528
534
|
}
|
|
529
535
|
async function handleFsMv(payload) {
|
|
@@ -536,6 +542,7 @@ async function handleFsMv(payload) {
|
|
|
536
542
|
const safeFrom = assertSafePath(from);
|
|
537
543
|
const safeTo = assertSafePath(to);
|
|
538
544
|
await fs.rename(safeFrom, safeTo);
|
|
545
|
+
await renameTrackedEditorFile(from, to);
|
|
539
546
|
return { from, to };
|
|
540
547
|
}
|
|
541
548
|
// Load gitignore patterns
|
|
@@ -943,6 +950,254 @@ function emitAppEvent(msg) {
|
|
|
943
950
|
});
|
|
944
951
|
}
|
|
945
952
|
}
|
|
953
|
+
function emitEditorFileChanged(requestPath, mtimeMs, size) {
|
|
954
|
+
logWithTimestamp("editor-watch", "emitting fileChanged", { path: requestPath, mtime: mtimeMs, size });
|
|
955
|
+
emitAppEvent({
|
|
956
|
+
v: 1,
|
|
957
|
+
id: `editor-change-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
958
|
+
ns: "editor",
|
|
959
|
+
action: "fileChanged",
|
|
960
|
+
payload: { path: requestPath, mtime: mtimeMs, size },
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
function emitEditorFileDeleted(requestPath) {
|
|
964
|
+
logWithTimestamp("editor-watch", "emitting fileDeleted", { path: requestPath });
|
|
965
|
+
emitAppEvent({
|
|
966
|
+
v: 1,
|
|
967
|
+
id: `editor-delete-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
968
|
+
ns: "editor",
|
|
969
|
+
action: "fileDeleted",
|
|
970
|
+
payload: { path: requestPath },
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
function releaseTrackedEditorDirectory(dirPath) {
|
|
974
|
+
const trackedDir = trackedEditorDirectories.get(dirPath);
|
|
975
|
+
if (!trackedDir || trackedDir.filePaths.size > 0) {
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
trackedDir.watcher.close();
|
|
979
|
+
trackedEditorDirectories.delete(dirPath);
|
|
980
|
+
}
|
|
981
|
+
function queueTrackedEditorFileCheck(safePath) {
|
|
982
|
+
if (pendingTrackedFileChecks.has(safePath)) {
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
pendingTrackedFileChecks.add(safePath);
|
|
986
|
+
void (async () => {
|
|
987
|
+
try {
|
|
988
|
+
const tracked = trackedEditorFiles.get(safePath);
|
|
989
|
+
if (!tracked) {
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
logWithTimestamp("editor-watch", "checking tracked file", { path: tracked.requestPath });
|
|
993
|
+
let stat;
|
|
994
|
+
try {
|
|
995
|
+
stat = await fs.stat(tracked.safePath);
|
|
996
|
+
}
|
|
997
|
+
catch (error) {
|
|
998
|
+
const nodeError = error;
|
|
999
|
+
if (nodeError?.code === "ENOENT") {
|
|
1000
|
+
trackedEditorFiles.delete(tracked.safePath);
|
|
1001
|
+
const trackedDir = trackedEditorDirectories.get(tracked.dirPath);
|
|
1002
|
+
if (trackedDir) {
|
|
1003
|
+
trackedDir.filePaths.delete(tracked.safePath);
|
|
1004
|
+
releaseTrackedEditorDirectory(tracked.dirPath);
|
|
1005
|
+
}
|
|
1006
|
+
emitEditorFileDeleted(tracked.requestPath);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
throw error;
|
|
1010
|
+
}
|
|
1011
|
+
if (!stat.isFile()) {
|
|
1012
|
+
trackedEditorFiles.delete(tracked.safePath);
|
|
1013
|
+
const trackedDir = trackedEditorDirectories.get(tracked.dirPath);
|
|
1014
|
+
if (trackedDir) {
|
|
1015
|
+
trackedDir.filePaths.delete(tracked.safePath);
|
|
1016
|
+
releaseTrackedEditorDirectory(tracked.dirPath);
|
|
1017
|
+
}
|
|
1018
|
+
emitEditorFileDeleted(tracked.requestPath);
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
const changed = tracked.lastMtimeMs !== stat.mtimeMs || tracked.lastSize !== stat.size;
|
|
1022
|
+
logWithTimestamp("editor-watch", "stat compared", {
|
|
1023
|
+
path: tracked.requestPath,
|
|
1024
|
+
changed,
|
|
1025
|
+
prevMtime: tracked.lastMtimeMs,
|
|
1026
|
+
nextMtime: stat.mtimeMs,
|
|
1027
|
+
prevSize: tracked.lastSize,
|
|
1028
|
+
nextSize: stat.size,
|
|
1029
|
+
suppressWatcherUntil: tracked.suppressWatcherUntil,
|
|
1030
|
+
});
|
|
1031
|
+
tracked.lastMtimeMs = stat.mtimeMs;
|
|
1032
|
+
tracked.lastSize = stat.size;
|
|
1033
|
+
if (!changed) {
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (Date.now() <= tracked.suppressWatcherUntil) {
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
emitEditorFileChanged(tracked.requestPath, stat.mtimeMs, stat.size);
|
|
1040
|
+
}
|
|
1041
|
+
catch (error) {
|
|
1042
|
+
if (DEBUG_MODE) {
|
|
1043
|
+
console.error("[editor-watch] file check failed:", error instanceof Error ? error.message : String(error));
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
finally {
|
|
1047
|
+
pendingTrackedFileChecks.delete(safePath);
|
|
1048
|
+
}
|
|
1049
|
+
})();
|
|
1050
|
+
}
|
|
1051
|
+
function ensureTrackedEditorDirectory(dirPath) {
|
|
1052
|
+
const existing = trackedEditorDirectories.get(dirPath);
|
|
1053
|
+
if (existing) {
|
|
1054
|
+
return existing;
|
|
1055
|
+
}
|
|
1056
|
+
const watcher = fssync.watch(dirPath, (_eventType, fileName) => {
|
|
1057
|
+
const trackedDir = trackedEditorDirectories.get(dirPath);
|
|
1058
|
+
if (!trackedDir) {
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
const changedName = fileName == null ? null : String(fileName);
|
|
1062
|
+
logWithTimestamp("editor-watch", "directory event", { dirPath, fileName: changedName });
|
|
1063
|
+
if (!changedName) {
|
|
1064
|
+
for (const safePath of trackedDir.filePaths) {
|
|
1065
|
+
queueTrackedEditorFileCheck(safePath);
|
|
1066
|
+
}
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
for (const safePath of trackedDir.filePaths) {
|
|
1070
|
+
const tracked = trackedEditorFiles.get(safePath);
|
|
1071
|
+
if (tracked && tracked.baseName === changedName) {
|
|
1072
|
+
queueTrackedEditorFileCheck(safePath);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
watcher.on("error", (error) => {
|
|
1077
|
+
if (DEBUG_MODE) {
|
|
1078
|
+
console.error("[editor-watch] directory watcher error:", dirPath, error instanceof Error ? error.message : String(error));
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
const trackedDir = {
|
|
1082
|
+
watcher,
|
|
1083
|
+
filePaths: new Set(),
|
|
1084
|
+
};
|
|
1085
|
+
trackedEditorDirectories.set(dirPath, trackedDir);
|
|
1086
|
+
return trackedDir;
|
|
1087
|
+
}
|
|
1088
|
+
async function trackEditorFile(requestPath) {
|
|
1089
|
+
const safePath = assertSafePath(requestPath);
|
|
1090
|
+
const stat = await fs.stat(safePath);
|
|
1091
|
+
if (!stat.isFile()) {
|
|
1092
|
+
throw Object.assign(new Error("Only files can be tracked by the editor"), { code: "EINVAL" });
|
|
1093
|
+
}
|
|
1094
|
+
const existing = trackedEditorFiles.get(safePath);
|
|
1095
|
+
if (existing) {
|
|
1096
|
+
existing.openCount += 1;
|
|
1097
|
+
existing.requestPath = requestPath;
|
|
1098
|
+
existing.lastMtimeMs = stat.mtimeMs;
|
|
1099
|
+
existing.lastSize = stat.size;
|
|
1100
|
+
logWithTimestamp("editor-watch", "increment tracked file", { path: requestPath, openCount: existing.openCount });
|
|
1101
|
+
return { path: requestPath, tracked: true };
|
|
1102
|
+
}
|
|
1103
|
+
const dirPath = path.dirname(safePath);
|
|
1104
|
+
const trackedDir = ensureTrackedEditorDirectory(dirPath);
|
|
1105
|
+
trackedDir.filePaths.add(safePath);
|
|
1106
|
+
trackedEditorFiles.set(safePath, {
|
|
1107
|
+
requestPath,
|
|
1108
|
+
safePath,
|
|
1109
|
+
dirPath,
|
|
1110
|
+
baseName: path.basename(safePath),
|
|
1111
|
+
openCount: 1,
|
|
1112
|
+
lastMtimeMs: stat.mtimeMs,
|
|
1113
|
+
lastSize: stat.size,
|
|
1114
|
+
suppressWatcherUntil: 0,
|
|
1115
|
+
});
|
|
1116
|
+
logWithTimestamp("editor-watch", "tracking file", { path: requestPath, dirPath, mtime: stat.mtimeMs, size: stat.size });
|
|
1117
|
+
return { path: requestPath, tracked: true };
|
|
1118
|
+
}
|
|
1119
|
+
function untrackEditorFile(requestPath) {
|
|
1120
|
+
const safePath = assertSafePath(requestPath);
|
|
1121
|
+
const tracked = trackedEditorFiles.get(safePath);
|
|
1122
|
+
if (!tracked) {
|
|
1123
|
+
return { path: requestPath, tracked: false };
|
|
1124
|
+
}
|
|
1125
|
+
tracked.openCount -= 1;
|
|
1126
|
+
logWithTimestamp("editor-watch", "decrement tracked file", { path: requestPath, openCount: tracked.openCount });
|
|
1127
|
+
if (tracked.openCount > 0) {
|
|
1128
|
+
return { path: requestPath, tracked: true };
|
|
1129
|
+
}
|
|
1130
|
+
trackedEditorFiles.delete(safePath);
|
|
1131
|
+
const trackedDir = trackedEditorDirectories.get(tracked.dirPath);
|
|
1132
|
+
if (trackedDir) {
|
|
1133
|
+
trackedDir.filePaths.delete(safePath);
|
|
1134
|
+
releaseTrackedEditorDirectory(tracked.dirPath);
|
|
1135
|
+
}
|
|
1136
|
+
return { path: requestPath, tracked: false };
|
|
1137
|
+
}
|
|
1138
|
+
async function renameTrackedEditorFile(fromPath, toPath) {
|
|
1139
|
+
const safeFrom = assertSafePath(fromPath);
|
|
1140
|
+
const safeTo = assertSafePath(toPath);
|
|
1141
|
+
const tracked = trackedEditorFiles.get(safeFrom);
|
|
1142
|
+
if (!tracked) {
|
|
1143
|
+
return { from: fromPath, to: toPath, tracked: false };
|
|
1144
|
+
}
|
|
1145
|
+
logWithTimestamp("editor-watch", "renaming tracked file", { from: fromPath, to: toPath });
|
|
1146
|
+
const fromDir = trackedEditorDirectories.get(tracked.dirPath);
|
|
1147
|
+
if (fromDir) {
|
|
1148
|
+
fromDir.filePaths.delete(tracked.safePath);
|
|
1149
|
+
releaseTrackedEditorDirectory(tracked.dirPath);
|
|
1150
|
+
}
|
|
1151
|
+
trackedEditorFiles.delete(tracked.safePath);
|
|
1152
|
+
const stat = await fs.stat(safeTo);
|
|
1153
|
+
const nextDirPath = path.dirname(safeTo);
|
|
1154
|
+
const nextDir = ensureTrackedEditorDirectory(nextDirPath);
|
|
1155
|
+
nextDir.filePaths.add(safeTo);
|
|
1156
|
+
tracked.requestPath = toPath;
|
|
1157
|
+
tracked.safePath = safeTo;
|
|
1158
|
+
tracked.dirPath = nextDirPath;
|
|
1159
|
+
tracked.baseName = path.basename(safeTo);
|
|
1160
|
+
tracked.lastMtimeMs = stat.mtimeMs;
|
|
1161
|
+
tracked.lastSize = stat.size;
|
|
1162
|
+
trackedEditorFiles.set(safeTo, tracked);
|
|
1163
|
+
return { from: fromPath, to: toPath, tracked: true };
|
|
1164
|
+
}
|
|
1165
|
+
function deleteTrackedEditorFile(requestPath) {
|
|
1166
|
+
const safePath = assertSafePath(requestPath);
|
|
1167
|
+
const tracked = trackedEditorFiles.get(safePath);
|
|
1168
|
+
if (!tracked) {
|
|
1169
|
+
return { path: requestPath, tracked: false };
|
|
1170
|
+
}
|
|
1171
|
+
logWithTimestamp("editor-watch", "deleting tracked file", { path: requestPath });
|
|
1172
|
+
trackedEditorFiles.delete(safePath);
|
|
1173
|
+
const trackedDir = trackedEditorDirectories.get(tracked.dirPath);
|
|
1174
|
+
if (trackedDir) {
|
|
1175
|
+
trackedDir.filePaths.delete(safePath);
|
|
1176
|
+
releaseTrackedEditorDirectory(tracked.dirPath);
|
|
1177
|
+
}
|
|
1178
|
+
return { path: requestPath, tracked: false };
|
|
1179
|
+
}
|
|
1180
|
+
async function noteTrackedFileWrite(requestPath, source) {
|
|
1181
|
+
const safePath = assertSafePath(requestPath);
|
|
1182
|
+
const tracked = trackedEditorFiles.get(safePath);
|
|
1183
|
+
if (!tracked) {
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
1186
|
+
const stat = await fs.stat(safePath);
|
|
1187
|
+
tracked.lastMtimeMs = stat.mtimeMs;
|
|
1188
|
+
tracked.lastSize = stat.size;
|
|
1189
|
+
tracked.suppressWatcherUntil = Date.now() + 1500;
|
|
1190
|
+
logWithTimestamp("editor-watch", "tracked file write noted", {
|
|
1191
|
+
path: tracked.requestPath,
|
|
1192
|
+
source,
|
|
1193
|
+
mtime: stat.mtimeMs,
|
|
1194
|
+
size: stat.size,
|
|
1195
|
+
suppressWatcherUntil: tracked.suppressWatcherUntil,
|
|
1196
|
+
});
|
|
1197
|
+
if (source !== "editor") {
|
|
1198
|
+
emitEditorFileChanged(tracked.requestPath, stat.mtimeMs, stat.size);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
946
1201
|
let ensurePtyBinaryPromise = null;
|
|
947
1202
|
function normalizeJsonWithTrailingCommas(text) {
|
|
948
1203
|
return text.replace(/,\s*([}\]])/g, "$1");
|
|
@@ -1246,7 +1501,7 @@ function handleTerminalScroll(payload) {
|
|
|
1246
1501
|
function handleSystemCapabilities() {
|
|
1247
1502
|
return {
|
|
1248
1503
|
version: VERSION,
|
|
1249
|
-
namespaces: ["fs", "git", "terminal", "processes", "ports", "monitor", "http", "ai", "proxy"],
|
|
1504
|
+
namespaces: ["fs", "git", "terminal", "processes", "ports", "monitor", "http", "ai", "proxy", "editor"],
|
|
1250
1505
|
platform: os.platform(),
|
|
1251
1506
|
rootDir: ROOT_DIR,
|
|
1252
1507
|
hostname: os.hostname(),
|
|
@@ -2152,6 +2407,24 @@ async function processMessage(message) {
|
|
|
2152
2407
|
throw Object.assign(new Error(`Unknown action: ${ns}.${action}`), { code: "EINVAL" });
|
|
2153
2408
|
}
|
|
2154
2409
|
break;
|
|
2410
|
+
case "editor":
|
|
2411
|
+
switch (action) {
|
|
2412
|
+
case "open":
|
|
2413
|
+
result = await trackEditorFile(payload.path);
|
|
2414
|
+
break;
|
|
2415
|
+
case "close":
|
|
2416
|
+
result = untrackEditorFile(payload.path);
|
|
2417
|
+
break;
|
|
2418
|
+
case "rename":
|
|
2419
|
+
result = await renameTrackedEditorFile(payload.from, payload.to);
|
|
2420
|
+
break;
|
|
2421
|
+
case "delete":
|
|
2422
|
+
result = deleteTrackedEditorFile(payload.path);
|
|
2423
|
+
break;
|
|
2424
|
+
default:
|
|
2425
|
+
throw Object.assign(new Error(`Unknown action: ${ns}.${action}`), { code: "EINVAL" });
|
|
2426
|
+
}
|
|
2427
|
+
break;
|
|
2155
2428
|
case "git":
|
|
2156
2429
|
switch (action) {
|
|
2157
2430
|
case "status":
|
|
@@ -2537,6 +2810,12 @@ function gracefulShutdown() {
|
|
|
2537
2810
|
console.log("\nShutting down...");
|
|
2538
2811
|
void aiManager?.destroy();
|
|
2539
2812
|
stopPortSync();
|
|
2813
|
+
for (const trackedDir of trackedEditorDirectories.values()) {
|
|
2814
|
+
trackedDir.watcher.close();
|
|
2815
|
+
}
|
|
2816
|
+
trackedEditorDirectories.clear();
|
|
2817
|
+
trackedEditorFiles.clear();
|
|
2818
|
+
pendingTrackedFileChecks.clear();
|
|
2540
2819
|
activeV2Transport?.close();
|
|
2541
2820
|
activeV2Transport = null;
|
|
2542
2821
|
if (ptyProcess) {
|
|
@@ -74,5 +74,4 @@ export declare function decodeV2BinaryFrame(data: Uint8Array): {
|
|
|
74
74
|
type: number;
|
|
75
75
|
payload: Uint8Array;
|
|
76
76
|
} | null;
|
|
77
|
-
export declare function buildSessionV1WsUrl(gatewayUrl: string, role: "cli" | "app", channel: "control" | "data", password: string): string;
|
|
78
77
|
export declare function buildSessionV2WsUrl(gatewayUrl: string, role: "cli" | "app", password: string): string;
|
|
@@ -66,14 +66,6 @@ export function decodeV2BinaryFrame(data) {
|
|
|
66
66
|
payload: data.subarray(3),
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
|
-
export function buildSessionV1WsUrl(gatewayUrl, role, channel, password) {
|
|
70
|
-
const wsBase = gatewayUrl.replace(/^https:/, "wss:");
|
|
71
|
-
if (!wsBase.startsWith("wss://")) {
|
|
72
|
-
throw new Error("Gateway URL must use https://");
|
|
73
|
-
}
|
|
74
|
-
const query = new URLSearchParams({ password });
|
|
75
|
-
return `${wsBase}/v1/ws/${role}/${channel}?${query.toString()}`;
|
|
76
|
-
}
|
|
77
69
|
export function buildSessionV2WsUrl(gatewayUrl, role, password) {
|
|
78
70
|
const wsBase = gatewayUrl.replace(/^https:/, "wss:");
|
|
79
71
|
if (!wsBase.startsWith("wss://")) {
|
package/dist/transport/v2.js
CHANGED
|
@@ -246,10 +246,11 @@ export class V2SessionTransport {
|
|
|
246
246
|
rx: sodium.from_base64(payload.c2s, sodium.base64_variants.URLSAFE_NO_PADDING),
|
|
247
247
|
tx: sodium.from_base64(payload.s2c, sodium.base64_variants.URLSAFE_NO_PADDING),
|
|
248
248
|
};
|
|
249
|
+
const auth = this.computeHandshakeAuth("server_ready", "cli", sodium.to_base64(keyPair.publicKey, sodium.base64_variants.URLSAFE_NO_PADDING));
|
|
249
250
|
this.sendJsonFrame({
|
|
250
251
|
t: "lunel_v2",
|
|
251
252
|
kind: "server_ready",
|
|
252
|
-
auth
|
|
253
|
+
auth,
|
|
253
254
|
});
|
|
254
255
|
this.markSecure();
|
|
255
256
|
return;
|