sunnah 1.1.4 → 1.2.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.
Potentially problematic release.
This version of sunnah might be problematic. Click here for more details.
- package/bin/index.js +400 -169
- package/package.json +1 -1
package/bin/index.js
CHANGED
|
@@ -41,6 +41,7 @@ const gray = (t) => clr(c.gray, t);
|
|
|
41
41
|
const red = (t) => clr(c.red, t);
|
|
42
42
|
const dim = (t) => clr(c.dim, t);
|
|
43
43
|
const white = (t) => clr(c.white, t);
|
|
44
|
+
const blue = (t) => clr(c.blue, t);
|
|
44
45
|
|
|
45
46
|
// ── Available packages ────────────────────────────────────────────────────────
|
|
46
47
|
const PACKAGES = [
|
|
@@ -78,26 +79,40 @@ const PACKAGES = [
|
|
|
78
79
|
},
|
|
79
80
|
];
|
|
80
81
|
|
|
81
|
-
// ── Terminal
|
|
82
|
+
// ── Terminal: use alternate screen buffer to avoid scroll issues ──────────────
|
|
82
83
|
const W = () => process.stdout.columns || 80;
|
|
84
|
+
const H = () => process.stdout.rows || 24;
|
|
83
85
|
|
|
84
|
-
function
|
|
85
|
-
process.stdout.write("\
|
|
86
|
+
function enterAltScreen() {
|
|
87
|
+
process.stdout.write("\x1b[?1049h");
|
|
86
88
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
function leaveAltScreen() {
|
|
90
|
+
process.stdout.write("\x1b[?1049l");
|
|
91
|
+
}
|
|
92
|
+
function clearScreen() {
|
|
93
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
94
|
+
}
|
|
95
|
+
function moveTo(row, col) {
|
|
96
|
+
process.stdout.write(`\x1b[${row};${col}H`);
|
|
90
97
|
}
|
|
91
|
-
|
|
92
98
|
function hideCursor() {
|
|
93
99
|
process.stdout.write("\x1b[?25l");
|
|
94
100
|
}
|
|
95
101
|
function showCursor() {
|
|
96
102
|
process.stdout.write("\x1b[?25h");
|
|
97
103
|
}
|
|
104
|
+
function clearToEOL() {
|
|
105
|
+
process.stdout.write("\x1b[K");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function writeLine(row, text) {
|
|
109
|
+
moveTo(row, 1);
|
|
110
|
+
clearToEOL();
|
|
111
|
+
process.stdout.write(text);
|
|
112
|
+
}
|
|
98
113
|
|
|
99
114
|
// ── Progress bar ──────────────────────────────────────────────────────────────
|
|
100
|
-
function drawBar(label, percent, barWidth =
|
|
115
|
+
function drawBar(label, percent, barWidth = 40) {
|
|
101
116
|
const filled = Math.round((percent / 100) * barWidth);
|
|
102
117
|
const empty = barWidth - filled;
|
|
103
118
|
const bar =
|
|
@@ -106,43 +121,45 @@ function drawBar(label, percent, barWidth = 38) {
|
|
|
106
121
|
return ` ${bar} ${pct} ${dim(label)}`;
|
|
107
122
|
}
|
|
108
123
|
|
|
109
|
-
// ── Install a package with animated progress bar
|
|
124
|
+
// ── Install a package with animated single progress bar ───────────────────────
|
|
110
125
|
function animateInstall(pkgName) {
|
|
111
126
|
return new Promise((resolve) => {
|
|
112
127
|
const stages = [
|
|
113
|
-
{ label: "Resolving packages…", end: 12, ms:
|
|
114
|
-
{ label: "Fetching metadata…", end:
|
|
115
|
-
{ label: "Downloading tarball…", end:
|
|
116
|
-
{ label: "Extracting files…", end:
|
|
117
|
-
{ label: "Linking binaries…", end: 98, ms:
|
|
128
|
+
{ label: "Resolving packages…", end: 12, ms: 80 },
|
|
129
|
+
{ label: "Fetching metadata…", end: 30, ms: 60 },
|
|
130
|
+
{ label: "Downloading tarball…", end: 75, ms: 22 },
|
|
131
|
+
{ label: "Extracting files…", end: 90, ms: 50 },
|
|
132
|
+
{ label: "Linking binaries…", end: 98, ms: 80 },
|
|
118
133
|
];
|
|
119
134
|
|
|
120
135
|
let percent = 0;
|
|
121
136
|
let stageIdx = 0;
|
|
122
137
|
let npmDone = false;
|
|
123
138
|
|
|
124
|
-
|
|
139
|
+
// Write bar ONCE — all updates overwrite this same line with \r
|
|
140
|
+
process.stdout.write(drawBar(stages[0].label, 0));
|
|
125
141
|
|
|
126
142
|
const tick = () => {
|
|
127
143
|
const stage = stages[stageIdx];
|
|
128
144
|
if (!stage) return;
|
|
129
145
|
|
|
130
146
|
const prevEnd = stageIdx > 0 ? stages[stageIdx - 1].end : 0;
|
|
131
|
-
const step = (stage.end - prevEnd) /
|
|
147
|
+
const step = (stage.end - prevEnd) / 24;
|
|
132
148
|
percent = Math.min(percent + step, stage.end);
|
|
133
149
|
|
|
134
|
-
|
|
135
|
-
process.stdout.write(drawBar(stage.label, percent));
|
|
150
|
+
// Overwrite the SAME line — no \n, just \r
|
|
151
|
+
process.stdout.write("\r\x1b[K" + drawBar(stage.label, percent));
|
|
136
152
|
|
|
137
153
|
if (percent >= stage.end) {
|
|
138
154
|
stageIdx++;
|
|
139
155
|
if (stageIdx >= stages.length) {
|
|
156
|
+
// Spin until npm actually finishes
|
|
140
157
|
const poll = setInterval(() => {
|
|
141
158
|
if (npmDone) {
|
|
142
159
|
clearInterval(poll);
|
|
143
|
-
process.stdout.write(
|
|
144
|
-
|
|
145
|
-
|
|
160
|
+
process.stdout.write(
|
|
161
|
+
"\r\x1b[K" + drawBar("Complete!", 100) + "\n",
|
|
162
|
+
);
|
|
146
163
|
resolve();
|
|
147
164
|
}
|
|
148
165
|
}, 80);
|
|
@@ -155,26 +172,21 @@ function animateInstall(pkgName) {
|
|
|
155
172
|
|
|
156
173
|
setTimeout(tick, stages[0].ms);
|
|
157
174
|
|
|
158
|
-
//
|
|
175
|
+
// Actually run npm install -g
|
|
159
176
|
const proc = spawn(NPM, ["install", "-g", pkgName], {
|
|
160
177
|
stdio: ["ignore", "pipe", "pipe"],
|
|
161
178
|
shell: isWin,
|
|
162
179
|
});
|
|
163
|
-
|
|
164
180
|
proc.on("error", () => {
|
|
165
|
-
// resolve anyway so the UI doesn't hang on spawn failure
|
|
166
181
|
npmDone = true;
|
|
167
182
|
});
|
|
168
|
-
|
|
169
183
|
proc.on("close", () => {
|
|
170
184
|
npmDone = true;
|
|
171
185
|
});
|
|
172
186
|
});
|
|
173
187
|
}
|
|
174
188
|
|
|
175
|
-
// ──
|
|
176
|
-
// Calling npm on every render was freezing the terminal. We run one
|
|
177
|
-
// 'npm list -g' at startup, parse the output, and never shell out again.
|
|
189
|
+
// ── Installed cache — built ONCE, never during render ────────────────────────
|
|
178
190
|
function buildInstalledCache() {
|
|
179
191
|
const cache = new Map();
|
|
180
192
|
let out = "";
|
|
@@ -182,7 +194,7 @@ function buildInstalledCache() {
|
|
|
182
194
|
out = execSync(`${NPM} list -g --depth=0`, {
|
|
183
195
|
encoding: "utf8",
|
|
184
196
|
shell: isWin,
|
|
185
|
-
timeout:
|
|
197
|
+
timeout: 10000,
|
|
186
198
|
stdio: ["ignore", "pipe", "ignore"],
|
|
187
199
|
});
|
|
188
200
|
} catch (e) {
|
|
@@ -194,86 +206,207 @@ function buildInstalledCache() {
|
|
|
194
206
|
return cache;
|
|
195
207
|
}
|
|
196
208
|
|
|
209
|
+
// Get latest version from npm registry (fast, single HTTP call)
|
|
210
|
+
function getLatestVersion(name) {
|
|
211
|
+
try {
|
|
212
|
+
return execSync(`${NPM} show ${name} version`, {
|
|
213
|
+
encoding: "utf8",
|
|
214
|
+
shell: isWin,
|
|
215
|
+
timeout: 8000,
|
|
216
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
217
|
+
}).trim();
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Get currently installed version
|
|
224
|
+
function getInstalledVersion(name) {
|
|
225
|
+
try {
|
|
226
|
+
const out = execSync(`${NPM} list -g ${name} --depth=0`, {
|
|
227
|
+
encoding: "utf8",
|
|
228
|
+
shell: isWin,
|
|
229
|
+
timeout: 8000,
|
|
230
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
231
|
+
});
|
|
232
|
+
const match = out.match(new RegExp(name + "@([\\d.]+)"));
|
|
233
|
+
return match ? match[1] : null;
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
197
239
|
let installedCache = new Map();
|
|
198
240
|
const isInstalled = (name) => installedCache.get(name) ?? false;
|
|
199
241
|
|
|
200
|
-
// ──
|
|
201
|
-
const
|
|
242
|
+
// ── Modes ─────────────────────────────────────────────────────────────────────
|
|
243
|
+
const MODE = { LIST: "list", CONFIRM_UNINSTALL: "confirm_uninstall" };
|
|
244
|
+
|
|
245
|
+
// ── Render: full-screen, uses alt buffer so no scroll ever ───────────────────
|
|
246
|
+
function render(state) {
|
|
247
|
+
const { cursor, selected, mode, confirmTarget, statusMsg } = state;
|
|
248
|
+
const divW = Math.min(W() - 2, 72);
|
|
249
|
+
const div = gray("─".repeat(divW));
|
|
250
|
+
const div2 = gray("═".repeat(divW));
|
|
251
|
+
|
|
252
|
+
clearScreen();
|
|
202
253
|
|
|
203
|
-
|
|
204
|
-
const div = gray("─".repeat(DIV_W()));
|
|
205
|
-
const div2 = gray("═".repeat(DIV_W()));
|
|
206
|
-
const lines = [];
|
|
254
|
+
let row = 1;
|
|
207
255
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
256
|
+
// Header
|
|
257
|
+
writeLine(row++, div2);
|
|
258
|
+
writeLine(
|
|
259
|
+
row++,
|
|
211
260
|
bold(cyan(" 📚 Sunnah Package Manager")) + gray(" v" + pkg.version),
|
|
212
261
|
);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
gray("
|
|
218
|
-
|
|
262
|
+
writeLine(
|
|
263
|
+
row++,
|
|
264
|
+
gray(" ↑↓") +
|
|
265
|
+
" navigate " +
|
|
266
|
+
gray("space") +
|
|
267
|
+
" select " +
|
|
268
|
+
gray("a") +
|
|
269
|
+
" all " +
|
|
270
|
+
gray("i") +
|
|
271
|
+
" info " +
|
|
272
|
+
gray("u") +
|
|
273
|
+
" uninstall " +
|
|
274
|
+
gray("enter") +
|
|
275
|
+
" install " +
|
|
276
|
+
gray("q") +
|
|
277
|
+
" quit",
|
|
219
278
|
);
|
|
220
|
-
|
|
221
|
-
|
|
279
|
+
writeLine(row++, div2);
|
|
280
|
+
row++; // blank
|
|
222
281
|
|
|
282
|
+
// Package list
|
|
223
283
|
PACKAGES.forEach((p, i) => {
|
|
224
284
|
const isCursor = i === cursor;
|
|
225
|
-
const
|
|
226
|
-
const
|
|
285
|
+
const isSel = selected.has(i);
|
|
286
|
+
const inst = isInstalled(p.name);
|
|
227
287
|
|
|
228
|
-
const checkbox =
|
|
288
|
+
const checkbox = isSel ? green("[✓]") : gray("[ ]");
|
|
229
289
|
const arrow = isCursor ? cyan("▶") : " ";
|
|
230
290
|
const label = isCursor
|
|
231
291
|
? bold(white(p.label))
|
|
232
|
-
:
|
|
292
|
+
: isSel
|
|
233
293
|
? green(p.label)
|
|
234
294
|
: white(p.label);
|
|
235
|
-
const badge =
|
|
295
|
+
const badge = inst
|
|
296
|
+
? dim(green(" ● installed"))
|
|
297
|
+
: dim(gray(" ○ not installed"));
|
|
236
298
|
|
|
237
|
-
|
|
299
|
+
writeLine(row++, ` ${arrow} ${checkbox} ${label}${badge}`);
|
|
238
300
|
|
|
239
301
|
if (isCursor) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
302
|
+
writeLine(row++, ` ${dim(p.author)}`);
|
|
303
|
+
writeLine(row++, ` ${gray(p.desc)}`);
|
|
304
|
+
writeLine(
|
|
305
|
+
row++,
|
|
306
|
+
` ${gray("Hadiths: ")}${yellow(p.hadiths)}` +
|
|
307
|
+
` ${gray("CLI: ")}${cyan(p.cmd + " --help")}` +
|
|
308
|
+
(inst ? ` ${gray("run: ")}${cyan(p.cmd + " 1")}` : ""),
|
|
244
309
|
);
|
|
245
|
-
|
|
310
|
+
row++; // blank after expanded
|
|
246
311
|
}
|
|
247
312
|
});
|
|
248
313
|
|
|
249
|
-
|
|
250
|
-
|
|
314
|
+
row++; // blank
|
|
315
|
+
writeLine(row++, div);
|
|
251
316
|
|
|
252
|
-
|
|
253
|
-
if (
|
|
317
|
+
// Status / selection footer
|
|
318
|
+
if (statusMsg) {
|
|
319
|
+
writeLine(row++, ` ${yellow("⚠")} ${yellow(statusMsg)}`);
|
|
320
|
+
} else if (selected.size > 0) {
|
|
254
321
|
const names = [...selected].map((i) => cyan(PACKAGES[i].name)).join(", ");
|
|
255
|
-
|
|
322
|
+
writeLine(
|
|
323
|
+
row++,
|
|
324
|
+
` ${green("●")} ${bold(String(selected.size))} selected: ${names}`,
|
|
325
|
+
);
|
|
326
|
+
writeLine(
|
|
327
|
+
row++,
|
|
328
|
+
` ${dim("Press enter to install, u to uninstall selected")}`,
|
|
329
|
+
);
|
|
256
330
|
} else {
|
|
257
|
-
|
|
331
|
+
writeLine(
|
|
332
|
+
row++,
|
|
258
333
|
` ${gray("Nothing selected — press space to select a package")}`,
|
|
259
334
|
);
|
|
260
335
|
}
|
|
261
|
-
lines.push(div);
|
|
262
|
-
lines.push("");
|
|
263
336
|
|
|
264
|
-
|
|
337
|
+
writeLine(row++, div);
|
|
338
|
+
|
|
339
|
+
// Confirm uninstall overlay
|
|
340
|
+
if (mode === MODE.CONFIRM_UNINSTALL && confirmTarget !== null) {
|
|
341
|
+
const p = PACKAGES[confirmTarget];
|
|
342
|
+
row++;
|
|
343
|
+
writeLine(
|
|
344
|
+
row++,
|
|
345
|
+
` ${red("⚠ Uninstall ")}${bold(white(p.label))}${red("?")}`,
|
|
346
|
+
);
|
|
347
|
+
writeLine(
|
|
348
|
+
row++,
|
|
349
|
+
` ${green("y")} ${gray("confirm")} ${red("n")} ${gray("cancel")}`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
265
352
|
}
|
|
266
353
|
|
|
267
|
-
|
|
268
|
-
|
|
354
|
+
// ── Non-interactive: --list ───────────────────────────────────────────────────
|
|
355
|
+
function cmdList() {
|
|
356
|
+
installedCache = buildInstalledCache();
|
|
357
|
+
const div = gray("─".repeat(60));
|
|
358
|
+
console.log("");
|
|
359
|
+
console.log(div);
|
|
360
|
+
console.log(bold(cyan(" Available Sunnah Packages")));
|
|
361
|
+
console.log(div);
|
|
362
|
+
PACKAGES.forEach((p) => {
|
|
363
|
+
const inst = isInstalled(p.name)
|
|
364
|
+
? green(" ✓ installed")
|
|
365
|
+
: red(" ✗ not installed");
|
|
366
|
+
console.log("");
|
|
367
|
+
console.log(` ${bold(white(p.label))}${inst}`);
|
|
368
|
+
console.log(` ${cyan("npm install -g " + p.name)}`);
|
|
369
|
+
console.log(` ${dim(p.desc)}`);
|
|
370
|
+
console.log(
|
|
371
|
+
` ${gray("Hadiths: ")}${yellow(p.hadiths)} ${gray("Author: ")}${magenta(p.author)}`,
|
|
372
|
+
);
|
|
373
|
+
});
|
|
374
|
+
console.log("");
|
|
375
|
+
console.log(div);
|
|
376
|
+
console.log("");
|
|
269
377
|
}
|
|
270
378
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
379
|
+
// ── Non-interactive: --update ─────────────────────────────────────────────────
|
|
380
|
+
function cmdUpdate() {
|
|
381
|
+
installedCache = buildInstalledCache();
|
|
382
|
+
const installed = PACKAGES.filter((p) => isInstalled(p.name));
|
|
383
|
+
if (!installed.length) {
|
|
384
|
+
console.log("\n " + yellow("No sunnah packages installed.\n"));
|
|
385
|
+
return;
|
|
275
386
|
}
|
|
276
|
-
|
|
387
|
+
const div = gray("─".repeat(60));
|
|
388
|
+
console.log("\n" + div);
|
|
389
|
+
console.log(bold(cyan(" Checking for updates…")));
|
|
390
|
+
console.log(div + "\n");
|
|
391
|
+
|
|
392
|
+
for (const p of installed) {
|
|
393
|
+
const current = getInstalledVersion(p.name);
|
|
394
|
+
const latest = getLatestVersion(p.name);
|
|
395
|
+
if (!current || !latest) {
|
|
396
|
+
console.log(` ${yellow("?")} ${p.label} ${gray("(could not check)")}`);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (current === latest) {
|
|
400
|
+
console.log(
|
|
401
|
+
` ${green("✓")} ${bold(p.label)} ${gray(current + " — up to date")}`,
|
|
402
|
+
);
|
|
403
|
+
} else {
|
|
404
|
+
console.log(
|
|
405
|
+
` ${yellow("↑")} ${bold(p.label)} ${gray(current)} → ${green(latest)} ${dim("(run: npm install -g " + p.name + ")")}`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
console.log("\n" + div + "\n");
|
|
277
410
|
}
|
|
278
411
|
|
|
279
412
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
@@ -283,80 +416,69 @@ async function main() {
|
|
|
283
416
|
|
|
284
417
|
// --version
|
|
285
418
|
if (flags.some((f) => f === "-v" || f === "--version")) {
|
|
286
|
-
console.log("");
|
|
287
|
-
console.log(" " + bold(cyan("sunnah")) + gray(" v" + pkg.version));
|
|
419
|
+
console.log("\n " + bold(cyan("sunnah")) + gray(" v" + pkg.version));
|
|
288
420
|
console.log(
|
|
289
|
-
" " + gray("
|
|
421
|
+
" " + gray("Packages: ") + yellow(String(PACKAGES.length)) + "\n",
|
|
290
422
|
);
|
|
291
|
-
console.log("");
|
|
292
423
|
process.exit(0);
|
|
293
424
|
}
|
|
294
425
|
|
|
295
426
|
// --help
|
|
296
427
|
if (flags.some((f) => f === "-h" || f === "--help")) {
|
|
297
428
|
const div = gray("─".repeat(60));
|
|
298
|
-
console.log("");
|
|
299
|
-
console.log(div);
|
|
429
|
+
console.log("\n" + div);
|
|
300
430
|
console.log(
|
|
301
431
|
bold(cyan(" Sunnah Package Manager")) + gray(" v" + pkg.version),
|
|
302
432
|
);
|
|
303
|
-
console.log(div);
|
|
304
|
-
console.log("");
|
|
433
|
+
console.log(div + "\n");
|
|
305
434
|
console.log(" " + bold("Usage:"));
|
|
435
|
+
console.log(
|
|
436
|
+
" " + cyan("sunnah") + gray(" Open interactive UI"),
|
|
437
|
+
);
|
|
306
438
|
console.log(
|
|
307
439
|
" " +
|
|
308
440
|
cyan("sunnah") +
|
|
309
|
-
|
|
441
|
+
green(" --list") +
|
|
442
|
+
gray(" List all packages + install status"),
|
|
310
443
|
);
|
|
311
444
|
console.log(
|
|
312
445
|
" " +
|
|
313
446
|
cyan("sunnah") +
|
|
314
|
-
green(" --
|
|
315
|
-
gray("
|
|
447
|
+
green(" --update") +
|
|
448
|
+
gray(" Check all installed packages for updates"),
|
|
316
449
|
);
|
|
317
450
|
console.log(
|
|
318
|
-
" " + cyan("sunnah") + green(" -v") + gray("
|
|
451
|
+
" " + cyan("sunnah") + green(" -v") + gray(" Show version"),
|
|
319
452
|
);
|
|
320
453
|
console.log(
|
|
321
454
|
" " +
|
|
322
455
|
cyan("sunnah") +
|
|
323
456
|
green(" -h") +
|
|
324
|
-
gray("
|
|
457
|
+
gray(" Show this help"),
|
|
325
458
|
);
|
|
326
|
-
console.log("");
|
|
327
|
-
console.log("
|
|
328
|
-
console.log(" " + green("
|
|
329
|
-
console.log(
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
console.log(" " + green("
|
|
333
|
-
console.log("");
|
|
334
|
-
console.log(
|
|
335
|
-
console.log("");
|
|
459
|
+
console.log("\n " + bold("Interactive controls:"));
|
|
460
|
+
console.log(" " + green("↑ ↓") + gray(" Navigate"));
|
|
461
|
+
console.log(" " + green("space") + gray(" Toggle select"));
|
|
462
|
+
console.log(
|
|
463
|
+
" " + green("a") + gray(" Select all / deselect all"),
|
|
464
|
+
);
|
|
465
|
+
console.log(" " + green("i") + gray(" Show package info"));
|
|
466
|
+
console.log(" " + green("u") + gray(" Uninstall selected"));
|
|
467
|
+
console.log(" " + green("enter") + gray(" Install selected"));
|
|
468
|
+
console.log(" " + green("q") + gray(" Quit"));
|
|
469
|
+
console.log("\n" + div + "\n");
|
|
336
470
|
process.exit(0);
|
|
337
471
|
}
|
|
338
472
|
|
|
339
473
|
// --list
|
|
340
474
|
if (flags.some((f) => f === "--list" || f === "-l")) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const inst = isInstalled(p.name) ? green(" ✓ installed") : "";
|
|
349
|
-
console.log("");
|
|
350
|
-
console.log(` ${bold(white(p.label))}${inst}`);
|
|
351
|
-
console.log(` ${cyan("npm install -g " + p.name)}`);
|
|
352
|
-
console.log(` ${dim(p.desc)}`);
|
|
353
|
-
console.log(
|
|
354
|
-
` ${gray("Hadiths: ")}${yellow(p.hadiths)} ${gray("Author: ")}${magenta(p.author)}`,
|
|
355
|
-
);
|
|
356
|
-
});
|
|
357
|
-
console.log("");
|
|
358
|
-
console.log(div);
|
|
359
|
-
console.log("");
|
|
475
|
+
cmdList();
|
|
476
|
+
process.exit(0);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// --update
|
|
480
|
+
if (flags.some((f) => f === "--update")) {
|
|
481
|
+
cmdUpdate();
|
|
360
482
|
process.exit(0);
|
|
361
483
|
}
|
|
362
484
|
|
|
@@ -366,32 +488,40 @@ async function main() {
|
|
|
366
488
|
process.exit(1);
|
|
367
489
|
}
|
|
368
490
|
|
|
369
|
-
// Build
|
|
370
|
-
process.stdout.write(
|
|
371
|
-
"\n " + "\x1b[90m" + "Checking installed packages…" + "\x1b[0m",
|
|
372
|
-
);
|
|
491
|
+
// Build cache before entering alt screen
|
|
492
|
+
process.stdout.write("\n " + gray("Checking installed packages…"));
|
|
373
493
|
installedCache = buildInstalledCache();
|
|
374
494
|
process.stdout.write("\r\x1b[K");
|
|
375
495
|
|
|
376
|
-
|
|
377
|
-
|
|
496
|
+
// Enter alternate screen buffer — this completely prevents scroll issues
|
|
497
|
+
enterAltScreen();
|
|
378
498
|
hideCursor();
|
|
379
499
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (prevCount > 0) eraseLines(prevCount);
|
|
387
|
-
printLines(lines);
|
|
388
|
-
prevCount = lines.length;
|
|
500
|
+
const state = {
|
|
501
|
+
cursor: 0,
|
|
502
|
+
selected: new Set(),
|
|
503
|
+
mode: MODE.LIST,
|
|
504
|
+
confirmTarget: null,
|
|
505
|
+
statusMsg: "",
|
|
389
506
|
};
|
|
390
507
|
|
|
391
|
-
|
|
508
|
+
let statusTimer = null;
|
|
509
|
+
|
|
510
|
+
function setStatus(msg, ms = 2000) {
|
|
511
|
+
state.statusMsg = msg;
|
|
512
|
+
render(state);
|
|
513
|
+
if (statusTimer) clearTimeout(statusTimer);
|
|
514
|
+
statusTimer = setTimeout(() => {
|
|
515
|
+
state.statusMsg = "";
|
|
516
|
+
render(state);
|
|
517
|
+
}, ms);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
render(state);
|
|
392
521
|
|
|
393
522
|
const cleanup = () => {
|
|
394
523
|
showCursor();
|
|
524
|
+
leaveAltScreen();
|
|
395
525
|
try {
|
|
396
526
|
process.stdin.setRawMode(false);
|
|
397
527
|
} catch {}
|
|
@@ -400,63 +530,151 @@ async function main() {
|
|
|
400
530
|
|
|
401
531
|
process.on("SIGINT", () => {
|
|
402
532
|
cleanup();
|
|
403
|
-
console.log("");
|
|
404
533
|
process.exit(0);
|
|
405
534
|
});
|
|
406
535
|
|
|
536
|
+
readline.emitKeypressEvents(process.stdin);
|
|
537
|
+
process.stdin.setRawMode(true);
|
|
538
|
+
|
|
407
539
|
process.stdin.on("keypress", async (str, key) => {
|
|
408
540
|
if (!key) return;
|
|
409
541
|
|
|
542
|
+
// ── Confirm uninstall mode ──────────────────────────────────────────────
|
|
543
|
+
if (state.mode === MODE.CONFIRM_UNINSTALL) {
|
|
544
|
+
if (str === "y" || str === "Y") {
|
|
545
|
+
const p = PACKAGES[state.confirmTarget];
|
|
546
|
+
state.mode = MODE.LIST;
|
|
547
|
+
state.confirmTarget = null;
|
|
548
|
+
|
|
549
|
+
cleanup();
|
|
550
|
+
console.log(
|
|
551
|
+
"\n " +
|
|
552
|
+
yellow("Uninstalling ") +
|
|
553
|
+
bold(white(p.label)) +
|
|
554
|
+
yellow("…\n"),
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
execSync(`${NPM} uninstall -g ${p.name}`, {
|
|
559
|
+
stdio: "inherit",
|
|
560
|
+
shell: isWin,
|
|
561
|
+
});
|
|
562
|
+
installedCache.set(p.name, false);
|
|
563
|
+
state.selected.delete(PACKAGES.indexOf(p));
|
|
564
|
+
console.log(
|
|
565
|
+
"\n " +
|
|
566
|
+
green("✓ ") +
|
|
567
|
+
bold(green(p.label)) +
|
|
568
|
+
green(" uninstalled.\n"),
|
|
569
|
+
);
|
|
570
|
+
} catch {
|
|
571
|
+
console.log("\n " + red("✗ Failed to uninstall " + p.label + "\n"));
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
await sleep(1200);
|
|
575
|
+
|
|
576
|
+
// Re-enter interactive UI
|
|
577
|
+
enterAltScreen();
|
|
578
|
+
hideCursor();
|
|
579
|
+
readline.emitKeypressEvents(process.stdin);
|
|
580
|
+
process.stdin.setRawMode(true);
|
|
581
|
+
render(state);
|
|
582
|
+
} else {
|
|
583
|
+
state.mode = MODE.LIST;
|
|
584
|
+
state.confirmTarget = null;
|
|
585
|
+
render(state);
|
|
586
|
+
}
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ── Normal list mode ────────────────────────────────────────────────────
|
|
591
|
+
|
|
410
592
|
// Quit
|
|
411
593
|
if (key.name === "q" || (key.ctrl && key.name === "c")) {
|
|
412
594
|
cleanup();
|
|
413
|
-
if (prevCount > 0) eraseLines(prevCount);
|
|
414
595
|
console.log("\n " + gray("Goodbye.\n"));
|
|
415
596
|
process.exit(0);
|
|
416
597
|
}
|
|
417
598
|
|
|
418
599
|
// Navigate
|
|
419
600
|
if (key.name === "up") {
|
|
420
|
-
cursor = (cursor - 1 + PACKAGES.length) % PACKAGES.length;
|
|
421
|
-
|
|
601
|
+
state.cursor = (state.cursor - 1 + PACKAGES.length) % PACKAGES.length;
|
|
602
|
+
render(state);
|
|
422
603
|
return;
|
|
423
604
|
}
|
|
424
605
|
if (key.name === "down") {
|
|
425
|
-
cursor = (cursor + 1) % PACKAGES.length;
|
|
426
|
-
|
|
606
|
+
state.cursor = (state.cursor + 1) % PACKAGES.length;
|
|
607
|
+
render(state);
|
|
427
608
|
return;
|
|
428
609
|
}
|
|
429
610
|
|
|
430
|
-
// Toggle
|
|
611
|
+
// Toggle select
|
|
431
612
|
if (str === " ") {
|
|
432
|
-
if (selected.has(cursor)) selected.delete(cursor);
|
|
433
|
-
else selected.add(cursor);
|
|
434
|
-
|
|
613
|
+
if (state.selected.has(state.cursor)) state.selected.delete(state.cursor);
|
|
614
|
+
else state.selected.add(state.cursor);
|
|
615
|
+
render(state);
|
|
435
616
|
return;
|
|
436
617
|
}
|
|
437
618
|
|
|
438
|
-
//
|
|
619
|
+
// Select all / deselect all
|
|
439
620
|
if (str === "a" || str === "A") {
|
|
440
|
-
if (selected.size === PACKAGES.length) selected.clear();
|
|
441
|
-
else PACKAGES.forEach((_, i) => selected.add(i));
|
|
442
|
-
|
|
621
|
+
if (state.selected.size === PACKAGES.length) state.selected.clear();
|
|
622
|
+
else PACKAGES.forEach((_, i) => state.selected.add(i));
|
|
623
|
+
render(state);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Info
|
|
628
|
+
if (str === "i" || str === "I") {
|
|
629
|
+
const p = PACKAGES[state.cursor];
|
|
630
|
+
const inst = isInstalled(p.name);
|
|
631
|
+
const version = inst ? getInstalledVersion(p.name) : null;
|
|
632
|
+
const msg = `${p.label} | ${p.hadiths} hadiths | ${inst ? "v" + version + " installed" : "not installed"}`;
|
|
633
|
+
setStatus(msg, 3000);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Uninstall
|
|
638
|
+
if (str === "u" || str === "U") {
|
|
639
|
+
const targets =
|
|
640
|
+
state.selected.size > 0 ? [...state.selected] : [state.cursor];
|
|
641
|
+
|
|
642
|
+
// Only uninstall packages that are actually installed
|
|
643
|
+
const toRemove = targets.filter((i) => isInstalled(PACKAGES[i].name));
|
|
644
|
+
if (!toRemove.length) {
|
|
645
|
+
setStatus("No installed packages selected to uninstall.");
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Confirm one by one
|
|
650
|
+
state.mode = MODE.CONFIRM_UNINSTALL;
|
|
651
|
+
state.confirmTarget = toRemove[0];
|
|
652
|
+
render(state);
|
|
443
653
|
return;
|
|
444
654
|
}
|
|
445
655
|
|
|
446
656
|
// Install
|
|
447
657
|
if (key.name === "return") {
|
|
448
|
-
|
|
658
|
+
const targets =
|
|
659
|
+
state.selected.size > 0
|
|
660
|
+
? [...state.selected].map((i) => PACKAGES[i])
|
|
661
|
+
: [PACKAGES[state.cursor]];
|
|
662
|
+
|
|
663
|
+
const toInstall = targets.filter((p) => !isInstalled(p.name));
|
|
664
|
+
|
|
665
|
+
if (!toInstall.length) {
|
|
666
|
+
setStatus("All selected packages are already installed.");
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
449
669
|
|
|
450
670
|
cleanup();
|
|
451
|
-
if (prevCount > 0) eraseLines(prevCount);
|
|
452
671
|
|
|
453
|
-
const toInstall = [...selected].map((i) => PACKAGES[i]);
|
|
454
672
|
const total = toInstall.length;
|
|
455
|
-
const
|
|
456
|
-
const
|
|
673
|
+
const divW = Math.min(W() - 2, 72);
|
|
674
|
+
const div = gray("─".repeat(divW));
|
|
675
|
+
const div2 = gray("═".repeat(divW));
|
|
457
676
|
|
|
458
|
-
console.log("");
|
|
459
|
-
console.log(div2);
|
|
677
|
+
console.log("\n" + div2);
|
|
460
678
|
console.log(
|
|
461
679
|
bold(cyan(" Installing ")) +
|
|
462
680
|
bold(yellow(String(total))) +
|
|
@@ -466,37 +684,45 @@ async function main() {
|
|
|
466
684
|
|
|
467
685
|
for (let i = 0; i < toInstall.length; i++) {
|
|
468
686
|
const p = toInstall[i];
|
|
469
|
-
console.log("");
|
|
470
687
|
console.log(
|
|
471
|
-
|
|
688
|
+
"\n " +
|
|
689
|
+
cyan("[" + (i + 1) + "/" + total + "]") +
|
|
690
|
+
" " +
|
|
691
|
+
bold(white(p.label)),
|
|
472
692
|
);
|
|
473
|
-
console.log(
|
|
474
|
-
console.log("");
|
|
693
|
+
console.log(" " + dim("npm install -g " + p.name) + "\n");
|
|
475
694
|
|
|
476
695
|
await animateInstall(p.name);
|
|
477
|
-
installedCache.set(p.name, true);
|
|
696
|
+
installedCache.set(p.name, true);
|
|
478
697
|
|
|
479
698
|
console.log(
|
|
480
|
-
|
|
699
|
+
" " + green("✓") + " " + bold(green(p.label)) + " installed",
|
|
481
700
|
);
|
|
482
|
-
console.log(
|
|
701
|
+
console.log(" " + gray("Usage: ") + cyan(p.cmd + " --help"));
|
|
483
702
|
}
|
|
484
703
|
|
|
485
|
-
console.log("");
|
|
486
|
-
console.log(div2);
|
|
704
|
+
console.log("\n" + div2);
|
|
487
705
|
console.log(
|
|
488
|
-
|
|
706
|
+
" " +
|
|
707
|
+
green("✓ All done! ") +
|
|
489
708
|
bold(yellow(String(total))) +
|
|
490
|
-
|
|
709
|
+
" package" +
|
|
710
|
+
(total > 1 ? "s" : "") +
|
|
711
|
+
" installed.",
|
|
491
712
|
);
|
|
492
713
|
console.log("");
|
|
493
|
-
toInstall.forEach((p) =>
|
|
714
|
+
toInstall.forEach((p) =>
|
|
494
715
|
console.log(
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
716
|
+
" " +
|
|
717
|
+
cyan("▸") +
|
|
718
|
+
" " +
|
|
719
|
+
bold(p.cmd) +
|
|
720
|
+
gray(" --help") +
|
|
721
|
+
" " +
|
|
722
|
+
dim(p.label),
|
|
723
|
+
),
|
|
724
|
+
);
|
|
725
|
+
console.log(div2 + "\n");
|
|
500
726
|
|
|
501
727
|
showCursor();
|
|
502
728
|
process.exit(0);
|
|
@@ -504,8 +730,13 @@ async function main() {
|
|
|
504
730
|
});
|
|
505
731
|
}
|
|
506
732
|
|
|
733
|
+
function sleep(ms) {
|
|
734
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
735
|
+
}
|
|
736
|
+
|
|
507
737
|
main().catch((err) => {
|
|
508
738
|
showCursor();
|
|
739
|
+
leaveAltScreen();
|
|
509
740
|
console.error(red("\n ✗ " + err.message + "\n"));
|
|
510
741
|
process.exit(1);
|
|
511
742
|
});
|