vexp-cli 3.2.4 → 3.3.0

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/serve.js CHANGED
@@ -6,6 +6,7 @@ import { spawn } from "child_process";
6
6
  import { getBinaryPath, binaryEnv } from "./binary.js";
7
7
  import { ensureMcpHttpServer } from "./mcp-supervisor.js";
8
8
  import { isStateHome } from "./state-home.js";
9
+ import { canonicalWorkspaceRoot } from "./socket-path.js";
9
10
  const HEALTH_INTERVAL_MS = 60_000;
10
11
  const DEFAULT_MCP_PORT = 7821;
11
12
  function registryPath() {
@@ -241,7 +242,7 @@ async function resurrectAll() {
241
242
  // rewrite here would otherwise immortalize the phantom twin.
242
243
  const seenKeys = new Set();
243
244
  for (const [ws, sock] of Object.entries(reg)) {
244
- const canonical = process.platform === "win32" ? ws.toLowerCase() : ws;
245
+ const canonical = process.platform === "win32" ? canonicalWorkspaceRoot(ws).toLowerCase() : ws;
245
246
  if (seenKeys.has(canonical)) {
246
247
  appendLog(`registry prune: ${ws} (case-variant duplicate)`);
247
248
  continue;
@@ -299,7 +300,7 @@ async function resurrectAll() {
299
300
  * report what it could not find, never delete what it does not own.
300
301
  */
301
302
  export async function survivingRegistryRows(before, resurrected, alive, log = () => { }) {
302
- const canonical = (ws) => (process.platform === "win32" ? ws.toLowerCase() : ws);
303
+ const canonical = (ws) => (process.platform === "win32" ? canonicalWorkspaceRoot(ws).toLowerCase() : ws);
303
304
  const out = { ...resurrected };
304
305
  const kept = new Set(Object.keys(out).map(canonical));
305
306
  for (const [ws, sock] of Object.entries(before)) {
@@ -1,3 +1,4 @@
1
+ import * as fs from "fs";
1
2
  import * as path from "path";
2
3
  /**
3
4
  * Where the daemon listens for a given workspace root.
@@ -18,7 +19,7 @@ import * as path from "path";
18
19
  */
19
20
  export function socketPathFor(workspaceRoot) {
20
21
  if (process.platform === "win32") {
21
- return `\\\\.\\pipe\\vexp-${fnvHash(workspaceRoot.toLowerCase()).slice(0, 8)}`;
22
+ return `\\\\.\\pipe\\vexp-${fnvHash(canonicalWorkspaceRoot(workspaceRoot).toLowerCase()).slice(0, 8)}`;
22
23
  }
23
24
  const candidate = path.join(workspaceRoot, ".vexp", "daemon.sock");
24
25
  // macOS/BSD sockaddr_un caps the path at 104 bytes; vexp-core falls back to
@@ -27,6 +28,29 @@ export function socketPathFor(workspaceRoot) {
27
28
  return candidate;
28
29
  return `/tmp/vexp-${fnvHash(workspaceRoot).slice(0, 12)}.sock`;
29
30
  }
31
+ /**
32
+ * The workspace root as the daemon spells it. vexp-core canonicalizes the
33
+ * root before deriving the pipe name and the daemons.json key, and on
34
+ * Windows a folder on a mapped drive canonicalizes to its share:
35
+ * `X:\proj` becomes `\\server\share\proj`. Every hash computed from the
36
+ * drive-letter spelling named a pipe the daemon never bound, so `vexp
37
+ * status` on a share reported the daemon as down and started a second one,
38
+ * and the MCP bridge could not reach it (field report, Samba share from
39
+ * Windows 11, 2026-09-20). Node's native realpath resolves the same way; a
40
+ * local path only gets its case corrected, which the lowercase hash never
41
+ * saw. Falls back to the spelling given when the folder cannot be resolved
42
+ * (a stale registry row, a path that is not there yet).
43
+ */
44
+ export function canonicalWorkspaceRoot(root) {
45
+ if (process.platform !== "win32")
46
+ return root;
47
+ try {
48
+ return fs.realpathSync.native(root);
49
+ }
50
+ catch {
51
+ return root;
52
+ }
53
+ }
30
54
  /** Longest `<root>/.vexp/daemon.sock` vexp binds in place; past it the socket lives in /tmp. */
31
55
  export const UNIX_SOCKET_PATH_LIMIT = 100;
32
56
  /**