vexp-cli 3.2.2 → 3.2.4
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/agent-config.js +16 -0
- package/dist/cli.js +67 -1
- package/dist/doctor.js +16 -8
- package/dist/forget.js +120 -0
- package/dist/serve.js +20 -7
- package/dist/state-home.js +23 -0
- package/mcp/mcp-server.cjs +44 -44
- package/package.json +6 -6
package/dist/agent-config.js
CHANGED
|
@@ -1983,6 +1983,22 @@ function extractCodexVexpSection(content) {
|
|
|
1983
1983
|
const m = content.match(CANONICAL_VEXP_TOML_SECTION_RE);
|
|
1984
1984
|
return m ? m.join("\n") : "";
|
|
1985
1985
|
}
|
|
1986
|
+
/** True when ~/.codex/config.toml carries a hand-written [mcp_servers.vexp]
|
|
1987
|
+
* section — one `vexp use` and `vexp setup` will never rewrite, so a caller
|
|
1988
|
+
* that promised to repoint Codex can say it did not. */
|
|
1989
|
+
export function codexGlobalSectionIsUserManaged() {
|
|
1990
|
+
const home = os.homedir();
|
|
1991
|
+
if (!home)
|
|
1992
|
+
return false;
|
|
1993
|
+
let content = "";
|
|
1994
|
+
try {
|
|
1995
|
+
content = fs.readFileSync(path.join(home, ".codex", "config.toml"), "utf-8");
|
|
1996
|
+
}
|
|
1997
|
+
catch {
|
|
1998
|
+
return false;
|
|
1999
|
+
}
|
|
2000
|
+
return isUserManagedCodexSection(extractCodexVexpSection(content));
|
|
2001
|
+
}
|
|
1986
2002
|
/** True when the existing [mcp_servers.vexp] section was written by the user
|
|
1987
2003
|
* (not by vexp) and must NOT be overwritten. */
|
|
1988
2004
|
function isUserManagedCodexSection(section) {
|
package/dist/cli.js
CHANGED
|
@@ -18,6 +18,8 @@ import { isWsl, windsurfWslBridgeNote } from "./agent-config.js";
|
|
|
18
18
|
import { runServe } from "./serve.js";
|
|
19
19
|
import { runDoctor } from "./doctor.js";
|
|
20
20
|
import { socketPathFor } from "./socket-path.js";
|
|
21
|
+
import { isStateHome } from "./state-home.js";
|
|
22
|
+
import { forgetRegistryRows, removeIndexArtifacts } from "./forget.js";
|
|
21
23
|
import { mutableOutput, askSecretOn } from "./secret-prompt.js";
|
|
22
24
|
import { isTraceEnabled } from "./trace.js";
|
|
23
25
|
import { resolveParentWorkspace, listWorkspaceRepos, addRepoToWorkspace, } from "./workspace-repos.js";
|
|
@@ -48,6 +50,9 @@ const AUTOSTART_SKIP = new Set([
|
|
|
48
50
|
// followed by `daemons` bring the stopped daemon straight back
|
|
49
51
|
// (3.1 e2e, 2026-08).
|
|
50
52
|
"daemons", "stop",
|
|
53
|
+
// `vexp forget` exists to make a workspace stay down; booting one first
|
|
54
|
+
// would be the joke `stop` used to play.
|
|
55
|
+
"forget", "unregister",
|
|
51
56
|
]);
|
|
52
57
|
program.hook("preAction", async (_thisCmd, actionCmd) => {
|
|
53
58
|
// One-shot config migration for the multi-session fix — de-pins a legacy
|
|
@@ -545,10 +550,62 @@ program
|
|
|
545
550
|
console.error(`No such directory: ${target}`);
|
|
546
551
|
process.exit(1);
|
|
547
552
|
}
|
|
553
|
+
// A stop is not a good-bye: while `vexp serve` runs, every registered
|
|
554
|
+
// workspace that still has a manifest is brought back within a minute,
|
|
555
|
+
// and a user who wanted it gone read that as vexp ignoring him (field
|
|
556
|
+
// report, 2026-09-19). Say which command means gone. Printed before the
|
|
557
|
+
// stop because runBinary exits the process with the child.
|
|
558
|
+
if (fs.existsSync(path.join(target, ".vexp", "manifest.json"))) {
|
|
559
|
+
console.log(chalk.dim(` (a login supervisor restarts registered workspaces within a minute — to unregister this one for good: vexp forget "${target}")`));
|
|
560
|
+
}
|
|
548
561
|
// daemon-cmd stop resolves the daemon from its cwd, so run it from the
|
|
549
562
|
// target workspace — this is what makes `vexp stop <workspace>` global.
|
|
550
563
|
runBinary(binaryPath, ["daemon-cmd", "stop"], { cwd: target });
|
|
551
564
|
});
|
|
565
|
+
program
|
|
566
|
+
.command("forget [workspace]")
|
|
567
|
+
.alias("unregister")
|
|
568
|
+
.description("Unregister a workspace for good: stop its daemon, drop it from ~/.vexp/daemons.json and delete its index files, so no launcher brings it back (default: current directory). Safe on the home directory: only index files go — config, licence and registry stay")
|
|
569
|
+
.option("--purge", "Also delete the whole .vexp/ directory, config included (refused for the home directory)")
|
|
570
|
+
.action(async (workspace, opts) => {
|
|
571
|
+
const target = path.resolve(workspace ?? process.cwd());
|
|
572
|
+
if (!fs.existsSync(target)) {
|
|
573
|
+
console.error(`No such directory: ${target}`);
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
const home = isStateHome(target);
|
|
577
|
+
if (opts.purge && home) {
|
|
578
|
+
console.error(chalk.red(`${target} is your home directory: .vexp there is vexp's state dir (config, licence, daemon registry). --purge is refused; without it only the index files go.`));
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
const binaryPath = ensureBinary();
|
|
582
|
+
// 1. The daemon. `daemon-cmd stop` resolves it from its cwd.
|
|
583
|
+
const stop = spawnSync(binaryPath, ["daemon-cmd", "stop"], {
|
|
584
|
+
cwd: target,
|
|
585
|
+
encoding: "utf-8",
|
|
586
|
+
env: binaryEnv(binaryPath),
|
|
587
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
588
|
+
});
|
|
589
|
+
const stopSaid = `${stop.stdout ?? ""}${stop.stderr ?? ""}`.trim().split(/\r?\n/)[0] ?? "";
|
|
590
|
+
console.log(` daemon: ${stopSaid || (stop.error ? stop.error.message : "stopped")}`);
|
|
591
|
+
// 2. The registry — this row and any connected repo routed to its socket.
|
|
592
|
+
const dropped = forgetRegistryRows(target, socketPathFor(target));
|
|
593
|
+
console.log(` registry: ${dropped.length ? `dropped ${dropped.join(", ")}` : "no row for this workspace"}`);
|
|
594
|
+
// 3. The files that would bring it back. `--purge` takes config too.
|
|
595
|
+
const vexpDir = path.join(target, ".vexp");
|
|
596
|
+
if (opts.purge) {
|
|
597
|
+
fs.rmSync(vexpDir, { recursive: true, force: true });
|
|
598
|
+
console.log(` files: removed ${vexpDir}`);
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
const removed = removeIndexArtifacts(vexpDir);
|
|
602
|
+
console.log(` files: ${removed.length ? `removed ${removed.join(", ")}` : "no index files"}${home ? " (home directory: config, licence and registry untouched)" : ""}`);
|
|
603
|
+
}
|
|
604
|
+
console.log(chalk.green(`✓ ${target} forgotten`));
|
|
605
|
+
if (home) {
|
|
606
|
+
console.log(chalk.dim(" Nothing starts a daemon on the home directory any more: every launcher refuses it (VEXP_ALLOW_HOME_WORKSPACE=1 overrides)."));
|
|
607
|
+
}
|
|
608
|
+
});
|
|
552
609
|
program
|
|
553
610
|
.command("shield <action>")
|
|
554
611
|
.description("PII/secret shield: 'scan' reports what comments and string literals would hand to any AI agent (free, read-only, local)")
|
|
@@ -1255,7 +1312,16 @@ program
|
|
|
1255
1312
|
const binaryPath = ensureBinary();
|
|
1256
1313
|
const mcpServerPath = getMcpServerPath();
|
|
1257
1314
|
// Import lazily so configureCodexGlobal's side effects (TOML rewrite + legacy cleanup) only fire here.
|
|
1258
|
-
const { configureSelectedAgents } = await import("./agent-config.js");
|
|
1315
|
+
const { configureSelectedAgents, codexGlobalSectionIsUserManaged } = await import("./agent-config.js");
|
|
1316
|
+
// A hand-written [mcp_servers.vexp] is never rewritten (no-clobber), and
|
|
1317
|
+
// this command used to print its success line anyway — the user then
|
|
1318
|
+
// "repointed" Codex and watched it launch from the old cwd (field
|
|
1319
|
+
// report, 2026-09-19). Say what did not happen and how to do it by hand.
|
|
1320
|
+
if (codexGlobalSectionIsUserManaged()) {
|
|
1321
|
+
console.error(chalk.yellow(`~/.codex/config.toml has a hand-written [mcp_servers.vexp] section, and vexp never rewrites one: Codex keeps launching it exactly as written, so nothing changed.`));
|
|
1322
|
+
console.error(chalk.dim(` Point it yourself — under [mcp_servers.vexp] add cwd = ${JSON.stringify(resolved)}, and under [mcp_servers.vexp.env] add VEXP_WORKSPACE = ${JSON.stringify(resolved)} — or delete the section and run 'vexp use' again.`));
|
|
1323
|
+
process.exit(1);
|
|
1324
|
+
}
|
|
1259
1325
|
const version = (await getInstalledVersion()) ?? CLI_VERSION;
|
|
1260
1326
|
// "Codex" is a no-op file-wise in non-detect mode but the shared flow
|
|
1261
1327
|
// also writes codex global config via configureCodexGlobal — pass an
|
package/dist/doctor.js
CHANGED
|
@@ -10,6 +10,7 @@ import { resolveParentWorkspace, listWorkspaceRepos } from "./workspace-repos.js
|
|
|
10
10
|
import { CLI_VERSION } from "./version.js";
|
|
11
11
|
import { parentPid } from "./mcp-supervisor.js";
|
|
12
12
|
import { slowMountNotice } from "./slow-mount.js";
|
|
13
|
+
import { vexpHome, isStateHome } from "./state-home.js";
|
|
13
14
|
// `vexp doctor` — audit the vexp MCP/daemon state WITHOUT connecting to a daemon.
|
|
14
15
|
// Surfaces the failure modes behind the Codex drift report: stale daemons.json
|
|
15
16
|
// entries, wrong-workspace resolution, mixed Codex transport (url+stdio),
|
|
@@ -17,12 +18,6 @@ import { slowMountNotice } from "./slow-mount.js";
|
|
|
17
18
|
const OK = chalk.green("OK");
|
|
18
19
|
const WARN = chalk.yellow("WARN");
|
|
19
20
|
const BAD = chalk.red("FAIL");
|
|
20
|
-
function vexpHome() {
|
|
21
|
-
const h = process.env.VEXP_HOME;
|
|
22
|
-
if (h && path.isAbsolute(h))
|
|
23
|
-
return h;
|
|
24
|
-
return os.homedir();
|
|
25
|
-
}
|
|
26
21
|
/** Walk up for the nearest INITIALIZED .vexp (manifest/index), then bare .vexp,
|
|
27
22
|
* then .git — mirrors discover_workspace_root / discoverWorkspaceRoot. */
|
|
28
23
|
function discoverWorkspaceRoot(start) {
|
|
@@ -773,6 +768,13 @@ async function doctorChecks(onWorkspace) {
|
|
|
773
768
|
return false;
|
|
774
769
|
} })();
|
|
775
770
|
line(stale ? WARN : OK, `${root} → ${s}${stale ? " (STALE: socket file gone)" : ""}`);
|
|
771
|
+
// A row for the home directory is never right: its .vexp is vexp's
|
|
772
|
+
// state dir, and a daemon there indexes everything under home. Every
|
|
773
|
+
// launcher now refuses it; the row and the daemon behind it are the
|
|
774
|
+
// residue (field report, 2026-09-19).
|
|
775
|
+
if (isStateHome(root)) {
|
|
776
|
+
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
|
+
}
|
|
776
778
|
}
|
|
777
779
|
const inReg = entries.some(([r]) => r.toLowerCase() === ws.root.toLowerCase());
|
|
778
780
|
if (!inReg)
|
|
@@ -837,7 +839,11 @@ async function doctorChecks(onWorkspace) {
|
|
|
837
839
|
continue;
|
|
838
840
|
}
|
|
839
841
|
const toml = fs.readFileSync(file, "utf-8");
|
|
840
|
-
|
|
842
|
+
// The section INCLUDES its own subsections ([mcp_servers.vexp.env],
|
|
843
|
+
// .http_headers): the pin lives in .env, and a lookahead that stopped at
|
|
844
|
+
// any '[' hid it, so doctor reported "stdio (command)" for a stanza it
|
|
845
|
+
// could not tell apart from an unpinned one.
|
|
846
|
+
const m = toml.match(/\n?\[mcp_servers\.vexp\][\s\S]*?(?=\n\[(?!mcp_servers\.vexp\.)|$)/);
|
|
841
847
|
const section = m ? m[0] : "";
|
|
842
848
|
if (!section) {
|
|
843
849
|
line(OK, `${label}: no [mcp_servers.vexp] stanza`);
|
|
@@ -852,7 +858,9 @@ async function doctorChecks(onWorkspace) {
|
|
|
852
858
|
line(OK, `${label}: transport http (url)`);
|
|
853
859
|
else if (hasCmd) {
|
|
854
860
|
const wsm = section.match(/VEXP_WORKSPACE\s*=\s*['"]([^'"]+)['"]/);
|
|
855
|
-
|
|
861
|
+
const cwdm = section.match(/^\s*cwd\s*=\s*['"]([^'"]+)['"]/m);
|
|
862
|
+
const pins = [wsm ? `VEXP_WORKSPACE=${wsm[1]}` : "", cwdm ? `cwd=${cwdm[1]}` : ""].filter(Boolean).join(", ");
|
|
863
|
+
line(OK, `${label}: transport stdio (command)${pins ? `, ${pins}` : ", no workspace pin — the server resolves from Codex's cwd"}`);
|
|
856
864
|
}
|
|
857
865
|
else
|
|
858
866
|
line(WARN, `${label}: stanza present but neither url nor command found`);
|
package/dist/forget.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { vexpHome } from "./state-home.js";
|
|
4
|
+
/**
|
|
5
|
+
* `vexp forget`: the pieces that make a workspace stay forgotten.
|
|
6
|
+
*
|
|
7
|
+
* Stopping a daemon is not enough on a machine running `vexp serve`: the
|
|
8
|
+
* supervisor resurrects every registry row whose manifest still exists,
|
|
9
|
+
* within a minute. Removing the registry row by hand is not enough either:
|
|
10
|
+
* a daemon that is still alive is written back on the next health tick, and
|
|
11
|
+
* the daemon re-registers itself on every start. What keeps a workspace
|
|
12
|
+
* down is all three at once — daemon stopped, row gone, manifest and index
|
|
13
|
+
* gone — and that is what `vexp forget` does (field report, 2026-09-19: a
|
|
14
|
+
* user did each of the three by hand, in turn, four times).
|
|
15
|
+
*/
|
|
16
|
+
/** Everything a daemon or an index run leaves in `<workspace>/.vexp`.
|
|
17
|
+
* Config is absent on purpose (`vexp.toml`, `workspace.json`,
|
|
18
|
+
* `parent_workspace.json`, `.gitattributes`, `.gitignore`): forgetting a
|
|
19
|
+
* workspace removes what brings its daemon back, not what the user wrote.
|
|
20
|
+
* At the home directory the same `.vexp` is vexp's state dir, and this list
|
|
21
|
+
* is exactly what may go there — `config.toml`, the licence tokens,
|
|
22
|
+
* `daemons.json`, `mcp.pid`/`mcp.token`, `gpu-unusable`, models and plugins
|
|
23
|
+
* are not on it. Keep in sync with vexp-core hook_installer VEXP_IGNORE_ENTRIES. */
|
|
24
|
+
export const INDEX_ARTIFACTS = [
|
|
25
|
+
"index.db",
|
|
26
|
+
"index.db-wal",
|
|
27
|
+
"index.db-shm",
|
|
28
|
+
"index.lock",
|
|
29
|
+
"manifest.json",
|
|
30
|
+
"coverage.json",
|
|
31
|
+
"daemon.pid",
|
|
32
|
+
"daemon.pipe",
|
|
33
|
+
"daemon.sock",
|
|
34
|
+
"healthy",
|
|
35
|
+
"start-blocked",
|
|
36
|
+
"daemon-job-warning",
|
|
37
|
+
"mcp.port",
|
|
38
|
+
"daily-limit.json",
|
|
39
|
+
];
|
|
40
|
+
/** Rotating logs and per-session markers, matched by prefix. */
|
|
41
|
+
export const INDEX_ARTIFACT_PREFIXES = [
|
|
42
|
+
"daemon.log",
|
|
43
|
+
"vexp.log",
|
|
44
|
+
"stop-gate-",
|
|
45
|
+
"idle-gate-",
|
|
46
|
+
"task-",
|
|
47
|
+
];
|
|
48
|
+
/** Per-session scratch directories the hooks write. */
|
|
49
|
+
export const INDEX_ARTIFACT_DIRS = ["edit-hints", "search-hints", "read-hints"];
|
|
50
|
+
/** True when an entry of `.vexp/` is index residue rather than config. */
|
|
51
|
+
export function isIndexArtifact(name, isDir) {
|
|
52
|
+
if (isDir)
|
|
53
|
+
return INDEX_ARTIFACT_DIRS.includes(name);
|
|
54
|
+
if (INDEX_ARTIFACTS.includes(name))
|
|
55
|
+
return true;
|
|
56
|
+
return INDEX_ARTIFACT_PREFIXES.some((p) => name === p || name.startsWith(p));
|
|
57
|
+
}
|
|
58
|
+
/** Delete the index artifacts in `vexpDir`, nothing else. Returns what went. */
|
|
59
|
+
export function removeIndexArtifacts(vexpDir) {
|
|
60
|
+
const removed = [];
|
|
61
|
+
let entries;
|
|
62
|
+
try {
|
|
63
|
+
entries = fs.readdirSync(vexpDir, { withFileTypes: true });
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return removed;
|
|
67
|
+
}
|
|
68
|
+
for (const e of entries) {
|
|
69
|
+
if (!isIndexArtifact(e.name, e.isDirectory()))
|
|
70
|
+
continue;
|
|
71
|
+
try {
|
|
72
|
+
fs.rmSync(path.join(vexpDir, e.name), { recursive: true, force: true });
|
|
73
|
+
removed.push(e.name);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* a file the daemon still holds on Windows; reported by omission */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return removed;
|
|
80
|
+
}
|
|
81
|
+
function registryFile() {
|
|
82
|
+
return path.join(vexpHome(), ".vexp", "daemons.json");
|
|
83
|
+
}
|
|
84
|
+
/** The registry without every row that names `target` or routes to its
|
|
85
|
+
* socket (a connected repo mapped to this workspace's socket is dangling
|
|
86
|
+
* once the workspace is gone). Windows keys are lowercased by the daemon. */
|
|
87
|
+
export function registryRowsWithout(reg, target, targetSocket) {
|
|
88
|
+
const canon = (p) => (process.platform === "win32" ? path.resolve(p).toLowerCase() : path.resolve(p));
|
|
89
|
+
const t = canon(target);
|
|
90
|
+
const kept = {};
|
|
91
|
+
const dropped = [];
|
|
92
|
+
for (const [ws, sock] of Object.entries(reg)) {
|
|
93
|
+
if (canon(ws) === t || sock === targetSocket)
|
|
94
|
+
dropped.push(ws);
|
|
95
|
+
else
|
|
96
|
+
kept[ws] = sock;
|
|
97
|
+
}
|
|
98
|
+
return { kept, dropped };
|
|
99
|
+
}
|
|
100
|
+
/** Apply `registryRowsWithout` to ~/.vexp/daemons.json. */
|
|
101
|
+
export function forgetRegistryRows(target, targetSocket) {
|
|
102
|
+
const p = registryFile();
|
|
103
|
+
let reg = {};
|
|
104
|
+
try {
|
|
105
|
+
reg = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
const { kept, dropped } = registryRowsWithout(reg, target, targetSocket);
|
|
111
|
+
if (dropped.length === 0)
|
|
112
|
+
return [];
|
|
113
|
+
try {
|
|
114
|
+
fs.writeFileSync(p, JSON.stringify(kept, null, 2), "utf-8");
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
return dropped;
|
|
120
|
+
}
|
package/dist/serve.js
CHANGED
|
@@ -5,6 +5,7 @@ import * as path from "path";
|
|
|
5
5
|
import { spawn } from "child_process";
|
|
6
6
|
import { getBinaryPath, binaryEnv } from "./binary.js";
|
|
7
7
|
import { ensureMcpHttpServer } from "./mcp-supervisor.js";
|
|
8
|
+
import { isStateHome } from "./state-home.js";
|
|
8
9
|
const HEALTH_INTERVAL_MS = 60_000;
|
|
9
10
|
const DEFAULT_MCP_PORT = 7821;
|
|
10
11
|
function registryPath() {
|
|
@@ -187,6 +188,22 @@ export function isLinkedWorktree(ws) {
|
|
|
187
188
|
return false;
|
|
188
189
|
}
|
|
189
190
|
}
|
|
191
|
+
/** Why a registry row must never be spawned by this supervisor, or null.
|
|
192
|
+
* The home directory is refused by the daemon itself (vexp-core
|
|
193
|
+
* forbidden_workspace_reason); vetoing it here too spares a spawn per
|
|
194
|
+
* minute that would only write "daemon did not start" to ~/.vexp
|
|
195
|
+
* (field report, 2026-09-19: the supervisor was one of the launchers
|
|
196
|
+
* that kept bringing the home daemon back). */
|
|
197
|
+
export function resurrectionVeto(ws) {
|
|
198
|
+
if (isStateHome(ws)) {
|
|
199
|
+
return "the home directory is vexp's state dir, never a workspace — `vexp forget` removes this row";
|
|
200
|
+
}
|
|
201
|
+
if (!fs.existsSync(path.join(ws, ".vexp", "manifest.json")))
|
|
202
|
+
return "no manifest";
|
|
203
|
+
if (isLinkedWorktree(ws))
|
|
204
|
+
return "linked git worktree — on-demand only, never resurrected";
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
190
207
|
export function resurrectionPlan(reg) {
|
|
191
208
|
const bySock = new Map();
|
|
192
209
|
for (const [ws, sock] of Object.entries(reg)) {
|
|
@@ -230,13 +247,9 @@ async function resurrectAll() {
|
|
|
230
247
|
continue;
|
|
231
248
|
}
|
|
232
249
|
seenKeys.add(canonical);
|
|
233
|
-
const
|
|
234
|
-
if (
|
|
235
|
-
appendLog(`registry prune: ${ws} (
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
if (isLinkedWorktree(ws)) {
|
|
239
|
-
appendLog(`registry prune: ${ws} (linked git worktree — on-demand only, never resurrected)`);
|
|
250
|
+
const veto = resurrectionVeto(ws);
|
|
251
|
+
if (veto) {
|
|
252
|
+
appendLog(`registry prune: ${ws} (${veto})`);
|
|
240
253
|
continue;
|
|
241
254
|
}
|
|
242
255
|
pruned[ws] = sock;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as os from "os";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
/** The directory vexp keeps its own state in (`<here>/.vexp`): VEXP_HOME when
|
|
4
|
+
* set to an absolute path, else the OS home. */
|
|
5
|
+
export function vexpHome() {
|
|
6
|
+
const h = process.env.VEXP_HOME;
|
|
7
|
+
if (h && path.isAbsolute(h))
|
|
8
|
+
return h;
|
|
9
|
+
return os.homedir();
|
|
10
|
+
}
|
|
11
|
+
/** True when `dir` IS the home / VEXP_HOME directory. Its `.vexp` is vexp's
|
|
12
|
+
* state dir (config, licence, daemons.json), never a workspace: a daemon
|
|
13
|
+
* there indexes everything under home, and deleting the directory to be rid
|
|
14
|
+
* of it takes the licence and the registry along (field report, 2026-09-19).
|
|
15
|
+
* Lockstep with vexp-core utils::forbidden_workspace_reason and
|
|
16
|
+
* vexp-mcp isVexpStateHome; VEXP_ALLOW_HOME_WORKSPACE=1 lifts it everywhere. */
|
|
17
|
+
export function isStateHome(dir) {
|
|
18
|
+
if (process.env.VEXP_ALLOW_HOME_WORKSPACE === "1")
|
|
19
|
+
return false;
|
|
20
|
+
const a = path.resolve(dir);
|
|
21
|
+
const b = path.resolve(vexpHome());
|
|
22
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
23
|
+
}
|