vexp-cli 3.2.4 → 3.2.5

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.
@@ -11,6 +11,7 @@ import * as path from "path";
11
11
  import * as os from "os";
12
12
  import { spawnSync } from "child_process";
13
13
  import * as crypto from "crypto";
14
+ import { canonicalWorkspaceRoot } from "./socket-path.js";
14
15
  import { VEXP_GUARD_HOOK, VEXP_OPENCODE_GUARD, VEXP_CURSOR_GUARD, vexpHintHookScript, vexpSearchHookScript, vexpHintHookCmdScript, vexpStopGateHookScript, vexpSessionContextHookScript, vexpOpencodeHintPlugin, vexpOpencodeCompressPlugin, bakeEditHintHook, bakeReadHintHook, bakeBashCapHook } from "./hook-template.js";
15
16
  // ---------------------------------------------------------------------------
16
17
  // Constants
@@ -39,7 +40,9 @@ const CANONICAL_VEXP_TOML_SECTION_RE = /\n?\[mcp_servers\.vexp(?:\.[a-z_]+)?\][\
39
40
  * MUST stay in lockstep with that function.
40
41
  */
41
42
  function workspaceHash(workspaceRoot) {
42
- const input = workspaceRoot.toLowerCase();
43
+ // The route hash must name the row the daemon registers: its canonical
44
+ // root, which on a mapped drive is the share, not the drive letter.
45
+ const input = canonicalWorkspaceRoot(workspaceRoot).toLowerCase();
43
46
  let hash = BigInt("0xcbf29ce484222325");
44
47
  const prime = BigInt("0x100000001b3");
45
48
  const mask = BigInt("0xffffffffffffffff");
package/dist/doctor.js CHANGED
@@ -11,6 +11,7 @@ import { CLI_VERSION } from "./version.js";
11
11
  import { parentPid } from "./mcp-supervisor.js";
12
12
  import { slowMountNotice } from "./slow-mount.js";
13
13
  import { vexpHome, isStateHome } from "./state-home.js";
14
+ import { canonicalWorkspaceRoot } from "./socket-path.js";
14
15
  // `vexp doctor` — audit the vexp MCP/daemon state WITHOUT connecting to a daemon.
15
16
  // Surfaces the failure modes behind the Codex drift report: stale daemons.json
16
17
  // entries, wrong-workspace resolution, mixed Codex transport (url+stdio),
@@ -292,7 +293,7 @@ export function vsCodeMcpVerdict(cfg, wsRoot, exists = (p) => fs.existsSync(p))
292
293
  if (script && !exists(script))
293
294
  return { level: WARN, message: `.vscode/mcp.json vexp server bundle is missing: ${script} (an editor upgrade removed the old extension folder?) — re-run 'vexp setup'` };
294
295
  const pinned = typeof vexp.env?.VEXP_WORKSPACE === "string" ? vexp.env.VEXP_WORKSPACE : undefined;
295
- if (pinned && path.resolve(pinned).toLowerCase() !== path.resolve(wsRoot).toLowerCase()) {
296
+ if (pinned && !samePath(pinned, wsRoot)) {
296
297
  return { level: WARN, message: `.vscode/mcp.json vexp server is pinned to ${pinned}, but this workspace is ${wsRoot} — re-run 'vexp setup' here` };
297
298
  }
298
299
  return { level: OK, message: `.vscode/mcp.json vexp server: ${command} ${script ?? args.join(" ")}` };
@@ -314,8 +315,9 @@ export function codexHookSpawnSpec(cmdLine, platform = process.platform, comspec
314
315
  return { file: "sh", args: ["-c", cmdLine], windowsVerbatimArguments: false };
315
316
  }
316
317
  function samePath(a, b) {
317
- // The daemon registry lower-cases Windows roots; compare accordingly.
318
- return path.resolve(a).toLowerCase() === path.resolve(b).toLowerCase();
318
+ // The daemon registry lower-cases Windows roots and spells a mapped drive
319
+ // as its share; compare accordingly.
320
+ return canonicalWorkspaceRoot(path.resolve(a)).toLowerCase() === canonicalWorkspaceRoot(path.resolve(b)).toLowerCase();
319
321
  }
320
322
  /**
321
323
  * A connected repo of a multi-repo workspace has no daemon of its own: the
@@ -633,7 +635,7 @@ async function doctorChecks(onWorkspace) {
633
635
  // disagrees with this directory's natural target.
634
636
  if (ws.source === "VEXP_WORKSPACE") {
635
637
  const discovered = discoverWorkspaceRoot(process.cwd());
636
- if (discovered.toLowerCase() !== ws.root.toLowerCase()) {
638
+ if (!samePath(discovered, ws.root)) {
637
639
  line(WARN, `global VEXP_WORKSPACE env var pins this session to ${ws.root}, but this directory's natural target is ${discovered} → agents here get [] / wrong-repo results. Unset the VEXP_WORKSPACE OS/shell env var for per-session targeting.`);
638
640
  }
639
641
  }
@@ -776,7 +778,7 @@ async function doctorChecks(onWorkspace) {
776
778
  line(BAD, `${root} is your home directory — never a workspace (its .vexp is vexp's state dir). A daemon here indexes everything under home. Run: vexp forget "${root}" (stops it, drops this row, deletes only its index files — licence, config and registry stay)`);
777
779
  }
778
780
  }
779
- const inReg = entries.some(([r]) => r.toLowerCase() === ws.root.toLowerCase());
781
+ const inReg = entries.some(([r]) => samePath(r, ws.root));
780
782
  if (!inReg)
781
783
  line(WARN, `current workspace (${ws.root}) is NOT registered — a child here could mis-route`);
782
784
  // Multi-daemon + global pin: catches the case the section-1 check misses
package/dist/forget.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { vexpHome } from "./state-home.js";
4
+ import { canonicalWorkspaceRoot } from "./socket-path.js";
4
5
  /**
5
6
  * `vexp forget`: the pieces that make a workspace stay forgotten.
6
7
  *
@@ -85,7 +86,8 @@ function registryFile() {
85
86
  * socket (a connected repo mapped to this workspace's socket is dangling
86
87
  * once the workspace is gone). Windows keys are lowercased by the daemon. */
87
88
  export function registryRowsWithout(reg, target, targetSocket) {
88
- const canon = (p) => (process.platform === "win32" ? path.resolve(p).toLowerCase() : path.resolve(p));
89
+ // A mapped drive is registered as its share; canonicalize before comparing.
90
+ const canon = (p) => (process.platform === "win32" ? canonicalWorkspaceRoot(path.resolve(p)).toLowerCase() : path.resolve(p));
89
91
  const t = canon(target);
90
92
  const kept = {};
91
93
  const dropped = [];
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
  /**