code-graph-builder 0.19.0 → 0.21.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/bin/cli.mjs +464 -254
- package/package.json +1 -1
package/bin/cli.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { spawn, execFileSync, execSync } from "node:child_process";
|
|
14
14
|
import { createInterface } from "node:readline";
|
|
15
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync } from "node:fs";
|
|
16
16
|
import { homedir, platform } from "node:os";
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
|
|
@@ -21,6 +21,147 @@ const MODULE_PATH = "code_graph_builder.mcp.server";
|
|
|
21
21
|
const WORKSPACE_DIR = join(homedir(), ".code-graph-builder");
|
|
22
22
|
const ENV_FILE = join(WORKSPACE_DIR, ".env");
|
|
23
23
|
const IS_WIN = platform() === "win32";
|
|
24
|
+
const PYPI_INDEX = "https://pypi.org/simple";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Tree-style UI helpers
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
const T = {
|
|
31
|
+
// Box drawing
|
|
32
|
+
TOP: "╭",
|
|
33
|
+
BOT: "╰",
|
|
34
|
+
SIDE: "│",
|
|
35
|
+
TEE: "├",
|
|
36
|
+
BEND: "╰",
|
|
37
|
+
DASH: "─",
|
|
38
|
+
// Status
|
|
39
|
+
OK: "✓",
|
|
40
|
+
FAIL: "✗",
|
|
41
|
+
WARN: "⚠",
|
|
42
|
+
WORK: "…",
|
|
43
|
+
DOT: "●",
|
|
44
|
+
// Indents
|
|
45
|
+
PIPE: "│ ",
|
|
46
|
+
SPACE: " ",
|
|
47
|
+
BRANCH: "├─ ",
|
|
48
|
+
LAST: "╰─ ",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Interactive single-select menu.
|
|
53
|
+
* Arrow keys to navigate, Space to select, Enter to confirm.
|
|
54
|
+
* Returns the index of the selected option, or -1 if cancelled (Ctrl+C).
|
|
55
|
+
*
|
|
56
|
+
* @param {string[]} options - Display labels for each option
|
|
57
|
+
* @param {string} prefix - Tree prefix for each line (e.g. " │ ")
|
|
58
|
+
* @param {number} defaultIndex - Initially highlighted index
|
|
59
|
+
* @returns {Promise<number>}
|
|
60
|
+
*/
|
|
61
|
+
function selectMenu(options, prefix = " ", defaultIndex = 0) {
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
const out = process.stderr;
|
|
64
|
+
let cursor = defaultIndex;
|
|
65
|
+
let selected = -1;
|
|
66
|
+
|
|
67
|
+
const RADIO_ON = "◉";
|
|
68
|
+
const RADIO_OFF = "○";
|
|
69
|
+
const DIM = "\x1b[2m";
|
|
70
|
+
const BOLD = "\x1b[1m";
|
|
71
|
+
const CYAN = "\x1b[36m";
|
|
72
|
+
const RESET = "\x1b[0m";
|
|
73
|
+
|
|
74
|
+
function render(initial = false) {
|
|
75
|
+
// Move cursor up to overwrite previous render (skip on first draw)
|
|
76
|
+
if (!initial) {
|
|
77
|
+
out.write(`\x1b[${options.length}A`);
|
|
78
|
+
}
|
|
79
|
+
for (let i = 0; i < options.length; i++) {
|
|
80
|
+
const isActive = i === cursor;
|
|
81
|
+
const isSelected = i === selected;
|
|
82
|
+
const radio = (isSelected || (selected === -1 && isActive)) && isActive
|
|
83
|
+
? `${CYAN}${RADIO_ON}${RESET}`
|
|
84
|
+
: `${DIM}${RADIO_OFF}${RESET}`;
|
|
85
|
+
const label = isActive
|
|
86
|
+
? `${BOLD}${CYAN}${options[i]}${RESET}`
|
|
87
|
+
: `${options[i]}`;
|
|
88
|
+
// Clear line then write
|
|
89
|
+
out.write(`\x1b[2K${prefix}${radio} ${label}\n`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Hide cursor
|
|
94
|
+
out.write("\x1b[?25l");
|
|
95
|
+
render(true);
|
|
96
|
+
|
|
97
|
+
const stdin = process.stdin;
|
|
98
|
+
const wasRaw = stdin.isRaw;
|
|
99
|
+
stdin.setRawMode(true);
|
|
100
|
+
stdin.resume();
|
|
101
|
+
|
|
102
|
+
function cleanup() {
|
|
103
|
+
stdin.setRawMode(wasRaw || false);
|
|
104
|
+
stdin.removeListener("data", onKey);
|
|
105
|
+
// Show cursor
|
|
106
|
+
out.write("\x1b[?25h");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function onKey(buf) {
|
|
110
|
+
const key = buf.toString();
|
|
111
|
+
|
|
112
|
+
// Ctrl+C
|
|
113
|
+
if (key === "\x03") {
|
|
114
|
+
cleanup();
|
|
115
|
+
resolve(-1);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Arrow up / k
|
|
120
|
+
if (key === "\x1b[A" || key === "k") {
|
|
121
|
+
cursor = (cursor - 1 + options.length) % options.length;
|
|
122
|
+
render();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Arrow down / j
|
|
127
|
+
if (key === "\x1b[B" || key === "j") {
|
|
128
|
+
cursor = (cursor + 1) % options.length;
|
|
129
|
+
render();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Space — toggle selection
|
|
134
|
+
if (key === " ") {
|
|
135
|
+
selected = cursor;
|
|
136
|
+
render();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Enter — confirm
|
|
141
|
+
if (key === "\r" || key === "\n") {
|
|
142
|
+
if (selected === -1) selected = cursor;
|
|
143
|
+
cleanup();
|
|
144
|
+
resolve(selected);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
stdin.on("data", onKey);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function box(title) {
|
|
154
|
+
const pad = 54;
|
|
155
|
+
const inner = ` ${title} `;
|
|
156
|
+
const fill = pad - inner.length;
|
|
157
|
+
const left = Math.floor(fill / 2);
|
|
158
|
+
const right = fill - left;
|
|
159
|
+
return [
|
|
160
|
+
` ${T.TOP}${"─".repeat(pad)}╮`,
|
|
161
|
+
` ${T.SIDE}${" ".repeat(left)}${inner}${" ".repeat(right)}${T.SIDE}`,
|
|
162
|
+
` ${T.BOT}${"─".repeat(pad)}╯`,
|
|
163
|
+
].join("\n");
|
|
164
|
+
}
|
|
24
165
|
|
|
25
166
|
// ---------------------------------------------------------------------------
|
|
26
167
|
// Utilities
|
|
@@ -28,7 +169,6 @@ const IS_WIN = platform() === "win32";
|
|
|
28
169
|
|
|
29
170
|
function commandExists(cmd) {
|
|
30
171
|
try {
|
|
31
|
-
// "which" on Unix/macOS, "where" on Windows
|
|
32
172
|
const checker = IS_WIN ? "where" : "which";
|
|
33
173
|
execFileSync(checker, [cmd], { stdio: "pipe" });
|
|
34
174
|
return true;
|
|
@@ -37,11 +177,6 @@ function commandExists(cmd) {
|
|
|
37
177
|
}
|
|
38
178
|
}
|
|
39
179
|
|
|
40
|
-
/**
|
|
41
|
-
* Find a working Python command. On Windows the command is typically
|
|
42
|
-
* "python" (the py-launcher or Store stub), while on Unix it is "python3".
|
|
43
|
-
* Returns the command string or null if none is found.
|
|
44
|
-
*/
|
|
45
180
|
function findPython() {
|
|
46
181
|
const candidates = IS_WIN
|
|
47
182
|
? ["python", "python3", "py"]
|
|
@@ -49,14 +184,15 @@ function findPython() {
|
|
|
49
184
|
for (const cmd of candidates) {
|
|
50
185
|
try {
|
|
51
186
|
const ver = execFileSync(cmd, ["--version"], { stdio: "pipe" }).toString().trim();
|
|
52
|
-
|
|
53
|
-
if (ver.includes("3.")) return cmd;
|
|
187
|
+
if (ver.includes("3.")) return { cmd, ver };
|
|
54
188
|
} catch { /* skip */ }
|
|
55
189
|
}
|
|
56
190
|
return null;
|
|
57
191
|
}
|
|
58
192
|
|
|
59
|
-
const
|
|
193
|
+
const pythonInfo = findPython();
|
|
194
|
+
const PYTHON_CMD = pythonInfo?.cmd || null;
|
|
195
|
+
const PYTHON_VER = pythonInfo?.ver || null;
|
|
60
196
|
|
|
61
197
|
function pythonPackageInstalled() {
|
|
62
198
|
if (!PYTHON_CMD) return false;
|
|
@@ -70,6 +206,17 @@ function pythonPackageInstalled() {
|
|
|
70
206
|
}
|
|
71
207
|
}
|
|
72
208
|
|
|
209
|
+
function getPackageVersion() {
|
|
210
|
+
if (!PYTHON_CMD) return null;
|
|
211
|
+
try {
|
|
212
|
+
return execFileSync(PYTHON_CMD, ["-c",
|
|
213
|
+
`import code_graph_builder; print(getattr(code_graph_builder, '__version__', 'unknown'))`
|
|
214
|
+
], { stdio: "pipe" }).toString().trim();
|
|
215
|
+
} catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
73
220
|
function loadEnvFile() {
|
|
74
221
|
if (!existsSync(ENV_FILE)) return {};
|
|
75
222
|
const vars = {};
|
|
@@ -80,7 +227,6 @@ function loadEnvFile() {
|
|
|
80
227
|
if (eq === -1) continue;
|
|
81
228
|
const key = trimmed.slice(0, eq).trim();
|
|
82
229
|
let val = trimmed.slice(eq + 1).trim();
|
|
83
|
-
// Strip surrounding quotes
|
|
84
230
|
if ((val.startsWith('"') && val.endsWith('"')) ||
|
|
85
231
|
(val.startsWith("'") && val.endsWith("'"))) {
|
|
86
232
|
val = val.slice(1, -1);
|
|
@@ -109,154 +255,247 @@ function mask(s) {
|
|
|
109
255
|
return s.slice(0, 4) + "****" + s.slice(-4);
|
|
110
256
|
}
|
|
111
257
|
|
|
258
|
+
function findPip() {
|
|
259
|
+
for (const cmd of IS_WIN ? ["pip", "pip3"] : ["pip3", "pip"]) {
|
|
260
|
+
if (commandExists(cmd)) return [cmd];
|
|
261
|
+
}
|
|
262
|
+
if (PYTHON_CMD) {
|
|
263
|
+
try {
|
|
264
|
+
execFileSync(PYTHON_CMD, ["-m", "pip", "--version"], { stdio: "pipe" });
|
|
265
|
+
return [PYTHON_CMD, "-m", "pip"];
|
|
266
|
+
} catch { /* skip */ }
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Clear npx cache for code-graph-builder to ensure latest version.
|
|
273
|
+
*/
|
|
274
|
+
function clearNpxCache() {
|
|
275
|
+
try {
|
|
276
|
+
const cacheDir = execSync("npm config get cache", { stdio: "pipe", shell: true })
|
|
277
|
+
.toString().trim();
|
|
278
|
+
const npxCacheDir = join(cacheDir, "_npx");
|
|
279
|
+
|
|
280
|
+
if (existsSync(npxCacheDir)) {
|
|
281
|
+
for (const entry of readdirSync(npxCacheDir)) {
|
|
282
|
+
const pkgJsonPath = join(npxCacheDir, entry, "node_modules", "code-graph-builder", "package.json");
|
|
283
|
+
const altPkgJson = join(npxCacheDir, entry, "package.json");
|
|
284
|
+
try {
|
|
285
|
+
let found = false;
|
|
286
|
+
if (existsSync(pkgJsonPath)) {
|
|
287
|
+
found = true;
|
|
288
|
+
} else if (existsSync(altPkgJson)) {
|
|
289
|
+
const content = readFileSync(altPkgJson, "utf-8");
|
|
290
|
+
if (content.includes("code-graph-builder")) found = true;
|
|
291
|
+
}
|
|
292
|
+
if (found) {
|
|
293
|
+
rmSync(join(npxCacheDir, entry), { recursive: true, force: true });
|
|
294
|
+
}
|
|
295
|
+
} catch { /* skip */ }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} catch { /* cache clear is best-effort */ }
|
|
299
|
+
}
|
|
300
|
+
|
|
112
301
|
// ---------------------------------------------------------------------------
|
|
113
|
-
// Interactive setup wizard
|
|
302
|
+
// Interactive setup wizard
|
|
114
303
|
// ---------------------------------------------------------------------------
|
|
115
304
|
|
|
116
305
|
async function runSetup() {
|
|
117
|
-
|
|
306
|
+
let rl = createInterface({
|
|
118
307
|
input: process.stdin,
|
|
119
308
|
output: process.stderr,
|
|
120
309
|
});
|
|
121
310
|
|
|
122
|
-
|
|
123
|
-
const log = (msg) => process.stderr.write(msg + "\n");
|
|
311
|
+
let ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
312
|
+
const log = (msg = "") => process.stderr.write(msg + "\n");
|
|
124
313
|
|
|
125
|
-
log(
|
|
126
|
-
log("
|
|
127
|
-
log(
|
|
128
|
-
|
|
129
|
-
|
|
314
|
+
log();
|
|
315
|
+
log(box("code-graph-builder Setup Wizard"));
|
|
316
|
+
log();
|
|
317
|
+
|
|
318
|
+
// --- Step 0: Clear npx cache ---
|
|
319
|
+
log(` ${T.DOT} Preparing`);
|
|
320
|
+
log(` ${T.SIDE}`);
|
|
321
|
+
log(` ${T.BRANCH} Clearing npx cache...`);
|
|
322
|
+
|
|
323
|
+
await clearNpxCache();
|
|
324
|
+
|
|
325
|
+
log(` ${T.LAST} ${T.OK} Cache cleared`);
|
|
326
|
+
log();
|
|
130
327
|
|
|
131
328
|
// Load existing config
|
|
132
329
|
const existing = loadEnvFile();
|
|
133
330
|
|
|
134
|
-
// --- Workspace ---
|
|
135
|
-
log(
|
|
136
|
-
log(`
|
|
331
|
+
// --- Step 1: Workspace ---
|
|
332
|
+
log(` ${T.DOT} Step 1/3 Workspace`);
|
|
333
|
+
log(` ${T.SIDE}`);
|
|
334
|
+
log(` ${T.BRANCH} Stores indexed repos, graphs, and embeddings`);
|
|
335
|
+
|
|
137
336
|
const workspace =
|
|
138
|
-
(await ask(`
|
|
139
|
-
log("");
|
|
337
|
+
(await ask(` ${T.SIDE} Path [${WORKSPACE_DIR}]: `)).trim() || WORKSPACE_DIR;
|
|
140
338
|
|
|
141
|
-
|
|
142
|
-
log(
|
|
143
|
-
log("");
|
|
144
|
-
log(" Select your LLM provider:");
|
|
145
|
-
log("");
|
|
146
|
-
log(" 1) Moonshot / Kimi https://platform.moonshot.cn");
|
|
147
|
-
log(" 2) OpenAI https://platform.openai.com");
|
|
148
|
-
log(" 3) DeepSeek https://platform.deepseek.com");
|
|
149
|
-
log(" 4) OpenRouter https://openrouter.ai");
|
|
150
|
-
log(" 5) LiteLLM Proxy (OpenAI-compatible gateway)");
|
|
151
|
-
log(" 6) Custom (any OpenAI-compatible endpoint)");
|
|
152
|
-
log(" 7) Skip (configure later)");
|
|
153
|
-
log("");
|
|
339
|
+
log(` ${T.LAST} ${T.OK} ${workspace}`);
|
|
340
|
+
log();
|
|
154
341
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
};
|
|
342
|
+
// --- Step 2: LLM Provider ---
|
|
343
|
+
log(` ${T.DOT} Step 2/3 LLM Provider`);
|
|
344
|
+
log(` ${T.SIDE}`);
|
|
345
|
+
log(` ${T.BRANCH} For natural language queries & descriptions`);
|
|
346
|
+
log(` ${T.SIDE} Use ↑↓ to navigate, Space to select, Enter to confirm`);
|
|
347
|
+
log(` ${T.SIDE}`);
|
|
162
348
|
|
|
163
349
|
if (existing.LLM_API_KEY) {
|
|
164
|
-
log(` Current: ${mask(existing.LLM_API_KEY)} → ${existing.LLM_BASE_URL || "?"}`);
|
|
350
|
+
log(` ${T.SIDE} Current: ${mask(existing.LLM_API_KEY)} → ${existing.LLM_BASE_URL || "?"}`);
|
|
351
|
+
log(` ${T.SIDE}`);
|
|
165
352
|
}
|
|
166
353
|
|
|
167
|
-
const
|
|
354
|
+
const llmOptions = [
|
|
355
|
+
"Moonshot / Kimi platform.moonshot.cn",
|
|
356
|
+
"OpenAI platform.openai.com",
|
|
357
|
+
"DeepSeek platform.deepseek.com",
|
|
358
|
+
"OpenRouter openrouter.ai",
|
|
359
|
+
"LiteLLM Proxy localhost:4000",
|
|
360
|
+
"Custom endpoint",
|
|
361
|
+
"Skip (configure later)",
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
const llmProviders = [
|
|
365
|
+
{ name: "Moonshot", url: "https://api.moonshot.cn/v1", model: "kimi-k2.5" },
|
|
366
|
+
{ name: "OpenAI", url: "https://api.openai.com/v1", model: "gpt-4o" },
|
|
367
|
+
{ name: "DeepSeek", url: "https://api.deepseek.com/v1", model: "deepseek-chat" },
|
|
368
|
+
{ name: "OpenRouter", url: "https://openrouter.ai/api/v1", model: "anthropic/claude-sonnet-4" },
|
|
369
|
+
{ name: "LiteLLM", url: "http://localhost:4000/v1", model: "gpt-4o" },
|
|
370
|
+
];
|
|
371
|
+
|
|
372
|
+
// Close readline before raw mode menu, reopen after
|
|
373
|
+
rl.close();
|
|
374
|
+
const llmChoice = await selectMenu(llmOptions, ` ${T.SIDE} `, 6);
|
|
375
|
+
rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
376
|
+
ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
168
377
|
|
|
169
378
|
let llmKey = existing.LLM_API_KEY || "";
|
|
170
379
|
let llmBaseUrl = existing.LLM_BASE_URL || "";
|
|
171
380
|
let llmModel = existing.LLM_MODEL || "";
|
|
381
|
+
let llmProviderName = "skipped";
|
|
172
382
|
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
llmModel = provider.model;
|
|
180
|
-
} else {
|
|
181
|
-
// Choice "6" or invalid → custom
|
|
182
|
-
log("\n → Custom provider");
|
|
183
|
-
llmBaseUrl = (await ask(" API Base URL: ")).trim() || llmBaseUrl;
|
|
184
|
-
llmModel = (await ask(" Model name: ")).trim() || llmModel || "gpt-4o";
|
|
185
|
-
}
|
|
383
|
+
if (llmChoice >= 0 && llmChoice < 5) {
|
|
384
|
+
// Known provider
|
|
385
|
+
const provider = llmProviders[llmChoice];
|
|
386
|
+
llmBaseUrl = provider.url;
|
|
387
|
+
llmModel = provider.model;
|
|
388
|
+
llmProviderName = provider.name;
|
|
186
389
|
|
|
187
|
-
|
|
390
|
+
log(` ${T.SIDE}`);
|
|
391
|
+
llmKey = (await ask(` ${T.SIDE} API Key (sk-...): `)).trim() || existing.LLM_API_KEY || "";
|
|
188
392
|
|
|
189
393
|
if (llmKey) {
|
|
190
|
-
|
|
191
|
-
const urlOverride = (await ask(` Base URL [${llmBaseUrl}]: `)).trim();
|
|
394
|
+
const urlOverride = (await ask(` ${T.SIDE} Base URL [${llmBaseUrl}]: `)).trim();
|
|
192
395
|
if (urlOverride) llmBaseUrl = urlOverride;
|
|
193
|
-
const modelOverride = (await ask(` Model [${llmModel}]: `)).trim();
|
|
396
|
+
const modelOverride = (await ask(` ${T.SIDE} Model [${llmModel}]: `)).trim();
|
|
194
397
|
if (modelOverride) llmModel = modelOverride;
|
|
195
398
|
}
|
|
399
|
+
} else if (llmChoice === 5) {
|
|
400
|
+
// Custom
|
|
401
|
+
llmProviderName = "Custom";
|
|
402
|
+
const defUrl = llmBaseUrl || existing.LLM_BASE_URL || "";
|
|
403
|
+
const defModel = llmModel || existing.LLM_MODEL || "gpt-4o";
|
|
404
|
+
const defKey = existing.LLM_API_KEY || "";
|
|
405
|
+
log(` ${T.SIDE}`);
|
|
406
|
+
llmBaseUrl = (await ask(` ${T.SIDE} API Base URL${defUrl ? ` [${defUrl}]` : ""}: `)).trim() || defUrl;
|
|
407
|
+
llmModel = (await ask(` ${T.SIDE} Model${defModel ? ` [${defModel}]` : ""}: `)).trim() || defModel;
|
|
408
|
+
llmKey = (await ask(` ${T.SIDE} API Key${defKey ? ` [${mask(defKey)}]` : " (sk-...)"}: `)).trim() || defKey;
|
|
196
409
|
}
|
|
197
|
-
|
|
410
|
+
// llmChoice === 6 or -1 → skip
|
|
198
411
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
log(
|
|
205
|
-
log(" 2) OpenAI Embeddings https://platform.openai.com");
|
|
206
|
-
log(" 3) Custom (any OpenAI-compatible embedding endpoint)");
|
|
207
|
-
log(" 4) Skip (configure later)");
|
|
208
|
-
log("");
|
|
412
|
+
if (llmKey) {
|
|
413
|
+
log(` ${T.LAST} ${T.OK} ${llmProviderName} / ${llmModel}`);
|
|
414
|
+
} else {
|
|
415
|
+
log(` ${T.LAST} ${T.WARN} Skipped (configure later in ${ENV_FILE})`);
|
|
416
|
+
}
|
|
417
|
+
log();
|
|
209
418
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
};
|
|
419
|
+
// --- Step 3: Embedding Provider ---
|
|
420
|
+
log(` ${T.DOT} Step 3/3 Embedding Provider`);
|
|
421
|
+
log(` ${T.SIDE}`);
|
|
422
|
+
log(` ${T.BRANCH} For semantic code search`);
|
|
423
|
+
log(` ${T.SIDE} Use ↑↓ to navigate, Space to select, Enter to confirm`);
|
|
424
|
+
log(` ${T.SIDE}`);
|
|
214
425
|
|
|
215
426
|
if (existing.DASHSCOPE_API_KEY || existing.EMBED_API_KEY) {
|
|
216
427
|
const ek = existing.DASHSCOPE_API_KEY || existing.EMBED_API_KEY;
|
|
217
|
-
log(` Current: ${mask(ek)} → ${existing.DASHSCOPE_BASE_URL || existing.EMBED_BASE_URL || "?"}`);
|
|
428
|
+
log(` ${T.SIDE} Current: ${mask(ek)} → ${existing.DASHSCOPE_BASE_URL || existing.EMBED_BASE_URL || "?"}`);
|
|
429
|
+
log(` ${T.SIDE}`);
|
|
218
430
|
}
|
|
219
431
|
|
|
220
|
-
const
|
|
432
|
+
const embedOptions = [
|
|
433
|
+
"DashScope / Qwen dashscope.console.aliyun.com (free tier)",
|
|
434
|
+
"OpenAI Embeddings platform.openai.com",
|
|
435
|
+
"Custom endpoint",
|
|
436
|
+
"Skip (configure later)",
|
|
437
|
+
];
|
|
438
|
+
|
|
439
|
+
const embedProvidersList = [
|
|
440
|
+
{ name: "DashScope", url: "https://dashscope.aliyuncs.com/api/v1", model: "text-embedding-v4", keyEnv: "DASHSCOPE_API_KEY", urlEnv: "DASHSCOPE_BASE_URL" },
|
|
441
|
+
{ name: "OpenAI", url: "https://api.openai.com/v1", model: "text-embedding-3-small", keyEnv: "OPENAI_API_KEY", urlEnv: "OPENAI_BASE_URL" },
|
|
442
|
+
];
|
|
443
|
+
|
|
444
|
+
rl.close();
|
|
445
|
+
const embedChoice = await selectMenu(embedOptions, ` ${T.SIDE} `, 3);
|
|
446
|
+
rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
447
|
+
ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
221
448
|
|
|
222
449
|
let embedKey = "";
|
|
223
450
|
let embedUrl = "";
|
|
224
451
|
let embedModel = "";
|
|
225
452
|
let embedKeyEnv = "DASHSCOPE_API_KEY";
|
|
226
453
|
let embedUrlEnv = "DASHSCOPE_BASE_URL";
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
embedUrl = (await ask(" Embedding API Base URL: ")).trim();
|
|
241
|
-
embedModel = (await ask(" Embedding model name: ")).trim() || "text-embedding-3-small";
|
|
242
|
-
embedKeyEnv = "EMBED_API_KEY";
|
|
243
|
-
embedUrlEnv = "EMBED_BASE_URL";
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
embedKey = (await ask(` API Key: `)).trim() ||
|
|
454
|
+
let embedProviderName = "skipped";
|
|
455
|
+
|
|
456
|
+
if (embedChoice >= 0 && embedChoice < 2) {
|
|
457
|
+
// Known provider
|
|
458
|
+
const ep = embedProvidersList[embedChoice];
|
|
459
|
+
embedUrl = ep.url;
|
|
460
|
+
embedModel = ep.model;
|
|
461
|
+
embedKeyEnv = ep.keyEnv;
|
|
462
|
+
embedUrlEnv = ep.urlEnv;
|
|
463
|
+
embedProviderName = ep.name;
|
|
464
|
+
|
|
465
|
+
log(` ${T.SIDE}`);
|
|
466
|
+
embedKey = (await ask(` ${T.SIDE} API Key: `)).trim() ||
|
|
247
467
|
existing[embedKeyEnv] || existing.DASHSCOPE_API_KEY || "";
|
|
248
468
|
|
|
249
469
|
if (embedKey) {
|
|
250
|
-
const urlOverride = (await ask(` Base URL [${embedUrl}]: `)).trim();
|
|
470
|
+
const urlOverride = (await ask(` ${T.SIDE} Base URL [${embedUrl}]: `)).trim();
|
|
251
471
|
if (urlOverride) embedUrl = urlOverride;
|
|
252
|
-
const modelOverride = (await ask(` Model [${embedModel}]: `)).trim();
|
|
472
|
+
const modelOverride = (await ask(` ${T.SIDE} Model [${embedModel}]: `)).trim();
|
|
253
473
|
if (modelOverride) embedModel = modelOverride;
|
|
254
474
|
}
|
|
475
|
+
} else if (embedChoice === 2) {
|
|
476
|
+
// Custom
|
|
477
|
+
embedProviderName = "Custom";
|
|
478
|
+
const defEmbedUrl = existing.EMBED_BASE_URL || existing.DASHSCOPE_BASE_URL || "";
|
|
479
|
+
const defEmbedModel = existing.EMBED_MODEL || "text-embedding-3-small";
|
|
480
|
+
const defEmbedKey = existing.EMBED_API_KEY || existing.DASHSCOPE_API_KEY || "";
|
|
481
|
+
log(` ${T.SIDE}`);
|
|
482
|
+
embedUrl = (await ask(` ${T.SIDE} API Base URL${defEmbedUrl ? ` [${defEmbedUrl}]` : ""}: `)).trim() || defEmbedUrl;
|
|
483
|
+
embedModel = (await ask(` ${T.SIDE} Model${defEmbedModel ? ` [${defEmbedModel}]` : ""}: `)).trim() || defEmbedModel;
|
|
484
|
+
embedKey = (await ask(` ${T.SIDE} API Key${defEmbedKey ? ` [${mask(defEmbedKey)}]` : ""}: `)).trim() || defEmbedKey;
|
|
485
|
+
embedKeyEnv = "EMBED_API_KEY";
|
|
486
|
+
embedUrlEnv = "EMBED_BASE_URL";
|
|
487
|
+
}
|
|
488
|
+
// embedChoice === 3 or -1 → skip
|
|
489
|
+
|
|
490
|
+
if (embedKey) {
|
|
491
|
+
log(` ${T.LAST} ${T.OK} ${embedProviderName} / ${embedModel}`);
|
|
492
|
+
} else {
|
|
493
|
+
log(` ${T.LAST} ${T.WARN} Skipped (configure later in ${ENV_FILE})`);
|
|
255
494
|
}
|
|
256
495
|
|
|
257
496
|
rl.close();
|
|
258
497
|
|
|
259
|
-
// --- Save ---
|
|
498
|
+
// --- Save config ---
|
|
260
499
|
const config = {
|
|
261
500
|
CGB_WORKSPACE: workspace,
|
|
262
501
|
LLM_API_KEY: llmKey,
|
|
@@ -264,7 +503,6 @@ async function runSetup() {
|
|
|
264
503
|
LLM_MODEL: llmModel,
|
|
265
504
|
};
|
|
266
505
|
|
|
267
|
-
// Save embedding config with the correct env var names
|
|
268
506
|
if (embedKey) {
|
|
269
507
|
config[embedKeyEnv] = embedKey;
|
|
270
508
|
config[embedUrlEnv] = embedUrl;
|
|
@@ -273,59 +511,65 @@ async function runSetup() {
|
|
|
273
511
|
|
|
274
512
|
saveEnvFile(config);
|
|
275
513
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
514
|
+
log();
|
|
515
|
+
log(` ${T.DOT} Configuration saved`);
|
|
516
|
+
log(` ${T.SIDE}`);
|
|
517
|
+
log(` ${T.BRANCH} File: ${ENV_FILE}`);
|
|
518
|
+
log(` ${T.BRANCH} LLM: ${llmKey ? `${llmProviderName} / ${llmModel}` : "not configured"}`);
|
|
519
|
+
log(` ${T.BRANCH} Embedding: ${embedKey ? `${embedProviderName} / ${embedModel}` : "not configured"}`);
|
|
520
|
+
log(` ${T.LAST} Workspace: ${workspace}`);
|
|
521
|
+
log();
|
|
279
522
|
|
|
280
|
-
|
|
281
|
-
log(
|
|
282
|
-
log(`
|
|
283
|
-
log("");
|
|
284
|
-
log(" LLM: " + (llmKey ? `${mask(llmKey)} → ${llmModel}` : "not configured (optional)"));
|
|
285
|
-
log(" Embedding: " + embedDisplay);
|
|
286
|
-
log(" Workspace: " + workspace);
|
|
287
|
-
log("");
|
|
288
|
-
|
|
289
|
-
// --- Verify installation ---
|
|
290
|
-
log("── Verifying installation ──────────────────────────────────");
|
|
291
|
-
log("");
|
|
523
|
+
// --- Verification ---
|
|
524
|
+
log(` ${T.DOT} Verification`);
|
|
525
|
+
log(` ${T.SIDE}`);
|
|
292
526
|
|
|
293
|
-
//
|
|
527
|
+
// 1. Python
|
|
294
528
|
if (!PYTHON_CMD) {
|
|
295
|
-
log(
|
|
296
|
-
log(
|
|
297
|
-
log(
|
|
298
|
-
rl.close();
|
|
529
|
+
log(` ${T.BRANCH} ${T.FAIL} Python 3 not found`);
|
|
530
|
+
log(` ${T.LAST} Install Python 3.10+ and re-run: npx code-graph-builder@latest --setup`);
|
|
531
|
+
log();
|
|
299
532
|
return;
|
|
300
533
|
}
|
|
301
|
-
log(`
|
|
534
|
+
log(` ${T.BRANCH} ${T.OK} ${PYTHON_VER}`);
|
|
302
535
|
|
|
303
|
-
//
|
|
536
|
+
// 2. Package — auto-install or upgrade
|
|
537
|
+
const pip = findPip();
|
|
304
538
|
if (!pythonPackageInstalled()) {
|
|
305
|
-
log(`
|
|
306
|
-
const pip = findPip();
|
|
539
|
+
log(` ${T.SIDE} ${T.WORK} Installing ${PYTHON_PACKAGE}...`);
|
|
307
540
|
if (pip) {
|
|
308
541
|
try {
|
|
309
542
|
execSync(
|
|
310
|
-
[...pip, "install", PYTHON_PACKAGE].map(s => `"${s}"`).join(" "),
|
|
543
|
+
[...pip, "install", "--upgrade", "-i", PYPI_INDEX, PYTHON_PACKAGE].map(s => `"${s}"`).join(" "),
|
|
311
544
|
{ stdio: "pipe", shell: true }
|
|
312
545
|
);
|
|
313
546
|
} catch { /* handled below */ }
|
|
314
547
|
}
|
|
548
|
+
} else {
|
|
549
|
+
// Already installed — upgrade to latest
|
|
550
|
+
log(` ${T.SIDE} ${T.WORK} Upgrading ${PYTHON_PACKAGE} to latest...`);
|
|
551
|
+
if (pip) {
|
|
552
|
+
try {
|
|
553
|
+
execSync(
|
|
554
|
+
[...pip, "install", "--upgrade", "-i", PYPI_INDEX, PYTHON_PACKAGE].map(s => `"${s}"`).join(" "),
|
|
555
|
+
{ stdio: "pipe", shell: true }
|
|
556
|
+
);
|
|
557
|
+
} catch { /* upgrade is best-effort */ }
|
|
558
|
+
}
|
|
315
559
|
}
|
|
316
560
|
|
|
317
561
|
if (pythonPackageInstalled()) {
|
|
318
|
-
|
|
562
|
+
const ver = getPackageVersion();
|
|
563
|
+
log(` ${T.BRANCH} ${T.OK} ${PYTHON_PACKAGE} ${ver || ""}`);
|
|
319
564
|
} else {
|
|
320
|
-
log(`
|
|
321
|
-
log(`
|
|
322
|
-
log(
|
|
323
|
-
rl.close();
|
|
565
|
+
log(` ${T.BRANCH} ${T.FAIL} Package not installed`);
|
|
566
|
+
log(` ${T.LAST} Run manually: pip install -i ${PYPI_INDEX} ${PYTHON_PACKAGE}`);
|
|
567
|
+
log();
|
|
324
568
|
return;
|
|
325
569
|
}
|
|
326
570
|
|
|
327
|
-
//
|
|
328
|
-
log(
|
|
571
|
+
// 3. MCP server smoke test
|
|
572
|
+
log(` ${T.SIDE} ${T.WORK} MCP server smoke test...`);
|
|
329
573
|
|
|
330
574
|
const verified = await new Promise((resolve) => {
|
|
331
575
|
const envVars = loadEnvFile();
|
|
@@ -348,14 +592,12 @@ async function runSetup() {
|
|
|
348
592
|
resolve({ success, detail });
|
|
349
593
|
};
|
|
350
594
|
|
|
351
|
-
// Timeout after 15s
|
|
352
595
|
const timer = setTimeout(() => finish(false, "Server did not respond within 15s"), 15000);
|
|
353
596
|
|
|
354
|
-
child.stderr.on("data", () => {});
|
|
597
|
+
child.stderr.on("data", () => {});
|
|
355
598
|
|
|
356
599
|
child.stdout.on("data", (chunk) => {
|
|
357
600
|
stdout += chunk.toString();
|
|
358
|
-
// MCP stdio uses JSON lines (one JSON-RPC message per line)
|
|
359
601
|
const lines = stdout.split("\n");
|
|
360
602
|
for (const line of lines) {
|
|
361
603
|
const trimmed = line.trim();
|
|
@@ -363,7 +605,6 @@ async function runSetup() {
|
|
|
363
605
|
try {
|
|
364
606
|
const msg = JSON.parse(trimmed);
|
|
365
607
|
if (msg.result && msg.result.capabilities) {
|
|
366
|
-
// Got initialize response, now request tools/list
|
|
367
608
|
const toolsReq = JSON.stringify({
|
|
368
609
|
jsonrpc: "2.0", id: 2, method: "tools/list", params: {},
|
|
369
610
|
});
|
|
@@ -373,10 +614,10 @@ async function runSetup() {
|
|
|
373
614
|
}
|
|
374
615
|
if (msg.result && msg.result.tools) {
|
|
375
616
|
clearTimeout(timer);
|
|
376
|
-
finish(true, `${msg.result.tools.length} tools
|
|
617
|
+
finish(true, `${msg.result.tools.length} tools`);
|
|
377
618
|
return;
|
|
378
619
|
}
|
|
379
|
-
} catch { /* partial JSON
|
|
620
|
+
} catch { /* partial JSON */ }
|
|
380
621
|
}
|
|
381
622
|
});
|
|
382
623
|
|
|
@@ -390,7 +631,6 @@ async function runSetup() {
|
|
|
390
631
|
if (!resolved) finish(false, `Server exited with code ${code}`);
|
|
391
632
|
});
|
|
392
633
|
|
|
393
|
-
// Send MCP initialize request as JSON line
|
|
394
634
|
const initReq = JSON.stringify({
|
|
395
635
|
jsonrpc: "2.0",
|
|
396
636
|
id: 1,
|
|
@@ -405,55 +645,51 @@ async function runSetup() {
|
|
|
405
645
|
});
|
|
406
646
|
|
|
407
647
|
if (verified.success) {
|
|
408
|
-
log(`
|
|
648
|
+
log(` ${T.BRANCH} ${T.OK} MCP server (${verified.detail})`);
|
|
409
649
|
} else {
|
|
410
|
-
log(`
|
|
411
|
-
log(" The server may still work — try: npx code-graph-builder --server");
|
|
650
|
+
log(` ${T.BRANCH} ${T.FAIL} MCP smoke test: ${verified.detail}`);
|
|
412
651
|
}
|
|
413
652
|
|
|
414
|
-
//
|
|
415
|
-
log("");
|
|
416
|
-
log("── Registering MCP server ─────────────────────────────────");
|
|
417
|
-
log("");
|
|
418
|
-
|
|
653
|
+
// 4. Claude Code registration
|
|
419
654
|
if (commandExists("claude")) {
|
|
420
655
|
try {
|
|
421
|
-
// Remove existing entry first (ignore errors if not found)
|
|
422
656
|
try {
|
|
423
657
|
execSync("claude mcp remove code-graph-builder", { stdio: "pipe", shell: true });
|
|
424
|
-
} catch { /* not found
|
|
658
|
+
} catch { /* not found */ }
|
|
425
659
|
|
|
426
660
|
const addCmd = IS_WIN
|
|
427
661
|
? 'claude mcp add --scope user --transport stdio code-graph-builder -- cmd /c npx -y code-graph-builder@latest --server'
|
|
428
662
|
: 'claude mcp add --scope user --transport stdio code-graph-builder -- npx -y code-graph-builder@latest --server';
|
|
429
663
|
|
|
430
664
|
execSync(addCmd, { stdio: "pipe", shell: true });
|
|
431
|
-
log(
|
|
432
|
-
} catch
|
|
433
|
-
log(
|
|
434
|
-
log(
|
|
665
|
+
log(` ${T.LAST} ${T.OK} Claude Code MCP registered (global)`);
|
|
666
|
+
} catch {
|
|
667
|
+
log(` ${T.LAST} ${T.WARN} Claude Code auto-register failed`);
|
|
668
|
+
log(` Run manually:`);
|
|
435
669
|
if (IS_WIN) {
|
|
436
|
-
log(
|
|
670
|
+
log(` claude mcp add --scope user --transport stdio code-graph-builder -- cmd /c npx -y code-graph-builder@latest --server`);
|
|
437
671
|
} else {
|
|
438
|
-
log(
|
|
672
|
+
log(` claude mcp add --scope user --transport stdio code-graph-builder -- npx -y code-graph-builder@latest --server`);
|
|
439
673
|
}
|
|
440
674
|
}
|
|
441
675
|
} else {
|
|
442
|
-
log(
|
|
443
|
-
log(
|
|
444
|
-
log(
|
|
445
|
-
log(
|
|
446
|
-
log(
|
|
447
|
-
log(
|
|
448
|
-
log(
|
|
449
|
-
log("
|
|
450
|
-
log("
|
|
451
|
-
log(
|
|
676
|
+
log(` ${T.LAST} ${T.WARN} Claude Code CLI not found`);
|
|
677
|
+
log();
|
|
678
|
+
log(` Add to your MCP client config manually:`);
|
|
679
|
+
log();
|
|
680
|
+
log(` {`);
|
|
681
|
+
log(` "mcpServers": {`);
|
|
682
|
+
log(` "code-graph-builder": {`);
|
|
683
|
+
log(` "command": "npx",`);
|
|
684
|
+
log(` "args": ["-y", "code-graph-builder@latest", "--server"]`);
|
|
685
|
+
log(` }`);
|
|
686
|
+
log(` }`);
|
|
687
|
+
log(` }`);
|
|
452
688
|
}
|
|
453
689
|
|
|
454
|
-
log(
|
|
455
|
-
log(
|
|
456
|
-
log(
|
|
690
|
+
log();
|
|
691
|
+
log(` ${T.DOT} Setup complete`);
|
|
692
|
+
log();
|
|
457
693
|
}
|
|
458
694
|
|
|
459
695
|
// ---------------------------------------------------------------------------
|
|
@@ -461,11 +697,9 @@ async function runSetup() {
|
|
|
461
697
|
// ---------------------------------------------------------------------------
|
|
462
698
|
|
|
463
699
|
function runServer(cmd, args) {
|
|
464
|
-
// Merge .env file into environment
|
|
465
700
|
const envVars = loadEnvFile();
|
|
466
701
|
const mergedEnv = { ...process.env, ...envVars };
|
|
467
702
|
|
|
468
|
-
// Ensure CGB_WORKSPACE is set
|
|
469
703
|
if (!mergedEnv.CGB_WORKSPACE) {
|
|
470
704
|
mergedEnv.CGB_WORKSPACE = WORKSPACE_DIR;
|
|
471
705
|
}
|
|
@@ -473,7 +707,7 @@ function runServer(cmd, args) {
|
|
|
473
707
|
const child = spawn(cmd, args, {
|
|
474
708
|
stdio: "inherit",
|
|
475
709
|
env: mergedEnv,
|
|
476
|
-
shell: IS_WIN,
|
|
710
|
+
shell: IS_WIN,
|
|
477
711
|
});
|
|
478
712
|
|
|
479
713
|
child.on("error", (err) => {
|
|
@@ -486,28 +720,6 @@ function runServer(cmd, args) {
|
|
|
486
720
|
});
|
|
487
721
|
}
|
|
488
722
|
|
|
489
|
-
/**
|
|
490
|
-
* Find a working pip command. Returns [cmd, ...prefixArgs] or null.
|
|
491
|
-
* Tries: pip3, pip, python3 -m pip, python -m pip
|
|
492
|
-
*/
|
|
493
|
-
function findPip() {
|
|
494
|
-
// Standalone pip
|
|
495
|
-
for (const cmd of IS_WIN ? ["pip", "pip3"] : ["pip3", "pip"]) {
|
|
496
|
-
if (commandExists(cmd)) return [cmd];
|
|
497
|
-
}
|
|
498
|
-
// python -m pip fallback
|
|
499
|
-
if (PYTHON_CMD) {
|
|
500
|
-
try {
|
|
501
|
-
execFileSync(PYTHON_CMD, ["-m", "pip", "--version"], { stdio: "pipe" });
|
|
502
|
-
return [PYTHON_CMD, "-m", "pip"];
|
|
503
|
-
} catch { /* skip */ }
|
|
504
|
-
}
|
|
505
|
-
return null;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Auto-install the Python package via pip, then start the server.
|
|
510
|
-
*/
|
|
511
723
|
function autoInstallAndStart(extraArgs) {
|
|
512
724
|
const pip = findPip();
|
|
513
725
|
if (!pip) {
|
|
@@ -526,22 +738,21 @@ function autoInstallAndStart(extraArgs) {
|
|
|
526
738
|
|
|
527
739
|
try {
|
|
528
740
|
execSync(
|
|
529
|
-
[...pip, "install", PYTHON_PACKAGE].map(s => `"${s}"`).join(" "),
|
|
741
|
+
[...pip, "install", "-i", PYPI_INDEX, PYTHON_PACKAGE].map(s => `"${s}"`).join(" "),
|
|
530
742
|
{ stdio: "inherit", shell: true }
|
|
531
743
|
);
|
|
532
744
|
} catch (err) {
|
|
533
745
|
process.stderr.write(
|
|
534
746
|
`\nFailed to install ${PYTHON_PACKAGE}.\n` +
|
|
535
|
-
`Try manually: ${pip.join(" ")} install ${PYTHON_PACKAGE}\n`
|
|
747
|
+
`Try manually: ${pip.join(" ")} install -i ${PYPI_INDEX} ${PYTHON_PACKAGE}\n`
|
|
536
748
|
);
|
|
537
749
|
process.exit(1);
|
|
538
750
|
}
|
|
539
751
|
|
|
540
|
-
// Verify installation succeeded
|
|
541
752
|
if (!pythonPackageInstalled()) {
|
|
542
753
|
process.stderr.write(
|
|
543
754
|
`\nInstallation completed but package not importable.\n` +
|
|
544
|
-
`Try manually: ${pip.join(" ")} install ${PYTHON_PACKAGE}\n`
|
|
755
|
+
`Try manually: ${pip.join(" ")} install -i ${PYPI_INDEX} ${PYTHON_PACKAGE}\n`
|
|
545
756
|
);
|
|
546
757
|
process.exit(1);
|
|
547
758
|
}
|
|
@@ -551,42 +762,39 @@ function autoInstallAndStart(extraArgs) {
|
|
|
551
762
|
}
|
|
552
763
|
|
|
553
764
|
// ---------------------------------------------------------------------------
|
|
554
|
-
// Uninstall
|
|
765
|
+
// Uninstall
|
|
555
766
|
// ---------------------------------------------------------------------------
|
|
556
767
|
|
|
557
768
|
async function runUninstall() {
|
|
558
769
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
559
770
|
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
560
|
-
const log = (msg) => process.stderr.write(msg + "\n");
|
|
771
|
+
const log = (msg = "") => process.stderr.write(msg + "\n");
|
|
561
772
|
|
|
562
|
-
log(
|
|
563
|
-
log("
|
|
564
|
-
log(
|
|
565
|
-
log("╚══════════════════════════════════════════════════════════╝");
|
|
566
|
-
log("");
|
|
773
|
+
log();
|
|
774
|
+
log(box("code-graph-builder Uninstall"));
|
|
775
|
+
log();
|
|
567
776
|
|
|
568
|
-
// 1. Show what will be removed
|
|
569
777
|
const pip = findPip();
|
|
570
778
|
const hasPythonPkg = pythonPackageInstalled();
|
|
571
779
|
const hasWorkspace = existsSync(WORKSPACE_DIR);
|
|
572
780
|
const hasEnv = existsSync(ENV_FILE);
|
|
573
781
|
|
|
574
|
-
// Check if code-graph-builder is registered in Claude Code
|
|
575
782
|
let hasClaudeConfig = false;
|
|
576
783
|
try {
|
|
577
784
|
const mcpList = execFileSync("claude", ["mcp", "list"], { stdio: "pipe" }).toString();
|
|
578
785
|
hasClaudeConfig = mcpList.includes("code-graph-builder");
|
|
579
786
|
} catch { /* claude CLI not available */ }
|
|
580
787
|
|
|
581
|
-
log(
|
|
582
|
-
log(
|
|
583
|
-
if (hasPythonPkg)
|
|
584
|
-
else
|
|
585
|
-
if (hasWorkspace)
|
|
586
|
-
else
|
|
587
|
-
if (hasEnv)
|
|
588
|
-
if (hasClaudeConfig) log(
|
|
589
|
-
log(
|
|
788
|
+
log(` ${T.DOT} Components detected`);
|
|
789
|
+
log(` ${T.SIDE}`);
|
|
790
|
+
if (hasPythonPkg) log(` ${T.BRANCH} Python package: code-graph-builder`);
|
|
791
|
+
else log(` ${T.BRANCH} Python package: (not installed)`);
|
|
792
|
+
if (hasWorkspace) log(` ${T.BRANCH} Workspace data: ${WORKSPACE_DIR}`);
|
|
793
|
+
else log(` ${T.BRANCH} Workspace data: (not found)`);
|
|
794
|
+
if (hasEnv) log(` ${T.BRANCH} Config file: ${ENV_FILE}`);
|
|
795
|
+
if (hasClaudeConfig) log(` ${T.BRANCH} Claude Code MCP: registered`);
|
|
796
|
+
log(` ${T.LAST}`);
|
|
797
|
+
log();
|
|
590
798
|
|
|
591
799
|
const answer = (await ask(" Proceed with uninstall? [y/N]: ")).trim().toLowerCase();
|
|
592
800
|
rl.close();
|
|
@@ -596,50 +804,54 @@ async function runUninstall() {
|
|
|
596
804
|
process.exit(0);
|
|
597
805
|
}
|
|
598
806
|
|
|
599
|
-
log(
|
|
807
|
+
log();
|
|
808
|
+
log(` ${T.DOT} Removing`);
|
|
809
|
+
log(` ${T.SIDE}`);
|
|
600
810
|
|
|
601
|
-
//
|
|
811
|
+
// Claude Code MCP entry
|
|
602
812
|
if (hasClaudeConfig) {
|
|
603
813
|
try {
|
|
604
814
|
execSync("claude mcp remove code-graph-builder", { stdio: "pipe", shell: true });
|
|
605
|
-
log(
|
|
815
|
+
log(` ${T.BRANCH} ${T.OK} Claude Code MCP entry`);
|
|
606
816
|
} catch {
|
|
607
|
-
log(
|
|
817
|
+
log(` ${T.BRANCH} ${T.WARN} Claude Code MCP entry (manual removal needed)`);
|
|
608
818
|
}
|
|
609
819
|
}
|
|
610
820
|
|
|
611
|
-
//
|
|
821
|
+
// Python package
|
|
612
822
|
if (hasPythonPkg && pip) {
|
|
613
823
|
try {
|
|
614
824
|
execSync(
|
|
615
825
|
[...pip, "uninstall", "-y", PYTHON_PACKAGE].map(s => `"${s}"`).join(" "),
|
|
616
|
-
{ stdio: "
|
|
826
|
+
{ stdio: "pipe", shell: true }
|
|
617
827
|
);
|
|
618
|
-
log(
|
|
828
|
+
log(` ${T.BRANCH} ${T.OK} Python package`);
|
|
619
829
|
} catch {
|
|
620
|
-
log(
|
|
830
|
+
log(` ${T.BRANCH} ${T.WARN} Python package (try: pip uninstall code-graph-builder)`);
|
|
621
831
|
}
|
|
622
832
|
}
|
|
623
833
|
|
|
624
|
-
//
|
|
834
|
+
// Workspace data
|
|
625
835
|
if (hasWorkspace) {
|
|
626
|
-
const { rmSync } = await import("node:fs");
|
|
627
836
|
try {
|
|
628
837
|
rmSync(WORKSPACE_DIR, { recursive: true, force: true });
|
|
629
|
-
log(`
|
|
838
|
+
log(` ${T.BRANCH} ${T.OK} Workspace data`);
|
|
630
839
|
} catch (err) {
|
|
631
|
-
log(`
|
|
840
|
+
log(` ${T.BRANCH} ${T.WARN} Workspace: ${err.message}`);
|
|
632
841
|
}
|
|
633
842
|
}
|
|
634
843
|
|
|
635
|
-
|
|
636
|
-
log(
|
|
637
|
-
|
|
638
|
-
log(
|
|
844
|
+
// npx cache
|
|
845
|
+
log(` ${T.SIDE} ${T.WORK} Clearing npx cache...`);
|
|
846
|
+
await clearNpxCache();
|
|
847
|
+
log(` ${T.LAST} ${T.OK} npx cache`);
|
|
848
|
+
|
|
849
|
+
log();
|
|
850
|
+
log(` ${T.DOT} Uninstall complete`);
|
|
851
|
+
log();
|
|
639
852
|
}
|
|
640
853
|
|
|
641
854
|
function startServer(extraArgs = []) {
|
|
642
|
-
// Prefer pip-installed package first (most reliable, includes all deps)
|
|
643
855
|
if (pythonPackageInstalled()) {
|
|
644
856
|
runServer(PYTHON_CMD, ["-m", MODULE_PATH]);
|
|
645
857
|
} else if (commandExists("uvx")) {
|
|
@@ -649,7 +861,6 @@ function startServer(extraArgs = []) {
|
|
|
649
861
|
} else if (commandExists("pipx")) {
|
|
650
862
|
runServer("pipx", ["run", PYTHON_PACKAGE, ...extraArgs]);
|
|
651
863
|
} else {
|
|
652
|
-
// Auto-install via pip
|
|
653
864
|
autoInstallAndStart(extraArgs);
|
|
654
865
|
}
|
|
655
866
|
}
|
|
@@ -662,15 +873,13 @@ const args = process.argv.slice(2);
|
|
|
662
873
|
const mode = args[0];
|
|
663
874
|
|
|
664
875
|
if (mode === "--setup") {
|
|
665
|
-
// Explicit setup request
|
|
666
876
|
runSetup();
|
|
667
877
|
} else if (mode === "--server" || mode === "--pip" || mode === "--python") {
|
|
668
|
-
// Start MCP server directly
|
|
669
878
|
if (mode === "--pip" || mode === "--python") {
|
|
670
879
|
if (!PYTHON_CMD || !pythonPackageInstalled()) {
|
|
671
880
|
process.stderr.write(
|
|
672
881
|
`Error: Python package '${PYTHON_PACKAGE}' is not installed.\n` +
|
|
673
|
-
`Run: pip install ${PYTHON_PACKAGE}\n`
|
|
882
|
+
`Run: pip install -i ${PYPI_INDEX} ${PYTHON_PACKAGE}\n`
|
|
674
883
|
);
|
|
675
884
|
process.exit(1);
|
|
676
885
|
}
|
|
@@ -681,23 +890,24 @@ if (mode === "--setup") {
|
|
|
681
890
|
} else if (mode === "--uninstall") {
|
|
682
891
|
runUninstall();
|
|
683
892
|
} else if (mode === "--help" || mode === "-h") {
|
|
684
|
-
process.stderr.write(
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
);
|
|
893
|
+
const log = (msg) => process.stderr.write(msg + "\n");
|
|
894
|
+
log("");
|
|
895
|
+
log(box("code-graph-builder"));
|
|
896
|
+
log("");
|
|
897
|
+
log(" Usage:");
|
|
898
|
+
log("");
|
|
899
|
+
log(" npx code-graph-builder Interactive setup wizard");
|
|
900
|
+
log(" npx code-graph-builder --server Start MCP server");
|
|
901
|
+
log(" npx code-graph-builder --setup Re-run setup wizard");
|
|
902
|
+
log(" npx code-graph-builder --uninstall Completely uninstall");
|
|
903
|
+
log(" npx code-graph-builder --help Show this help");
|
|
904
|
+
log("");
|
|
905
|
+
log(` Config: ${ENV_FILE}`);
|
|
906
|
+
log("");
|
|
694
907
|
} else {
|
|
695
|
-
// No args: auto-detect
|
|
696
908
|
if (!existsSync(ENV_FILE)) {
|
|
697
|
-
// First run → setup wizard
|
|
698
909
|
runSetup();
|
|
699
910
|
} else {
|
|
700
|
-
// Config exists → start server
|
|
701
911
|
startServer(args);
|
|
702
912
|
}
|
|
703
913
|
}
|