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.

Files changed (2) hide show
  1. package/bin/index.js +400 -169
  2. 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 helpers ──────────────────────────────────────────────────────────
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 clearLine() {
85
- process.stdout.write("\r\x1b[K");
86
+ function enterAltScreen() {
87
+ process.stdout.write("\x1b[?1049h");
86
88
  }
87
-
88
- function moveUp(n) {
89
- if (n > 0) process.stdout.write(`\x1b[${n}A`);
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 = 38) {
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: 90 },
114
- { label: "Fetching metadata…", end: 28, ms: 70 },
115
- { label: "Downloading tarball…", end: 72, ms: 25 },
116
- { label: "Extracting files…", end: 88, ms: 55 },
117
- { label: "Linking binaries…", end: 98, ms: 90 },
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
- process.stdout.write(drawBar(stages[0].label, 0) + "\n");
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) / 22;
147
+ const step = (stage.end - prevEnd) / 24;
132
148
  percent = Math.min(percent + step, stage.end);
133
149
 
134
- process.stdout.write("\r\x1b[K");
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("\r\x1b[K");
144
- process.stdout.write(drawBar("Complete!", 100));
145
- process.stdout.write("\n");
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
- // spawn npm.cmd on Windows, npm on Unix
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
- // ── Check installed status ONCE at startup, cache forever ───────────────────
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: 8000,
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
- // ── Render the interactive list ───────────────────────────────────────────────
201
- const DIV_W = () => Math.min(W() - 2, 70);
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
- function renderList(selected, cursor) {
204
- const div = gray("─".repeat(DIV_W()));
205
- const div2 = gray("═".repeat(DIV_W()));
206
- const lines = [];
254
+ let row = 1;
207
255
 
208
- lines.push("");
209
- lines.push(div2);
210
- lines.push(
256
+ // Header
257
+ writeLine(row++, div2);
258
+ writeLine(
259
+ row++,
211
260
  bold(cyan(" 📚 Sunnah Package Manager")) + gray(" v" + pkg.version),
212
261
  );
213
- lines.push(
214
- gray(" ↑↓ navigate ") +
215
- gray("space select ") +
216
- gray("a all ") +
217
- gray("enter install ") +
218
- gray("q quit"),
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
- lines.push(div2);
221
- lines.push("");
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 isSelected = selected.has(i);
226
- const installed = isInstalled(p.name);
285
+ const isSel = selected.has(i);
286
+ const inst = isInstalled(p.name);
227
287
 
228
- const checkbox = isSelected ? green("[✓]") : gray("[ ]");
288
+ const checkbox = isSel ? green("[✓]") : gray("[ ]");
229
289
  const arrow = isCursor ? cyan("▶") : " ";
230
290
  const label = isCursor
231
291
  ? bold(white(p.label))
232
- : isSelected
292
+ : isSel
233
293
  ? green(p.label)
234
294
  : white(p.label);
235
- const badge = installed ? dim(gray(" (installed)")) : "";
295
+ const badge = inst
296
+ ? dim(green(" ● installed"))
297
+ : dim(gray(" ○ not installed"));
236
298
 
237
- lines.push(` ${arrow} ${checkbox} ${label}${badge}`);
299
+ writeLine(row++, ` ${arrow} ${checkbox} ${label}${badge}`);
238
300
 
239
301
  if (isCursor) {
240
- lines.push(` ${dim(p.author)}`);
241
- lines.push(` ${gray(p.desc)}`);
242
- lines.push(
243
- ` ${gray("Hadiths: ")}${yellow(p.hadiths)} ${gray("CLI: ")}${cyan(p.cmd + " --help")}`,
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
- lines.push("");
310
+ row++; // blank after expanded
246
311
  }
247
312
  });
248
313
 
249
- lines.push("");
250
- lines.push(div);
314
+ row++; // blank
315
+ writeLine(row++, div);
251
316
 
252
- const count = selected.size;
253
- if (count > 0) {
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
- lines.push(` ${green("●")} ${bold(String(count))} selected: ${names}`);
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
- lines.push(
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
- return lines;
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
- function printLines(lines) {
268
- process.stdout.write(lines.join("\n") + "\n");
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
- function eraseLines(n) {
272
- for (let i = 0; i < n; i++) {
273
- clearLine();
274
- if (i < n - 1) moveUp(1);
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
- clearLine();
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("Available packages: ") + yellow(String(PACKAGES.length)),
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
- gray(" Open interactive installer"),
441
+ green(" --list") +
442
+ gray(" List all packages + install status"),
310
443
  );
311
444
  console.log(
312
445
  " " +
313
446
  cyan("sunnah") +
314
- green(" --list") +
315
- gray(" List all available packages"),
447
+ green(" --update") +
448
+ gray(" Check all installed packages for updates"),
316
449
  );
317
450
  console.log(
318
- " " + cyan("sunnah") + green(" -v") + gray(" Show version"),
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(" Show this help"),
457
+ gray(" Show this help"),
325
458
  );
326
- console.log("");
327
- console.log(" " + bold("Controls (interactive mode):"));
328
- console.log(" " + green("↑ ↓") + gray(" Navigate packages"));
329
- console.log(" " + green("space") + gray(" Toggle selection"));
330
- console.log(" " + green("a") + gray(" Toggle all / deselect all"));
331
- console.log(" " + green("enter") + gray(" Install selected packages"));
332
- console.log(" " + green("q") + gray(" Quit"));
333
- console.log("");
334
- console.log(div);
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
- installedCache = buildInstalledCache();
342
- const div = gray("─".repeat(60));
343
- console.log("");
344
- console.log(div);
345
- console.log(bold(cyan(" Available Sunnah Packages")));
346
- console.log(div);
347
- PACKAGES.forEach((p) => {
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 installed cache once never call npm during rendering
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
- readline.emitKeypressEvents(process.stdin);
377
- process.stdin.setRawMode(true);
496
+ // Enter alternate screen buffer — this completely prevents scroll issues
497
+ enterAltScreen();
378
498
  hideCursor();
379
499
 
380
- let cursor = 0;
381
- let selected = new Set();
382
- let prevCount = 0;
383
-
384
- const draw = () => {
385
- const lines = renderList(selected, cursor);
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
- draw();
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
- draw();
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
- draw();
606
+ state.cursor = (state.cursor + 1) % PACKAGES.length;
607
+ render(state);
427
608
  return;
428
609
  }
429
610
 
430
- // Toggle selection
611
+ // Toggle select
431
612
  if (str === " ") {
432
- if (selected.has(cursor)) selected.delete(cursor);
433
- else selected.add(cursor);
434
- draw();
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
- // Toggle all
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
- draw();
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
- if (selected.size === 0) return;
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 div = gray("─".repeat(DIV_W()));
456
- const div2 = gray("".repeat(DIV_W()));
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
- ` ${cyan("[" + (i + 1) + "/" + total + "]")} ${bold(white(p.label))}`,
688
+ "\n " +
689
+ cyan("[" + (i + 1) + "/" + total + "]") +
690
+ " " +
691
+ bold(white(p.label)),
472
692
  );
473
- console.log(` ${dim("npm install -g " + p.name)}`);
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); // update cache
696
+ installedCache.set(p.name, true);
478
697
 
479
698
  console.log(
480
- ` ${green("✓")} ${bold(green(p.label))} installed successfully`,
699
+ " " + green("✓") + " " + bold(green(p.label)) + " installed",
481
700
  );
482
- console.log(` ${gray("Usage: ")}${cyan(p.cmd + " --help")}`);
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
- ` ${green("✓")} All done! ` +
706
+ " " +
707
+ green("✓ All done! ") +
489
708
  bold(yellow(String(total))) +
490
- ` package${total > 1 ? "s" : ""} installed globally.`,
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
- ` ${cyan("▸")} ${bold(p.cmd)} ${gray("--help")} ${dim("·")} ${dim(p.label)}`,
496
- );
497
- });
498
- console.log(div2);
499
- console.log("");
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sunnah",
3
- "version": "1.1.4",
3
+ "version": "1.2.0",
4
4
  "description": "Interactive CLI installer for Sunnah hadith npm packages",
5
5
  "bin": {
6
6
  "sunnah": "./bin/index.js"