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.

Files changed (2) hide show
  1. package/bin/index.js +417 -165
  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,113 +172,241 @@ 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 if a package is already installed globally ─────────────────────────
176
- function isInstalled(name) {
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
- stdio: "ignore",
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
- return true;
232
+ const match = out.match(new RegExp(name + "@([\\d.]+)"));
233
+ return match ? match[1] : null;
183
234
  } catch {
184
- return false;
235
+ return null;
185
236
  }
186
237
  }
187
238
 
188
- // ── Render the interactive list ───────────────────────────────────────────────
189
- const DIV_W = () => Math.min(W() - 2, 70);
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
- function renderList(selected, cursor) {
192
- const div = gray("─".repeat(DIV_W()));
193
- const div2 = gray("═".repeat(DIV_W()));
194
- const lines = [];
252
+ clearScreen();
195
253
 
196
- lines.push("");
197
- lines.push(div2);
198
- lines.push(
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
- lines.push(
202
- gray(" ↑↓ navigate ") +
203
- gray("space select ") +
204
- gray("a all ") +
205
- gray("enter install ") +
206
- 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",
207
278
  );
208
- lines.push(div2);
209
- lines.push("");
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 isSelected = selected.has(i);
214
- const installed = isInstalled(p.name);
285
+ const isSel = selected.has(i);
286
+ const inst = isInstalled(p.name);
215
287
 
216
- const checkbox = isSelected ? green("[✓]") : gray("[ ]");
288
+ const checkbox = isSel ? green("[✓]") : gray("[ ]");
217
289
  const arrow = isCursor ? cyan("▶") : " ";
218
290
  const label = isCursor
219
291
  ? bold(white(p.label))
220
- : isSelected
292
+ : isSel
221
293
  ? green(p.label)
222
294
  : white(p.label);
223
- const badge = installed ? dim(gray(" (installed)")) : "";
295
+ const badge = inst
296
+ ? dim(green(" ● installed"))
297
+ : dim(gray(" ○ not installed"));
224
298
 
225
- lines.push(` ${arrow} ${checkbox} ${label}${badge}`);
299
+ writeLine(row++, ` ${arrow} ${checkbox} ${label}${badge}`);
226
300
 
227
301
  if (isCursor) {
228
- lines.push(` ${dim(p.author)}`);
229
- lines.push(` ${gray(p.desc)}`);
230
- lines.push(
231
- ` ${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")}` : ""),
232
309
  );
233
- lines.push("");
310
+ row++; // blank after expanded
234
311
  }
235
312
  });
236
313
 
237
- lines.push("");
238
- lines.push(div);
314
+ row++; // blank
315
+ writeLine(row++, div);
239
316
 
240
- const count = selected.size;
241
- if (count > 0) {
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
- 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
+ );
244
330
  } else {
245
- lines.push(
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
- 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
+ }
253
352
  }
254
353
 
255
- function printLines(lines) {
256
- 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("");
257
377
  }
258
378
 
259
- function eraseLines(n) {
260
- for (let i = 0; i < n; i++) {
261
- clearLine();
262
- 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;
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
- clearLine();
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("Available packages: ") + yellow(String(PACKAGES.length)),
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
- gray(" Open interactive installer"),
441
+ green(" --list") +
442
+ gray(" List all packages + install status"),
298
443
  );
299
444
  console.log(
300
445
  " " +
301
446
  cyan("sunnah") +
302
- green(" --list") +
303
- gray(" List all available packages"),
447
+ green(" --update") +
448
+ gray(" Check all installed packages for updates"),
304
449
  );
305
450
  console.log(
306
- " " + cyan("sunnah") + green(" -v") + gray(" Show version"),
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(" Show this help"),
457
+ gray(" Show this help"),
313
458
  );
314
- console.log("");
315
- console.log(" " + bold("Controls (interactive mode):"));
316
- console.log(" " + green("↑ ↓") + gray(" Navigate packages"));
317
- console.log(" " + green("space") + gray(" Toggle selection"));
318
- console.log(" " + green("a") + gray(" Toggle all / deselect all"));
319
- console.log(" " + green("enter") + gray(" Install selected packages"));
320
- console.log(" " + green("q") + gray(" Quit"));
321
- console.log("");
322
- console.log(div);
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
- const div = gray("─".repeat(60));
330
- console.log("");
331
- console.log(div);
332
- console.log(bold(cyan(" Available Sunnah Packages")));
333
- console.log(div);
334
- PACKAGES.forEach((p) => {
335
- const inst = isInstalled(p.name) ? green(" ✓ installed") : "";
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
- readline.emitKeypressEvents(process.stdin);
357
- process.stdin.setRawMode(true);
358
- hideCursor();
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
- let cursor = 0;
361
- let selected = new Set();
362
- let prevCount = 0;
496
+ // Enter alternate screen buffer — this completely prevents scroll issues
497
+ enterAltScreen();
498
+ hideCursor();
363
499
 
364
- const draw = () => {
365
- const lines = renderList(selected, cursor);
366
- if (prevCount > 0) eraseLines(prevCount);
367
- printLines(lines);
368
- prevCount = lines.length;
500
+ const state = {
501
+ cursor: 0,
502
+ selected: new Set(),
503
+ mode: MODE.LIST,
504
+ confirmTarget: null,
505
+ statusMsg: "",
369
506
  };
370
507
 
371
- 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);
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
- draw();
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
- draw();
606
+ state.cursor = (state.cursor + 1) % PACKAGES.length;
607
+ render(state);
407
608
  return;
408
609
  }
409
610
 
410
- // Toggle selection
611
+ // Toggle select
411
612
  if (str === " ") {
412
- if (selected.has(cursor)) selected.delete(cursor);
413
- else selected.add(cursor);
414
- draw();
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
- // Toggle all
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
- 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);
423
653
  return;
424
654
  }
425
655
 
426
656
  // Install
427
657
  if (key.name === "return") {
428
- 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
+ }
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 div = gray("─".repeat(DIV_W()));
436
- 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));
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
- ` ${cyan("[" + (i + 1) + "/" + total + "]")} ${bold(white(p.label))}`,
688
+ "\n " +
689
+ cyan("[" + (i + 1) + "/" + total + "]") +
690
+ " " +
691
+ bold(white(p.label)),
452
692
  );
453
- console.log(` ${dim("npm install -g " + p.name)}`);
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
- ` ${green("✓")} ${bold(green(p.label))} installed successfully`,
699
+ " " + green("✓") + " " + bold(green(p.label)) + " installed",
460
700
  );
461
- console.log(` ${gray("Usage: ")}${cyan(p.cmd + " --help")}`);
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
- ` ${green("✓")} All done! ` +
706
+ " " +
707
+ green("✓ All done! ") +
468
708
  bold(yellow(String(total))) +
469
- ` package${total > 1 ? "s" : ""} installed globally.`,
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
- ` ${cyan("▸")} ${bold(p.cmd)} ${gray("--help")} ${dim("·")} ${dim(p.label)}`,
475
- );
476
- });
477
- console.log(div2);
478
- 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");
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sunnah",
3
- "version": "1.1.3",
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"