code-graph-builder 0.21.0 → 0.23.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 +176 -131
- package/package.json +1 -1
package/bin/cli.mjs
CHANGED
|
@@ -51,14 +51,16 @@ const T = {
|
|
|
51
51
|
/**
|
|
52
52
|
* Interactive single-select menu.
|
|
53
53
|
* Arrow keys to navigate, Space to select, Enter to confirm.
|
|
54
|
-
* Returns the index of the selected option,
|
|
54
|
+
* Returns the index of the selected option, -1 if cancelled (Ctrl+C),
|
|
55
|
+
* or -2 if the user pressed ← (back to previous step).
|
|
55
56
|
*
|
|
56
57
|
* @param {string[]} options - Display labels for each option
|
|
57
58
|
* @param {string} prefix - Tree prefix for each line (e.g. " │ ")
|
|
58
59
|
* @param {number} defaultIndex - Initially highlighted index
|
|
60
|
+
* @param {boolean} allowBack - Whether ← arrow triggers back (-2)
|
|
59
61
|
* @returns {Promise<number>}
|
|
60
62
|
*/
|
|
61
|
-
function selectMenu(options, prefix = " ", defaultIndex = 0) {
|
|
63
|
+
function selectMenu(options, prefix = " ", defaultIndex = 0, allowBack = false) {
|
|
62
64
|
return new Promise((resolve) => {
|
|
63
65
|
const out = process.stderr;
|
|
64
66
|
let cursor = defaultIndex;
|
|
@@ -71,10 +73,12 @@ function selectMenu(options, prefix = " ", defaultIndex = 0) {
|
|
|
71
73
|
const CYAN = "\x1b[36m";
|
|
72
74
|
const RESET = "\x1b[0m";
|
|
73
75
|
|
|
76
|
+
const backHint = allowBack ? `${DIM} ← back${RESET}` : "";
|
|
77
|
+
|
|
74
78
|
function render(initial = false) {
|
|
75
79
|
// Move cursor up to overwrite previous render (skip on first draw)
|
|
76
80
|
if (!initial) {
|
|
77
|
-
out.write(`\x1b[${options.length}A`);
|
|
81
|
+
out.write(`\x1b[${options.length + (allowBack ? 1 : 0)}A`);
|
|
78
82
|
}
|
|
79
83
|
for (let i = 0; i < options.length; i++) {
|
|
80
84
|
const isActive = i === cursor;
|
|
@@ -88,6 +92,9 @@ function selectMenu(options, prefix = " ", defaultIndex = 0) {
|
|
|
88
92
|
// Clear line then write
|
|
89
93
|
out.write(`\x1b[2K${prefix}${radio} ${label}\n`);
|
|
90
94
|
}
|
|
95
|
+
if (allowBack) {
|
|
96
|
+
out.write(`\x1b[2K${prefix}${DIM}← Back to previous step${RESET}\n`);
|
|
97
|
+
}
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
// Hide cursor
|
|
@@ -116,6 +123,13 @@ function selectMenu(options, prefix = " ", defaultIndex = 0) {
|
|
|
116
123
|
return;
|
|
117
124
|
}
|
|
118
125
|
|
|
126
|
+
// Arrow left — back to previous step
|
|
127
|
+
if (key === "\x1b[D" && allowBack) {
|
|
128
|
+
cleanup();
|
|
129
|
+
resolve(-2);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
119
133
|
// Arrow up / k
|
|
120
134
|
if (key === "\x1b[A" || key === "k") {
|
|
121
135
|
cursor = (cursor - 1 + options.length) % options.length;
|
|
@@ -328,28 +342,18 @@ async function runSetup() {
|
|
|
328
342
|
// Load existing config
|
|
329
343
|
const existing = loadEnvFile();
|
|
330
344
|
|
|
331
|
-
//
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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}`);
|
|
348
|
-
|
|
349
|
-
if (existing.LLM_API_KEY) {
|
|
350
|
-
log(` ${T.SIDE} Current: ${mask(existing.LLM_API_KEY)} → ${existing.LLM_BASE_URL || "?"}`);
|
|
351
|
-
log(` ${T.SIDE}`);
|
|
352
|
-
}
|
|
345
|
+
// Step results — preserved across back/forward navigation
|
|
346
|
+
let workspace = existing.CGB_WORKSPACE || WORKSPACE_DIR;
|
|
347
|
+
let llmKey = existing.LLM_API_KEY || "";
|
|
348
|
+
let llmBaseUrl = existing.LLM_BASE_URL || "";
|
|
349
|
+
let llmModel = existing.LLM_MODEL || "";
|
|
350
|
+
let llmProviderName = "skipped";
|
|
351
|
+
let embedKey = "";
|
|
352
|
+
let embedUrl = "";
|
|
353
|
+
let embedModel = "";
|
|
354
|
+
let embedKeyEnv = "DASHSCOPE_API_KEY";
|
|
355
|
+
let embedUrlEnv = "DASHSCOPE_BASE_URL";
|
|
356
|
+
let embedProviderName = "skipped";
|
|
353
357
|
|
|
354
358
|
const llmOptions = [
|
|
355
359
|
"Moonshot / Kimi platform.moonshot.cn",
|
|
@@ -369,66 +373,6 @@ async function runSetup() {
|
|
|
369
373
|
{ name: "LiteLLM", url: "http://localhost:4000/v1", model: "gpt-4o" },
|
|
370
374
|
];
|
|
371
375
|
|
|
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));
|
|
377
|
-
|
|
378
|
-
let llmKey = existing.LLM_API_KEY || "";
|
|
379
|
-
let llmBaseUrl = existing.LLM_BASE_URL || "";
|
|
380
|
-
let llmModel = existing.LLM_MODEL || "";
|
|
381
|
-
let llmProviderName = "skipped";
|
|
382
|
-
|
|
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;
|
|
389
|
-
|
|
390
|
-
log(` ${T.SIDE}`);
|
|
391
|
-
llmKey = (await ask(` ${T.SIDE} API Key (sk-...): `)).trim() || existing.LLM_API_KEY || "";
|
|
392
|
-
|
|
393
|
-
if (llmKey) {
|
|
394
|
-
const urlOverride = (await ask(` ${T.SIDE} Base URL [${llmBaseUrl}]: `)).trim();
|
|
395
|
-
if (urlOverride) llmBaseUrl = urlOverride;
|
|
396
|
-
const modelOverride = (await ask(` ${T.SIDE} Model [${llmModel}]: `)).trim();
|
|
397
|
-
if (modelOverride) llmModel = modelOverride;
|
|
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;
|
|
409
|
-
}
|
|
410
|
-
// llmChoice === 6 or -1 → skip
|
|
411
|
-
|
|
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();
|
|
418
|
-
|
|
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}`);
|
|
425
|
-
|
|
426
|
-
if (existing.DASHSCOPE_API_KEY || existing.EMBED_API_KEY) {
|
|
427
|
-
const ek = existing.DASHSCOPE_API_KEY || existing.EMBED_API_KEY;
|
|
428
|
-
log(` ${T.SIDE} Current: ${mask(ek)} → ${existing.DASHSCOPE_BASE_URL || existing.EMBED_BASE_URL || "?"}`);
|
|
429
|
-
log(` ${T.SIDE}`);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
376
|
const embedOptions = [
|
|
433
377
|
"DashScope / Qwen dashscope.console.aliyun.com (free tier)",
|
|
434
378
|
"OpenAI Embeddings platform.openai.com",
|
|
@@ -441,56 +385,157 @@ async function runSetup() {
|
|
|
441
385
|
{ name: "OpenAI", url: "https://api.openai.com/v1", model: "text-embedding-3-small", keyEnv: "OPENAI_API_KEY", urlEnv: "OPENAI_BASE_URL" },
|
|
442
386
|
];
|
|
443
387
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
447
|
-
ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
388
|
+
// --- Step-based wizard with ← back support ---
|
|
389
|
+
let step = 1;
|
|
448
390
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
391
|
+
while (step >= 1 && step <= 3) {
|
|
392
|
+
|
|
393
|
+
// ─── Step 1: Workspace ───
|
|
394
|
+
if (step === 1) {
|
|
395
|
+
log(` ${T.DOT} Step 1/3 Workspace`);
|
|
396
|
+
log(` ${T.SIDE}`);
|
|
397
|
+
log(` ${T.BRANCH} Stores indexed repos, graphs, and embeddings`);
|
|
455
398
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
embedProviderName = ep.name;
|
|
464
|
-
|
|
465
|
-
log(` ${T.SIDE}`);
|
|
466
|
-
embedKey = (await ask(` ${T.SIDE} API Key: `)).trim() ||
|
|
467
|
-
existing[embedKeyEnv] || existing.DASHSCOPE_API_KEY || "";
|
|
468
|
-
|
|
469
|
-
if (embedKey) {
|
|
470
|
-
const urlOverride = (await ask(` ${T.SIDE} Base URL [${embedUrl}]: `)).trim();
|
|
471
|
-
if (urlOverride) embedUrl = urlOverride;
|
|
472
|
-
const modelOverride = (await ask(` ${T.SIDE} Model [${embedModel}]: `)).trim();
|
|
473
|
-
if (modelOverride) embedModel = modelOverride;
|
|
399
|
+
workspace =
|
|
400
|
+
(await ask(` ${T.SIDE} Path [${WORKSPACE_DIR}]: `)).trim() || WORKSPACE_DIR;
|
|
401
|
+
|
|
402
|
+
log(` ${T.LAST} ${T.OK} ${workspace}`);
|
|
403
|
+
log();
|
|
404
|
+
step = 2;
|
|
405
|
+
continue;
|
|
474
406
|
}
|
|
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
407
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
493
|
-
|
|
408
|
+
// ─── Step 2: LLM Provider ───
|
|
409
|
+
if (step === 2) {
|
|
410
|
+
log(` ${T.DOT} Step 2/3 LLM Provider`);
|
|
411
|
+
log(` ${T.SIDE}`);
|
|
412
|
+
log(` ${T.BRANCH} For natural language queries & descriptions`);
|
|
413
|
+
log(` ${T.SIDE} Use ↑↓ navigate, Enter confirm, ← back`);
|
|
414
|
+
log(` ${T.SIDE}`);
|
|
415
|
+
|
|
416
|
+
if (existing.LLM_API_KEY) {
|
|
417
|
+
log(` ${T.SIDE} Current: ${mask(existing.LLM_API_KEY)} → ${existing.LLM_BASE_URL || "?"}`);
|
|
418
|
+
log(` ${T.SIDE}`);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
rl.close();
|
|
422
|
+
const llmChoice = await selectMenu(llmOptions, ` ${T.SIDE} `, 6, true);
|
|
423
|
+
rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
424
|
+
ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
425
|
+
|
|
426
|
+
if (llmChoice === -2) { log(); step = 1; continue; }
|
|
427
|
+
if (llmChoice === -1) { rl.close(); return; }
|
|
428
|
+
|
|
429
|
+
llmKey = existing.LLM_API_KEY || "";
|
|
430
|
+
llmBaseUrl = existing.LLM_BASE_URL || "";
|
|
431
|
+
llmModel = existing.LLM_MODEL || "";
|
|
432
|
+
llmProviderName = "skipped";
|
|
433
|
+
|
|
434
|
+
if (llmChoice >= 0 && llmChoice < 5) {
|
|
435
|
+
const provider = llmProviders[llmChoice];
|
|
436
|
+
llmBaseUrl = provider.url;
|
|
437
|
+
llmModel = provider.model;
|
|
438
|
+
llmProviderName = provider.name;
|
|
439
|
+
|
|
440
|
+
log(` ${T.SIDE}`);
|
|
441
|
+
llmKey = (await ask(` ${T.SIDE} API Key (sk-...): `)).trim() || existing.LLM_API_KEY || "";
|
|
442
|
+
|
|
443
|
+
if (llmKey) {
|
|
444
|
+
const urlOverride = (await ask(` ${T.SIDE} Base URL [${llmBaseUrl}]: `)).trim();
|
|
445
|
+
if (urlOverride) llmBaseUrl = urlOverride;
|
|
446
|
+
const modelOverride = (await ask(` ${T.SIDE} Model [${llmModel}]: `)).trim();
|
|
447
|
+
if (modelOverride) llmModel = modelOverride;
|
|
448
|
+
}
|
|
449
|
+
} else if (llmChoice === 5) {
|
|
450
|
+
llmProviderName = "Custom";
|
|
451
|
+
const defUrl = llmBaseUrl || existing.LLM_BASE_URL || "";
|
|
452
|
+
const defModel = llmModel || existing.LLM_MODEL || "gpt-4o";
|
|
453
|
+
const defKey = existing.LLM_API_KEY || "";
|
|
454
|
+
log(` ${T.SIDE}`);
|
|
455
|
+
llmBaseUrl = (await ask(` ${T.SIDE} API Base URL${defUrl ? ` [${defUrl}]` : ""}: `)).trim() || defUrl;
|
|
456
|
+
llmModel = (await ask(` ${T.SIDE} Model${defModel ? ` [${defModel}]` : ""}: `)).trim() || defModel;
|
|
457
|
+
llmKey = (await ask(` ${T.SIDE} API Key${defKey ? ` [${mask(defKey)}]` : " (sk-...)"}: `)).trim() || defKey;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (llmKey) {
|
|
461
|
+
log(` ${T.LAST} ${T.OK} ${llmProviderName} / ${llmModel}`);
|
|
462
|
+
} else {
|
|
463
|
+
log(` ${T.LAST} ${T.WARN} Skipped (configure later in ${ENV_FILE})`);
|
|
464
|
+
}
|
|
465
|
+
log();
|
|
466
|
+
step = 3;
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ─── Step 3: Embedding Provider ───
|
|
471
|
+
if (step === 3) {
|
|
472
|
+
log(` ${T.DOT} Step 3/3 Embedding Provider`);
|
|
473
|
+
log(` ${T.SIDE}`);
|
|
474
|
+
log(` ${T.BRANCH} For semantic code search`);
|
|
475
|
+
log(` ${T.SIDE} Use ↑↓ navigate, Enter confirm, ← back`);
|
|
476
|
+
log(` ${T.SIDE}`);
|
|
477
|
+
|
|
478
|
+
if (existing.DASHSCOPE_API_KEY || existing.EMBED_API_KEY) {
|
|
479
|
+
const ek = existing.DASHSCOPE_API_KEY || existing.EMBED_API_KEY;
|
|
480
|
+
log(` ${T.SIDE} Current: ${mask(ek)} → ${existing.DASHSCOPE_BASE_URL || existing.EMBED_BASE_URL || "?"}`);
|
|
481
|
+
log(` ${T.SIDE}`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
rl.close();
|
|
485
|
+
const embedChoice = await selectMenu(embedOptions, ` ${T.SIDE} `, 3, true);
|
|
486
|
+
rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
487
|
+
ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
488
|
+
|
|
489
|
+
if (embedChoice === -2) { log(); step = 2; continue; }
|
|
490
|
+
if (embedChoice === -1) { rl.close(); return; }
|
|
491
|
+
|
|
492
|
+
embedKey = "";
|
|
493
|
+
embedUrl = "";
|
|
494
|
+
embedModel = "";
|
|
495
|
+
embedKeyEnv = "DASHSCOPE_API_KEY";
|
|
496
|
+
embedUrlEnv = "DASHSCOPE_BASE_URL";
|
|
497
|
+
embedProviderName = "skipped";
|
|
498
|
+
|
|
499
|
+
if (embedChoice >= 0 && embedChoice < 2) {
|
|
500
|
+
const ep = embedProvidersList[embedChoice];
|
|
501
|
+
embedUrl = ep.url;
|
|
502
|
+
embedModel = ep.model;
|
|
503
|
+
embedKeyEnv = ep.keyEnv;
|
|
504
|
+
embedUrlEnv = ep.urlEnv;
|
|
505
|
+
embedProviderName = ep.name;
|
|
506
|
+
|
|
507
|
+
log(` ${T.SIDE}`);
|
|
508
|
+
embedKey = (await ask(` ${T.SIDE} API Key: `)).trim() ||
|
|
509
|
+
existing[embedKeyEnv] || existing.DASHSCOPE_API_KEY || "";
|
|
510
|
+
|
|
511
|
+
if (embedKey) {
|
|
512
|
+
const urlOverride = (await ask(` ${T.SIDE} Base URL [${embedUrl}]: `)).trim();
|
|
513
|
+
if (urlOverride) embedUrl = urlOverride;
|
|
514
|
+
const modelOverride = (await ask(` ${T.SIDE} Model [${embedModel}]: `)).trim();
|
|
515
|
+
if (modelOverride) embedModel = modelOverride;
|
|
516
|
+
}
|
|
517
|
+
} else if (embedChoice === 2) {
|
|
518
|
+
embedProviderName = "Custom";
|
|
519
|
+
const defEmbedUrl = existing.EMBED_BASE_URL || existing.DASHSCOPE_BASE_URL || "";
|
|
520
|
+
const defEmbedModel = existing.EMBED_MODEL || "text-embedding-3-small";
|
|
521
|
+
const defEmbedKey = existing.EMBED_API_KEY || existing.DASHSCOPE_API_KEY || "";
|
|
522
|
+
log(` ${T.SIDE}`);
|
|
523
|
+
embedUrl = (await ask(` ${T.SIDE} API Base URL${defEmbedUrl ? ` [${defEmbedUrl}]` : ""}: `)).trim() || defEmbedUrl;
|
|
524
|
+
embedModel = (await ask(` ${T.SIDE} Model${defEmbedModel ? ` [${defEmbedModel}]` : ""}: `)).trim() || defEmbedModel;
|
|
525
|
+
embedKey = (await ask(` ${T.SIDE} API Key${defEmbedKey ? ` [${mask(defEmbedKey)}]` : ""}: `)).trim() || defEmbedKey;
|
|
526
|
+
embedKeyEnv = "EMBED_API_KEY";
|
|
527
|
+
embedUrlEnv = "EMBED_BASE_URL";
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (embedKey) {
|
|
531
|
+
log(` ${T.LAST} ${T.OK} ${embedProviderName} / ${embedModel}`);
|
|
532
|
+
} else {
|
|
533
|
+
log(` ${T.LAST} ${T.WARN} Skipped (configure later in ${ENV_FILE})`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
step = 4; // done
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
494
539
|
}
|
|
495
540
|
|
|
496
541
|
rl.close();
|