vexi-cli 0.5.1
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 +317 -0
- package/dist/agent.js +315 -0
- package/dist/cli.js +333 -0
- package/dist/config.js +36 -0
- package/dist/explain/index.js +238 -0
- package/dist/graph/html.js +225 -0
- package/dist/graph/index.js +129 -0
- package/dist/i18n/index.js +260 -0
- package/dist/index.js +25 -0
- package/dist/learn/index.js +131 -0
- package/dist/mcp/client.js +99 -0
- package/dist/mcp/config.js +27 -0
- package/dist/mcp/server.js +110 -0
- package/dist/memory/index.js +127 -0
- package/dist/providers/anthropic.js +59 -0
- package/dist/providers/detect.js +47 -0
- package/dist/providers/index.js +37 -0
- package/dist/providers/openai-compat.js +84 -0
- package/dist/providers/types.js +24 -0
- package/dist/replay/export.js +315 -0
- package/dist/replay/recorder.js +81 -0
- package/dist/scanner/gitignore.js +92 -0
- package/dist/scanner/index.js +230 -0
- package/dist/skills/index.js +121 -0
- package/dist/ui/index.js +39 -0
- package/dist/utils/fs-atomic.js +46 -0
- package/dist/utils/open.js +17 -0
- package/package.json +60 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual Code Understanding — interactive HTML export.
|
|
3
|
+
*
|
|
4
|
+
* Generates a single standalone HTML file (d3 loaded from CDN) with:
|
|
5
|
+
* - a force-directed graph of module relationships
|
|
6
|
+
* - heatmap coloring by impact (transitive dependents)
|
|
7
|
+
* - impact analysis on click: highlights every file that (transitively)
|
|
8
|
+
* depends on the selected module — i.e. what breaks if you change it
|
|
9
|
+
* - search box + top-externals sidebar
|
|
10
|
+
*
|
|
11
|
+
* No server needed: generated locally, opened in the default browser.
|
|
12
|
+
*/
|
|
13
|
+
import { promises as fs } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
const LABELS = {
|
|
16
|
+
en: {
|
|
17
|
+
title: 'Vexi code graph',
|
|
18
|
+
impact: 'Impact analysis',
|
|
19
|
+
impactHint: 'Click a node to see every file that breaks if you change it. Click the background to reset.',
|
|
20
|
+
search: 'Search files…',
|
|
21
|
+
externals: 'Top dependencies',
|
|
22
|
+
nodes: 'modules',
|
|
23
|
+
edges: 'imports',
|
|
24
|
+
affected: 'files affected by',
|
|
25
|
+
heat: 'Heat = how many files depend on it',
|
|
26
|
+
},
|
|
27
|
+
es: {
|
|
28
|
+
title: 'Grafo de código de Vexi',
|
|
29
|
+
impact: 'Análisis de impacto',
|
|
30
|
+
impactHint: 'Haz clic en un nodo para ver qué archivos se rompen si lo cambias. Clic en el fondo para reiniciar.',
|
|
31
|
+
search: 'Buscar archivos…',
|
|
32
|
+
externals: 'Dependencias principales',
|
|
33
|
+
nodes: 'módulos',
|
|
34
|
+
edges: 'imports',
|
|
35
|
+
affected: 'archivos afectados por',
|
|
36
|
+
heat: 'Calor = cuántos archivos dependen de él',
|
|
37
|
+
},
|
|
38
|
+
pt: {
|
|
39
|
+
title: 'Grafo de código do Vexi',
|
|
40
|
+
impact: 'Análise de impacto',
|
|
41
|
+
impactHint: 'Clique em um nó para ver quais arquivos quebram se você alterá-lo. Clique no fundo para reiniciar.',
|
|
42
|
+
search: 'Buscar arquivos…',
|
|
43
|
+
externals: 'Principais dependências',
|
|
44
|
+
nodes: 'módulos',
|
|
45
|
+
edges: 'imports',
|
|
46
|
+
affected: 'arquivos afetados por',
|
|
47
|
+
heat: 'Calor = quantos arquivos dependem dele',
|
|
48
|
+
},
|
|
49
|
+
fr: {
|
|
50
|
+
title: 'Graphe de code Vexi',
|
|
51
|
+
impact: "Analyse d'impact",
|
|
52
|
+
impactHint: 'Cliquez sur un nœud pour voir les fichiers cassés si vous le modifiez. Cliquez sur le fond pour réinitialiser.',
|
|
53
|
+
search: 'Rechercher des fichiers…',
|
|
54
|
+
externals: 'Dépendances principales',
|
|
55
|
+
nodes: 'modules',
|
|
56
|
+
edges: 'imports',
|
|
57
|
+
affected: 'fichiers affectés par',
|
|
58
|
+
heat: 'Chaleur = combien de fichiers en dépendent',
|
|
59
|
+
},
|
|
60
|
+
ar: {
|
|
61
|
+
title: 'مخطط الكود من Vexi',
|
|
62
|
+
impact: 'تحليل الأثر',
|
|
63
|
+
impactHint: 'انقر على عقدة لرؤية الملفات التي ستتأثر إذا غيّرتها. انقر على الخلفية لإعادة التعيين.',
|
|
64
|
+
search: 'ابحث عن الملفات…',
|
|
65
|
+
externals: 'أهم الاعتماديات',
|
|
66
|
+
nodes: 'وحدات',
|
|
67
|
+
edges: 'استيرادات',
|
|
68
|
+
affected: 'ملفات تتأثر بـ',
|
|
69
|
+
heat: 'الحرارة = عدد الملفات التي تعتمد عليها',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
/** Write the graph HTML file and return its path. */
|
|
73
|
+
export async function exportGraphHtml(root, graph, lang, out) {
|
|
74
|
+
const path = out ?? join(root, `vexi-graph-${graph.project.replace(/[^a-z0-9_-]+/gi, '-')}.html`);
|
|
75
|
+
await fs.writeFile(path, buildGraphHtml(graph, lang), 'utf8');
|
|
76
|
+
return path;
|
|
77
|
+
}
|
|
78
|
+
export function buildGraphHtml(graph, lang) {
|
|
79
|
+
const L = LABELS[lang];
|
|
80
|
+
const rtl = lang === 'ar';
|
|
81
|
+
const data = JSON.stringify(graph).replaceAll('<', '\\u003c');
|
|
82
|
+
return `<!DOCTYPE html>
|
|
83
|
+
<html lang="${lang}" dir="${rtl ? 'rtl' : 'ltr'}">
|
|
84
|
+
<head>
|
|
85
|
+
<meta charset="utf-8">
|
|
86
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
87
|
+
<title>${L.title} — ${escapeHtml(graph.project)}</title>
|
|
88
|
+
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
|
|
89
|
+
<style>
|
|
90
|
+
:root { --accent: #2979FF; --bg: #0a0a0f; --panel: #12121a; --text: #e8e8f0; --dim: #8888a0; }
|
|
91
|
+
* { box-sizing: border-box; }
|
|
92
|
+
body { margin: 0; background: var(--bg); color: var(--text); overflow: hidden;
|
|
93
|
+
font: 14px/1.5 ui-monospace, 'Cascadia Code', Consolas, monospace; }
|
|
94
|
+
#side { position: fixed; inset-block: 0; inset-inline-start: 0; width: 290px; padding: 18px;
|
|
95
|
+
background: var(--panel); border-inline-end: 1px solid #1e1e2c; overflow-y: auto; z-index: 5; }
|
|
96
|
+
#side h1 { color: var(--accent); font-size: 18px; letter-spacing: 2px; margin: 0; }
|
|
97
|
+
#side .meta { color: var(--dim); font-size: 12px; margin: 4px 0 14px; }
|
|
98
|
+
#side h2 { font-size: 12px; text-transform: uppercase; letter-spacing: 1px; color: var(--dim); margin: 18px 0 6px; }
|
|
99
|
+
#search { width: 100%; background: var(--bg); color: var(--text); border: 1px solid #2a2a3a;
|
|
100
|
+
border-radius: 8px; padding: 8px 10px; font: inherit; }
|
|
101
|
+
#info { font-size: 12px; color: var(--dim); }
|
|
102
|
+
#impact-result { color: var(--text); font-size: 12px; max-height: 260px; overflow-y: auto; }
|
|
103
|
+
#impact-result .file { padding: 2px 0; color: #ff8a65; direction: ltr; text-align: start; }
|
|
104
|
+
#impact-result .selected { color: var(--accent); font-weight: 700; }
|
|
105
|
+
.ext { display: flex; justify-content: space-between; font-size: 12px; padding: 2px 0; direction: ltr; }
|
|
106
|
+
.ext b { color: var(--accent); font-weight: 400; }
|
|
107
|
+
svg { display: block; }
|
|
108
|
+
.legend { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--dim); margin-top: 6px; }
|
|
109
|
+
.legend .bar { flex: 1; height: 8px; border-radius: 4px;
|
|
110
|
+
background: linear-gradient(90deg, #2979FF, #ffd54f, #ff5252); }
|
|
111
|
+
footer { position: fixed; bottom: 10px; inset-inline-end: 14px; color: var(--dim); font-size: 12px; z-index: 5; }
|
|
112
|
+
footer code { color: var(--accent); }
|
|
113
|
+
</style>
|
|
114
|
+
</head>
|
|
115
|
+
<body>
|
|
116
|
+
<aside id="side">
|
|
117
|
+
<h1>VEXI</h1>
|
|
118
|
+
<div class="meta">${L.title} · ${escapeHtml(graph.project)}<br>
|
|
119
|
+
<span id="counts"></span></div>
|
|
120
|
+
<input id="search" type="search" placeholder="${L.search}">
|
|
121
|
+
<h2>${L.impact}</h2>
|
|
122
|
+
<div id="info">${L.impactHint}</div>
|
|
123
|
+
<div id="impact-result"></div>
|
|
124
|
+
<div class="legend"><span>0</span><div class="bar"></div><span>max</span></div>
|
|
125
|
+
<div class="legend">${L.heat}</div>
|
|
126
|
+
<h2>${L.externals}</h2>
|
|
127
|
+
<div id="externals"></div>
|
|
128
|
+
</aside>
|
|
129
|
+
<svg id="canvas"></svg>
|
|
130
|
+
<footer><code>npm install -g vexi</code> · <a href="https://github.com/Elomami1976/vexi" style="color:var(--dim)">GitHub</a></footer>
|
|
131
|
+
<script>
|
|
132
|
+
const DATA = ${data};
|
|
133
|
+
const L = ${JSON.stringify({ nodes: L.nodes, edges: L.edges, affected: L.affected })};
|
|
134
|
+
document.getElementById('counts').textContent =
|
|
135
|
+
DATA.nodes.length + ' ' + L.nodes + ' · ' + DATA.edges.length + ' ' + L.edges;
|
|
136
|
+
document.getElementById('externals').innerHTML = DATA.externals
|
|
137
|
+
.map((e) => '<div class="ext"><span>' + esc(e.name) + '</span><b>' + e.count + '</b></div>').join('');
|
|
138
|
+
|
|
139
|
+
function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
140
|
+
|
|
141
|
+
const W = innerWidth, H = innerHeight;
|
|
142
|
+
const svg = d3.select('#canvas').attr('width', W).attr('height', H);
|
|
143
|
+
const g = svg.append('g');
|
|
144
|
+
svg.call(d3.zoom().scaleExtent([0.2, 4]).on('zoom', (ev) => g.attr('transform', ev.transform)));
|
|
145
|
+
|
|
146
|
+
// Reverse adjacency for impact analysis (target → its importers)
|
|
147
|
+
const dependents = new Map();
|
|
148
|
+
for (const e of DATA.edges) {
|
|
149
|
+
if (!dependents.has(e.target)) dependents.set(e.target, []);
|
|
150
|
+
dependents.get(e.target).push(e.source);
|
|
151
|
+
}
|
|
152
|
+
function transitiveDependents(id) {
|
|
153
|
+
const visited = new Set(); const queue = [...(dependents.get(id) || [])];
|
|
154
|
+
while (queue.length) { const c = queue.pop(); if (visited.has(c)) continue; visited.add(c); queue.push(...(dependents.get(c) || [])); }
|
|
155
|
+
return visited;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const maxImpact = Math.max(1, ...DATA.nodes.map((n) => n.impact));
|
|
159
|
+
const heat = d3.scaleLinear().domain([0, maxImpact / 2, maxImpact]).range(['#2979FF', '#ffd54f', '#ff5252']);
|
|
160
|
+
const radius = (n) => 5 + Math.min(14, Math.sqrt(n.dependents) * 3);
|
|
161
|
+
|
|
162
|
+
const nodes = DATA.nodes.map((n) => ({ ...n }));
|
|
163
|
+
const links = DATA.edges.map((e) => ({ source: e.source, target: e.target }));
|
|
164
|
+
|
|
165
|
+
const sim = d3.forceSimulation(nodes)
|
|
166
|
+
.force('link', d3.forceLink(links).id((d) => d.id).distance(70).strength(0.4))
|
|
167
|
+
.force('charge', d3.forceManyBody().strength(-180))
|
|
168
|
+
.force('center', d3.forceCenter(W / 2 + 130, H / 2))
|
|
169
|
+
.force('collide', d3.forceCollide().radius((d) => radius(d) + 4));
|
|
170
|
+
|
|
171
|
+
const link = g.append('g').selectAll('line').data(links).join('line')
|
|
172
|
+
.attr('stroke', '#2a2a3a').attr('stroke-width', 1.1);
|
|
173
|
+
const node = g.append('g').selectAll('circle').data(nodes).join('circle')
|
|
174
|
+
.attr('r', radius).attr('fill', (d) => heat(d.impact))
|
|
175
|
+
.attr('stroke', '#0a0a0f').attr('stroke-width', 1.5).style('cursor', 'pointer')
|
|
176
|
+
.call(d3.drag()
|
|
177
|
+
.on('start', (ev, d) => { if (!ev.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; })
|
|
178
|
+
.on('drag', (ev, d) => { d.fx = ev.x; d.fy = ev.y; })
|
|
179
|
+
.on('end', (ev, d) => { if (!ev.active) sim.alphaTarget(0); d.fx = null; d.fy = null; }));
|
|
180
|
+
node.append('title').text((d) => d.id + '\\n← ' + d.dependents + ' · → ' + d.deps + ' · impact ' + d.impact);
|
|
181
|
+
const label = g.append('g').selectAll('text').data(nodes).join('text')
|
|
182
|
+
.text((d) => d.id.split('/').pop()).attr('font-size', 9).attr('fill', '#8888a0')
|
|
183
|
+
.attr('dx', 10).attr('dy', 3).style('pointer-events', 'none');
|
|
184
|
+
|
|
185
|
+
sim.on('tick', () => {
|
|
186
|
+
link.attr('x1', (d) => d.source.x).attr('y1', (d) => d.source.y)
|
|
187
|
+
.attr('x2', (d) => d.target.x).attr('y2', (d) => d.target.y);
|
|
188
|
+
node.attr('cx', (d) => d.x).attr('cy', (d) => d.y);
|
|
189
|
+
label.attr('x', (d) => d.x).attr('y', (d) => d.y);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ── Impact analysis on click ──
|
|
193
|
+
const result = document.getElementById('impact-result');
|
|
194
|
+
function select(id) {
|
|
195
|
+
const affected = id ? transitiveDependents(id) : new Set();
|
|
196
|
+
node.attr('opacity', (d) => !id || d.id === id || affected.has(d.id) ? 1 : 0.12)
|
|
197
|
+
.attr('stroke', (d) => d.id === id ? '#fff' : '#0a0a0f');
|
|
198
|
+
label.attr('opacity', (d) => !id || d.id === id || affected.has(d.id) ? 1 : 0.1);
|
|
199
|
+
link.attr('stroke', (l) => id && (l.target.id === id || affected.has(l.target.id)) ? '#ff8a65' : '#2a2a3a');
|
|
200
|
+
result.innerHTML = id
|
|
201
|
+
? '<div class="selected">' + esc(id) + '</div><div>' + affected.size + ' ' + L.affected + '</div>' +
|
|
202
|
+
[...affected].sort().map((f) => '<div class="file">' + esc(f) + '</div>').join('')
|
|
203
|
+
: '';
|
|
204
|
+
}
|
|
205
|
+
node.on('click', (ev, d) => { ev.stopPropagation(); select(d.id); });
|
|
206
|
+
svg.on('click', () => select(null));
|
|
207
|
+
|
|
208
|
+
// ── Search ──
|
|
209
|
+
document.getElementById('search').addEventListener('input', (ev) => {
|
|
210
|
+
const q = ev.target.value.toLowerCase();
|
|
211
|
+
node.attr('opacity', (d) => !q || d.id.toLowerCase().includes(q) ? 1 : 0.12);
|
|
212
|
+
label.attr('opacity', (d) => !q || d.id.toLowerCase().includes(q) ? 1 : 0.1);
|
|
213
|
+
});
|
|
214
|
+
</script>
|
|
215
|
+
</body>
|
|
216
|
+
</html>
|
|
217
|
+
`;
|
|
218
|
+
}
|
|
219
|
+
function escapeHtml(text) {
|
|
220
|
+
return text
|
|
221
|
+
.replaceAll('&', '&')
|
|
222
|
+
.replaceAll('<', '<')
|
|
223
|
+
.replaceAll('>', '>')
|
|
224
|
+
.replaceAll('"', '"');
|
|
225
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual Code Understanding — Feature 5, graph builder.
|
|
3
|
+
*
|
|
4
|
+
* Builds a module dependency graph from import/require statements in
|
|
5
|
+
* JS/TS-family source files (plus CSS @import). The graph powers:
|
|
6
|
+
* - relationship visualization (force-directed d3 graph)
|
|
7
|
+
* - impact analysis: which files (transitively) depend on a module —
|
|
8
|
+
* i.e. what breaks if you change it
|
|
9
|
+
* - a "heat" score per node (number of transitive dependents)
|
|
10
|
+
*
|
|
11
|
+
* Reuses the Phase 2 scanner for the safe file list (.gitignore-aware,
|
|
12
|
+
* node_modules excluded, size caps), so the graph can never explode.
|
|
13
|
+
*/
|
|
14
|
+
import { promises as fs } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { scanProject } from '../scanner/index.js';
|
|
17
|
+
/** Only these files are parsed for imports. */
|
|
18
|
+
const PARSEABLE = /\.(ts|tsx|js|jsx|mjs|cjs|vue|svelte|css|scss)$/;
|
|
19
|
+
/** Candidate suffixes when resolving extensionless relative imports. */
|
|
20
|
+
const RESOLVE_SUFFIXES = [
|
|
21
|
+
'', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.vue', '.svelte',
|
|
22
|
+
'/index.ts', '/index.tsx', '/index.js', '/index.jsx',
|
|
23
|
+
];
|
|
24
|
+
/** Match import/require/export-from specifiers. */
|
|
25
|
+
const IMPORT_RE = /(?:import|export)\s+(?:[\s\S]*?from\s+)?['"]([^'"\n]+)['"]|require\(\s*['"]([^'"\n]+)['"]\s*\)|import\(\s*['"]([^'"\n]+)['"]\s*\)|@import\s+['"]([^'"\n]+)['"]/g;
|
|
26
|
+
/** Build the dependency graph for a project (uses the cached-safe scanner). */
|
|
27
|
+
export async function buildGraph(root, map) {
|
|
28
|
+
const project = map ?? (await scanProject(root));
|
|
29
|
+
const files = project.files.filter((f) => PARSEABLE.test(f));
|
|
30
|
+
const fileSet = new Set(files);
|
|
31
|
+
const edges = [];
|
|
32
|
+
const externals = new Map();
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
const content = await fs.readFile(join(root, file), 'utf8').catch(() => '');
|
|
35
|
+
if (!content)
|
|
36
|
+
continue;
|
|
37
|
+
for (const match of content.matchAll(IMPORT_RE)) {
|
|
38
|
+
const spec = match[1] ?? match[2] ?? match[3] ?? match[4];
|
|
39
|
+
if (!spec)
|
|
40
|
+
continue;
|
|
41
|
+
if (spec.startsWith('.')) {
|
|
42
|
+
const target = resolveRelative(file, spec, fileSet);
|
|
43
|
+
if (target && target !== file)
|
|
44
|
+
edges.push({ source: file, target });
|
|
45
|
+
}
|
|
46
|
+
else if (!spec.startsWith('node:')) {
|
|
47
|
+
// External package: count by top-level package name (@scope/pkg or pkg)
|
|
48
|
+
const name = spec.startsWith('@') ? spec.split('/').slice(0, 2).join('/') : spec.split('/')[0];
|
|
49
|
+
externals.set(name, (externals.get(name) ?? 0) + 1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Deduplicate edges
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
const uniqueEdges = edges.filter((e) => {
|
|
56
|
+
const key = `${e.source}→${e.target}`;
|
|
57
|
+
if (seen.has(key))
|
|
58
|
+
return false;
|
|
59
|
+
seen.add(key);
|
|
60
|
+
return true;
|
|
61
|
+
});
|
|
62
|
+
// Degrees
|
|
63
|
+
const out = new Map();
|
|
64
|
+
const inn = new Map();
|
|
65
|
+
const dependentsOf = new Map(); // target → sources
|
|
66
|
+
for (const e of uniqueEdges) {
|
|
67
|
+
out.set(e.source, (out.get(e.source) ?? 0) + 1);
|
|
68
|
+
inn.set(e.target, (inn.get(e.target) ?? 0) + 1);
|
|
69
|
+
(dependentsOf.get(e.target) ?? dependentsOf.set(e.target, []).get(e.target)).push(e.source);
|
|
70
|
+
}
|
|
71
|
+
// Impact = number of transitive dependents (BFS up the dependents tree)
|
|
72
|
+
const impact = (id) => {
|
|
73
|
+
const visited = new Set();
|
|
74
|
+
const queue = [...(dependentsOf.get(id) ?? [])];
|
|
75
|
+
while (queue.length) {
|
|
76
|
+
const cur = queue.pop();
|
|
77
|
+
if (visited.has(cur))
|
|
78
|
+
continue;
|
|
79
|
+
visited.add(cur);
|
|
80
|
+
queue.push(...(dependentsOf.get(cur) ?? []));
|
|
81
|
+
}
|
|
82
|
+
return visited.size;
|
|
83
|
+
};
|
|
84
|
+
const nodes = files.map((id) => ({
|
|
85
|
+
id,
|
|
86
|
+
dir: id.includes('/') ? id.slice(0, id.indexOf('/')) : '.',
|
|
87
|
+
deps: out.get(id) ?? 0,
|
|
88
|
+
dependents: inn.get(id) ?? 0,
|
|
89
|
+
impact: impact(id),
|
|
90
|
+
}));
|
|
91
|
+
return {
|
|
92
|
+
project: project.name,
|
|
93
|
+
generatedAt: new Date().toISOString(),
|
|
94
|
+
nodes,
|
|
95
|
+
edges: uniqueEdges,
|
|
96
|
+
externals: [...externals.entries()]
|
|
97
|
+
.sort((a, b) => b[1] - a[1])
|
|
98
|
+
.slice(0, 20)
|
|
99
|
+
.map(([name, count]) => ({ name, count })),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Resolve a relative import specifier against the scanned file set. */
|
|
103
|
+
function resolveRelative(fromFile, spec, files) {
|
|
104
|
+
// Normalize `from/dir/../x` style paths manually (always forward slashes)
|
|
105
|
+
const baseDir = fromFile.includes('/') ? fromFile.slice(0, fromFile.lastIndexOf('/')) : '';
|
|
106
|
+
const parts = (baseDir ? baseDir + '/' + spec : spec).split('/');
|
|
107
|
+
const stack = [];
|
|
108
|
+
for (const part of parts) {
|
|
109
|
+
if (part === '.' || part === '')
|
|
110
|
+
continue;
|
|
111
|
+
else if (part === '..')
|
|
112
|
+
stack.pop();
|
|
113
|
+
else
|
|
114
|
+
stack.push(part);
|
|
115
|
+
}
|
|
116
|
+
const base = stack.join('/');
|
|
117
|
+
for (const suffix of RESOLVE_SUFFIXES) {
|
|
118
|
+
const candidate = base + suffix;
|
|
119
|
+
if (files.has(candidate))
|
|
120
|
+
return candidate;
|
|
121
|
+
// TS quirk: `./x.js` in source resolves to x.ts on disk
|
|
122
|
+
if (suffix === '' && /\.(js|mjs|cjs)$/.test(candidate)) {
|
|
123
|
+
const tsCandidate = candidate.replace(/\.(m|c)?js$/, '.ts');
|
|
124
|
+
if (files.has(tsCandidate))
|
|
125
|
+
return tsCandidate;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-language support for the terminal UI.
|
|
3
|
+
*
|
|
4
|
+
* Supported: English (en), Spanish (es), Portuguese (pt), French (fr), Arabic (ar).
|
|
5
|
+
*
|
|
6
|
+
* RTL strategy: terminals render Arabic broken (disconnected letters, wrong
|
|
7
|
+
* direction), so when the language is Arabic the interactive UI stays in
|
|
8
|
+
* English and a note is shown. Arabic is used in generated outputs
|
|
9
|
+
* (.md / .html explanations and replay exports — Phase 3) where it renders
|
|
10
|
+
* perfectly with dir="rtl".
|
|
11
|
+
*/
|
|
12
|
+
export const SUPPORTED_LANGS = ['en', 'ar', 'es', 'pt', 'fr'];
|
|
13
|
+
const en = {
|
|
14
|
+
welcome: 'Welcome to Vexi — your AI coding agent in the terminal.',
|
|
15
|
+
firstRunIntro: 'First run: Vexi needs an API key (Anthropic, OpenAI, OpenRouter, Groq or Gemini).\nIt is stored locally in ~/.vexi/config.json — no login, no server, no telemetry.',
|
|
16
|
+
enterApiKey: 'Paste your API key',
|
|
17
|
+
detectedProvider: 'Provider detected: {provider}',
|
|
18
|
+
detectFailed: 'Could not auto-detect the provider for this key.',
|
|
19
|
+
selectProvider: 'Select your provider',
|
|
20
|
+
configSaved: 'Saved to {path} (readable only by your OS user).',
|
|
21
|
+
chatHint: 'Type your message. Commands: /help /model /clear /exit',
|
|
22
|
+
thinking: 'Thinking…',
|
|
23
|
+
goodbye: 'Goodbye! 👋',
|
|
24
|
+
invalidKey: 'The API key was rejected by the provider (unauthorized).',
|
|
25
|
+
reenterKey: 'Re-enter your API key?',
|
|
26
|
+
apiError: 'API error: {message}',
|
|
27
|
+
emptyKey: 'No key entered.',
|
|
28
|
+
historyCleared: 'Conversation history cleared.',
|
|
29
|
+
helpText: '/help show this help\n/model switch model (e.g. /model gpt-4o)\n/memory show compressed project memory\n/clear clear conversation history\n/exit quit Vexi',
|
|
30
|
+
modelSwitched: 'Model switched to {model}',
|
|
31
|
+
configReset: 'Configuration deleted. Run `vexi` to set up again.',
|
|
32
|
+
configResetNone: 'No configuration found.',
|
|
33
|
+
scanning: 'Scanning project…',
|
|
34
|
+
scanned: 'Project scanned: {files} files mapped.',
|
|
35
|
+
memoryLoaded: 'Project memory loaded ({decisions} decisions remembered).',
|
|
36
|
+
memoryEmpty: 'No project memory yet — it builds up as you chat.',
|
|
37
|
+
skillsLoaded: 'Active skills: {names}',
|
|
38
|
+
skillAdded: 'Skill "{name}" added.',
|
|
39
|
+
skillRemoved: 'Skill "{name}" removed.',
|
|
40
|
+
skillNotFound: 'Skill "{name}" not found.',
|
|
41
|
+
skillListEmpty: 'No skills yet. Add one with: vexi skill add <file-or-url>',
|
|
42
|
+
replayExported: 'Replay exported: {path}',
|
|
43
|
+
replayNone: 'No recorded sessions yet — chat with Vexi first.',
|
|
44
|
+
explaining: 'Analyzing code…',
|
|
45
|
+
explainSavedFile: 'Explanation saved and opened in your browser: {path}',
|
|
46
|
+
graphBuilding: 'Building dependency graph…',
|
|
47
|
+
graphExported: 'Graph exported and opened in your browser: {path}',
|
|
48
|
+
mcpConnecting: 'Connecting MCP servers…',
|
|
49
|
+
mcpConnected: 'MCP connected: {servers} ({tools} tools)',
|
|
50
|
+
mcpFailed: 'MCP server "{name}" failed to connect.',
|
|
51
|
+
mcpRunningTool: 'Running tool {tool}…',
|
|
52
|
+
mcpListEmpty: 'No MCP servers configured. Add one with: vexi mcp add <name> <command> [args...]',
|
|
53
|
+
mcpAdded: 'MCP server "{name}" added.',
|
|
54
|
+
mcpRemoved: 'MCP server "{name}" removed.',
|
|
55
|
+
mcpNotFound: 'MCP server "{name}" not found.',
|
|
56
|
+
learnAnalyzing: 'Learning your coding style from past sessions…',
|
|
57
|
+
learnNoSessions: 'No recorded sessions yet — chat with Vexi first, then run `vexi learn`.',
|
|
58
|
+
learnNothing: 'Not enough signal yet — keep coding with Vexi and try again later.',
|
|
59
|
+
learnPreview: 'Learned from {sessions} sessions ({signals} corrections found):',
|
|
60
|
+
learnApplyHint: 'Looks right? Save it with: vexi learn --apply',
|
|
61
|
+
learnApplied: 'Learned style saved: {path} — now injected into every session.',
|
|
62
|
+
};
|
|
63
|
+
const es = {
|
|
64
|
+
welcome: 'Bienvenido a Vexi — tu agente de programación con IA en la terminal.',
|
|
65
|
+
firstRunIntro: 'Primer uso: Vexi necesita una clave API (Anthropic, OpenAI, OpenRouter, Groq o Gemini).\nSe guarda localmente en ~/.vexi/config.json — sin registro, sin servidor, sin telemetría.',
|
|
66
|
+
enterApiKey: 'Pega tu clave API',
|
|
67
|
+
detectedProvider: 'Proveedor detectado: {provider}',
|
|
68
|
+
detectFailed: 'No se pudo detectar automáticamente el proveedor de esta clave.',
|
|
69
|
+
selectProvider: 'Selecciona tu proveedor',
|
|
70
|
+
configSaved: 'Guardado en {path} (solo legible por tu usuario del sistema).',
|
|
71
|
+
chatHint: 'Escribe tu mensaje. Comandos: /help /model /clear /exit',
|
|
72
|
+
thinking: 'Pensando…',
|
|
73
|
+
goodbye: '¡Hasta luego! 👋',
|
|
74
|
+
invalidKey: 'El proveedor rechazó la clave API (no autorizada).',
|
|
75
|
+
reenterKey: '¿Volver a introducir tu clave API?',
|
|
76
|
+
apiError: 'Error de API: {message}',
|
|
77
|
+
emptyKey: 'No se introdujo ninguna clave.',
|
|
78
|
+
historyCleared: 'Historial de conversación borrado.',
|
|
79
|
+
helpText: '/help mostrar esta ayuda\n/model cambiar modelo (p. ej. /model gpt-4o)\n/memory ver la memoria comprimida del proyecto\n/clear borrar historial de conversación\n/exit salir de Vexi',
|
|
80
|
+
modelSwitched: 'Modelo cambiado a {model}',
|
|
81
|
+
configReset: 'Configuración eliminada. Ejecuta `vexi` para configurar de nuevo.',
|
|
82
|
+
configResetNone: 'No se encontró configuración.',
|
|
83
|
+
scanning: 'Escaneando el proyecto…',
|
|
84
|
+
scanned: 'Proyecto escaneado: {files} archivos mapeados.',
|
|
85
|
+
memoryLoaded: 'Memoria del proyecto cargada ({decisions} decisiones recordadas).',
|
|
86
|
+
memoryEmpty: 'Aún no hay memoria del proyecto — se construye mientras conversas.',
|
|
87
|
+
skillsLoaded: 'Skills activas: {names}',
|
|
88
|
+
skillAdded: 'Skill "{name}" añadida.',
|
|
89
|
+
skillRemoved: 'Skill "{name}" eliminada.',
|
|
90
|
+
skillNotFound: 'Skill "{name}" no encontrada.',
|
|
91
|
+
skillListEmpty: 'Aún no hay skills. Añade una con: vexi skill add <archivo-o-url>',
|
|
92
|
+
replayExported: 'Replay exportado: {path}',
|
|
93
|
+
replayNone: 'Aún no hay sesiones grabadas — conversa con Vexi primero.',
|
|
94
|
+
explaining: 'Analizando el código…',
|
|
95
|
+
explainSavedFile: 'Explicación guardada y abierta en tu navegador: {path}',
|
|
96
|
+
graphBuilding: 'Construyendo el grafo de dependencias…',
|
|
97
|
+
graphExported: 'Grafo exportado y abierto en tu navegador: {path}',
|
|
98
|
+
mcpConnecting: 'Conectando servidores MCP…',
|
|
99
|
+
mcpConnected: 'MCP conectado: {servers} ({tools} herramientas)',
|
|
100
|
+
mcpFailed: 'El servidor MCP "{name}" no pudo conectarse.',
|
|
101
|
+
mcpRunningTool: 'Ejecutando herramienta {tool}…',
|
|
102
|
+
mcpListEmpty: 'No hay servidores MCP configurados. Añade uno con: vexi mcp add <nombre> <comando> [args...]',
|
|
103
|
+
mcpAdded: 'Servidor MCP "{name}" añadido.',
|
|
104
|
+
mcpRemoved: 'Servidor MCP "{name}" eliminado.',
|
|
105
|
+
mcpNotFound: 'Servidor MCP "{name}" no encontrado.',
|
|
106
|
+
learnAnalyzing: 'Aprendiendo tu estilo de programación de sesiones pasadas…',
|
|
107
|
+
learnNoSessions: 'Aún no hay sesiones grabadas — conversa con Vexi primero y luego ejecuta `vexi learn`.',
|
|
108
|
+
learnNothing: 'Aún no hay suficiente señal — sigue programando con Vexi e inténtalo más tarde.',
|
|
109
|
+
learnPreview: 'Aprendido de {sessions} sesiones ({signals} correcciones encontradas):',
|
|
110
|
+
learnApplyHint: '¿Se ve bien? Guárdalo con: vexi learn --apply',
|
|
111
|
+
learnApplied: 'Estilo aprendido guardado: {path} — ahora se inyecta en cada sesión.',
|
|
112
|
+
};
|
|
113
|
+
const pt = {
|
|
114
|
+
welcome: 'Bem-vindo ao Vexi — seu agente de programação com IA no terminal.',
|
|
115
|
+
firstRunIntro: 'Primeira execução: o Vexi precisa de uma chave de API (Anthropic, OpenAI, OpenRouter, Groq ou Gemini).\nEla é guardada localmente em ~/.vexi/config.json — sem login, sem servidor, sem telemetria.',
|
|
116
|
+
enterApiKey: 'Cole sua chave de API',
|
|
117
|
+
detectedProvider: 'Provedor detectado: {provider}',
|
|
118
|
+
detectFailed: 'Não foi possível detectar automaticamente o provedor desta chave.',
|
|
119
|
+
selectProvider: 'Selecione seu provedor',
|
|
120
|
+
configSaved: 'Salvo em {path} (legível apenas pelo seu usuário do sistema).',
|
|
121
|
+
chatHint: 'Digite sua mensagem. Comandos: /help /model /clear /exit',
|
|
122
|
+
thinking: 'Pensando…',
|
|
123
|
+
goodbye: 'Até logo! 👋',
|
|
124
|
+
invalidKey: 'O provedor rejeitou a chave de API (não autorizada).',
|
|
125
|
+
reenterKey: 'Inserir sua chave de API novamente?',
|
|
126
|
+
apiError: 'Erro de API: {message}',
|
|
127
|
+
emptyKey: 'Nenhuma chave inserida.',
|
|
128
|
+
historyCleared: 'Histórico de conversa apagado.',
|
|
129
|
+
helpText: '/help mostrar esta ajuda\n/model trocar modelo (ex.: /model gpt-4o)\n/memory ver a memória comprimida do projeto\n/clear apagar histórico de conversa\n/exit sair do Vexi',
|
|
130
|
+
modelSwitched: 'Modelo alterado para {model}',
|
|
131
|
+
configReset: 'Configuração excluída. Execute `vexi` para configurar novamente.',
|
|
132
|
+
configResetNone: 'Nenhuma configuração encontrada.',
|
|
133
|
+
scanning: 'Escaneando o projeto…',
|
|
134
|
+
scanned: 'Projeto escaneado: {files} arquivos mapeados.',
|
|
135
|
+
memoryLoaded: 'Memória do projeto carregada ({decisions} decisões lembradas).',
|
|
136
|
+
memoryEmpty: 'Ainda não há memória do projeto — ela se constrói enquanto você conversa.',
|
|
137
|
+
skillsLoaded: 'Skills ativas: {names}',
|
|
138
|
+
skillAdded: 'Skill "{name}" adicionada.',
|
|
139
|
+
skillRemoved: 'Skill "{name}" removida.',
|
|
140
|
+
skillNotFound: 'Skill "{name}" não encontrada.',
|
|
141
|
+
skillListEmpty: 'Ainda não há skills. Adicione uma com: vexi skill add <arquivo-ou-url>',
|
|
142
|
+
replayExported: 'Replay exportado: {path}',
|
|
143
|
+
replayNone: 'Ainda não há sessões gravadas — converse com o Vexi primeiro.',
|
|
144
|
+
explaining: 'Analisando o código…',
|
|
145
|
+
explainSavedFile: 'Explicação salva e aberta no seu navegador: {path}',
|
|
146
|
+
graphBuilding: 'Construindo o grafo de dependências…',
|
|
147
|
+
graphExported: 'Grafo exportado e aberto no seu navegador: {path}',
|
|
148
|
+
mcpConnecting: 'Conectando servidores MCP…',
|
|
149
|
+
mcpConnected: 'MCP conectado: {servers} ({tools} ferramentas)',
|
|
150
|
+
mcpFailed: 'O servidor MCP "{name}" não conseguiu conectar.',
|
|
151
|
+
mcpRunningTool: 'Executando ferramenta {tool}…',
|
|
152
|
+
mcpListEmpty: 'Nenhum servidor MCP configurado. Adicione um com: vexi mcp add <nome> <comando> [args...]',
|
|
153
|
+
mcpAdded: 'Servidor MCP "{name}" adicionado.',
|
|
154
|
+
mcpRemoved: 'Servidor MCP "{name}" removido.',
|
|
155
|
+
mcpNotFound: 'Servidor MCP "{name}" não encontrado.',
|
|
156
|
+
learnAnalyzing: 'Aprendendo seu estilo de programação com sessões passadas…',
|
|
157
|
+
learnNoSessions: 'Ainda não há sessões gravadas — converse com o Vexi primeiro e depois execute `vexi learn`.',
|
|
158
|
+
learnNothing: 'Ainda não há sinal suficiente — continue programando com o Vexi e tente mais tarde.',
|
|
159
|
+
learnPreview: 'Aprendido de {sessions} sessões ({signals} correções encontradas):',
|
|
160
|
+
learnApplyHint: 'Parece certo? Salve com: vexi learn --apply',
|
|
161
|
+
learnApplied: 'Estilo aprendido salvo: {path} — agora é injetado em cada sessão.',
|
|
162
|
+
};
|
|
163
|
+
const fr = {
|
|
164
|
+
welcome: 'Bienvenue dans Vexi — votre agent de codage IA dans le terminal.',
|
|
165
|
+
firstRunIntro: 'Première utilisation : Vexi a besoin d\'une clé API (Anthropic, OpenAI, OpenRouter, Groq ou Gemini).\nElle est stockée localement dans ~/.vexi/config.json — sans compte, sans serveur, sans télémétrie.',
|
|
166
|
+
enterApiKey: 'Collez votre clé API',
|
|
167
|
+
detectedProvider: 'Fournisseur détecté : {provider}',
|
|
168
|
+
detectFailed: 'Impossible de détecter automatiquement le fournisseur de cette clé.',
|
|
169
|
+
selectProvider: 'Sélectionnez votre fournisseur',
|
|
170
|
+
configSaved: 'Enregistré dans {path} (lisible uniquement par votre utilisateur système).',
|
|
171
|
+
chatHint: 'Tapez votre message. Commandes : /help /model /clear /exit',
|
|
172
|
+
thinking: 'Réflexion…',
|
|
173
|
+
goodbye: 'À bientôt ! 👋',
|
|
174
|
+
invalidKey: 'La clé API a été rejetée par le fournisseur (non autorisée).',
|
|
175
|
+
reenterKey: 'Saisir à nouveau votre clé API ?',
|
|
176
|
+
apiError: 'Erreur API : {message}',
|
|
177
|
+
emptyKey: 'Aucune clé saisie.',
|
|
178
|
+
historyCleared: 'Historique de conversation effacé.',
|
|
179
|
+
helpText: '/help afficher cette aide\n/model changer de modèle (ex. /model gpt-4o)\n/memory voir la mémoire compressée du projet\n/clear effacer l\'historique de conversation\n/exit quitter Vexi',
|
|
180
|
+
modelSwitched: 'Modèle changé pour {model}',
|
|
181
|
+
configReset: 'Configuration supprimée. Lancez `vexi` pour reconfigurer.',
|
|
182
|
+
configResetNone: 'Aucune configuration trouvée.',
|
|
183
|
+
scanning: 'Analyse du projet…',
|
|
184
|
+
scanned: 'Projet analysé : {files} fichiers cartographiés.',
|
|
185
|
+
memoryLoaded: 'Mémoire du projet chargée ({decisions} décisions mémorisées).',
|
|
186
|
+
memoryEmpty: 'Pas encore de mémoire de projet — elle se construit au fil de la conversation.',
|
|
187
|
+
skillsLoaded: 'Skills actives : {names}',
|
|
188
|
+
skillAdded: 'Skill « {name} » ajoutée.',
|
|
189
|
+
skillRemoved: 'Skill « {name} » supprimée.',
|
|
190
|
+
skillNotFound: 'Skill « {name} » introuvable.',
|
|
191
|
+
skillListEmpty: 'Aucune skill pour l\'instant. Ajoutez-en une avec : vexi skill add <fichier-ou-url>',
|
|
192
|
+
replayExported: 'Replay exporté : {path}',
|
|
193
|
+
replayNone: 'Aucune session enregistrée — discutez d\'abord avec Vexi.',
|
|
194
|
+
explaining: 'Analyse du code…',
|
|
195
|
+
explainSavedFile: 'Explication enregistrée et ouverte dans votre navigateur : {path}',
|
|
196
|
+
graphBuilding: 'Construction du graphe de dépendances…',
|
|
197
|
+
graphExported: 'Graphe exporté et ouvert dans votre navigateur : {path}',
|
|
198
|
+
mcpConnecting: 'Connexion aux serveurs MCP…',
|
|
199
|
+
mcpConnected: 'MCP connecté : {servers} ({tools} outils)',
|
|
200
|
+
mcpFailed: 'Le serveur MCP « {name} » n\'a pas pu se connecter.',
|
|
201
|
+
mcpRunningTool: 'Exécution de l\'outil {tool}…',
|
|
202
|
+
mcpListEmpty: 'Aucun serveur MCP configuré. Ajoutez-en un avec : vexi mcp add <nom> <commande> [args...]',
|
|
203
|
+
mcpAdded: 'Serveur MCP « {name} » ajouté.',
|
|
204
|
+
mcpRemoved: 'Serveur MCP « {name} » supprimé.',
|
|
205
|
+
mcpNotFound: 'Serveur MCP « {name} » introuvable.',
|
|
206
|
+
learnAnalyzing: 'Apprentissage de votre style de code à partir des sessions passées…',
|
|
207
|
+
learnNoSessions: 'Aucune session enregistrée — discutez d\'abord avec Vexi, puis lancez `vexi learn`.',
|
|
208
|
+
learnNothing: 'Pas encore assez de signal — continuez à coder avec Vexi et réessayez plus tard.',
|
|
209
|
+
learnPreview: 'Appris de {sessions} sessions ({signals} corrections trouvées) :',
|
|
210
|
+
learnApplyHint: 'Ça vous convient ? Enregistrez avec : vexi learn --apply',
|
|
211
|
+
learnApplied: 'Style appris enregistré : {path} — désormais injecté dans chaque session.',
|
|
212
|
+
};
|
|
213
|
+
const STRINGS = {
|
|
214
|
+
en,
|
|
215
|
+
es,
|
|
216
|
+
pt,
|
|
217
|
+
fr,
|
|
218
|
+
// Arabic uses the English UI in the terminal (see RTL strategy above).
|
|
219
|
+
ar: en,
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* Note shown when the language is Arabic: terminals can't render RTL text
|
|
223
|
+
* properly, so the interactive UI stays in English. Arabic will be used in
|
|
224
|
+
* generated .md/.html outputs (Phase 3) where it renders perfectly.
|
|
225
|
+
*/
|
|
226
|
+
export const ARABIC_RTL_NOTE = 'Note: terminals render Arabic incorrectly (RTL), so the interactive UI stays in English.\n' +
|
|
227
|
+
'Arabic will be used in generated explanations and HTML exports, where it renders perfectly.\n' +
|
|
228
|
+
'ملاحظة: واجهة الطرفية بالإنجليزية بسبب قيود عرض النص العربي، وستكون الشروحات والملفات المُصدَّرة بالعربية.';
|
|
229
|
+
/** Normalize a locale/lang value ("ar-EG", "es_ES", "fr") to a supported Lang. */
|
|
230
|
+
export function normalizeLang(value) {
|
|
231
|
+
if (!value)
|
|
232
|
+
return null;
|
|
233
|
+
const base = value.toLowerCase().replace(/[_-].*$/, '');
|
|
234
|
+
return SUPPORTED_LANGS.includes(base) ? base : null;
|
|
235
|
+
}
|
|
236
|
+
/** Detect the system language from the environment / Intl. */
|
|
237
|
+
export function detectSystemLang() {
|
|
238
|
+
const fromEnv = normalizeLang(process.env.LC_ALL) ||
|
|
239
|
+
normalizeLang(process.env.LC_MESSAGES) ||
|
|
240
|
+
normalizeLang(process.env.LANG);
|
|
241
|
+
if (fromEnv)
|
|
242
|
+
return fromEnv;
|
|
243
|
+
try {
|
|
244
|
+
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
245
|
+
const fromIntl = normalizeLang(locale);
|
|
246
|
+
if (fromIntl)
|
|
247
|
+
return fromIntl;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// fall through
|
|
251
|
+
}
|
|
252
|
+
return 'en';
|
|
253
|
+
}
|
|
254
|
+
/** Get the UI strings for a language, with {placeholder} interpolation. */
|
|
255
|
+
export function getStrings(lang) {
|
|
256
|
+
return STRINGS[lang];
|
|
257
|
+
}
|
|
258
|
+
export function t(template, vars = {}) {
|
|
259
|
+
return template.replace(/\{(\w+)\}/g, (_, name) => vars[name] ?? `{${name}}`);
|
|
260
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Vexi entry point.
|
|
4
|
+
*/
|
|
5
|
+
import { buildCli } from './cli.js';
|
|
6
|
+
// Node 18+ is required (built-in fetch / web streams).
|
|
7
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
8
|
+
if (major < 18) {
|
|
9
|
+
console.error(`Vexi requires Node.js 18 or newer (you have ${process.versions.node}).`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
// Exit cleanly on Ctrl+C anywhere outside a prompt.
|
|
13
|
+
process.on('SIGINT', () => {
|
|
14
|
+
process.exit(0);
|
|
15
|
+
});
|
|
16
|
+
buildCli()
|
|
17
|
+
.parseAsync(process.argv)
|
|
18
|
+
.catch((error) => {
|
|
19
|
+
// @inquirer throws ExitPromptError when the user presses Ctrl+C in a prompt
|
|
20
|
+
if (error instanceof Error && error.name === 'ExitPromptError') {
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
console.error(error instanceof Error ? error.message : error);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|