vexp-cli 3.3.0 → 3.3.1

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.
@@ -2074,8 +2074,9 @@ export function configureZcodeMcp(workspaceRoot, binaryPath) {
2074
2074
  export function zcodeUserConfigPath() {
2075
2075
  return path.join(os.homedir(), ".zcode", "cli", "config.json");
2076
2076
  }
2077
- /** Does this UserPromptSubmit entry belong to us? Ours runs the vexp binary's `prompt-hint`. */
2078
- function isVexpZcodeHookEntry(h) {
2077
+ /** Does this UserPromptSubmit entry belong to us? Ours runs the vexp binary's `prompt-hint`.
2078
+ * Exported for doctor, which must recognise the entry exactly as setup does. */
2079
+ export function isVexpZcodeHookEntry(h) {
2079
2080
  if (!h || typeof h !== "object")
2080
2081
  return false;
2081
2082
  const hks = h.hooks;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Cloud-synced workspace detection for doctor.
3
+ *
4
+ * Field report (2026-09-28, macOS, ZCode): the project lived under
5
+ * ~/Library/Mobile Documents/com~apple~CloudDocs, i.e. in iCloud Drive.
6
+ * Every file there is uploaded as it changes, .vexp/index.db included: a live
7
+ * SQLite database (plus its -wal and -shm) that the daemon rewrites on every
8
+ * index pass and every ledger row. On one machine that is sync traffic for
9
+ * nothing; opened from a second machine the sync client merges nothing and
10
+ * leaves "index (conflicted copy).db" files, or a database whose WAL and main
11
+ * file come from two different writers. Nothing in doctor said where the
12
+ * project was. Pure: the caller passes the platform, home and environment,
13
+ * so every rule is testable on any host.
14
+ */
15
+ import * as path from "path";
16
+ /** The provider macOS names its File Provider folders after
17
+ * (~/Library/CloudStorage/<Provider>-<account>). */
18
+ const CLOUD_STORAGE_PROVIDERS = [
19
+ [/^dropbox/i, "Dropbox"],
20
+ [/^onedrive/i, "OneDrive"],
21
+ [/^googledrive/i, "Google Drive"],
22
+ [/^box/i, "Box"],
23
+ ];
24
+ /** `dir` itself or a folder under it; case-folded where the platform's
25
+ * default filesystem is (APFS/HFS+ on macOS, NTFS on Windows). */
26
+ function isUnder(child, dir, platform) {
27
+ const p = platform === "win32" ? path.win32 : path.posix;
28
+ const fold = platform === "win32" || platform === "darwin";
29
+ const a = fold ? p.resolve(dir).toLowerCase() : p.resolve(dir);
30
+ const b = fold ? p.resolve(child).toLowerCase() : p.resolve(child);
31
+ const rel = p.relative(a, b);
32
+ return rel === "" || (rel !== ".." && !rel.startsWith(".." + p.sep) && !p.isAbsolute(rel));
33
+ }
34
+ /**
35
+ * The sync service that holds `root`, with the synced folder it was found
36
+ * under, or null. Only folders whose whole content a sync client uploads:
37
+ * iCloud Drive and the macOS File Provider folders, the OneDrive folders
38
+ * Windows exports as environment variables, and ~/Dropbox anywhere.
39
+ */
40
+ export function cloudSyncProvider(root, ctx) {
41
+ const p = ctx.platform === "win32" ? path.win32 : path.posix;
42
+ if (!root || !ctx.home)
43
+ return null;
44
+ if (ctx.platform === "darwin") {
45
+ const icloud = p.join(ctx.home, "Library", "Mobile Documents");
46
+ if (isUnder(root, icloud, ctx.platform)) {
47
+ return { provider: "iCloud Drive", folder: icloud };
48
+ }
49
+ // With "Desktop & Documents Folders" on, com~apple~CloudDocs/Desktop
50
+ // and /Documents are symlinks to the real ~/Desktop and ~/Documents, so
51
+ // a project there is spelled ~/Desktop/... by process.cwd() and by
52
+ // realpath alike, and never matches the Mobile Documents prefix (the
53
+ // field report's path went through CloudDocs/Desktop, 2026-09-28).
54
+ for (const dir of ctx.icloudLinked ?? []) {
55
+ if (dir && isUnder(root, dir, ctx.platform))
56
+ return { provider: "iCloud Drive", folder: dir };
57
+ }
58
+ const storage = p.join(ctx.home, "Library", "CloudStorage");
59
+ // The first component under CloudStorage is the provider's folder;
60
+ // CloudStorage itself is only their parent.
61
+ // Sliced, not path.relative'd: the case-folded match above may have
62
+ // matched a root spelled in another case than home.
63
+ const first = isUnder(root, storage, ctx.platform) ? p.resolve(root).slice(p.resolve(storage).length + 1).split(p.sep)[0] : "";
64
+ if (first) {
65
+ const known = CLOUD_STORAGE_PROVIDERS.find(([re]) => re.test(first));
66
+ const provider = known ? known[1] : first.split("-")[0] || "a cloud storage provider";
67
+ return { provider, folder: p.join(storage, first) };
68
+ }
69
+ }
70
+ if (ctx.platform === "win32") {
71
+ for (const key of ["OneDrive", "OneDriveConsumer", "OneDriveCommercial"]) {
72
+ const dir = envValue(ctx.env, key);
73
+ if (dir && p.isAbsolute(dir) && isUnder(root, dir, ctx.platform))
74
+ return { provider: "OneDrive", folder: dir };
75
+ }
76
+ }
77
+ const dropbox = p.join(ctx.home, "Dropbox");
78
+ if (isUnder(root, dropbox, ctx.platform))
79
+ return { provider: "Dropbox", folder: dropbox };
80
+ return null;
81
+ }
82
+ /** Windows environment keys are case-insensitive; a plain object here is not. */
83
+ function envValue(env, key) {
84
+ if (env[key])
85
+ return env[key];
86
+ const hit = Object.keys(env).find((k) => k.toLowerCase() === key.toLowerCase());
87
+ return hit ? env[hit] : undefined;
88
+ }
89
+ /** The WARN text for a workspace inside a synced folder; null otherwise. */
90
+ export function cloudSyncNotice(root, ctx) {
91
+ const hit = cloudSyncProvider(root, ctx);
92
+ if (!hit)
93
+ return null;
94
+ return (`workspace is inside ${hit.provider} (${hit.folder}), which syncs every file in it, including .vexp/index.db: ` +
95
+ "a live SQLite database vexp rewrites continuously. On one machine this costs sync traffic; opened from a second " +
96
+ "machine, the sync client can produce conflicted copies of the database. Move the project out of the synced folder " +
97
+ "(e.g. ~/Repos) and run 'vexp setup' there.");
98
+ }