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