sunnah 1.1.3 → 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 +417 -165
- 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,113 +172,241 @@ 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
|
-
function
|
|
189
|
+
// ── Installed cache — built ONCE, never during render ────────────────────────
|
|
190
|
+
function buildInstalledCache() {
|
|
191
|
+
const cache = new Map();
|
|
192
|
+
let out = "";
|
|
193
|
+
try {
|
|
194
|
+
out = execSync(`${NPM} list -g --depth=0`, {
|
|
195
|
+
encoding: "utf8",
|
|
196
|
+
shell: isWin,
|
|
197
|
+
timeout: 10000,
|
|
198
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
199
|
+
});
|
|
200
|
+
} catch (e) {
|
|
201
|
+
out = e.stdout || "";
|
|
202
|
+
}
|
|
203
|
+
for (const p of PACKAGES) {
|
|
204
|
+
cache.set(p.name, out.includes(p.name));
|
|
205
|
+
}
|
|
206
|
+
return cache;
|
|
207
|
+
}
|
|
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) {
|
|
177
225
|
try {
|
|
178
|
-
execSync(`${NPM} list -g ${name} --depth=0`, {
|
|
179
|
-
|
|
226
|
+
const out = execSync(`${NPM} list -g ${name} --depth=0`, {
|
|
227
|
+
encoding: "utf8",
|
|
180
228
|
shell: isWin,
|
|
229
|
+
timeout: 8000,
|
|
230
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
181
231
|
});
|
|
182
|
-
|
|
232
|
+
const match = out.match(new RegExp(name + "@([\\d.]+)"));
|
|
233
|
+
return match ? match[1] : null;
|
|
183
234
|
} catch {
|
|
184
|
-
return
|
|
235
|
+
return null;
|
|
185
236
|
}
|
|
186
237
|
}
|
|
187
238
|
|
|
188
|
-
|
|
189
|
-
const
|
|
239
|
+
let installedCache = new Map();
|
|
240
|
+
const isInstalled = (name) => installedCache.get(name) ?? false;
|
|
241
|
+
|
|
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));
|
|
190
251
|
|
|
191
|
-
|
|
192
|
-
const div = gray("─".repeat(DIV_W()));
|
|
193
|
-
const div2 = gray("═".repeat(DIV_W()));
|
|
194
|
-
const lines = [];
|
|
252
|
+
clearScreen();
|
|
195
253
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
254
|
+
let row = 1;
|
|
255
|
+
|
|
256
|
+
// Header
|
|
257
|
+
writeLine(row++, div2);
|
|
258
|
+
writeLine(
|
|
259
|
+
row++,
|
|
199
260
|
bold(cyan(" 📚 Sunnah Package Manager")) + gray(" v" + pkg.version),
|
|
200
261
|
);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
gray("
|
|
206
|
-
|
|
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",
|
|
207
278
|
);
|
|
208
|
-
|
|
209
|
-
|
|
279
|
+
writeLine(row++, div2);
|
|
280
|
+
row++; // blank
|
|
210
281
|
|
|
282
|
+
// Package list
|
|
211
283
|
PACKAGES.forEach((p, i) => {
|
|
212
284
|
const isCursor = i === cursor;
|
|
213
|
-
const
|
|
214
|
-
const
|
|
285
|
+
const isSel = selected.has(i);
|
|
286
|
+
const inst = isInstalled(p.name);
|
|
215
287
|
|
|
216
|
-
const checkbox =
|
|
288
|
+
const checkbox = isSel ? green("[✓]") : gray("[ ]");
|
|
217
289
|
const arrow = isCursor ? cyan("▶") : " ";
|
|
218
290
|
const label = isCursor
|
|
219
291
|
? bold(white(p.label))
|
|
220
|
-
:
|
|
292
|
+
: isSel
|
|
221
293
|
? green(p.label)
|
|
222
294
|
: white(p.label);
|
|
223
|
-
const badge =
|
|
295
|
+
const badge = inst
|
|
296
|
+
? dim(green(" ● installed"))
|
|
297
|
+
: dim(gray(" ○ not installed"));
|
|
224
298
|
|
|
225
|
-
|
|
299
|
+
writeLine(row++, ` ${arrow} ${checkbox} ${label}${badge}`);
|
|
226
300
|
|
|
227
301
|
if (isCursor) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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")}` : ""),
|
|
232
309
|
);
|
|
233
|
-
|
|
310
|
+
row++; // blank after expanded
|
|
234
311
|
}
|
|
235
312
|
});
|
|
236
313
|
|
|
237
|
-
|
|
238
|
-
|
|
314
|
+
row++; // blank
|
|
315
|
+
writeLine(row++, div);
|
|
239
316
|
|
|
240
|
-
|
|
241
|
-
if (
|
|
317
|
+
// Status / selection footer
|
|
318
|
+
if (statusMsg) {
|
|
319
|
+
writeLine(row++, ` ${yellow("⚠")} ${yellow(statusMsg)}`);
|
|
320
|
+
} else if (selected.size > 0) {
|
|
242
321
|
const names = [...selected].map((i) => cyan(PACKAGES[i].name)).join(", ");
|
|
243
|
-
|
|
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
|
+
);
|
|
244
330
|
} else {
|
|
245
|
-
|
|
331
|
+
writeLine(
|
|
332
|
+
row++,
|
|
246
333
|
` ${gray("Nothing selected — press space to select a package")}`,
|
|
247
334
|
);
|
|
248
335
|
}
|
|
249
|
-
lines.push(div);
|
|
250
|
-
lines.push("");
|
|
251
336
|
|
|
252
|
-
|
|
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
|
+
}
|
|
253
352
|
}
|
|
254
353
|
|
|
255
|
-
|
|
256
|
-
|
|
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("");
|
|
257
377
|
}
|
|
258
378
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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;
|
|
386
|
+
}
|
|
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
|
+
}
|
|
263
408
|
}
|
|
264
|
-
|
|
409
|
+
console.log("\n" + div + "\n");
|
|
265
410
|
}
|
|
266
411
|
|
|
267
412
|
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
@@ -271,79 +416,69 @@ async function main() {
|
|
|
271
416
|
|
|
272
417
|
// --version
|
|
273
418
|
if (flags.some((f) => f === "-v" || f === "--version")) {
|
|
274
|
-
console.log("");
|
|
275
|
-
console.log(" " + bold(cyan("sunnah")) + gray(" v" + pkg.version));
|
|
419
|
+
console.log("\n " + bold(cyan("sunnah")) + gray(" v" + pkg.version));
|
|
276
420
|
console.log(
|
|
277
|
-
" " + gray("
|
|
421
|
+
" " + gray("Packages: ") + yellow(String(PACKAGES.length)) + "\n",
|
|
278
422
|
);
|
|
279
|
-
console.log("");
|
|
280
423
|
process.exit(0);
|
|
281
424
|
}
|
|
282
425
|
|
|
283
426
|
// --help
|
|
284
427
|
if (flags.some((f) => f === "-h" || f === "--help")) {
|
|
285
428
|
const div = gray("─".repeat(60));
|
|
286
|
-
console.log("");
|
|
287
|
-
console.log(div);
|
|
429
|
+
console.log("\n" + div);
|
|
288
430
|
console.log(
|
|
289
431
|
bold(cyan(" Sunnah Package Manager")) + gray(" v" + pkg.version),
|
|
290
432
|
);
|
|
291
|
-
console.log(div);
|
|
292
|
-
console.log("");
|
|
433
|
+
console.log(div + "\n");
|
|
293
434
|
console.log(" " + bold("Usage:"));
|
|
435
|
+
console.log(
|
|
436
|
+
" " + cyan("sunnah") + gray(" Open interactive UI"),
|
|
437
|
+
);
|
|
294
438
|
console.log(
|
|
295
439
|
" " +
|
|
296
440
|
cyan("sunnah") +
|
|
297
|
-
|
|
441
|
+
green(" --list") +
|
|
442
|
+
gray(" List all packages + install status"),
|
|
298
443
|
);
|
|
299
444
|
console.log(
|
|
300
445
|
" " +
|
|
301
446
|
cyan("sunnah") +
|
|
302
|
-
green(" --
|
|
303
|
-
gray("
|
|
447
|
+
green(" --update") +
|
|
448
|
+
gray(" Check all installed packages for updates"),
|
|
304
449
|
);
|
|
305
450
|
console.log(
|
|
306
|
-
" " + cyan("sunnah") + green(" -v") + gray("
|
|
451
|
+
" " + cyan("sunnah") + green(" -v") + gray(" Show version"),
|
|
307
452
|
);
|
|
308
453
|
console.log(
|
|
309
454
|
" " +
|
|
310
455
|
cyan("sunnah") +
|
|
311
456
|
green(" -h") +
|
|
312
|
-
gray("
|
|
457
|
+
gray(" Show this help"),
|
|
313
458
|
);
|
|
314
|
-
console.log("");
|
|
315
|
-
console.log("
|
|
316
|
-
console.log(" " + green("
|
|
317
|
-
console.log(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
console.log(" " + green("
|
|
321
|
-
console.log("");
|
|
322
|
-
console.log(
|
|
323
|
-
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");
|
|
324
470
|
process.exit(0);
|
|
325
471
|
}
|
|
326
472
|
|
|
327
473
|
// --list
|
|
328
474
|
if (flags.some((f) => f === "--list" || f === "-l")) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
console.log("");
|
|
337
|
-
console.log(` ${bold(white(p.label))}${inst}`);
|
|
338
|
-
console.log(` ${cyan("npm install -g " + p.name)}`);
|
|
339
|
-
console.log(` ${dim(p.desc)}`);
|
|
340
|
-
console.log(
|
|
341
|
-
` ${gray("Hadiths: ")}${yellow(p.hadiths)} ${gray("Author: ")}${magenta(p.author)}`,
|
|
342
|
-
);
|
|
343
|
-
});
|
|
344
|
-
console.log("");
|
|
345
|
-
console.log(div);
|
|
346
|
-
console.log("");
|
|
475
|
+
cmdList();
|
|
476
|
+
process.exit(0);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// --update
|
|
480
|
+
if (flags.some((f) => f === "--update")) {
|
|
481
|
+
cmdUpdate();
|
|
347
482
|
process.exit(0);
|
|
348
483
|
}
|
|
349
484
|
|
|
@@ -353,25 +488,40 @@ async function main() {
|
|
|
353
488
|
process.exit(1);
|
|
354
489
|
}
|
|
355
490
|
|
|
356
|
-
|
|
357
|
-
process.
|
|
358
|
-
|
|
491
|
+
// Build cache before entering alt screen
|
|
492
|
+
process.stdout.write("\n " + gray("Checking installed packages…"));
|
|
493
|
+
installedCache = buildInstalledCache();
|
|
494
|
+
process.stdout.write("\r\x1b[K");
|
|
359
495
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
496
|
+
// Enter alternate screen buffer — this completely prevents scroll issues
|
|
497
|
+
enterAltScreen();
|
|
498
|
+
hideCursor();
|
|
363
499
|
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
500
|
+
const state = {
|
|
501
|
+
cursor: 0,
|
|
502
|
+
selected: new Set(),
|
|
503
|
+
mode: MODE.LIST,
|
|
504
|
+
confirmTarget: null,
|
|
505
|
+
statusMsg: "",
|
|
369
506
|
};
|
|
370
507
|
|
|
371
|
-
|
|
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);
|
|
372
521
|
|
|
373
522
|
const cleanup = () => {
|
|
374
523
|
showCursor();
|
|
524
|
+
leaveAltScreen();
|
|
375
525
|
try {
|
|
376
526
|
process.stdin.setRawMode(false);
|
|
377
527
|
} catch {}
|
|
@@ -380,63 +530,151 @@ async function main() {
|
|
|
380
530
|
|
|
381
531
|
process.on("SIGINT", () => {
|
|
382
532
|
cleanup();
|
|
383
|
-
console.log("");
|
|
384
533
|
process.exit(0);
|
|
385
534
|
});
|
|
386
535
|
|
|
536
|
+
readline.emitKeypressEvents(process.stdin);
|
|
537
|
+
process.stdin.setRawMode(true);
|
|
538
|
+
|
|
387
539
|
process.stdin.on("keypress", async (str, key) => {
|
|
388
540
|
if (!key) return;
|
|
389
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
|
+
|
|
390
592
|
// Quit
|
|
391
593
|
if (key.name === "q" || (key.ctrl && key.name === "c")) {
|
|
392
594
|
cleanup();
|
|
393
|
-
if (prevCount > 0) eraseLines(prevCount);
|
|
394
595
|
console.log("\n " + gray("Goodbye.\n"));
|
|
395
596
|
process.exit(0);
|
|
396
597
|
}
|
|
397
598
|
|
|
398
599
|
// Navigate
|
|
399
600
|
if (key.name === "up") {
|
|
400
|
-
cursor = (cursor - 1 + PACKAGES.length) % PACKAGES.length;
|
|
401
|
-
|
|
601
|
+
state.cursor = (state.cursor - 1 + PACKAGES.length) % PACKAGES.length;
|
|
602
|
+
render(state);
|
|
402
603
|
return;
|
|
403
604
|
}
|
|
404
605
|
if (key.name === "down") {
|
|
405
|
-
cursor = (cursor + 1) % PACKAGES.length;
|
|
406
|
-
|
|
606
|
+
state.cursor = (state.cursor + 1) % PACKAGES.length;
|
|
607
|
+
render(state);
|
|
407
608
|
return;
|
|
408
609
|
}
|
|
409
610
|
|
|
410
|
-
// Toggle
|
|
611
|
+
// Toggle select
|
|
411
612
|
if (str === " ") {
|
|
412
|
-
if (selected.has(cursor)) selected.delete(cursor);
|
|
413
|
-
else selected.add(cursor);
|
|
414
|
-
|
|
613
|
+
if (state.selected.has(state.cursor)) state.selected.delete(state.cursor);
|
|
614
|
+
else state.selected.add(state.cursor);
|
|
615
|
+
render(state);
|
|
415
616
|
return;
|
|
416
617
|
}
|
|
417
618
|
|
|
418
|
-
//
|
|
619
|
+
// Select all / deselect all
|
|
419
620
|
if (str === "a" || str === "A") {
|
|
420
|
-
if (selected.size === PACKAGES.length) selected.clear();
|
|
421
|
-
else PACKAGES.forEach((_, i) => selected.add(i));
|
|
422
|
-
|
|
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);
|
|
423
653
|
return;
|
|
424
654
|
}
|
|
425
655
|
|
|
426
656
|
// Install
|
|
427
657
|
if (key.name === "return") {
|
|
428
|
-
|
|
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
|
+
}
|
|
429
669
|
|
|
430
670
|
cleanup();
|
|
431
|
-
if (prevCount > 0) eraseLines(prevCount);
|
|
432
671
|
|
|
433
|
-
const toInstall = [...selected].map((i) => PACKAGES[i]);
|
|
434
672
|
const total = toInstall.length;
|
|
435
|
-
const
|
|
436
|
-
const
|
|
673
|
+
const divW = Math.min(W() - 2, 72);
|
|
674
|
+
const div = gray("─".repeat(divW));
|
|
675
|
+
const div2 = gray("═".repeat(divW));
|
|
437
676
|
|
|
438
|
-
console.log("");
|
|
439
|
-
console.log(div2);
|
|
677
|
+
console.log("\n" + div2);
|
|
440
678
|
console.log(
|
|
441
679
|
bold(cyan(" Installing ")) +
|
|
442
680
|
bold(yellow(String(total))) +
|
|
@@ -446,36 +684,45 @@ async function main() {
|
|
|
446
684
|
|
|
447
685
|
for (let i = 0; i < toInstall.length; i++) {
|
|
448
686
|
const p = toInstall[i];
|
|
449
|
-
console.log("");
|
|
450
687
|
console.log(
|
|
451
|
-
|
|
688
|
+
"\n " +
|
|
689
|
+
cyan("[" + (i + 1) + "/" + total + "]") +
|
|
690
|
+
" " +
|
|
691
|
+
bold(white(p.label)),
|
|
452
692
|
);
|
|
453
|
-
console.log(
|
|
454
|
-
console.log("");
|
|
693
|
+
console.log(" " + dim("npm install -g " + p.name) + "\n");
|
|
455
694
|
|
|
456
695
|
await animateInstall(p.name);
|
|
696
|
+
installedCache.set(p.name, true);
|
|
457
697
|
|
|
458
698
|
console.log(
|
|
459
|
-
|
|
699
|
+
" " + green("✓") + " " + bold(green(p.label)) + " installed",
|
|
460
700
|
);
|
|
461
|
-
console.log(
|
|
701
|
+
console.log(" " + gray("Usage: ") + cyan(p.cmd + " --help"));
|
|
462
702
|
}
|
|
463
703
|
|
|
464
|
-
console.log("");
|
|
465
|
-
console.log(div2);
|
|
704
|
+
console.log("\n" + div2);
|
|
466
705
|
console.log(
|
|
467
|
-
|
|
706
|
+
" " +
|
|
707
|
+
green("✓ All done! ") +
|
|
468
708
|
bold(yellow(String(total))) +
|
|
469
|
-
|
|
709
|
+
" package" +
|
|
710
|
+
(total > 1 ? "s" : "") +
|
|
711
|
+
" installed.",
|
|
470
712
|
);
|
|
471
713
|
console.log("");
|
|
472
|
-
toInstall.forEach((p) =>
|
|
714
|
+
toInstall.forEach((p) =>
|
|
473
715
|
console.log(
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
716
|
+
" " +
|
|
717
|
+
cyan("▸") +
|
|
718
|
+
" " +
|
|
719
|
+
bold(p.cmd) +
|
|
720
|
+
gray(" --help") +
|
|
721
|
+
" " +
|
|
722
|
+
dim(p.label),
|
|
723
|
+
),
|
|
724
|
+
);
|
|
725
|
+
console.log(div2 + "\n");
|
|
479
726
|
|
|
480
727
|
showCursor();
|
|
481
728
|
process.exit(0);
|
|
@@ -483,8 +730,13 @@ async function main() {
|
|
|
483
730
|
});
|
|
484
731
|
}
|
|
485
732
|
|
|
733
|
+
function sleep(ms) {
|
|
734
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
735
|
+
}
|
|
736
|
+
|
|
486
737
|
main().catch((err) => {
|
|
487
738
|
showCursor();
|
|
739
|
+
leaveAltScreen();
|
|
488
740
|
console.error(red("\n ✗ " + err.message + "\n"));
|
|
489
741
|
process.exit(1);
|
|
490
742
|
});
|