@alucard_24/pi-codeindex 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +345 -2
- package/config.schema.json +16 -0
- package/eval/queries.jsonl +8 -0
- package/package.json +33 -3
- package/scripts/setup-gpu.sh +65 -0
- package/scripts/setup-vulkan.mjs +140 -0
- package/scripts/vulkan-sources.json +29 -0
- package/src/backend.js +120 -0
- package/src/chunker.js +276 -0
- package/src/cli.js +196 -0
- package/src/config-ui.js +35 -0
- package/src/config.js +126 -0
- package/src/doctor.js +96 -0
- package/src/embed.js +88 -0
- package/src/eval.js +72 -0
- package/src/extension.js +485 -0
- package/src/graph.js +306 -0
- package/src/indexer.js +302 -0
- package/src/model.js +71 -0
- package/src/present.js +177 -0
- package/src/probe.js +20 -0
- package/src/search.js +189 -0
- package/src/store.js +142 -0
- package/src/sync.js +106 -0
- package/src/util.js +108 -0
- package/src/vulkan.js +151 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"revision": "f2c39fe2f838cf35ce7da92824f5a5e3ee6e88a7",
|
|
3
|
+
"files": {
|
|
4
|
+
"js/node/src/common.h": "b60d059bb673b9685b132a6189ff32976b492c04",
|
|
5
|
+
"js/node/src/inference_session_wrap.cc": "648a4586ababc855be8ca77a0ee655552c1eb71c",
|
|
6
|
+
"js/node/src/inference_session_wrap.h": "13132a8a2d744d5e471db72037c825928f19c7c9",
|
|
7
|
+
"js/node/src/main.cc": "c6c2a5cb8dc42c7e92b0c87181663b68c7d7c639",
|
|
8
|
+
"js/node/src/ort_instance_data.cc": "825cb953e01371baec5ab627e6f1ad92ecaee0cf",
|
|
9
|
+
"js/node/src/ort_instance_data.h": "68cb6028548d4cb2656d8f9e0d7cb44bfac75681",
|
|
10
|
+
"js/node/src/ort_singleton_data.cc": "d1d57a10340c5a90996b6bc5affbff8869a7a85c",
|
|
11
|
+
"js/node/src/ort_singleton_data.h": "a0577cb71e98bd37851852040ffc7e244b7d41dd",
|
|
12
|
+
"js/node/src/run_options_helper.cc": "352f828970c66767b1a80a34a29da06a272d111d",
|
|
13
|
+
"js/node/src/run_options_helper.h": "104fae150bb0eb6661b670926974c81eabc454fa",
|
|
14
|
+
"js/node/src/session_options_helper.cc": "9cfb44eb9acb75d38e845b30f847c6fdd3632592",
|
|
15
|
+
"js/node/src/session_options_helper.h": "c6338f28e03c6f21f657c1ff654864d696a5bbb8",
|
|
16
|
+
"js/node/src/tensor_helper.cc": "c4bf5628ce6fe0e662f60f2340adb83773dabc90",
|
|
17
|
+
"js/node/src/tensor_helper.h": "4a51e5240602accae3d5e75fd249f756ef34dea7",
|
|
18
|
+
"include/onnxruntime/core/session/onnxruntime_c_api.h": "a254e990237d5575186cf3871392db8c70835a56",
|
|
19
|
+
"include/onnxruntime/core/session/onnxruntime_cxx_api.h": "8a64ec9159063d97f3c5085716dd8b456bd9b768",
|
|
20
|
+
"include/onnxruntime/core/session/onnxruntime_cxx_inline.h": "0db4a7029a16389c4140c497840b83492313d0fb",
|
|
21
|
+
"include/onnxruntime/core/session/onnxruntime_ep_c_api.h": "0424a95c2d2c1102f328b4e3aeb77ec72601a120",
|
|
22
|
+
"include/onnxruntime/core/session/onnxruntime_float16.h": "625a1e82a6504577da24e83074643f7dfc13303d",
|
|
23
|
+
"include/onnxruntime/core/session/onnxruntime_error_code.h": "732fcd7a82eaa0208c7c97aa5f2fb2b0c694456f",
|
|
24
|
+
"include/onnxruntime/core/session/onnxruntime_run_options_config_keys.h": "f40ea6591059eb4afb574037732de8f7bc7575c1",
|
|
25
|
+
"include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h": "84e16a692d33178bcf29de7eb410c45680ab84b5",
|
|
26
|
+
"include/onnxruntime/core/providers/webgpu/webgpu_provider_factory.h": "0b45b847d651ff922a8e72856354e4ea6f95f439",
|
|
27
|
+
"LICENSE": "48bc6bb4996ac924359e8e28b9ae88970e5ed3fc"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/backend.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Policy dei backend separata dall'inferenza: testabile senza GPU né download.
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
export const MODEL_ID = 'onnx-community/embeddinggemma-2-ONNX';
|
|
7
|
+
export const DIMS = 768;
|
|
8
|
+
export const QUERY_PREFIX = 'task: code retrieval | query: ';
|
|
9
|
+
export const docPrefix = (filePath, title) => `title: ${title ?? filePath ?? 'none'} | text: `;
|
|
10
|
+
export const BACKENDS = ['cpu', 'cuda', 'webgpu', 'dml', 'coreml'];
|
|
11
|
+
const PLATFORMS = { cpu: ['linux', 'darwin', 'win32'], cuda: ['linux'],
|
|
12
|
+
webgpu: ['linux', 'darwin', 'win32'], dml: ['win32'], coreml: ['darwin'] };
|
|
13
|
+
|
|
14
|
+
export function nativeBackends() {
|
|
15
|
+
const req = createRequire(import.meta.url);
|
|
16
|
+
const ort = req('onnxruntime-node');
|
|
17
|
+
const dir = path.dirname(req.resolve('onnxruntime-node/package.json'));
|
|
18
|
+
const cudaLibrary = existsSync(path.join(dir, 'bin', 'napi-v6', process.platform, process.arch, 'libonnxruntime_providers_cuda.so'));
|
|
19
|
+
return { version: ort.env.versions.node, backends: backendInventory(ort.listSupportedBackends(), { cudaLibrary }) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function backendInventory(listed, { platform = process.platform, arch = process.arch, cudaLibrary = false } = {}) {
|
|
23
|
+
return BACKENDS.map((device) => {
|
|
24
|
+
const entry = listed.find((b) => b.name === device);
|
|
25
|
+
const platformOk = PLATFORMS[device].includes(platform) && (device !== 'cuda' || arch === 'x64');
|
|
26
|
+
const available = platformOk && Boolean(entry && (entry.bundled || (device === 'cuda' && cudaLibrary)));
|
|
27
|
+
const reason = !platformOk ? `non supportato su ${platform}/${arch}` : !entry ? 'non esposto dal binding nativo'
|
|
28
|
+
: !available ? 'provider non installato (CUDA/Linux: npm run setup-gpu)' : 'provider presente; inferenza da verificare';
|
|
29
|
+
return { device, available, reason };
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function backendPlan(config, inventory, platform = process.platform) {
|
|
34
|
+
const order = platform === 'win32' ? ['dml', 'webgpu', 'cpu']
|
|
35
|
+
: platform === 'darwin' ? ['coreml', 'webgpu', 'cpu'] : ['cuda', 'webgpu', 'cpu'];
|
|
36
|
+
const devices = config.device === 'auto' ? order.filter((d) => inventory.some((b) => b.device === d && b.available))
|
|
37
|
+
: [config.device, ...(config.fallback === 'cpu' && config.device !== 'cpu' ? ['cpu'] : [])];
|
|
38
|
+
return devices.map((device) => ({
|
|
39
|
+
device,
|
|
40
|
+
dtype: config.dtype === 'auto' ? (device === 'cpu' ? 'q8' : 'fp16') : config.dtype,
|
|
41
|
+
...inventory.find((b) => b.device === device),
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function webgpuBackendOf(profile) {
|
|
46
|
+
return profile.device === 'webgpu' ? (profile.webgpuBackend ?? 'auto') : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatProfile(profile) {
|
|
50
|
+
return `${profile.device}${profile.device === 'webgpu' ? `→${webgpuBackendOf(profile)}` : ''}/${profile.dtype}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function assertCompatibleIndex(index, profile) {
|
|
54
|
+
const mismatches = ['model', 'dtype', 'dims', 'device'].filter((key) => index[key] !== profile[key]);
|
|
55
|
+
if (webgpuBackendOf(index) !== webgpuBackendOf(profile)) mismatches.push('webgpuBackend');
|
|
56
|
+
if (mismatches.length) throw new Error(`indice incompatibile (${mismatches.join(', ')}): esegui /codeindex reindex o il comando index con questo profilo`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function validateEmbeddings(rows, count, dims = DIMS) {
|
|
60
|
+
if (!Array.isArray(rows) || rows.length !== count) throw new Error('embedding: numero di righe inatteso');
|
|
61
|
+
for (const row of rows) {
|
|
62
|
+
if (!row || row.length !== dims) throw new Error(`embedding: attese ${dims} dimensioni`);
|
|
63
|
+
let norm = 0;
|
|
64
|
+
for (const n of row) {
|
|
65
|
+
if (!Number.isFinite(n)) throw new Error('embedding: valori non finiti');
|
|
66
|
+
norm += n * n;
|
|
67
|
+
}
|
|
68
|
+
if (norm < 1e-12) throw new Error('embedding: vettore nullo');
|
|
69
|
+
}
|
|
70
|
+
return rows;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Il profilo si fissa SOLO dopo un'inferenza riuscita. Non cambia in seguito,
|
|
74
|
+
// neanche dopo OOM/errori: nessun cambio di dtype durante un'indicizzazione.
|
|
75
|
+
export function createEmbeddingRuntime({ settings, inventory, initialize, platform = process.platform, log = () => {} }) {
|
|
76
|
+
let ready = null;
|
|
77
|
+
let loading = null;
|
|
78
|
+
let attempts = [];
|
|
79
|
+
let error = null;
|
|
80
|
+
let activeConfig = null;
|
|
81
|
+
async function get() {
|
|
82
|
+
if (ready) return ready;
|
|
83
|
+
if (loading) return loading;
|
|
84
|
+
loading = (async () => {
|
|
85
|
+
attempts = [];
|
|
86
|
+
error = null;
|
|
87
|
+
try {
|
|
88
|
+
const loaded = await settings();
|
|
89
|
+
activeConfig = loaded.config;
|
|
90
|
+
const plan = backendPlan(activeConfig, await inventory(), platform);
|
|
91
|
+
for (const candidate of plan) {
|
|
92
|
+
const profile = Object.freeze({ model: MODEL_ID, dims: DIMS, device: candidate.device, dtype: candidate.dtype,
|
|
93
|
+
webgpuBackend: candidate.device === 'webgpu' ? (activeConfig.webgpuBackend ?? 'auto') : null });
|
|
94
|
+
try {
|
|
95
|
+
if (!candidate.available) throw new Error(candidate.reason ?? 'provider assente');
|
|
96
|
+
const pipe = await initialize(profile, activeConfig);
|
|
97
|
+
attempts.push({ ...profile, ok: true });
|
|
98
|
+
ready = { pipe, profile, config: activeConfig };
|
|
99
|
+
log(`[codeindex] modello pronto · ${formatProfile(profile)}`);
|
|
100
|
+
return ready;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
const message = String(e.message ?? e).slice(0, 1000);
|
|
103
|
+
attempts.push({ ...profile, ok: false, error: message });
|
|
104
|
+
log(`[codeindex] ${formatProfile(profile)} non disponibile: ${message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`nessun backend utilizzabile: ${attempts.map((a) => `${formatProfile(a)}: ${a.error}`).join('; ')}. Usa doctor --probe per la diagnostica.`);
|
|
108
|
+
} catch (e) {
|
|
109
|
+
error = e.message;
|
|
110
|
+
throw e;
|
|
111
|
+
}
|
|
112
|
+
})();
|
|
113
|
+
try { return await loading; } finally { loading = null; }
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
get,
|
|
117
|
+
info: () => ({ profile: ready?.profile ?? null, config: activeConfig, loading: Boolean(loading),
|
|
118
|
+
attempts: attempts.map((a) => ({ ...a })), error }),
|
|
119
|
+
};
|
|
120
|
+
}
|
package/src/chunker.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// Chunker declaration-aware + fallback a finestre.
|
|
2
|
+
// Puro: nessun I/O, nessuna dipendenza esterna. Versionato: cambiare la logica
|
|
3
|
+
// richiede un bump di CHUNKER_VERSION (invalida i vettori esistenti).
|
|
4
|
+
|
|
5
|
+
export const CHUNKER_VERSION = 1;
|
|
6
|
+
export const MAX_LINES = 120; // oltre: split a finestre
|
|
7
|
+
export const SPLIT_LINES = 60;
|
|
8
|
+
export const MAX_EMBED_CHARS = 4000;
|
|
9
|
+
|
|
10
|
+
export const LANG_BY_EXT = {
|
|
11
|
+
'.js': 'js', '.mjs': 'js', '.cjs': 'js', '.jsx': 'js',
|
|
12
|
+
'.ts': 'js', '.tsx': 'js', '.mts': 'js', '.cts': 'js',
|
|
13
|
+
'.py': 'py', '.pyi': 'py',
|
|
14
|
+
'.go': 'go',
|
|
15
|
+
'.rs': 'rust',
|
|
16
|
+
'.c': 'c', '.h': 'c',
|
|
17
|
+
'.cc': 'cpp', '.cpp': 'cpp', '.cxx': 'cpp', '.hpp': 'cpp', '.hh': 'cpp', '.hxx': 'cpp',
|
|
18
|
+
'.cs': 'csharp',
|
|
19
|
+
'.srw': 'powerbuilder', '.sru': 'powerbuilder', '.srf': 'powerbuilder',
|
|
20
|
+
'.srs': 'powerbuilder', '.srm': 'powerbuilder', '.srd': 'powerbuilder',
|
|
21
|
+
'.sra': 'powerbuilder',
|
|
22
|
+
'.md': 'md', '.mdx': 'md',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function langOf(filePath) {
|
|
26
|
+
const m = /\.[^./]+$/.exec(filePath);
|
|
27
|
+
return m ? (LANG_BY_EXT[m[0].toLowerCase()] ?? 'generic') : 'generic';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const COMMENT = /^\s*(?:\/\/|\/\*|\*|#|--|;|'|<!--)/;
|
|
31
|
+
|
|
32
|
+
const JS_KEYWORDS = new Set([
|
|
33
|
+
'if', 'for', 'while', 'switch', 'catch', 'return', 'function', 'else', 'do',
|
|
34
|
+
'try', 'with', 'await', 'yield', 'typeof', 'new', 'super', 'this',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const C_KEYWORDS = new Set([
|
|
38
|
+
'if', 'for', 'while', 'switch', 'return', 'sizeof', 'do', 'else', 'case',
|
|
39
|
+
'typedef', 'struct', 'union', 'enum', 'class', 'namespace', 'template',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
/** Riconosce l'inizio di una dichiarazione. Ritorna {symbol, kind} o null. */
|
|
43
|
+
export function matchBoundary(lang, line) {
|
|
44
|
+
let m;
|
|
45
|
+
switch (lang) {
|
|
46
|
+
case 'js': {
|
|
47
|
+
if ((m = /^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)/.exec(line)))
|
|
48
|
+
return { symbol: m[1], kind: 'function' };
|
|
49
|
+
if ((m = /^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/.exec(line)))
|
|
50
|
+
return { symbol: m[1], kind: 'class' };
|
|
51
|
+
if ((m = /^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(?:async\s*)?(?:function\b|\()/.exec(line)))
|
|
52
|
+
return { symbol: m[1], kind: 'function' };
|
|
53
|
+
if ((m = /^\s*(?:export\s+)?(?:declare\s+)?(?:interface|type|enum)\s+([A-Za-z_$][\w$]*)/.exec(line)))
|
|
54
|
+
return { symbol: m[1], kind: 'type' };
|
|
55
|
+
if ((m = /^ {2,}(?:(?:public|private|protected|static|async|readonly|override|get|set)\s+)*([A-Za-z_$][\w$]*)\s*\([^;]*\)\s*(?::[^{]+)?\{/.exec(line))) {
|
|
56
|
+
// `if (x) {`, `for (...) {` ecc. non sono metodi: spezzerebbero il corpo.
|
|
57
|
+
if (JS_KEYWORDS.has(m[1])) return null;
|
|
58
|
+
return { symbol: m[1], kind: 'method' };
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
case 'py': {
|
|
63
|
+
if (/^\s*(?:async\s+)?def\s+(\w+)/.test(line)) {
|
|
64
|
+
const mm = /^\s*(?:async\s+)?def\s+(\w+)/.exec(line);
|
|
65
|
+
return { symbol: mm[1], kind: 'function' };
|
|
66
|
+
}
|
|
67
|
+
if ((m = /^class\s+(\w+)/.exec(line))) return { symbol: m[1], kind: 'class' };
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
case 'go': {
|
|
71
|
+
if ((m = /^func\s+(?:\([^)]*\)\s*)?(\w+)/.exec(line))) return { symbol: m[1], kind: 'function' };
|
|
72
|
+
if ((m = /^type\s+(\w+)/.exec(line))) return { symbol: m[1], kind: 'type' };
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
case 'rust': {
|
|
76
|
+
if ((m = /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:const\s+)?(?:unsafe\s+)?(?:async\s+)?(?:extern\s+"[^"]*"\s+)?fn\s+(\w+)/.exec(line)))
|
|
77
|
+
return { symbol: m[1], kind: 'function' };
|
|
78
|
+
if ((m = /^\s*(?:pub(?:\([^)]*\))?\s+)?(struct|enum|union|trait|mod)\s+(\w+)/.exec(line)))
|
|
79
|
+
return { symbol: m[2], kind: m[1] };
|
|
80
|
+
if ((m = /^\s*(?:pub(?:\([^)]*\))?\s+)?impl\b[^{]*?(?:for\s+)?([\w:<>, ]+?)\s*(?:where|$|\{)/.exec(line)))
|
|
81
|
+
return { symbol: m[1].trim(), kind: 'impl' };
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
case 'c':
|
|
85
|
+
case 'cpp': {
|
|
86
|
+
if (line.startsWith('#') || /^\s*\/[/*]/.test(line)) return null;
|
|
87
|
+
if ((m = /^\s*(?:typedef\s+)?(?:class|struct|enum(?:\s+class)?|namespace|union)\s+(\w+)/.exec(line)))
|
|
88
|
+
return { symbol: m[1], kind: 'type' };
|
|
89
|
+
if ((m = /^([A-Za-z_][\w\s\*&:<>,]*?[\s\*&])([A-Za-z_]\w*)\s*\([^;]*$/.exec(line))) {
|
|
90
|
+
if (C_KEYWORDS.has(m[2])) return null;
|
|
91
|
+
if (/^\s*(if|for|while|switch|return|else|do|case)\b/.test(line)) return null;
|
|
92
|
+
return { symbol: m[2], kind: 'function' };
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
case 'csharp': {
|
|
97
|
+
if ((m = /^\s*(?:\[[^\]]*\]\s*)*(?:(?:public|private|protected|internal|static|sealed|abstract|partial|readonly|unsafe|file)\s+)*(?:class|struct|interface|enum|record)\s+(\w+)/.exec(line)))
|
|
98
|
+
return { symbol: m[1], kind: 'type' };
|
|
99
|
+
if ((m = /^\s*(?:(?:public|private|protected|internal|static|async|virtual|override|sealed|new|extern|unsafe|partial|abstract)\s+)+[\w<>\[\],\.\?]+\s+(\w+)\s*\(/.exec(line)))
|
|
100
|
+
return { symbol: m[1], kind: 'function' };
|
|
101
|
+
if ((m = /^\s*(?:(?:public|private|protected|internal|static|virtual|override|abstract|sealed)\s+)+[\w<>\[\],\.\?]+\s+(\w+)\s*\{\s*(?:get|set)/.exec(line)))
|
|
102
|
+
return { symbol: m[1], kind: 'property' };
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
case 'powerbuilder': {
|
|
106
|
+
if ((m = /^global\s+type\s+(\w+)/i.exec(line))) return { symbol: m[1], kind: 'type' };
|
|
107
|
+
if (/^forward\s+prototypes/i.test(line)) return { symbol: 'forward prototypes', kind: 'prototypes' };
|
|
108
|
+
if ((m = /^(?:public|private|protected|global)\s+function\s+[\w\[\]]+\s+(\w+)/i.exec(line)))
|
|
109
|
+
return { symbol: m[1], kind: 'function' };
|
|
110
|
+
if ((m = /^event\s+(\w+)/i.exec(line))) return { symbol: m[1], kind: 'event' };
|
|
111
|
+
if ((m = /^on\s+(\w+)/i.exec(line))) return { symbol: m[1], kind: 'event-handler' };
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
case 'md': {
|
|
115
|
+
if ((m = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line)))
|
|
116
|
+
return { symbol: m[2].slice(0, 80), kind: `h${m[1].length}` };
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
default:
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function trimRange(lines, start, end) {
|
|
125
|
+
while (start <= end && lines[start - 1].trim() === '') start++;
|
|
126
|
+
while (end >= start && lines[end - 1].trim() === '') end--;
|
|
127
|
+
return { start, end };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Estende l'inizio verso l'alto includendo il blocco di commento adiacente. */
|
|
131
|
+
function includeLeadingComments(lines, start) {
|
|
132
|
+
let s = start;
|
|
133
|
+
while (s > 1 && COMMENT.test(lines[s - 2]) && lines[s - 2].trim() !== '') {
|
|
134
|
+
s--;
|
|
135
|
+
if (s > 1 && lines[s - 2].trim() === '') break; // una riga vuota chiude il blocco
|
|
136
|
+
}
|
|
137
|
+
return s;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Split di un range troppo lungo: finestre a ridosso di righe vuote. */
|
|
141
|
+
function splitRange(lines, start, end, kind) {
|
|
142
|
+
const out = [];
|
|
143
|
+
let cur = start;
|
|
144
|
+
while (cur <= end) {
|
|
145
|
+
const targetEnd = Math.min(cur + SPLIT_LINES - 1, end);
|
|
146
|
+
let cut = targetEnd;
|
|
147
|
+
if (targetEnd < end) {
|
|
148
|
+
for (let l = targetEnd; l > cur + 10; l--) {
|
|
149
|
+
if (lines[l - 1].trim() === '') {
|
|
150
|
+
cut = l;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
out.push({ start: cur, end: cut, kind });
|
|
156
|
+
cur = cut + 1;
|
|
157
|
+
while (cur <= end && lines[cur - 1].trim() === '') cur++;
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Fonduzione di chunk adiacenti: riduce il numero di forward pass (le sequenze
|
|
164
|
+
* corte sprecano padding) senza perdere righe. Con `smallOnly`, fonde solo se
|
|
165
|
+
* ENTRAMBI i chunk sono più corti della soglia (per non perdere granularità
|
|
166
|
+
* sulle dichiarazioni di codice).
|
|
167
|
+
*/
|
|
168
|
+
export function mergeRanges(lines, ranges, { maxLines = 40, maxChars = 1000, smallOnly = 0 } = {}) {
|
|
169
|
+
const len = (r) => r.end - r.start + 1;
|
|
170
|
+
const out = [];
|
|
171
|
+
for (const r of ranges) {
|
|
172
|
+
const prev = out[out.length - 1];
|
|
173
|
+
if (!prev) {
|
|
174
|
+
out.push({ ...r, symbols: r.symbol ? [r.symbol] : [] });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const joinedLines = r.end - prev.start + 1;
|
|
178
|
+
const joinedChars = lines.slice(prev.start - 1, r.end).join('\n').length;
|
|
179
|
+
if (prev.kind === 'preamble') {
|
|
180
|
+
out.push({ ...r, symbols: r.symbol ? [r.symbol] : [] });
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const smallEnough = !smallOnly || (len(prev) <= smallOnly && len(r) <= smallOnly);
|
|
184
|
+
if (smallEnough && joinedLines <= maxLines && joinedChars <= maxChars) {
|
|
185
|
+
prev.end = r.end;
|
|
186
|
+
if (r.symbol) prev.symbols.push(r.symbol);
|
|
187
|
+
if (prev.kind !== r.kind) prev.kind = 'group';
|
|
188
|
+
prev.symbol = prev.symbols[0] ?? null;
|
|
189
|
+
} else {
|
|
190
|
+
out.push({ ...r, symbols: r.symbol ? [r.symbol] : [] });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return out.map((c) => ({
|
|
194
|
+
start: c.start,
|
|
195
|
+
end: c.end,
|
|
196
|
+
symbol: c.symbol,
|
|
197
|
+
symbols: c.symbols,
|
|
198
|
+
kind: c.kind,
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Politica di merge per linguaggio. Markdown: sezioni intere. Codice: solo micro-chunk. */
|
|
203
|
+
const MERGE_POLICY = {
|
|
204
|
+
md: { maxLines: 80, maxChars: 1500 },
|
|
205
|
+
generic: { maxLines: 0, maxChars: 0 }, // finestre da 60 righe: già ottimali
|
|
206
|
+
default: { maxLines: 30, maxChars: 900, smallOnly: 6 },
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Testo -> chunk. Copertura completa del file (nessuna riga persa).
|
|
211
|
+
* @returns {{start:number,end:number,symbol:string|null,kind:string}[]}
|
|
212
|
+
*/
|
|
213
|
+
export function chunkText(text, lang = 'generic') {
|
|
214
|
+
const lines = text.split('\n');
|
|
215
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
|
216
|
+
if (lines.length === 0) return [];
|
|
217
|
+
return chunkLines(lines, lang);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Variante su array di righe (per test e per il chunker di file già letti). */
|
|
221
|
+
export function chunkLines(lines, lang = 'generic') {
|
|
222
|
+
if (lines.length === 0) return [];
|
|
223
|
+
|
|
224
|
+
const boundaries = [];
|
|
225
|
+
for (let i = 0; i < lines.length; i++) {
|
|
226
|
+
const hit = matchBoundary(lang, lines[i]);
|
|
227
|
+
if (hit) boundaries.push({ line: i + 1, ...hit });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (boundaries.length === 0) {
|
|
231
|
+
return splitRange(lines, 1, lines.length, 'window').map((r) => ({ ...r, symbol: null }));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const ranges = [];
|
|
235
|
+
|
|
236
|
+
// Preambolo (import, header, prime righe) prima della prima dichiarazione.
|
|
237
|
+
// Il confine include l'eventuale blocco di commento della dichiarazione:
|
|
238
|
+
// altrimenti il commento finirebbe nel chunk sbagliato.
|
|
239
|
+
const firstStart = includeLeadingComments(lines, boundaries[0].line);
|
|
240
|
+
const preamble = trimRange(lines, 1, firstStart - 1);
|
|
241
|
+
if (preamble.start <= preamble.end && lines.slice(preamble.start - 1, preamble.end).some((l) => l.trim() !== ''))
|
|
242
|
+
ranges.push({ ...preamble, symbol: null, kind: 'preamble' });
|
|
243
|
+
|
|
244
|
+
for (let i = 0; i < boundaries.length; i++) {
|
|
245
|
+
const b = boundaries[i];
|
|
246
|
+
const nextLine = i + 1 < boundaries.length ? boundaries[i + 1].line : lines.length + 1;
|
|
247
|
+
let start = includeLeadingComments(lines, b.line);
|
|
248
|
+
const prevEnd = ranges.length ? ranges[ranges.length - 1].end : 0;
|
|
249
|
+
if (start <= prevEnd) start = b.line; // non sovrapporre al chunk precedente
|
|
250
|
+
const r = trimRange(lines, start, Math.min(nextLine - 1, lines.length));
|
|
251
|
+
if (r.start > r.end) continue;
|
|
252
|
+
const body = r.end - r.start + 1;
|
|
253
|
+
if (body > MAX_LINES) {
|
|
254
|
+
for (const s of splitRange(lines, r.start, r.end, b.kind)) ranges.push({ ...s, symbol: b.symbol });
|
|
255
|
+
} else {
|
|
256
|
+
ranges.push({ ...r, symbol: b.symbol, kind: b.kind });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const policy = MERGE_POLICY[lang] ?? MERGE_POLICY.default;
|
|
260
|
+
if (!policy.maxLines) return mergeRanges(lines, ranges, { maxLines: 1, maxChars: 0 });
|
|
261
|
+
return mergeRanges(lines, ranges, policy);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Righe del chunk + eventuale troncamento a MAX_EMBED_CHARS. */
|
|
265
|
+
export function chunkCode(lines, chunk) {
|
|
266
|
+
const code = lines.slice(chunk.start - 1, chunk.end).join('\n');
|
|
267
|
+
return code.length > MAX_EMBED_CHARS ? code.slice(0, MAX_EMBED_CHARS) : code;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Payload testuale per l'embedding (senza prefisso modello: lo aggiunge il chiamante). */
|
|
271
|
+
export function chunkPayload(chunk, code) {
|
|
272
|
+
const symbols = chunk.symbols?.length ? chunk.symbols : chunk.symbol ? [chunk.symbol] : [];
|
|
273
|
+
const head = `${chunk.start}-${chunk.end} ${chunk.kind}${symbols.length ? ` ${symbols.slice(0, 5).join(', ')}` : ''}`;
|
|
274
|
+
const body = code.length > MAX_EMBED_CHARS ? code.slice(0, MAX_EMBED_CHARS) : code;
|
|
275
|
+
return `${head}\n${body}`;
|
|
276
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// CLI: embedding, indice, configurazione persistente e diagnostica.
|
|
3
|
+
// Uso: node src/cli.js <comando> [opzioni]
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { embed, embedQuery, embedDocs, cosine, MODEL_ID, getEmbeddingProfile, modelDir } from './embed.js';
|
|
9
|
+
import { configCommand } from './config.js';
|
|
10
|
+
import { formatProfile } from './backend.js';
|
|
11
|
+
import { doctor, formatDoctor, parseDoctorArgs } from './doctor.js';
|
|
12
|
+
import { indexRepo, statusOf } from './indexer.js';
|
|
13
|
+
import { loadSearcher, search } from './search.js';
|
|
14
|
+
import { formatBundle, formatHits } from './present.js';
|
|
15
|
+
import { buildBundle, buildGraph } from './graph.js';
|
|
16
|
+
import { runEval } from './eval.js';
|
|
17
|
+
import { indexDir } from './store.js';
|
|
18
|
+
import { fmtBytes, fmtDuration } from './util.js';
|
|
19
|
+
|
|
20
|
+
function arg(name, def = null) {
|
|
21
|
+
const i = process.argv.indexOf(`--${name}`);
|
|
22
|
+
if (i === -1) return def;
|
|
23
|
+
const v = process.argv[i + 1];
|
|
24
|
+
return v && !v.startsWith('--') ? v : true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function rssMb() {
|
|
28
|
+
return Math.round(process.memoryUsage().rss / 1024 / 1024);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function embedTest() {
|
|
32
|
+
const SAME = 'export function parseArgs(argv) { return argv.slice(2); }';
|
|
33
|
+
const OTHER =
|
|
34
|
+
'async function waitForIdle(timeoutMs) { await new Promise((r) => setTimeout(r, timeoutMs)); }';
|
|
35
|
+
|
|
36
|
+
const t0 = performance.now();
|
|
37
|
+
const [sameA, sameB, other] = await embed([SAME, SAME, OTHER]);
|
|
38
|
+
const tCold = Math.round(performance.now() - t0);
|
|
39
|
+
|
|
40
|
+
const profile = await getEmbeddingProfile();
|
|
41
|
+
console.log('runtime :', MODEL_ID, formatProfile(profile));
|
|
42
|
+
console.log('cache :', await modelDir());
|
|
43
|
+
console.log('dims :', sameA.length, '(atteso 768)');
|
|
44
|
+
console.log('cos(same) :', cosine(sameA, sameB).toFixed(6), '(atteso ~1.0)');
|
|
45
|
+
console.log('cos(diverse) :', cosine(sameA, other).toFixed(6), '(atteso < 0.95)');
|
|
46
|
+
console.log('latency cold :', tCold, 'ms per 3 testi');
|
|
47
|
+
console.log('rss :', rssMb(), 'MB');
|
|
48
|
+
|
|
49
|
+
const batch = Array.from({ length: 16 }, (_, i) => `${SAME} // variante ${i}`);
|
|
50
|
+
const t1 = performance.now();
|
|
51
|
+
await embed(batch);
|
|
52
|
+
const warm = Math.round(performance.now() - t1);
|
|
53
|
+
console.log('latency warm :', warm, `ms per 16 testi (${(warm / 16).toFixed(1)} ms/testo)`);
|
|
54
|
+
|
|
55
|
+
const q = await embedQuery('dove vengono parsati gli argomenti da riga di comando');
|
|
56
|
+
const docs = await embedDocs([
|
|
57
|
+
{ path: 'src/cli/args.ts', text: SAME },
|
|
58
|
+
{ path: 'src/async/idle.ts', text: OTHER },
|
|
59
|
+
]);
|
|
60
|
+
console.log('query->args :', cosine(q[0], docs[0]).toFixed(4));
|
|
61
|
+
console.log('query->idle :', cosine(q[0], docs[1]).toFixed(4));
|
|
62
|
+
console.log('rss finale :', rssMb(), 'MB');
|
|
63
|
+
|
|
64
|
+
const dump = arg('dump');
|
|
65
|
+
if (dump && dump !== true) {
|
|
66
|
+
fs.writeFileSync(dump, JSON.stringify({ ...profile, vectors: { same: sameA, other } }));
|
|
67
|
+
console.log('dump :', dump);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function cmdIndex() {
|
|
72
|
+
const root = arg('root') ?? process.cwd();
|
|
73
|
+
const maxFiles = Number(arg('max-files', 5000));
|
|
74
|
+
const maxBytes = Math.round(Number(arg('max-bytes', 200)) * 1024 * 1024);
|
|
75
|
+
const batchSize = Number(arg('batch', 16));
|
|
76
|
+
const force = Boolean(arg('force', false));
|
|
77
|
+
|
|
78
|
+
const r = await indexRepo({ root, maxFiles, maxBytes, batchSize, force });
|
|
79
|
+
const out = {
|
|
80
|
+
repo: r.repo,
|
|
81
|
+
files: r.index.stats.files,
|
|
82
|
+
chunks: r.total,
|
|
83
|
+
embedded: r.embedded,
|
|
84
|
+
reused: r.reused,
|
|
85
|
+
unchangedFiles: r.unchanged,
|
|
86
|
+
bytes: r.index.stats.bytes,
|
|
87
|
+
durationMs: Math.round(r.duration),
|
|
88
|
+
skip: r.index.skipped,
|
|
89
|
+
};
|
|
90
|
+
if (arg('json')) {
|
|
91
|
+
console.log(JSON.stringify(out));
|
|
92
|
+
} else {
|
|
93
|
+
console.log(`repo : ${r.repo}`);
|
|
94
|
+
console.log(`file : ${out.files} (${fmtBytes(out.bytes)}) · invariati ${out.unchangedFiles}`);
|
|
95
|
+
console.log(`chunk : ${out.chunks} · riusati ${out.reused} · embeddati ${out.embedded}`);
|
|
96
|
+
console.log(`esclusi : estensione ${out.skip.ext}, dimensione ${out.skip.big}, binari ${out.skip.binary}, minified ${out.skip.minified}, cap ${out.skip.cap}`);
|
|
97
|
+
console.log(`durata : ${fmtDuration(out.durationMs)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function cmdStatus() {
|
|
102
|
+
const root = arg('root') ?? process.cwd();
|
|
103
|
+
const { repo, index, hasVectors } = await statusOf(root);
|
|
104
|
+
if (!index) {
|
|
105
|
+
console.log(`nessun indice per ${repo}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const info = {
|
|
109
|
+
repo,
|
|
110
|
+
files: index.stats?.files,
|
|
111
|
+
chunks: index.chunks.length,
|
|
112
|
+
dims: index.dims,
|
|
113
|
+
model: `${index.model} (${index.dtype})`,
|
|
114
|
+
device: index.device ?? 'legacy (reindex richiesto)',
|
|
115
|
+
...(index.device === 'webgpu' ? { webgpuBackend: index.webgpuBackend ?? 'auto' } : {}),
|
|
116
|
+
chunkerVersion: index.chunkerVersion,
|
|
117
|
+
head: index.head,
|
|
118
|
+
updatedAt: index.updatedAt,
|
|
119
|
+
vectors: hasVectors,
|
|
120
|
+
};
|
|
121
|
+
if (arg('json')) {
|
|
122
|
+
console.log(JSON.stringify(info));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
for (const [k, v] of Object.entries(info)) console.log(`${k.padEnd(14)}: ${v}`);
|
|
126
|
+
console.log(`${'cache'.padEnd(14)}: ${indexDir(repo)}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function cmdSearch() {
|
|
130
|
+
const query = process.argv[3];
|
|
131
|
+
if (!query) throw new Error('uso: search "query" [--k 8] [--path-prefix src/] [--json]');
|
|
132
|
+
const res = await search(query, {
|
|
133
|
+
root: arg('root') ?? process.cwd(),
|
|
134
|
+
k: Number(arg('k', 8)),
|
|
135
|
+
pathPrefix: arg('path-prefix'),
|
|
136
|
+
checkStale: Boolean(arg('stale', false)),
|
|
137
|
+
});
|
|
138
|
+
if (arg('bundle')) {
|
|
139
|
+
const { index } = await loadSearcher(res.repo);
|
|
140
|
+
const graph = await buildGraph(res.repo, index);
|
|
141
|
+
await buildBundle(res.repo, index, res.results, graph);
|
|
142
|
+
console.log(formatBundle(query, res));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (arg('json')) console.log(JSON.stringify({ ...res, repo: undefined }));
|
|
146
|
+
else console.log(formatHits(query, res));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function cmdEval() {
|
|
150
|
+
const file = arg('file') ?? fileURLToPath(new URL('../eval/queries.jsonl', import.meta.url));
|
|
151
|
+
const r = await runEval({
|
|
152
|
+
root: arg('root') ?? process.cwd(),
|
|
153
|
+
file,
|
|
154
|
+
k: Number(arg('k', 8)),
|
|
155
|
+
});
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log(`recall@3: ${(r.recall3 * 100).toFixed(0)}% recall@8: ${(r.recall8 * 100).toFixed(0)}% (${r.queries} query)`);
|
|
158
|
+
console.log(`latenza: p50 ${r.latencyP50} ms · p95 ${r.latencyP95} ms · cold ${r.coldMs} ms`);
|
|
159
|
+
if (arg('json')) console.log(JSON.stringify(r));
|
|
160
|
+
const ok = r.recall3 >= 0.625 && r.latencyP50 < 300;
|
|
161
|
+
if (!ok) process.exitCode = 2;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const cmd = process.argv[2];
|
|
165
|
+
try {
|
|
166
|
+
if (cmd === 'config') console.log(await configCommand(process.argv.slice(3)));
|
|
167
|
+
else if (cmd === 'setup-vulkan') {
|
|
168
|
+
const { setupVulkan } = await import('../scripts/setup-vulkan.mjs');
|
|
169
|
+
await setupVulkan(process.argv.slice(3));
|
|
170
|
+
}
|
|
171
|
+
else if (cmd === 'setup-gpu') {
|
|
172
|
+
if (process.platform !== 'linux' || process.arch !== 'x64') throw new Error('setup-gpu CUDA disponibile solo su Linux x64; usa doctor per i backend nativi della tua piattaforma');
|
|
173
|
+
const result = spawnSync('bash', [fileURLToPath(new URL('../scripts/setup-gpu.sh', import.meta.url))], { stdio: 'inherit' });
|
|
174
|
+
if (result.error) throw result.error;
|
|
175
|
+
process.exitCode = result.status ?? 1;
|
|
176
|
+
}
|
|
177
|
+
else if (cmd === 'doctor') {
|
|
178
|
+
const options = parseDoctorArgs(process.argv.slice(3));
|
|
179
|
+
const report = await doctor(options);
|
|
180
|
+
console.log(options.json ? JSON.stringify(report) : formatDoctor(report));
|
|
181
|
+
if (!report.ok) process.exitCode = 1;
|
|
182
|
+
}
|
|
183
|
+
else if (cmd === 'embed-test') await embedTest();
|
|
184
|
+
else if (cmd === 'index') await cmdIndex();
|
|
185
|
+
else if (cmd === 'status') await cmdStatus();
|
|
186
|
+
else if (cmd === 'search') await cmdSearch();
|
|
187
|
+
else if (cmd === 'eval') await cmdEval();
|
|
188
|
+
else {
|
|
189
|
+
console.error('comandi: embed-test | index | status | search | eval | setup-gpu | setup-vulkan | config [show|path|set CHIAVE VALORE|unset CHIAVE] | doctor [--probe] [--all] [--device DEVICE] [--dtype DTYPE] [--webgpu-backend auto|vulkan] [--json]');
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {
|
|
193
|
+
console.error(`errore: ${e.message}`);
|
|
194
|
+
if (process.env.CODEINDEX_DEBUG) console.error(e.stack);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
package/src/config-ui.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Solo dialoghi standard pi: utilizzabili anche tramite RPC. Nessun salvataggio
|
|
2
|
+
// prima della conferma; gli override d'ambiente non vengono copiati nel file.
|
|
3
|
+
import { DEVICES, DTYPES, WEBGPU_BACKENDS, ENV_KEYS, formatConfig, loadConfig, parseSetting, updateConfig } from './config.js';
|
|
4
|
+
|
|
5
|
+
export async function configure(ctx, options = {}) {
|
|
6
|
+
if (!ctx.hasUI) throw new Error('configurazione interattiva non disponibile; usa /codeindex config set CHIAVE VALORE');
|
|
7
|
+
const loaded = await loadConfig(options);
|
|
8
|
+
const draft = { ...loaded.config };
|
|
9
|
+
const patch = {};
|
|
10
|
+
const keys = Object.keys(ENV_KEYS);
|
|
11
|
+
while (true) {
|
|
12
|
+
const entries = [...keys.map((key) => `${key}: ${draft[key]}`), 'Salva', 'Annulla'];
|
|
13
|
+
const selected = await ctx.ui.select('Embedding locali — configurazione (riavvio richiesto)', entries);
|
|
14
|
+
if (!selected || selected === 'Annulla') return null;
|
|
15
|
+
if (selected === 'Salva') {
|
|
16
|
+
if (!Object.keys(patch).length) return null;
|
|
17
|
+
if (!(await ctx.ui.confirm('Salvare la configurazione?', `${loaded.file}\n${JSON.stringify(patch, null, 2)}\nRiavvia pi dopo il salvataggio. Cambiare device/dtype/backend WebGPU richiede un re-index.`))) return null;
|
|
18
|
+
const file = await updateConfig(patch, options);
|
|
19
|
+
const overrides = Object.keys(patch).filter((key) => loaded.sources[key] === ENV_KEYS[key]);
|
|
20
|
+
return `salvato: ${file}\nRiavvia pi (nuovo processo) per applicare.` +
|
|
21
|
+
(overrides.length ? `\nAttenzione: override d'ambiente attivi per ${overrides.join(', ')}.` : '') +
|
|
22
|
+
`\n${formatConfig(await loadConfig(options))}`;
|
|
23
|
+
}
|
|
24
|
+
const key = keys[entries.indexOf(selected)];
|
|
25
|
+
if (!key) continue;
|
|
26
|
+
const choices = key === 'device' ? DEVICES : key === 'dtype' ? DTYPES
|
|
27
|
+
: key === 'webgpuBackend' ? WEBGPU_BACKENDS : key === 'fallback' ? ['error', 'cpu'] : key === 'offline' ? ['false', 'true'] : null;
|
|
28
|
+
const hint = key === 'webgpuBackend' ? ' — solo WebGPU; vulkan richiede setup-vulkan' : '';
|
|
29
|
+
const text = choices ? await ctx.ui.select(`${key}${hint} (attuale: ${draft[key]})`, choices)
|
|
30
|
+
: await ctx.ui.input(`${key} (attuale: ${draft[key]})`, String(draft[key]));
|
|
31
|
+
if (text === undefined) continue;
|
|
32
|
+
try { draft[key] = patch[key] = parseSetting(key, text); }
|
|
33
|
+
catch (e) { ctx.ui.notify(e.message, 'error'); }
|
|
34
|
+
}
|
|
35
|
+
}
|