vexp-cli 2.2.1 → 2.2.3
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 +420 -43
- package/dist/cli.js +200 -31
- package/dist/hook-template.js +259 -26
- package/dist/workspace-repos.js +166 -0
- package/mcp/mcp-server.cjs +48 -43
- package/package.json +6 -6
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import * as fs from "fs";
|
|
|
8
8
|
import * as net from "net";
|
|
9
9
|
import { checkbox, confirm } from "@inquirer/prompts";
|
|
10
10
|
import { getBinaryPath, getInstalledVersion, getMcpServerPath, binaryEnv } from "./binary.js";
|
|
11
|
-
import { detectAgents, getAgentList, configureSelectedAgents } from "./agent-config.js";
|
|
11
|
+
import { detectAgents, getAgentList, configureSelectedAgents, resolveAgentName, suggestAgentName } from "./agent-config.js";
|
|
12
12
|
import { CLI_VERSION } from "./version.js";
|
|
13
13
|
import { activateLicense, deactivateLicense, readLicenseLimits, readDeviceBlocked, } from "./license.js";
|
|
14
14
|
import { checkForUpdate } from "./update-check.js";
|
|
@@ -18,6 +18,7 @@ import { runServe } from "./serve.js";
|
|
|
18
18
|
import { runDoctor } from "./doctor.js";
|
|
19
19
|
import { socketPathFor } from "./socket-path.js";
|
|
20
20
|
import { isTraceEnabled } from "./trace.js";
|
|
21
|
+
import { resolveParentWorkspace, listWorkspaceRepos, addRepoToWorkspace, } from "./workspace-repos.js";
|
|
21
22
|
const program = new Command();
|
|
22
23
|
program
|
|
23
24
|
.name("vexp")
|
|
@@ -54,8 +55,11 @@ program.hook("preAction", async (_thisCmd, actionCmd) => {
|
|
|
54
55
|
return;
|
|
55
56
|
if (AUTOSTART_SKIP.has(actionCmd.name()))
|
|
56
57
|
return;
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
// Effective workspace: inside a connected secondary repo this resolves to
|
|
59
|
+
// the parent, so we boot (or reuse) the parent daemon instead of spawning
|
|
60
|
+
// a conflicting one for the child.
|
|
61
|
+
const eff = findEffectiveWorkspace();
|
|
62
|
+
if (!eff)
|
|
59
63
|
return;
|
|
60
64
|
let binaryPath;
|
|
61
65
|
try {
|
|
@@ -66,7 +70,7 @@ program.hook("preAction", async (_thisCmd, actionCmd) => {
|
|
|
66
70
|
}
|
|
67
71
|
// Full bootstrap: daemon + MCP + autostart-if-needed. All three steps are
|
|
68
72
|
// internally idempotent — no duplicate processes across invocations.
|
|
69
|
-
await ensureBootstrap(
|
|
73
|
+
await ensureBootstrap(eff.root, binaryPath);
|
|
70
74
|
});
|
|
71
75
|
// ────────────────────────────────────────────────────
|
|
72
76
|
// Resolve the platform binary (no network — bundled via npm)
|
|
@@ -83,7 +87,7 @@ function ensureBinary() {
|
|
|
83
87
|
// Tier limits are now enforced directly by the Rust daemon via license.jwt verification.
|
|
84
88
|
// Track whether we're in interactive REPL mode (don't call process.exit)
|
|
85
89
|
let replMode = false;
|
|
86
|
-
function runBinary(binaryPath, args) {
|
|
90
|
+
function runBinary(binaryPath, args, options) {
|
|
87
91
|
if (replMode) {
|
|
88
92
|
// In REPL mode, run synchronously and don't exit the process.
|
|
89
93
|
// CRITICAL: stdin must be "ignore" (not "inherit") — inheriting stdin
|
|
@@ -94,6 +98,7 @@ function runBinary(binaryPath, args) {
|
|
|
94
98
|
// CLI, never stdin, so ignoring it is safe.
|
|
95
99
|
const result = spawnSync(binaryPath, args, {
|
|
96
100
|
stdio: ["ignore", "inherit", "inherit"],
|
|
101
|
+
cwd: options?.cwd,
|
|
97
102
|
env: binaryEnv(binaryPath),
|
|
98
103
|
});
|
|
99
104
|
if (result.error) {
|
|
@@ -103,6 +108,7 @@ function runBinary(binaryPath, args) {
|
|
|
103
108
|
}
|
|
104
109
|
const child = spawn(binaryPath, args, {
|
|
105
110
|
stdio: "inherit",
|
|
111
|
+
cwd: options?.cwd,
|
|
106
112
|
env: binaryEnv(binaryPath),
|
|
107
113
|
});
|
|
108
114
|
child.on("exit", (code) => {
|
|
@@ -143,12 +149,16 @@ function runBinaryAsync(binaryPath, args, options) {
|
|
|
143
149
|
// ────────────────────────────────────────────────────
|
|
144
150
|
/**
|
|
145
151
|
* Walk up from cwd looking for `.vexp/manifest.json` — the marker that
|
|
146
|
-
* `vexp setup` has been run on this tree.
|
|
152
|
+
* `vexp setup` has been run on this tree. A `.vexp/parent_workspace.json`
|
|
153
|
+
* counts too: a freshly-connected secondary repo has the parent link before
|
|
154
|
+
* its first index writes a manifest, and it must already resolve to the
|
|
155
|
+
* parent workspace instead of looking unconfigured. Returns null if none found.
|
|
147
156
|
*/
|
|
148
157
|
function findConfiguredWorkspace(startDir = process.cwd()) {
|
|
149
158
|
let current = path.resolve(startDir);
|
|
150
159
|
while (true) {
|
|
151
|
-
if (fs.existsSync(path.join(current, ".vexp", "manifest.json"))
|
|
160
|
+
if (fs.existsSync(path.join(current, ".vexp", "manifest.json")) ||
|
|
161
|
+
fs.existsSync(path.join(current, ".vexp", "parent_workspace.json")))
|
|
152
162
|
return current;
|
|
153
163
|
const parent = path.dirname(current);
|
|
154
164
|
if (parent === current)
|
|
@@ -156,6 +166,22 @@ function findConfiguredWorkspace(startDir = process.cwd()) {
|
|
|
156
166
|
current = parent;
|
|
157
167
|
}
|
|
158
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Workspace the CLI should actually operate on. When cwd is inside a
|
|
171
|
+
* secondary repo of a multi-repo workspace (it carries a valid
|
|
172
|
+
* `.vexp/parent_workspace.json`), every daemon interaction targets the PARENT:
|
|
173
|
+
* spawning a second daemon in the child made both fight over the same repo
|
|
174
|
+
* DBs (double watchers, duplicate LLM in VRAM) until the user killed
|
|
175
|
+
* everything. `parent` is set only when redirected, so callers can tell the
|
|
176
|
+
* user what happened.
|
|
177
|
+
*/
|
|
178
|
+
function findEffectiveWorkspace(startDir = process.cwd()) {
|
|
179
|
+
const ws = findConfiguredWorkspace(startDir);
|
|
180
|
+
if (!ws)
|
|
181
|
+
return null;
|
|
182
|
+
const parent = resolveParentWorkspace(ws);
|
|
183
|
+
return parent ? { root: parent.parentRoot, parent } : { root: ws };
|
|
184
|
+
}
|
|
159
185
|
/** True if the Unix socket at `socketPath` accepts a connection within `timeoutMs`. */
|
|
160
186
|
function isSocketAlive(socketPath, timeoutMs = 300) {
|
|
161
187
|
return new Promise((resolve) => {
|
|
@@ -486,7 +512,13 @@ program
|
|
|
486
512
|
const args = ["daemon-cmd", action];
|
|
487
513
|
if (opts.follow)
|
|
488
514
|
args.push("--follow");
|
|
489
|
-
|
|
515
|
+
// Inside a connected secondary repo, daemon commands target the PARENT
|
|
516
|
+
// workspace daemon — the child must never run its own competing daemon.
|
|
517
|
+
const eff = findEffectiveWorkspace();
|
|
518
|
+
if (eff?.parent) {
|
|
519
|
+
console.log(chalk.cyan(`This repo is part of workspace "${eff.parent.workspaceName}" — targeting the parent daemon (${shortPath(eff.root)})`));
|
|
520
|
+
}
|
|
521
|
+
runBinary(binaryPath, args, eff?.parent ? { cwd: eff.root } : undefined);
|
|
490
522
|
});
|
|
491
523
|
program
|
|
492
524
|
.command("doctor")
|
|
@@ -555,8 +587,24 @@ program
|
|
|
555
587
|
const detectedNames = new Set(detected.map((a) => a.agent));
|
|
556
588
|
let selectedNames;
|
|
557
589
|
if (typeof opts.agents === "string") {
|
|
558
|
-
// Explicit list from --agents flag
|
|
559
|
-
|
|
590
|
+
// Explicit list from --agents flag. Resolve every name BEFORE writing
|
|
591
|
+
// anything: matching is exact downstream, so an unrecognised name used
|
|
592
|
+
// to configure nothing while setup still reported success — the worst
|
|
593
|
+
// possible outcome for the flag whose whole point is automation.
|
|
594
|
+
const requested = opts.agents.split(",").map((s) => s.trim()).filter(Boolean);
|
|
595
|
+
const unknown = requested.filter((n) => resolveAgentName(n) === null);
|
|
596
|
+
if (unknown.length > 0 || requested.length === 0) {
|
|
597
|
+
console.error(chalk.red(`\n✖ Unknown agent: ${unknown.map((u) => `"${u}"`).join(", ") || "(empty --agents list)"}`));
|
|
598
|
+
for (const u of unknown) {
|
|
599
|
+
const hint = suggestAgentName(u);
|
|
600
|
+
if (hint)
|
|
601
|
+
console.error(chalk.yellow(` Did you mean: ${hint}?`));
|
|
602
|
+
}
|
|
603
|
+
console.error(chalk.dim(` Valid agents: ${allAgents.map((a) => a.agent).join(", ")}`));
|
|
604
|
+
console.error(chalk.dim(" Names are matched ignoring case and punctuation.\n"));
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
selectedNames = requested.map((n) => resolveAgentName(n));
|
|
560
608
|
console.log(chalk.dim(` Agents (from flag): ${selectedNames.join(", ")}`));
|
|
561
609
|
}
|
|
562
610
|
else {
|
|
@@ -981,10 +1029,10 @@ await checkForUpdate();
|
|
|
981
1029
|
// ── Sub-menu: Setup & Config ──
|
|
982
1030
|
const MENU_CONFIG = [
|
|
983
1031
|
{ key: "1", label: "init", hint: "Create .vexp/ and run first index", description: "Creates .vexp/ directory, runs a full index, installs git hooks.\n Run once when adding vexp to a new project." },
|
|
984
|
-
{ key: "2", label: "index", hint: "Re-index the
|
|
1032
|
+
{ key: "2", label: "index", hint: "Re-index the workspace (all connected repos)", description: "Scans all source files, builds the AST graph (nodes + edges),\n and saves it to .vexp/index.db. In a multi-repo workspace every\n connected repo is re-indexed, not just the current one.\n Output: file count, node count, edge count, time elapsed." },
|
|
985
1033
|
{ key: "3", label: "setup", hint: "Full setup (index + agents + hooks)", description: "One-command bootstrap: indexes the codebase, detects AI agents,\n writes MCP configs, installs git hooks." },
|
|
986
1034
|
{ key: "4", label: "setup-agents", hint: "Choose which AI agents to configure", description: "Select agents (Claude, Cursor, Windsurf, Copilot, …) to configure.\n Creates config directories and writes MCP settings for each." },
|
|
987
|
-
{ key: "5", label: "daemon-cmd", hint: "Manage daemon (start/stop/status/logs)", description: "Controls the background daemon:\n start
|
|
1035
|
+
{ key: "5", label: "daemon-cmd", hint: "Manage daemon (start/stop/restart/status/logs)", description: "Controls the background daemon:\n start → launches the daemon (indexes + watches for changes)\n stop → stops the running daemon\n restart → stop + start (reloads workspace.json, indexes new repos)\n status → shows node/edge/file counts\n logs → prints .vexp/vexp.log" },
|
|
988
1036
|
{ key: "6", label: "setup-llm", hint: "Configure local LLM compression (vexp-devmind)", description: "Install, update, or disable the optional local LLM (vexp-devmind).\n Runs entirely on your machine (~3.5 GB download).\n Enables smart prompt preprocessing and higher-quality memory compression." },
|
|
989
1037
|
];
|
|
990
1038
|
// ── Sub-menu: Explore & Analyze ──
|
|
@@ -999,6 +1047,11 @@ const MENU_SAVINGS = [
|
|
|
999
1047
|
{ key: "1", label: "summary", hint: "View savings summary", description: "Shows total tokens saved, average saving %, and breakdown by tool.\n Choose a period: 7 / 14 / 30 / 60 / 90 days." },
|
|
1000
1048
|
{ key: "2", label: "export", hint: "Export report to .vexp/token-savings-report.md", description: "Generates a markdown report with breakdown by tool and daily trend,\n then saves it to .vexp/token-savings-report.md." },
|
|
1001
1049
|
];
|
|
1050
|
+
// ── Sub-menu: Repos & Workspace ──
|
|
1051
|
+
const MENU_REPOS = [
|
|
1052
|
+
{ key: "1", label: "list", hint: "Show repos connected to this workspace", description: "Lists the repositories of this multi-repo workspace\n (.vexp/workspace.json): alias, path, primary marker." },
|
|
1053
|
+
{ key: "2", label: "add", hint: "Connect another repository to this workspace", description: "Adds a repository to .vexp/workspace.json and links it back via\n .vexp/parent_workspace.json — same writes as the VS Code\n \"vexp: Add Repository\" command. Restart the daemon to index it." },
|
|
1054
|
+
];
|
|
1002
1055
|
// ── Sub-menu: License ──
|
|
1003
1056
|
const MENU_LICENSE = [
|
|
1004
1057
|
{ key: "1", label: "status", hint: "Show current license plan and limits", description: "Displays your current plan (free / pro / team), node limits,\n repo limits, and license expiry." },
|
|
@@ -1040,12 +1093,18 @@ async function printBanner() {
|
|
|
1040
1093
|
// banner says "not running", but the Rust daemon-cmd status probes the
|
|
1041
1094
|
// socket and reports "running". Using the socket here makes the banner
|
|
1042
1095
|
// agree with status.
|
|
1043
|
-
const
|
|
1044
|
-
if (!
|
|
1096
|
+
const eff = findEffectiveWorkspace();
|
|
1097
|
+
if (!eff) {
|
|
1045
1098
|
console.log(chalk.dim(" ○ Daemon: no workspace configured here"));
|
|
1046
1099
|
console.log(chalk.dim(" → type '1 > 3' to run full setup, or `vexp setup`"));
|
|
1047
1100
|
}
|
|
1048
1101
|
else {
|
|
1102
|
+
// Connected secondary repo: every daemon interaction targets the parent
|
|
1103
|
+
// workspace — say so up front, since the status below is the PARENT's.
|
|
1104
|
+
if (eff.parent) {
|
|
1105
|
+
console.log(chalk.cyan(` ◆ Workspace: this repo is part of "${eff.parent.workspaceName}" — using the parent daemon (${shortPath(eff.root)})`));
|
|
1106
|
+
}
|
|
1107
|
+
const ws = eff.root;
|
|
1049
1108
|
const socketAlive = await isSocketAlive(socketPathFor(ws));
|
|
1050
1109
|
if (socketAlive) {
|
|
1051
1110
|
// Display PID + uptime when we can read them; otherwise a plain "running".
|
|
@@ -1081,7 +1140,7 @@ async function printBanner() {
|
|
|
1081
1140
|
}
|
|
1082
1141
|
else if (installed) {
|
|
1083
1142
|
console.log(chalk.yellow(" ○ LLM: installed but not active"));
|
|
1084
|
-
console.log(chalk.dim(" → type '1 > 6 > start' to start it, or `vexp setup-llm --
|
|
1143
|
+
console.log(chalk.dim(" → type '1 > 6 > start' to start it, or `vexp setup-llm --enable`"));
|
|
1085
1144
|
}
|
|
1086
1145
|
else {
|
|
1087
1146
|
console.log(chalk.dim(" ○ LLM: not installed"));
|
|
@@ -1098,6 +1157,7 @@ function printMainMenu() {
|
|
|
1098
1157
|
console.log(` ${chalk.cyan.bold("2")} ${chalk.green("Explore & Analyze".padEnd(20))} ${chalk.dim("Query context, skeletons, impact, flow")}`);
|
|
1099
1158
|
console.log(` ${chalk.cyan.bold("3")} ${chalk.green("Token Savings".padEnd(20))} ${chalk.dim("View savings stats, export report")}`);
|
|
1100
1159
|
console.log(` ${chalk.cyan.bold("4")} ${chalk.green("License".padEnd(20))} ${chalk.dim("View or activate your license")}`);
|
|
1160
|
+
console.log(` ${chalk.cyan.bold("5")} ${chalk.green("Repos & Workspace".padEnd(20))} ${chalk.dim("List / connect workspace repositories")}`);
|
|
1101
1161
|
console.log(` ${chalk.cyan.bold("q")} ${chalk.red("Exit".padEnd(20))} ${chalk.dim("Quit")}`);
|
|
1102
1162
|
console.log("");
|
|
1103
1163
|
}
|
|
@@ -1120,11 +1180,13 @@ async function interactiveMode() {
|
|
|
1120
1180
|
// `vexp setup` still get a working state (daemon + MCP :7821 up, OS
|
|
1121
1181
|
// autostart installed for the next reboot). All steps are idempotent so
|
|
1122
1182
|
// running this alongside `vexp setup` never creates duplicates.
|
|
1123
|
-
const
|
|
1124
|
-
if (
|
|
1183
|
+
const eff = findEffectiveWorkspace();
|
|
1184
|
+
if (eff) {
|
|
1125
1185
|
try {
|
|
1126
1186
|
const binaryPath = getBinaryPath();
|
|
1127
|
-
|
|
1187
|
+
// Effective root: in a connected secondary repo this boots the PARENT
|
|
1188
|
+
// daemon (which serves this repo too) instead of a conflicting child one.
|
|
1189
|
+
await ensureBootstrap(eff.root, binaryPath);
|
|
1128
1190
|
}
|
|
1129
1191
|
catch { /* banner will reflect actual state */ }
|
|
1130
1192
|
}
|
|
@@ -1139,6 +1201,7 @@ async function interactiveMode() {
|
|
|
1139
1201
|
explore: chalk.cyan.bold(" vexp/explore> "),
|
|
1140
1202
|
savings: chalk.cyan.bold(" vexp/savings> "),
|
|
1141
1203
|
license: chalk.cyan.bold(" vexp/license> "),
|
|
1204
|
+
repos: chalk.cyan.bold(" vexp/repos> "),
|
|
1142
1205
|
};
|
|
1143
1206
|
while (true) {
|
|
1144
1207
|
const input = (await ask(rl, prompts[currentMenu])).trim().toLowerCase();
|
|
@@ -1174,8 +1237,13 @@ async function interactiveMode() {
|
|
|
1174
1237
|
printSubMenu("License", MENU_LICENSE);
|
|
1175
1238
|
continue;
|
|
1176
1239
|
}
|
|
1240
|
+
if (input === "5" || input === "repos") {
|
|
1241
|
+
currentMenu = "repos";
|
|
1242
|
+
printSubMenu("Repos & Workspace", MENU_REPOS);
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1177
1245
|
// Allow direct command names from any sub-menu
|
|
1178
|
-
const allItems = [...MENU_CONFIG, ...MENU_EXPLORE, ...MENU_SAVINGS, ...MENU_LICENSE];
|
|
1246
|
+
const allItems = [...MENU_CONFIG, ...MENU_EXPLORE, ...MENU_SAVINGS, ...MENU_LICENSE, ...MENU_REPOS];
|
|
1179
1247
|
const direct = allItems.find((m) => m.label === input);
|
|
1180
1248
|
if (direct) {
|
|
1181
1249
|
await executeCommand(direct.label, rl);
|
|
@@ -1183,12 +1251,12 @@ async function interactiveMode() {
|
|
|
1183
1251
|
printMainMenu();
|
|
1184
1252
|
continue;
|
|
1185
1253
|
}
|
|
1186
|
-
console.log(chalk.dim(" Choose 1-
|
|
1254
|
+
console.log(chalk.dim(" Choose 1-5 or type a command name. 'q' to exit.\n"));
|
|
1187
1255
|
continue;
|
|
1188
1256
|
}
|
|
1189
1257
|
// ── Sub-menus ──
|
|
1190
|
-
const menuMap = { config: MENU_CONFIG, explore: MENU_EXPLORE, savings: MENU_SAVINGS, license: MENU_LICENSE };
|
|
1191
|
-
const titles = { config: "Setup & Config", explore: "Explore & Analyze", savings: "Token Savings", license: "License" };
|
|
1258
|
+
const menuMap = { config: MENU_CONFIG, explore: MENU_EXPLORE, savings: MENU_SAVINGS, license: MENU_LICENSE, repos: MENU_REPOS };
|
|
1259
|
+
const titles = { config: "Setup & Config", explore: "Explore & Analyze", savings: "Token Savings", license: "License", repos: "Repos & Workspace" };
|
|
1192
1260
|
if (input === "b" || input === "back") {
|
|
1193
1261
|
currentMenu = "main";
|
|
1194
1262
|
printMainMenu();
|
|
@@ -1225,7 +1293,27 @@ async function executeCommand(label, rl) {
|
|
|
1225
1293
|
}
|
|
1226
1294
|
case "index": {
|
|
1227
1295
|
const binaryPath = ensureBinary();
|
|
1228
|
-
|
|
1296
|
+
// Workspace-aware: re-index every connected repo, not just the cwd
|
|
1297
|
+
// one. From a connected child this targets the parent workspace, so
|
|
1298
|
+
// the result matches what the daemon serves.
|
|
1299
|
+
const eff = findEffectiveWorkspace();
|
|
1300
|
+
const repos = eff ? listWorkspaceRepos(eff.root) : [];
|
|
1301
|
+
if (!eff || repos.length === 0) {
|
|
1302
|
+
runBinary(binaryPath, ["index"]);
|
|
1303
|
+
break;
|
|
1304
|
+
}
|
|
1305
|
+
if (eff.parent) {
|
|
1306
|
+
console.log(chalk.cyan(` This repo is part of workspace "${eff.parent.workspaceName}" — indexing all its repos`));
|
|
1307
|
+
}
|
|
1308
|
+
console.log(chalk.dim(` Indexing ${repos.length} workspace repo(s)…\n`));
|
|
1309
|
+
for (const r of repos) {
|
|
1310
|
+
if (!r.exists) {
|
|
1311
|
+
console.log(chalk.yellow(` ⚠ Skipping "${r.alias}" — directory not found: ${shortPath(r.resolvedPath)}`));
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
console.log(chalk.bold(` ● ${r.alias}`));
|
|
1315
|
+
runBinary(binaryPath, ["index", r.resolvedPath]);
|
|
1316
|
+
}
|
|
1229
1317
|
break;
|
|
1230
1318
|
}
|
|
1231
1319
|
case "setup": {
|
|
@@ -1239,15 +1327,23 @@ async function executeCommand(label, rl) {
|
|
|
1239
1327
|
case "daemon-cmd": {
|
|
1240
1328
|
// Sticky sub-sub-menu: stay here until user types 'b' (back).
|
|
1241
1329
|
const binaryPath = ensureBinary();
|
|
1242
|
-
const valid = new Set(["start", "stop", "status", "logs"]);
|
|
1330
|
+
const valid = new Set(["start", "stop", "restart", "status", "logs"]);
|
|
1331
|
+
// Inside a connected secondary repo every action targets the parent
|
|
1332
|
+
// workspace daemon — running one here would fight over the same DBs.
|
|
1333
|
+
const eff = findEffectiveWorkspace();
|
|
1334
|
+
if (eff?.parent) {
|
|
1335
|
+
console.log(chalk.cyan(` This repo is part of workspace "${eff.parent.workspaceName}" — daemon commands target the parent (${shortPath(eff.root)})`));
|
|
1336
|
+
}
|
|
1337
|
+
const daemonCwd = eff?.parent ? { cwd: eff.root } : undefined;
|
|
1243
1338
|
const printDaemonMenu = () => {
|
|
1244
1339
|
console.log("");
|
|
1245
1340
|
console.log(chalk.cyan(" ── Daemon ──"));
|
|
1246
|
-
console.log(` ${chalk.bold("start")}
|
|
1247
|
-
console.log(` ${chalk.bold("stop")}
|
|
1248
|
-
console.log(` ${chalk.bold("
|
|
1249
|
-
console.log(` ${chalk.bold("
|
|
1250
|
-
console.log(` ${chalk.
|
|
1341
|
+
console.log(` ${chalk.bold("start")} ${chalk.dim("Launch the daemon for this workspace")}`);
|
|
1342
|
+
console.log(` ${chalk.bold("stop")} ${chalk.dim("Stop the running daemon")}`);
|
|
1343
|
+
console.log(` ${chalk.bold("restart")} ${chalk.dim("Stop + start (reloads workspace.json, indexes new repos)")}`);
|
|
1344
|
+
console.log(` ${chalk.bold("status")} ${chalk.dim("Show node/edge/file counts and uptime")}`);
|
|
1345
|
+
console.log(` ${chalk.bold("logs")} ${chalk.dim("Tail .vexp/vexp.log")}`);
|
|
1346
|
+
console.log(` ${chalk.dim("b")} ${chalk.dim("Back to Setup & Config")}`);
|
|
1251
1347
|
console.log("");
|
|
1252
1348
|
};
|
|
1253
1349
|
printDaemonMenu();
|
|
@@ -1258,11 +1354,11 @@ async function executeCommand(label, rl) {
|
|
|
1258
1354
|
if (action === "b" || action === "back")
|
|
1259
1355
|
break;
|
|
1260
1356
|
if (valid.has(action)) {
|
|
1261
|
-
runBinary(binaryPath, ["daemon-cmd", action]);
|
|
1357
|
+
runBinary(binaryPath, ["daemon-cmd", action], daemonCwd);
|
|
1262
1358
|
printDaemonMenu();
|
|
1263
1359
|
continue;
|
|
1264
1360
|
}
|
|
1265
|
-
console.log(chalk.dim(" Unknown action. Use start, stop, status, logs, or 'b' to go back."));
|
|
1361
|
+
console.log(chalk.dim(" Unknown action. Use start, stop, restart, status, logs, or 'b' to go back."));
|
|
1266
1362
|
}
|
|
1267
1363
|
break;
|
|
1268
1364
|
}
|
|
@@ -1383,6 +1479,79 @@ async function executeCommand(label, rl) {
|
|
|
1383
1479
|
}
|
|
1384
1480
|
break;
|
|
1385
1481
|
}
|
|
1482
|
+
// ── Repos & Workspace commands ──
|
|
1483
|
+
case "list": {
|
|
1484
|
+
const eff = findEffectiveWorkspace();
|
|
1485
|
+
if (!eff) {
|
|
1486
|
+
console.log(chalk.yellow(" No configured workspace here — run setup first (1 > 3, or `vexp setup`)."));
|
|
1487
|
+
break;
|
|
1488
|
+
}
|
|
1489
|
+
if (eff.parent) {
|
|
1490
|
+
console.log(chalk.cyan(` This repo is part of workspace "${eff.parent.workspaceName}" — showing the repos of ${shortPath(eff.root)}`));
|
|
1491
|
+
}
|
|
1492
|
+
const repos = listWorkspaceRepos(eff.root);
|
|
1493
|
+
if (repos.length === 0) {
|
|
1494
|
+
console.log(chalk.dim(" Single-repo workspace (no .vexp/workspace.json yet)."));
|
|
1495
|
+
console.log(chalk.dim(" Use 'add' to connect another repository."));
|
|
1496
|
+
break;
|
|
1497
|
+
}
|
|
1498
|
+
console.log("");
|
|
1499
|
+
for (const r of repos) {
|
|
1500
|
+
const marker = r.isPrimary ? chalk.cyan(" (primary)") : "";
|
|
1501
|
+
const missing = r.exists ? "" : chalk.red(" [missing]");
|
|
1502
|
+
console.log(` ${chalk.green("●")} ${chalk.bold(r.alias.padEnd(20))} ${chalk.dim(shortPath(r.resolvedPath))}${marker}${missing}`);
|
|
1503
|
+
}
|
|
1504
|
+
console.log(chalk.dim(`\n ${repos.length} repo(s) in workspace "${path.basename(eff.root)}"`));
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1507
|
+
case "add": {
|
|
1508
|
+
const eff = findEffectiveWorkspace();
|
|
1509
|
+
if (!eff) {
|
|
1510
|
+
console.log(chalk.yellow(" No configured workspace here — run setup first (1 > 3, or `vexp setup`)."));
|
|
1511
|
+
break;
|
|
1512
|
+
}
|
|
1513
|
+
if (eff.parent) {
|
|
1514
|
+
console.log(chalk.cyan(` This repo is part of workspace "${eff.parent.workspaceName}" — the repository will be added to ${shortPath(eff.root)}`));
|
|
1515
|
+
}
|
|
1516
|
+
const repoPath = (await ask(rl, chalk.cyan(" Path of the repository to connect: "))).trim();
|
|
1517
|
+
if (!repoPath)
|
|
1518
|
+
break;
|
|
1519
|
+
const limits = readLicenseLimits();
|
|
1520
|
+
const result = addRepoToWorkspace(eff.root, repoPath, limits.maxRepos);
|
|
1521
|
+
if (result.status === "invalid") {
|
|
1522
|
+
console.log(chalk.red(` ✗ ${result.reason}`));
|
|
1523
|
+
break;
|
|
1524
|
+
}
|
|
1525
|
+
if (result.status === "limit") {
|
|
1526
|
+
console.log(chalk.yellow(` Your ${limits.plan} plan supports up to ${result.maxRepos} repo${result.maxRepos === 1 ? "" : "s"}.`));
|
|
1527
|
+
console.log(chalk.dim(" Upgrade at https://vexp.dev/#pricing to add more repositories."));
|
|
1528
|
+
break;
|
|
1529
|
+
}
|
|
1530
|
+
if (result.status === "exists") {
|
|
1531
|
+
console.log(chalk.dim(` "${result.alias}" is already part of this workspace.`));
|
|
1532
|
+
break;
|
|
1533
|
+
}
|
|
1534
|
+
console.log(chalk.green(` ✓ Added "${result.alias}" to workspace "${path.basename(eff.root)}"`));
|
|
1535
|
+
// A live 2.2.3+ daemon hot-reloads workspace.json (2s poll): the new
|
|
1536
|
+
// repo is indexed and watched automatically, no restart needed. Only
|
|
1537
|
+
// when the daemon is down do we offer to start it.
|
|
1538
|
+
if (await isSocketAlive(socketPathFor(eff.root))) {
|
|
1539
|
+
console.log(chalk.dim(` The running daemon will detect the change and index "${result.alias}" automatically within seconds.`));
|
|
1540
|
+
}
|
|
1541
|
+
else {
|
|
1542
|
+
const yn = (await ask(rl, chalk.cyan(` Start the daemon now to index "${result.alias}"? [Y/n]: `))).trim().toLowerCase();
|
|
1543
|
+
if (yn === "" || yn === "y" || yn === "yes") {
|
|
1544
|
+
const binaryPath = ensureBinary();
|
|
1545
|
+
console.log(chalk.dim(" Starting vexp daemon…\n"));
|
|
1546
|
+
runBinary(binaryPath, ["daemon-cmd", "start"], { cwd: eff.root });
|
|
1547
|
+
console.log(chalk.green(` ✓ Now indexing "${result.alias}".`));
|
|
1548
|
+
}
|
|
1549
|
+
else {
|
|
1550
|
+
console.log(chalk.dim(` Run 'vexp daemon-cmd start' in ${shortPath(eff.root)} later to index it.`));
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
break;
|
|
1554
|
+
}
|
|
1386
1555
|
// ── License commands ──
|
|
1387
1556
|
case "status": {
|
|
1388
1557
|
const limits = readLicenseLimits();
|