mcmodding-mcp 0.2.2 → 0.3.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/README.md +43 -0
- package/dist/cli/{install.d.ts → manage.d.ts} +1 -1
- package/dist/cli/manage.d.ts.map +1 -0
- package/dist/cli/manage.js +749 -0
- package/dist/cli/manage.js.map +1 -0
- package/dist/db-versioning.d.ts +1 -1
- package/dist/db-versioning.d.ts.map +1 -1
- package/dist/db-versioning.js +14 -6
- package/dist/db-versioning.js.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/indexer/store.d.ts +15 -0
- package/dist/indexer/store.d.ts.map +1 -1
- package/dist/indexer/store.js +44 -26
- package/dist/indexer/store.js.map +1 -1
- package/dist/services/example-service.d.ts +2 -1
- package/dist/services/example-service.d.ts.map +1 -1
- package/dist/services/example-service.js +71 -3
- package/dist/services/example-service.js.map +1 -1
- package/dist/services/search-service.js +3 -3
- package/dist/services/search-service.js.map +1 -1
- package/dist/services/search-utils.d.ts.map +1 -1
- package/dist/services/search-utils.js +66 -9
- package/dist/services/search-utils.js.map +1 -1
- package/dist/tools/getExample.d.ts +1 -1
- package/dist/tools/getExample.d.ts.map +1 -1
- package/dist/tools/getExample.js +2 -2
- package/dist/tools/getExample.js.map +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +36 -18
- package/dist/cli/install.d.ts.map +0 -1
- package/dist/cli/install.js +0 -397
- package/dist/cli/install.js.map +0 -1
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import https from 'https';
|
|
4
|
+
import readline from 'readline';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const CONFIG = {
|
|
9
|
+
repoOwner: 'OGMatrix',
|
|
10
|
+
repoName: 'mcmodding-mcp',
|
|
11
|
+
dataDir: path.join(__dirname, '..', '..', 'data'),
|
|
12
|
+
userAgent: 'mcmodding-mcp-installer',
|
|
13
|
+
};
|
|
14
|
+
const AVAILABLE_DBS = [
|
|
15
|
+
{
|
|
16
|
+
id: 'mcmodding-docs',
|
|
17
|
+
name: 'Documentation Database',
|
|
18
|
+
fileName: 'mcmodding-docs.db',
|
|
19
|
+
manifestName: 'db-manifest.json',
|
|
20
|
+
description: 'Core Fabric & NeoForge documentation - installed by default',
|
|
21
|
+
tagPrefix: 'v',
|
|
22
|
+
icon: '📚',
|
|
23
|
+
isRequired: true,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'mod-examples',
|
|
27
|
+
name: 'Mod Examples Database',
|
|
28
|
+
fileName: 'mod-examples.db',
|
|
29
|
+
manifestName: 'mod-examples-manifest.json',
|
|
30
|
+
description: '1000+ high-quality modding examples for Fabric & NeoForge',
|
|
31
|
+
tagPrefix: 'examples-v',
|
|
32
|
+
icon: '🧩',
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
const isColorSupported = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
36
|
+
const c = {
|
|
37
|
+
reset: isColorSupported ? '\x1b[0m' : '',
|
|
38
|
+
bold: isColorSupported ? '\x1b[1m' : '',
|
|
39
|
+
dim: isColorSupported ? '\x1b[2m' : '',
|
|
40
|
+
italic: isColorSupported ? '\x1b[3m' : '',
|
|
41
|
+
underline: isColorSupported ? '\x1b[4m' : '',
|
|
42
|
+
black: isColorSupported ? '\x1b[30m' : '',
|
|
43
|
+
red: isColorSupported ? '\x1b[31m' : '',
|
|
44
|
+
green: isColorSupported ? '\x1b[32m' : '',
|
|
45
|
+
yellow: isColorSupported ? '\x1b[33m' : '',
|
|
46
|
+
blue: isColorSupported ? '\x1b[34m' : '',
|
|
47
|
+
magenta: isColorSupported ? '\x1b[35m' : '',
|
|
48
|
+
cyan: isColorSupported ? '\x1b[36m' : '',
|
|
49
|
+
white: isColorSupported ? '\x1b[37m' : '',
|
|
50
|
+
brightBlack: isColorSupported ? '\x1b[90m' : '',
|
|
51
|
+
brightRed: isColorSupported ? '\x1b[91m' : '',
|
|
52
|
+
brightGreen: isColorSupported ? '\x1b[92m' : '',
|
|
53
|
+
brightYellow: isColorSupported ? '\x1b[93m' : '',
|
|
54
|
+
brightBlue: isColorSupported ? '\x1b[94m' : '',
|
|
55
|
+
brightMagenta: isColorSupported ? '\x1b[95m' : '',
|
|
56
|
+
brightCyan: isColorSupported ? '\x1b[96m' : '',
|
|
57
|
+
brightWhite: isColorSupported ? '\x1b[97m' : '',
|
|
58
|
+
bgBlue: isColorSupported ? '\x1b[44m' : '',
|
|
59
|
+
clearLine: isColorSupported ? '\x1b[2K' : '',
|
|
60
|
+
cursorUp: isColorSupported ? '\x1b[1A' : '',
|
|
61
|
+
cursorHide: isColorSupported ? '\x1b[?25l' : '',
|
|
62
|
+
cursorShow: isColorSupported ? '\x1b[?25h' : '',
|
|
63
|
+
};
|
|
64
|
+
const sym = {
|
|
65
|
+
topLeft: '╔',
|
|
66
|
+
topRight: '╗',
|
|
67
|
+
bottomLeft: '╚',
|
|
68
|
+
bottomRight: '╝',
|
|
69
|
+
horizontal: '═',
|
|
70
|
+
vertical: '║',
|
|
71
|
+
sTopLeft: '┌',
|
|
72
|
+
sTopRight: '┐',
|
|
73
|
+
sBottomLeft: '└',
|
|
74
|
+
sBottomRight: '┘',
|
|
75
|
+
sHorizontal: '─',
|
|
76
|
+
sVertical: '│',
|
|
77
|
+
barFull: '█',
|
|
78
|
+
barThreeQuarter: '▓',
|
|
79
|
+
barHalf: '▒',
|
|
80
|
+
barQuarter: '░',
|
|
81
|
+
barEmpty: '░',
|
|
82
|
+
check: '✔',
|
|
83
|
+
cross: '✖',
|
|
84
|
+
warning: '⚠',
|
|
85
|
+
info: 'ℹ',
|
|
86
|
+
star: '★',
|
|
87
|
+
sparkle: '✨',
|
|
88
|
+
rocket: '🚀',
|
|
89
|
+
package: '📦',
|
|
90
|
+
database: '🗄️',
|
|
91
|
+
download: '⬇',
|
|
92
|
+
shield: '🛡️',
|
|
93
|
+
clock: '⏱',
|
|
94
|
+
lightning: '⚡',
|
|
95
|
+
arrowRight: '▶',
|
|
96
|
+
dot: '●',
|
|
97
|
+
circle: '○',
|
|
98
|
+
selected: '◉',
|
|
99
|
+
unselected: '○',
|
|
100
|
+
update: '↻',
|
|
101
|
+
cube: '◆',
|
|
102
|
+
pause: '⏸',
|
|
103
|
+
play: '▶',
|
|
104
|
+
stop: '⏹',
|
|
105
|
+
};
|
|
106
|
+
const cursor = {
|
|
107
|
+
save: isColorSupported ? '\x1b[s' : '',
|
|
108
|
+
restore: isColorSupported ? '\x1b[u' : '',
|
|
109
|
+
toColumn: (col) => (isColorSupported ? `\x1b[${col}G` : ''),
|
|
110
|
+
up: (n) => (isColorSupported ? `\x1b[${n}A` : ''),
|
|
111
|
+
down: (n) => (isColorSupported ? `\x1b[${n}B` : ''),
|
|
112
|
+
};
|
|
113
|
+
function getTerminalWidth() {
|
|
114
|
+
return process.stdout.columns || 80;
|
|
115
|
+
}
|
|
116
|
+
function centerText(text, width) {
|
|
117
|
+
const cleanText = text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
118
|
+
const totalPadding = Math.max(0, width - cleanText.length);
|
|
119
|
+
const leftPadding = Math.floor(totalPadding / 2);
|
|
120
|
+
const rightPadding = totalPadding - leftPadding;
|
|
121
|
+
return ' '.repeat(leftPadding) + text + ' '.repeat(rightPadding);
|
|
122
|
+
}
|
|
123
|
+
function formatBytes(bytes) {
|
|
124
|
+
if (bytes === 0)
|
|
125
|
+
return '0 B';
|
|
126
|
+
const k = 1024;
|
|
127
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
128
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
129
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
130
|
+
}
|
|
131
|
+
function formatSpeed(bytesPerSecond) {
|
|
132
|
+
return formatBytes(bytesPerSecond) + '/s';
|
|
133
|
+
}
|
|
134
|
+
function formatTime(seconds) {
|
|
135
|
+
if (!isFinite(seconds) || seconds <= 0)
|
|
136
|
+
return '--:--';
|
|
137
|
+
if (seconds < 60)
|
|
138
|
+
return `${Math.round(seconds)}s`;
|
|
139
|
+
const mins = Math.floor(seconds / 60);
|
|
140
|
+
const secs = Math.round(seconds % 60);
|
|
141
|
+
return `${mins}m ${secs.toString().padStart(2, '0')}s`;
|
|
142
|
+
}
|
|
143
|
+
function padLine(text, width) {
|
|
144
|
+
const cleanText = text.replace(/\x1b\[[0-9;]*m/g, '');
|
|
145
|
+
const padding = Math.max(0, width - cleanText.length);
|
|
146
|
+
return text + ' '.repeat(padding);
|
|
147
|
+
}
|
|
148
|
+
async function fetchJson(url) {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const options = {
|
|
151
|
+
headers: { 'User-Agent': CONFIG.userAgent },
|
|
152
|
+
};
|
|
153
|
+
https
|
|
154
|
+
.get(url, options, (res) => {
|
|
155
|
+
if (res.statusCode !== 200) {
|
|
156
|
+
res.resume();
|
|
157
|
+
return reject(new Error(`Request failed with status code ${res.statusCode}`));
|
|
158
|
+
}
|
|
159
|
+
let data = '';
|
|
160
|
+
res.on('data', (chunk) => (data += chunk));
|
|
161
|
+
res.on('end', () => {
|
|
162
|
+
try {
|
|
163
|
+
resolve(JSON.parse(data));
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
})
|
|
170
|
+
.on('error', (err) => reject(err instanceof Error ? err : new Error(String(err))));
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async function downloadFile(url, destPath, onProgress) {
|
|
174
|
+
return new Promise((resolve, reject) => {
|
|
175
|
+
const file = fs.createWriteStream(destPath);
|
|
176
|
+
const options = {
|
|
177
|
+
headers: { 'User-Agent': CONFIG.userAgent },
|
|
178
|
+
};
|
|
179
|
+
https
|
|
180
|
+
.get(url, options, (res) => {
|
|
181
|
+
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
182
|
+
if (!res.headers.location) {
|
|
183
|
+
return reject(new Error('Redirect location missing'));
|
|
184
|
+
}
|
|
185
|
+
file.close();
|
|
186
|
+
downloadFile(res.headers.location, destPath, onProgress).then(resolve).catch(reject);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (res.statusCode !== 200) {
|
|
190
|
+
res.resume();
|
|
191
|
+
return reject(new Error(`Download failed with status code ${res.statusCode}`));
|
|
192
|
+
}
|
|
193
|
+
const totalSize = parseInt(res.headers['content-length'] || '0', 10);
|
|
194
|
+
let downloadedSize = 0;
|
|
195
|
+
res.on('data', (chunk) => {
|
|
196
|
+
downloadedSize += chunk.length;
|
|
197
|
+
file.write(chunk);
|
|
198
|
+
if (onProgress) {
|
|
199
|
+
onProgress(downloadedSize, totalSize);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
res.on('end', () => {
|
|
203
|
+
file.end();
|
|
204
|
+
resolve();
|
|
205
|
+
});
|
|
206
|
+
res.on('error', (err) => {
|
|
207
|
+
fs.unlink(destPath, () => { });
|
|
208
|
+
reject(err);
|
|
209
|
+
});
|
|
210
|
+
})
|
|
211
|
+
.on('error', (err) => {
|
|
212
|
+
fs.unlink(destPath, () => { });
|
|
213
|
+
reject(err);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
function downloadFileInteractive(url, destPath, progress) {
|
|
218
|
+
let abortController = null;
|
|
219
|
+
let pauseResolve = null;
|
|
220
|
+
let keyHandler = null;
|
|
221
|
+
let currentRes = null;
|
|
222
|
+
const cleanup = () => {
|
|
223
|
+
if (keyHandler) {
|
|
224
|
+
process.stdin.removeListener('data', keyHandler);
|
|
225
|
+
process.stdin.setRawMode(false);
|
|
226
|
+
process.stdin.pause();
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
const promise = new Promise((resolve, reject) => {
|
|
230
|
+
const file = fs.createWriteStream(destPath);
|
|
231
|
+
const options = {
|
|
232
|
+
headers: { 'User-Agent': CONFIG.userAgent },
|
|
233
|
+
};
|
|
234
|
+
process.stdin.setRawMode(true);
|
|
235
|
+
process.stdin.resume();
|
|
236
|
+
process.stdin.setEncoding('utf8');
|
|
237
|
+
keyHandler = (key) => {
|
|
238
|
+
const keyStr = key.toString().toLowerCase();
|
|
239
|
+
if (keyStr === 'c' || keyStr === '\u0003') {
|
|
240
|
+
progress.cancel();
|
|
241
|
+
if (currentRes) {
|
|
242
|
+
currentRes.destroy();
|
|
243
|
+
}
|
|
244
|
+
cleanup();
|
|
245
|
+
fs.unlink(destPath, () => { });
|
|
246
|
+
reject(new Error('Download cancelled by user'));
|
|
247
|
+
}
|
|
248
|
+
else if (keyStr === 'p' || keyStr === ' ') {
|
|
249
|
+
progress.togglePause();
|
|
250
|
+
if (progress.paused) {
|
|
251
|
+
if (currentRes) {
|
|
252
|
+
currentRes.pause();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
if (currentRes) {
|
|
257
|
+
currentRes.resume();
|
|
258
|
+
}
|
|
259
|
+
if (pauseResolve) {
|
|
260
|
+
pauseResolve();
|
|
261
|
+
pauseResolve = null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
progress.forceRedraw();
|
|
265
|
+
}
|
|
266
|
+
else if (keyStr === 'i') {
|
|
267
|
+
progress.toggleDetail();
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
process.stdin.on('data', keyHandler);
|
|
271
|
+
const makeRequest = (requestUrl) => {
|
|
272
|
+
const req = https.get(requestUrl, options, (res) => {
|
|
273
|
+
currentRes = res;
|
|
274
|
+
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
275
|
+
if (!res.headers.location) {
|
|
276
|
+
cleanup();
|
|
277
|
+
return reject(new Error('Redirect location missing'));
|
|
278
|
+
}
|
|
279
|
+
makeRequest(res.headers.location);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (res.statusCode !== 200) {
|
|
283
|
+
res.resume();
|
|
284
|
+
cleanup();
|
|
285
|
+
return reject(new Error(`Download failed with status code ${res.statusCode}`));
|
|
286
|
+
}
|
|
287
|
+
const totalSize = parseInt(res.headers['content-length'] || '0', 10);
|
|
288
|
+
let downloadedSize = 0;
|
|
289
|
+
res.on('data', (chunk) => {
|
|
290
|
+
if (progress.cancelled)
|
|
291
|
+
return;
|
|
292
|
+
downloadedSize += chunk.length;
|
|
293
|
+
file.write(chunk);
|
|
294
|
+
progress.update(downloadedSize, totalSize);
|
|
295
|
+
});
|
|
296
|
+
res.on('end', () => {
|
|
297
|
+
file.end();
|
|
298
|
+
cleanup();
|
|
299
|
+
if (!progress.cancelled) {
|
|
300
|
+
resolve();
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
res.on('error', (err) => {
|
|
304
|
+
cleanup();
|
|
305
|
+
fs.unlink(destPath, () => { });
|
|
306
|
+
reject(err);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
req.on('error', (err) => {
|
|
310
|
+
cleanup();
|
|
311
|
+
fs.unlink(destPath, () => { });
|
|
312
|
+
reject(err);
|
|
313
|
+
});
|
|
314
|
+
abortController = {
|
|
315
|
+
abort: () => {
|
|
316
|
+
req.destroy();
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
makeRequest(url);
|
|
321
|
+
});
|
|
322
|
+
return {
|
|
323
|
+
promise,
|
|
324
|
+
abort: () => {
|
|
325
|
+
if (abortController) {
|
|
326
|
+
abortController.abort();
|
|
327
|
+
}
|
|
328
|
+
cleanup();
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function printHeader() {
|
|
333
|
+
console.clear();
|
|
334
|
+
const width = Math.min(getTerminalWidth(), 80);
|
|
335
|
+
const innerWidth = width - 4;
|
|
336
|
+
console.log(c.brightCyan + sym.topLeft + sym.horizontal.repeat(width - 2) + sym.topRight + c.reset);
|
|
337
|
+
console.log(c.brightCyan +
|
|
338
|
+
sym.vertical +
|
|
339
|
+
c.reset +
|
|
340
|
+
centerText(`${c.brightWhite}${c.bold}MCModding-MCP Database Manager${c.reset}`, innerWidth) +
|
|
341
|
+
c.brightCyan +
|
|
342
|
+
sym.vertical +
|
|
343
|
+
c.reset);
|
|
344
|
+
console.log(c.brightCyan +
|
|
345
|
+
sym.vertical +
|
|
346
|
+
c.reset +
|
|
347
|
+
centerText(`${c.dim}Install, update, and manage your documentation databases${c.reset}`, innerWidth) +
|
|
348
|
+
c.brightCyan +
|
|
349
|
+
sym.vertical +
|
|
350
|
+
c.reset);
|
|
351
|
+
console.log(c.brightCyan + sym.bottomLeft + sym.horizontal.repeat(width - 2) + sym.bottomRight + c.reset);
|
|
352
|
+
console.log();
|
|
353
|
+
}
|
|
354
|
+
class ProgressDisplay {
|
|
355
|
+
lines = 0;
|
|
356
|
+
lastUpdate = 0;
|
|
357
|
+
label;
|
|
358
|
+
startTime;
|
|
359
|
+
speeds = [];
|
|
360
|
+
detailedView = false;
|
|
361
|
+
isPaused = false;
|
|
362
|
+
isCancelled = false;
|
|
363
|
+
lastDownloaded = 0;
|
|
364
|
+
lastTotal = 0;
|
|
365
|
+
speedCalcDownloaded = 0;
|
|
366
|
+
lastSpeedTime = 0;
|
|
367
|
+
initialized = false;
|
|
368
|
+
constructor(label = 'Downloading') {
|
|
369
|
+
this.label = label;
|
|
370
|
+
this.startTime = Date.now();
|
|
371
|
+
this.lastSpeedTime = this.startTime;
|
|
372
|
+
}
|
|
373
|
+
get paused() {
|
|
374
|
+
return this.isPaused;
|
|
375
|
+
}
|
|
376
|
+
get cancelled() {
|
|
377
|
+
return this.isCancelled;
|
|
378
|
+
}
|
|
379
|
+
togglePause() {
|
|
380
|
+
this.isPaused = !this.isPaused;
|
|
381
|
+
if (!this.isPaused) {
|
|
382
|
+
this.lastSpeedTime = Date.now();
|
|
383
|
+
this.speedCalcDownloaded = this.lastDownloaded;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
cancel() {
|
|
387
|
+
this.isCancelled = true;
|
|
388
|
+
}
|
|
389
|
+
toggleDetail() {
|
|
390
|
+
this.detailedView = !this.detailedView;
|
|
391
|
+
}
|
|
392
|
+
forceRedraw() {
|
|
393
|
+
this.lastUpdate = 0;
|
|
394
|
+
if (this.lastTotal > 0) {
|
|
395
|
+
this.update(this.lastDownloaded, this.lastTotal);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
calculateSpeed(downloaded) {
|
|
399
|
+
const now = Date.now();
|
|
400
|
+
const timeDelta = (now - this.lastSpeedTime) / 1000;
|
|
401
|
+
if (timeDelta >= 0.5) {
|
|
402
|
+
const bytesDelta = downloaded - this.speedCalcDownloaded;
|
|
403
|
+
const instantSpeed = bytesDelta / timeDelta;
|
|
404
|
+
this.speeds.push(instantSpeed);
|
|
405
|
+
if (this.speeds.length > 5)
|
|
406
|
+
this.speeds.shift();
|
|
407
|
+
this.speedCalcDownloaded = downloaded;
|
|
408
|
+
this.lastSpeedTime = now;
|
|
409
|
+
}
|
|
410
|
+
if (this.speeds.length === 0)
|
|
411
|
+
return 0;
|
|
412
|
+
return this.speeds.reduce((a, b) => a + b, 0) / this.speeds.length;
|
|
413
|
+
}
|
|
414
|
+
update(downloaded, total) {
|
|
415
|
+
if (this.isCancelled)
|
|
416
|
+
return;
|
|
417
|
+
this.lastDownloaded = downloaded;
|
|
418
|
+
this.lastTotal = total;
|
|
419
|
+
const now = Date.now();
|
|
420
|
+
if (this.initialized && now - this.lastUpdate < 80)
|
|
421
|
+
return;
|
|
422
|
+
this.lastUpdate = now;
|
|
423
|
+
const width = Math.min(getTerminalWidth(), 80);
|
|
424
|
+
const barWidth = Math.max(10, width - 35);
|
|
425
|
+
const percentage = total > 0 ? Math.min(100, (downloaded / total) * 100) : 0;
|
|
426
|
+
const filledWidth = Math.round((percentage / 100) * barWidth);
|
|
427
|
+
const emptyWidth = barWidth - filledWidth;
|
|
428
|
+
const filledBar = c.brightGreen + sym.barFull.repeat(filledWidth) + c.reset;
|
|
429
|
+
const emptyBar = c.brightBlack + sym.barEmpty.repeat(emptyWidth) + c.reset;
|
|
430
|
+
const speed = this.isPaused ? 0 : this.calculateSpeed(downloaded);
|
|
431
|
+
const eta = speed > 0 ? (total - downloaded) / speed : 0;
|
|
432
|
+
const elapsed = (now - this.startTime) / 1000;
|
|
433
|
+
const sizeStr = `${c.white}${formatBytes(downloaded)}${c.brightBlack}/${c.cyan}${formatBytes(total)}${c.reset}`;
|
|
434
|
+
const speedStr = this.isPaused
|
|
435
|
+
? `${c.yellow}${sym.pause} Paused${c.reset}`
|
|
436
|
+
: `${c.brightGreen}${formatSpeed(speed)}${c.reset}`;
|
|
437
|
+
const etaStr = `${c.brightMagenta}${formatTime(eta)}${c.reset}`;
|
|
438
|
+
const statusIcon = this.isPaused ? `${c.yellow}${sym.pause}` : `${c.brightCyan}${sym.download}`;
|
|
439
|
+
const statusText = this.isPaused ? `${c.yellow}Paused` : `${c.brightCyan}${this.label}`;
|
|
440
|
+
const lines = [];
|
|
441
|
+
lines.push(padLine(`${statusIcon} ${statusText}...${c.reset}`, width));
|
|
442
|
+
const pctColor = percentage >= 100 ? c.brightGreen : percentage >= 50 ? c.brightCyan : c.brightYellow;
|
|
443
|
+
const progressLine = `${filledBar}${emptyBar} ${pctColor}${percentage.toFixed(1)}%${c.reset}`;
|
|
444
|
+
lines.push(padLine(progressLine, width));
|
|
445
|
+
const statsLine = `${sizeStr} ${c.brightBlack}${sym.lightning}${c.reset} ${speedStr} ${c.brightBlack}${sym.clock}${c.reset} ${etaStr}`;
|
|
446
|
+
lines.push(padLine(statsLine, width));
|
|
447
|
+
if (this.detailedView) {
|
|
448
|
+
const avgSpeed = elapsed > 0 ? formatSpeed(downloaded / elapsed) : '0 B/s';
|
|
449
|
+
const detailLine = `${c.brightBlack}Elapsed: ${c.brightMagenta}${formatTime(elapsed)}${c.brightBlack} Average: ${c.brightGreen}${avgSpeed}${c.reset}`;
|
|
450
|
+
lines.push(padLine(detailLine, width));
|
|
451
|
+
}
|
|
452
|
+
const pauseKey = this.isPaused ? c.brightGreen : c.yellow;
|
|
453
|
+
const pauseAction = this.isPaused ? 'Resume' : 'Pause';
|
|
454
|
+
const pauseHint = `${pauseKey}[P]${c.reset} ${c.white}${pauseAction}${c.reset}`;
|
|
455
|
+
const cancelHint = `${c.brightRed}[C]${c.reset} ${c.white}Cancel${c.reset}`;
|
|
456
|
+
const infoState = this.detailedView
|
|
457
|
+
? `${c.brightGreen}on${c.reset}`
|
|
458
|
+
: `${c.brightBlack}off${c.reset}`;
|
|
459
|
+
const infoHint = `${c.cyan}[I]${c.reset} ${c.white}Info${c.reset} ${infoState}`;
|
|
460
|
+
lines.push(padLine(`${pauseHint} ${cancelHint} ${infoHint}`, width));
|
|
461
|
+
let output = '';
|
|
462
|
+
if (this.initialized) {
|
|
463
|
+
output += cursor.up(this.lines);
|
|
464
|
+
}
|
|
465
|
+
output += c.cursorHide;
|
|
466
|
+
output += lines.map((line) => cursor.toColumn(1) + line).join('\n') + '\n';
|
|
467
|
+
if (this.initialized && lines.length < this.lines) {
|
|
468
|
+
const extraLines = this.lines - lines.length;
|
|
469
|
+
for (let i = 0; i < extraLines; i++) {
|
|
470
|
+
output += cursor.toColumn(1) + ' '.repeat(width) + '\n';
|
|
471
|
+
}
|
|
472
|
+
output += cursor.up(extraLines);
|
|
473
|
+
}
|
|
474
|
+
process.stdout.write(output);
|
|
475
|
+
this.lines = lines.length;
|
|
476
|
+
this.initialized = true;
|
|
477
|
+
}
|
|
478
|
+
finish(success = true, message = '') {
|
|
479
|
+
if (this.initialized && this.lines > 0) {
|
|
480
|
+
process.stdout.write(cursor.up(this.lines));
|
|
481
|
+
const width = Math.min(getTerminalWidth(), 80);
|
|
482
|
+
for (let i = 0; i < this.lines; i++) {
|
|
483
|
+
process.stdout.write(cursor.toColumn(1) + ' '.repeat(width) + '\n');
|
|
484
|
+
}
|
|
485
|
+
process.stdout.write(cursor.up(this.lines));
|
|
486
|
+
}
|
|
487
|
+
const icon = success ? c.brightGreen + sym.check : c.brightRed + sym.cross;
|
|
488
|
+
const color = success ? c.brightGreen : c.brightRed;
|
|
489
|
+
process.stdout.write(`${icon}${c.reset} ${color}${message}${c.reset}\n` + c.cursorShow);
|
|
490
|
+
this.lines = 0;
|
|
491
|
+
this.initialized = false;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function getLocalVersion(dbConfig) {
|
|
495
|
+
const manifestPath = path.join(CONFIG.dataDir, dbConfig.manifestName);
|
|
496
|
+
if (fs.existsSync(manifestPath)) {
|
|
497
|
+
try {
|
|
498
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
499
|
+
return manifest.version;
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
async function getRemoteVersion(dbConfig) {
|
|
508
|
+
try {
|
|
509
|
+
const releases = (await fetchJson(`https://api.github.com/repos/${CONFIG.repoOwner}/${CONFIG.repoName}/releases`));
|
|
510
|
+
const release = releases.find((r) => r.tag_name.startsWith(dbConfig.tagPrefix));
|
|
511
|
+
if (!release)
|
|
512
|
+
return null;
|
|
513
|
+
let version = release.tag_name.replace(dbConfig.tagPrefix, '');
|
|
514
|
+
const dbAsset = release.assets.find((a) => a.name === dbConfig.fileName);
|
|
515
|
+
const manifestAsset = release.assets.find((a) => a.name === dbConfig.manifestName);
|
|
516
|
+
if (!dbAsset)
|
|
517
|
+
return null;
|
|
518
|
+
if (manifestAsset) {
|
|
519
|
+
try {
|
|
520
|
+
const manifest = (await fetchJson(manifestAsset.browser_download_url));
|
|
521
|
+
if (manifest && manifest.version) {
|
|
522
|
+
version = manifest.version;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
version,
|
|
530
|
+
releaseId: release.id,
|
|
531
|
+
downloadUrl: dbAsset.browser_download_url,
|
|
532
|
+
manifestUrl: manifestAsset ? manifestAsset.browser_download_url : null,
|
|
533
|
+
size: dbAsset.size,
|
|
534
|
+
publishedAt: release.published_at,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
async function promptSelection(options) {
|
|
542
|
+
const rl = readline.createInterface({
|
|
543
|
+
input: process.stdin,
|
|
544
|
+
output: process.stdout,
|
|
545
|
+
});
|
|
546
|
+
let selectedIndex = 0;
|
|
547
|
+
const render = () => {
|
|
548
|
+
printHeader();
|
|
549
|
+
console.log(`${c.brightWhite}Select databases to install or update:${c.reset}\n`);
|
|
550
|
+
options.forEach((opt, idx) => {
|
|
551
|
+
const isSelected = idx === selectedIndex;
|
|
552
|
+
const prefix = isSelected ? `${c.brightCyan}${sym.arrowRight} ` : ' ';
|
|
553
|
+
const checkbox = opt.selected
|
|
554
|
+
? `${c.brightGreen}${sym.selected}${c.reset}`
|
|
555
|
+
: `${c.brightBlack}${sym.unselected}${c.reset}`;
|
|
556
|
+
const style = isSelected ? c.brightWhite + c.bold : c.white;
|
|
557
|
+
const requiredBadge = opt.isRequired ? ` ${c.brightBlack}[core]${c.reset}` : '';
|
|
558
|
+
console.log(`${prefix}${checkbox} ${style}${opt.icon} ${opt.name}${c.reset}${requiredBadge}`);
|
|
559
|
+
let status = '';
|
|
560
|
+
if (opt.localVersion) {
|
|
561
|
+
status += `${c.green}${sym.check} v${opt.localVersion}${c.reset}`;
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
status += `${c.yellow}${sym.warning} Not installed${c.reset}`;
|
|
565
|
+
}
|
|
566
|
+
if (opt.remoteInfo) {
|
|
567
|
+
if (opt.hasUpdate) {
|
|
568
|
+
status += ` ${c.brightBlack}→${c.reset} ${c.brightYellow}v${opt.remoteInfo.version}${c.reset}`;
|
|
569
|
+
}
|
|
570
|
+
else if (!opt.localVersion) {
|
|
571
|
+
status += ` ${c.brightBlack}→${c.reset} ${c.cyan}v${opt.remoteInfo.version}${c.reset}`;
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
status += ` ${c.brightBlack}(up to date)${c.reset}`;
|
|
575
|
+
}
|
|
576
|
+
status += ` ${c.brightBlack}[${formatBytes(opt.remoteInfo.size)}]${c.reset}`;
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
status += ` ${c.red}(offline)${c.reset}`;
|
|
580
|
+
}
|
|
581
|
+
console.log(` ${status}`);
|
|
582
|
+
console.log(` ${c.brightBlack}${opt.description}${c.reset}\n`);
|
|
583
|
+
});
|
|
584
|
+
console.log(`${c.brightBlack}↑/↓${c.reset} Navigate ${c.brightBlack}Space${c.reset} Toggle ${c.brightBlack}Enter${c.reset} Confirm ${c.brightBlack}Ctrl+C${c.reset} Exit`);
|
|
585
|
+
};
|
|
586
|
+
return new Promise((resolve) => {
|
|
587
|
+
if (options.length > 0 && options[0]) {
|
|
588
|
+
options[0].selected = true;
|
|
589
|
+
}
|
|
590
|
+
process.stdin.setRawMode(true);
|
|
591
|
+
process.stdin.resume();
|
|
592
|
+
render();
|
|
593
|
+
process.stdin.on('data', (key) => {
|
|
594
|
+
const keyStr = key.toString();
|
|
595
|
+
if (keyStr === '\u0003') {
|
|
596
|
+
process.exit(0);
|
|
597
|
+
}
|
|
598
|
+
else if (keyStr === '\u001b[A') {
|
|
599
|
+
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : options.length - 1;
|
|
600
|
+
render();
|
|
601
|
+
}
|
|
602
|
+
else if (keyStr === '\u001b[B') {
|
|
603
|
+
selectedIndex = selectedIndex < options.length - 1 ? selectedIndex + 1 : 0;
|
|
604
|
+
render();
|
|
605
|
+
}
|
|
606
|
+
else if (keyStr === ' ') {
|
|
607
|
+
const selectedOption = options[selectedIndex];
|
|
608
|
+
if (selectedOption) {
|
|
609
|
+
selectedOption.selected = !selectedOption.selected;
|
|
610
|
+
render();
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
else if (keyStr === '\r') {
|
|
614
|
+
process.stdin.setRawMode(false);
|
|
615
|
+
process.stdin.pause();
|
|
616
|
+
rl.close();
|
|
617
|
+
resolve(options.filter((o) => o.selected));
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
function compareVersions(v1, v2) {
|
|
623
|
+
const parts1 = v1.split('.').map(Number);
|
|
624
|
+
const parts2 = v2.split('.').map(Number);
|
|
625
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
626
|
+
const p1 = parts1[i] || 0;
|
|
627
|
+
const p2 = parts2[i] || 0;
|
|
628
|
+
if (p1 > p2)
|
|
629
|
+
return 1;
|
|
630
|
+
if (p1 < p2)
|
|
631
|
+
return -1;
|
|
632
|
+
}
|
|
633
|
+
return 0;
|
|
634
|
+
}
|
|
635
|
+
export async function runInstaller() {
|
|
636
|
+
printHeader();
|
|
637
|
+
console.log(`${c.cyan}${sym.info} Checking for available databases...${c.reset}\n`);
|
|
638
|
+
const choices = [];
|
|
639
|
+
for (const db of AVAILABLE_DBS) {
|
|
640
|
+
console.log(`${c.dim} Checking ${db.name}...${c.reset}`);
|
|
641
|
+
const localVersion = getLocalVersion(db);
|
|
642
|
+
const remoteInfo = await getRemoteVersion(db);
|
|
643
|
+
let hasUpdate = false;
|
|
644
|
+
if (localVersion && remoteInfo) {
|
|
645
|
+
hasUpdate = compareVersions(remoteInfo.version, localVersion) > 0;
|
|
646
|
+
}
|
|
647
|
+
choices.push({ ...db, localVersion, remoteInfo, selected: false, hasUpdate });
|
|
648
|
+
}
|
|
649
|
+
process.stdout.write((c.cursorUp + c.clearLine).repeat(choices.length + 1));
|
|
650
|
+
if (choices.length === 0) {
|
|
651
|
+
console.log(`${c.yellow}No databases found in configuration.${c.reset}`);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
const needsUpdate = choices.filter((c) => c.hasUpdate);
|
|
655
|
+
const notInstalled = choices.filter((c) => !c.localVersion);
|
|
656
|
+
if (needsUpdate.length > 0) {
|
|
657
|
+
console.log(`${c.brightYellow}${sym.update}${c.reset} ${c.white}${needsUpdate.length} update(s) available${c.reset}`);
|
|
658
|
+
}
|
|
659
|
+
if (notInstalled.length > 0) {
|
|
660
|
+
console.log(`${c.cyan}${sym.download}${c.reset} ${c.white}${notInstalled.length} database(s) not installed${c.reset}`);
|
|
661
|
+
}
|
|
662
|
+
if (needsUpdate.length === 0 && notInstalled.length === 0) {
|
|
663
|
+
console.log(`${c.green}${sym.check}${c.reset} ${c.white}All databases are up to date!${c.reset}`);
|
|
664
|
+
}
|
|
665
|
+
console.log();
|
|
666
|
+
const selected = await promptSelection(choices);
|
|
667
|
+
if (selected.length === 0) {
|
|
668
|
+
console.log(`\n${c.yellow}No databases selected. Exiting.${c.reset}`);
|
|
669
|
+
process.stdout.write(c.cursorShow);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
console.log(`\n${c.brightWhite}Starting installation/update...${c.reset}\n`);
|
|
673
|
+
let successCount = 0;
|
|
674
|
+
let failCount = 0;
|
|
675
|
+
for (const item of selected) {
|
|
676
|
+
if (!item.remoteInfo) {
|
|
677
|
+
console.log(`${c.red}${sym.cross} Skipping ${item.name}: Remote version unavailable.${c.reset}`);
|
|
678
|
+
failCount++;
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
const destDbPath = path.join(CONFIG.dataDir, item.fileName);
|
|
682
|
+
const destManifestPath = path.join(CONFIG.dataDir, item.manifestName);
|
|
683
|
+
const tempDbPath = destDbPath + '.tmp';
|
|
684
|
+
const action = item.localVersion ? 'Updating' : 'Installing';
|
|
685
|
+
const versionInfo = item.localVersion
|
|
686
|
+
? `${c.brightBlack}v${item.localVersion}${c.reset} ${c.white}→${c.reset} ${c.brightCyan}v${item.remoteInfo.version}${c.reset}`
|
|
687
|
+
: `${c.brightCyan}v${item.remoteInfo.version}${c.reset}`;
|
|
688
|
+
console.log(`${c.cyan}${sym.package}${c.reset} ${c.white}${action}${c.reset} ${item.icon} ${c.brightWhite}${item.name}${c.reset} ${c.brightBlack}(${c.reset}${versionInfo}${c.brightBlack})${c.reset}`);
|
|
689
|
+
if (!fs.existsSync(CONFIG.dataDir)) {
|
|
690
|
+
fs.mkdirSync(CONFIG.dataDir, { recursive: true });
|
|
691
|
+
}
|
|
692
|
+
try {
|
|
693
|
+
const progress = new ProgressDisplay(`Downloading ${item.fileName}`);
|
|
694
|
+
console.log();
|
|
695
|
+
const download = downloadFileInteractive(item.remoteInfo.downloadUrl, tempDbPath, progress);
|
|
696
|
+
try {
|
|
697
|
+
await download.promise;
|
|
698
|
+
progress.finish(true, 'Download complete!');
|
|
699
|
+
}
|
|
700
|
+
catch (err) {
|
|
701
|
+
if (err instanceof Error && err.message === 'Download cancelled by user') {
|
|
702
|
+
progress.finish(false, 'Download cancelled');
|
|
703
|
+
console.log(`\n${c.brightBlack}Returning to menu...${c.reset}\n`);
|
|
704
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
705
|
+
return runInstaller();
|
|
706
|
+
}
|
|
707
|
+
throw err;
|
|
708
|
+
}
|
|
709
|
+
if (fs.existsSync(destDbPath)) {
|
|
710
|
+
fs.unlinkSync(destDbPath);
|
|
711
|
+
}
|
|
712
|
+
fs.renameSync(tempDbPath, destDbPath);
|
|
713
|
+
if (item.remoteInfo.manifestUrl) {
|
|
714
|
+
console.log(`${c.dim} Fetching manifest...${c.reset}`);
|
|
715
|
+
await downloadFile(item.remoteInfo.manifestUrl, destManifestPath);
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
const minimalManifest = {
|
|
719
|
+
version: item.remoteInfo.version,
|
|
720
|
+
updatedAt: new Date().toISOString(),
|
|
721
|
+
source: 'manual-install',
|
|
722
|
+
};
|
|
723
|
+
fs.writeFileSync(destManifestPath, JSON.stringify(minimalManifest, null, 2));
|
|
724
|
+
}
|
|
725
|
+
console.log(`${c.brightGreen}${sym.sparkle} Successfully ${item.localVersion ? 'updated' : 'installed'} ${item.name}!${c.reset}\n`);
|
|
726
|
+
successCount++;
|
|
727
|
+
}
|
|
728
|
+
catch (err) {
|
|
729
|
+
if (fs.existsSync(tempDbPath)) {
|
|
730
|
+
fs.unlinkSync(tempDbPath);
|
|
731
|
+
}
|
|
732
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
733
|
+
console.error(`\n${c.red}${sym.cross} Failed to ${item.localVersion ? 'update' : 'install'} ${item.name}: ${message}${c.reset}\n`);
|
|
734
|
+
failCount++;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
console.log();
|
|
738
|
+
if (successCount > 0 && failCount === 0) {
|
|
739
|
+
console.log(`${c.green}${sym.sparkle}${c.reset} ${c.white}All ${successCount} operation(s) completed successfully!${c.reset}`);
|
|
740
|
+
}
|
|
741
|
+
else if (successCount > 0 && failCount > 0) {
|
|
742
|
+
console.log(`${c.yellow}${sym.warning}${c.reset} ${c.white}Completed: ${c.green}${successCount} succeeded${c.white}, ${c.red}${failCount} failed${c.reset}`);
|
|
743
|
+
}
|
|
744
|
+
else if (failCount > 0) {
|
|
745
|
+
console.log(`${c.red}${sym.cross}${c.reset} ${c.white}All operations failed${c.reset}`);
|
|
746
|
+
}
|
|
747
|
+
process.stdout.write(c.cursorShow);
|
|
748
|
+
}
|
|
749
|
+
//# sourceMappingURL=manage.js.map
|