lunel-cli 0.1.91 → 0.1.93

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 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) {
@@ -37,6 +37,7 @@ export declare class V2SessionTransport {
37
37
  private handleHandshakeFrame;
38
38
  private encryptEnvelope;
39
39
  private ensureKeyPair;
40
+ private resetPeerSession;
40
41
  private computeHandshakeAuth;
41
42
  private sendJsonFrame;
42
43
  private sendBinaryFrame;
@@ -124,6 +124,7 @@ export class V2SessionTransport {
124
124
  if ("type" in raw) {
125
125
  await this.options.handlers.onSystemMessage(raw);
126
126
  if (raw.type === "peer_connected") {
127
+ this.resetPeerSession();
127
128
  await this.maybeStartHandshake();
128
129
  }
129
130
  return;
@@ -290,6 +291,16 @@ export class V2SessionTransport {
290
291
  };
291
292
  return this.keyPair;
292
293
  }
294
+ resetPeerSession() {
295
+ this.remotePublicKey = null;
296
+ this.sessionKeys = null;
297
+ if (this.ws?.readyState === WebSocket.OPEN) {
298
+ this.state = "open";
299
+ }
300
+ else {
301
+ this.state = "idle";
302
+ }
303
+ }
293
304
  computeHandshakeAuth(phase, senderRole, peerPubkeyB64, nonce, boxed) {
294
305
  const authKey = sodium.crypto_generichash(sodium.crypto_auth_KEYBYTES, encodeUtf8(this.options.sessionCode), undefined);
295
306
  const parts = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lunel-cli",
3
- "version": "0.1.91",
3
+ "version": "0.1.93",
4
4
  "author": [
5
5
  {
6
6
  "name": "Soham Bharambe",