sunnah 1.2.0 → 1.2.1
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/README.md +158 -111
- package/bin/index.js +675 -132
- package/package.json +1 -1
package/bin/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import fs from "fs";
|
|
6
|
+
import os from "os";
|
|
6
7
|
import { execSync, spawn } from "child_process";
|
|
7
8
|
import readline from "readline";
|
|
8
9
|
|
|
@@ -16,6 +17,28 @@ const pkg = JSON.parse(
|
|
|
16
17
|
const isWin = process.platform === "win32";
|
|
17
18
|
const NPM = isWin ? "npm.cmd" : "npm";
|
|
18
19
|
|
|
20
|
+
// ── Persistence: remember last selections ────────────────────────────────────
|
|
21
|
+
const STATE_FILE = path.join(os.homedir(), ".sunnah-state.json");
|
|
22
|
+
|
|
23
|
+
function loadPersistedState() {
|
|
24
|
+
try {
|
|
25
|
+
const raw = fs.readFileSync(STATE_FILE, "utf8");
|
|
26
|
+
return JSON.parse(raw);
|
|
27
|
+
} catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function savePersistedState(data) {
|
|
33
|
+
try {
|
|
34
|
+
const existing = loadPersistedState();
|
|
35
|
+
fs.writeFileSync(
|
|
36
|
+
STATE_FILE,
|
|
37
|
+
JSON.stringify({ ...existing, ...data }, null, 2),
|
|
38
|
+
);
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
|
|
19
42
|
// ── Colors ────────────────────────────────────────────────────────────────────
|
|
20
43
|
const c = {
|
|
21
44
|
reset: "\x1b[0m",
|
|
@@ -52,6 +75,7 @@ const PACKAGES = [
|
|
|
52
75
|
desc: "The most authentic collection of hadith, widely regarded as the most sahih after the Quran.",
|
|
53
76
|
hadiths: "7,563",
|
|
54
77
|
cmd: "bukhari",
|
|
78
|
+
hook: "useBukhari",
|
|
55
79
|
},
|
|
56
80
|
{
|
|
57
81
|
name: "sahih-muslim",
|
|
@@ -60,6 +84,7 @@ const PACKAGES = [
|
|
|
60
84
|
desc: "Second most authentic hadith collection, known for its strict methodology and chain verification.",
|
|
61
85
|
hadiths: "7,470",
|
|
62
86
|
cmd: "muslim",
|
|
87
|
+
hook: "useMuslim",
|
|
63
88
|
},
|
|
64
89
|
{
|
|
65
90
|
name: "sunan-abi-dawud",
|
|
@@ -68,6 +93,7 @@ const PACKAGES = [
|
|
|
68
93
|
desc: "One of the six canonical hadith collections, focused on legal rulings and jurisprudence.",
|
|
69
94
|
hadiths: "5,274",
|
|
70
95
|
cmd: "dawud",
|
|
96
|
+
hook: "useDawud",
|
|
71
97
|
},
|
|
72
98
|
{
|
|
73
99
|
name: "jami-al-tirmidhi",
|
|
@@ -76,10 +102,14 @@ const PACKAGES = [
|
|
|
76
102
|
desc: "Part of the six major hadith collections, unique for grading each hadith's authenticity.",
|
|
77
103
|
hadiths: "3,956",
|
|
78
104
|
cmd: "tirmidhi",
|
|
105
|
+
hook: "useTirmidhi",
|
|
79
106
|
},
|
|
80
107
|
];
|
|
81
108
|
|
|
82
|
-
//
|
|
109
|
+
// cmd → package lookup
|
|
110
|
+
const CMD_MAP = Object.fromEntries(PACKAGES.map((p) => [p.cmd, p]));
|
|
111
|
+
|
|
112
|
+
// ── Terminal: alternate screen buffer = no scroll ─────────────────────────────
|
|
83
113
|
const W = () => process.stdout.columns || 80;
|
|
84
114
|
const H = () => process.stdout.rows || 24;
|
|
85
115
|
|
|
@@ -92,8 +122,8 @@ function leaveAltScreen() {
|
|
|
92
122
|
function clearScreen() {
|
|
93
123
|
process.stdout.write("\x1b[2J\x1b[H");
|
|
94
124
|
}
|
|
95
|
-
function moveTo(
|
|
96
|
-
process.stdout.write(`\x1b[${
|
|
125
|
+
function moveTo(r, col) {
|
|
126
|
+
process.stdout.write(`\x1b[${r};${col}H`);
|
|
97
127
|
}
|
|
98
128
|
function hideCursor() {
|
|
99
129
|
process.stdout.write("\x1b[?25l");
|
|
@@ -121,7 +151,7 @@ function drawBar(label, percent, barWidth = 40) {
|
|
|
121
151
|
return ` ${bar} ${pct} ${dim(label)}`;
|
|
122
152
|
}
|
|
123
153
|
|
|
124
|
-
// ──
|
|
154
|
+
// ── Animate install with real npm running behind ──────────────────────────────
|
|
125
155
|
function animateInstall(pkgName) {
|
|
126
156
|
return new Promise((resolve) => {
|
|
127
157
|
const stages = [
|
|
@@ -131,29 +161,22 @@ function animateInstall(pkgName) {
|
|
|
131
161
|
{ label: "Extracting files…", end: 90, ms: 50 },
|
|
132
162
|
{ label: "Linking binaries…", end: 98, ms: 80 },
|
|
133
163
|
];
|
|
134
|
-
|
|
135
164
|
let percent = 0;
|
|
136
165
|
let stageIdx = 0;
|
|
137
166
|
let npmDone = false;
|
|
138
167
|
|
|
139
|
-
// Write bar ONCE — all updates overwrite this same line with \r
|
|
140
168
|
process.stdout.write(drawBar(stages[0].label, 0));
|
|
141
169
|
|
|
142
170
|
const tick = () => {
|
|
143
171
|
const stage = stages[stageIdx];
|
|
144
172
|
if (!stage) return;
|
|
145
|
-
|
|
146
173
|
const prevEnd = stageIdx > 0 ? stages[stageIdx - 1].end : 0;
|
|
147
174
|
const step = (stage.end - prevEnd) / 24;
|
|
148
175
|
percent = Math.min(percent + step, stage.end);
|
|
149
|
-
|
|
150
|
-
// Overwrite the SAME line — no \n, just \r
|
|
151
176
|
process.stdout.write("\r\x1b[K" + drawBar(stage.label, percent));
|
|
152
|
-
|
|
153
177
|
if (percent >= stage.end) {
|
|
154
178
|
stageIdx++;
|
|
155
179
|
if (stageIdx >= stages.length) {
|
|
156
|
-
// Spin until npm actually finishes
|
|
157
180
|
const poll = setInterval(() => {
|
|
158
181
|
if (npmDone) {
|
|
159
182
|
clearInterval(poll);
|
|
@@ -166,13 +189,10 @@ function animateInstall(pkgName) {
|
|
|
166
189
|
return;
|
|
167
190
|
}
|
|
168
191
|
}
|
|
169
|
-
|
|
170
192
|
setTimeout(tick, stages[stageIdx]?.ms ?? 50);
|
|
171
193
|
};
|
|
172
194
|
|
|
173
195
|
setTimeout(tick, stages[0].ms);
|
|
174
|
-
|
|
175
|
-
// Actually run npm install -g
|
|
176
196
|
const proc = spawn(NPM, ["install", "-g", pkgName], {
|
|
177
197
|
stdio: ["ignore", "pipe", "pipe"],
|
|
178
198
|
shell: isWin,
|
|
@@ -186,7 +206,7 @@ function animateInstall(pkgName) {
|
|
|
186
206
|
});
|
|
187
207
|
}
|
|
188
208
|
|
|
189
|
-
// ── Installed cache —
|
|
209
|
+
// ── Installed cache — ONE npm call at startup, Map lookup during render ───────
|
|
190
210
|
function buildInstalledCache() {
|
|
191
211
|
const cache = new Map();
|
|
192
212
|
let out = "";
|
|
@@ -206,7 +226,6 @@ function buildInstalledCache() {
|
|
|
206
226
|
return cache;
|
|
207
227
|
}
|
|
208
228
|
|
|
209
|
-
// Get latest version from npm registry (fast, single HTTP call)
|
|
210
229
|
function getLatestVersion(name) {
|
|
211
230
|
try {
|
|
212
231
|
return execSync(`${NPM} show ${name} version`, {
|
|
@@ -220,7 +239,6 @@ function getLatestVersion(name) {
|
|
|
220
239
|
}
|
|
221
240
|
}
|
|
222
241
|
|
|
223
|
-
// Get currently installed version
|
|
224
242
|
function getInstalledVersion(name) {
|
|
225
243
|
try {
|
|
226
244
|
const out = execSync(`${NPM} list -g ${name} --depth=0`, {
|
|
@@ -239,10 +257,253 @@ function getInstalledVersion(name) {
|
|
|
239
257
|
let installedCache = new Map();
|
|
240
258
|
const isInstalled = (name) => installedCache.get(name) ?? false;
|
|
241
259
|
|
|
260
|
+
// ── Clipboard ─────────────────────────────────────────────────────────────────
|
|
261
|
+
function copyToClipboard(text) {
|
|
262
|
+
try {
|
|
263
|
+
if (isWin) {
|
|
264
|
+
execSync("clip", { input: text, shell: true });
|
|
265
|
+
} else if (process.platform === "darwin") {
|
|
266
|
+
execSync("pbcopy", { input: text });
|
|
267
|
+
} else {
|
|
268
|
+
execSync("xclip -selection clipboard || xsel --clipboard --input", {
|
|
269
|
+
input: text,
|
|
270
|
+
shell: true,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return true;
|
|
274
|
+
} catch {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── React hook generator ──────────────────────────────────────────────────────
|
|
280
|
+
function generateUnifiedHook(books) {
|
|
281
|
+
const cwd = process.cwd();
|
|
282
|
+
const srcDir = path.join(cwd, "src");
|
|
283
|
+
const hooksDir = path.join(srcDir, "hooks");
|
|
284
|
+
|
|
285
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
286
|
+
if (!fs.existsSync(pkgPath)) {
|
|
287
|
+
console.error(
|
|
288
|
+
red("\n ✗ No package.json found. Run inside your React project.\n"),
|
|
289
|
+
);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
const projectPkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
293
|
+
const deps = { ...projectPkg.dependencies, ...projectPkg.devDependencies };
|
|
294
|
+
if (!deps["react"]) {
|
|
295
|
+
console.error(red("\n ✗ React not found in package.json.\n"));
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
if (!fs.existsSync(srcDir)) {
|
|
299
|
+
console.error(red("\n ✗ No src/ directory found.\n"));
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
if (!fs.existsSync(hooksDir)) {
|
|
303
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
304
|
+
console.log(green(" ✓ Created src/hooks/"));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Build per-book loader blocks
|
|
308
|
+
const loaderBlocks = books
|
|
309
|
+
.map((p) => {
|
|
310
|
+
const CDN = `https://cdn.jsdelivr.net/npm/${p.name}@latest/chapters`;
|
|
311
|
+
return `
|
|
312
|
+
// ── ${p.label} ──
|
|
313
|
+
const _${p.cmd}CDN = '${CDN}';
|
|
314
|
+
let _${p.cmd}Cache = null;
|
|
315
|
+
let _${p.cmd}Prom = null;
|
|
316
|
+
const _${p.cmd}Subs = new Set();
|
|
317
|
+
|
|
318
|
+
function _load${p.hook.replace("use", "")}() {
|
|
319
|
+
if (_${p.cmd}Cache) return Promise.resolve(_${p.cmd}Cache);
|
|
320
|
+
if (_${p.cmd}Prom) return _${p.cmd}Prom;
|
|
321
|
+
_${p.cmd}Prom = fetch(_${p.cmd}CDN + '/meta.json')
|
|
322
|
+
.then(r => r.json())
|
|
323
|
+
.then(meta => Promise.all(
|
|
324
|
+
meta.chapters.map(c => fetch(_${p.cmd}CDN + '/' + c.id + '.json').then(r => r.json()))
|
|
325
|
+
).then(results => {
|
|
326
|
+
const hadiths = results.flat();
|
|
327
|
+
const _byId = new Map();
|
|
328
|
+
hadiths.forEach(h => _byId.set(h.id, h));
|
|
329
|
+
_${p.cmd}Cache = Object.assign([], hadiths, {
|
|
330
|
+
metadata: meta.metadata,
|
|
331
|
+
chapters: meta.chapters,
|
|
332
|
+
get: (id) => _byId.get(id),
|
|
333
|
+
getByChapter: (id) => hadiths.filter(h => h.chapterId === id),
|
|
334
|
+
search: (q, limit = 0) => {
|
|
335
|
+
const ql = q.toLowerCase();
|
|
336
|
+
const r = hadiths.filter(h =>
|
|
337
|
+
h.english?.text?.toLowerCase().includes(ql) ||
|
|
338
|
+
h.english?.narrator?.toLowerCase().includes(ql)
|
|
339
|
+
);
|
|
340
|
+
return limit > 0 ? r.slice(0, limit) : r;
|
|
341
|
+
},
|
|
342
|
+
getRandom: () => hadiths[Math.floor(Math.random() * hadiths.length)],
|
|
343
|
+
});
|
|
344
|
+
_${p.cmd}Subs.forEach(fn => fn(_${p.cmd}Cache));
|
|
345
|
+
_${p.cmd}Subs.clear();
|
|
346
|
+
return _${p.cmd}Cache;
|
|
347
|
+
}));
|
|
348
|
+
return _${p.cmd}Prom;
|
|
349
|
+
}
|
|
350
|
+
_load${p.hook.replace("use", "")}();
|
|
351
|
+
|
|
352
|
+
export function ${p.hook}() {
|
|
353
|
+
const [data, setData] = useState(_${p.cmd}Cache);
|
|
354
|
+
useEffect(() => {
|
|
355
|
+
if (_${p.cmd}Cache) { setData(_${p.cmd}Cache); }
|
|
356
|
+
else { _${p.cmd}Subs.add(setData); return () => _${p.cmd}Subs.delete(setData); }
|
|
357
|
+
}, []);
|
|
358
|
+
return data;
|
|
359
|
+
}`;
|
|
360
|
+
})
|
|
361
|
+
.join("\n");
|
|
362
|
+
|
|
363
|
+
const hookNames = books.map((p) => p.hook).join(", ");
|
|
364
|
+
|
|
365
|
+
const hookSrc = `// Auto-generated by: sunnah --react
|
|
366
|
+
// Re-run to regenerate after installing more sunnah packages.
|
|
367
|
+
//
|
|
368
|
+
// Included books: ${books.map((p) => p.label).join(", ")}
|
|
369
|
+
//
|
|
370
|
+
// Usage:
|
|
371
|
+
${books.map((p) => `// import { ${p.hook} } from '../hooks/useSunnah';`).join("\n")}
|
|
372
|
+
//
|
|
373
|
+
// const bukhari = useBukhari();
|
|
374
|
+
// if (!bukhari) return <p>Loading...</p>;
|
|
375
|
+
// bukhari.get(1) // hadith by ID
|
|
376
|
+
// bukhari.search('prayer', 5) // top 5 results
|
|
377
|
+
// bukhari.getRandom() // random hadith
|
|
378
|
+
// bukhari.getByChapter(1) // all hadiths in chapter
|
|
379
|
+
|
|
380
|
+
import { useState, useEffect } from 'react';
|
|
381
|
+
${loaderBlocks}
|
|
382
|
+
|
|
383
|
+
// Unified hook — loads all books in parallel
|
|
384
|
+
export function useSunnah() {
|
|
385
|
+
const hooks = [${books.map((p) => `${p.hook}()`).join(", ")}];
|
|
386
|
+
if (hooks.some(h => !h)) return null;
|
|
387
|
+
return { ${books.map((p) => p.cmd + ": hooks[" + books.indexOf(p) + "]").join(", ")} };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export default useSunnah;
|
|
391
|
+
`;
|
|
392
|
+
|
|
393
|
+
const hookFile = path.join(hooksDir, "useSunnah.js");
|
|
394
|
+
fs.writeFileSync(hookFile, hookSrc, "utf8");
|
|
395
|
+
|
|
396
|
+
const div = gray("─".repeat(60));
|
|
397
|
+
console.log("\n" + div);
|
|
398
|
+
console.log(bold(cyan(" ✓ Generated: src/hooks/useSunnah.js")));
|
|
399
|
+
console.log(div);
|
|
400
|
+
console.log("\n " + gray("Included books:"));
|
|
401
|
+
books.forEach((p) =>
|
|
402
|
+
console.log(
|
|
403
|
+
" " + green("▸") + " " + bold(p.label) + gray(" " + p.hook + "()"),
|
|
404
|
+
),
|
|
405
|
+
);
|
|
406
|
+
console.log("\n " + gray("Usage:"));
|
|
407
|
+
console.log(
|
|
408
|
+
" " +
|
|
409
|
+
cyan("import { useSunnah") +
|
|
410
|
+
(books.length > 1
|
|
411
|
+
? ", " +
|
|
412
|
+
books
|
|
413
|
+
.slice(0, 2)
|
|
414
|
+
.map((p) => p.hook)
|
|
415
|
+
.join(", ")
|
|
416
|
+
: "") +
|
|
417
|
+
cyan(" } from '../hooks/useSunnah';"),
|
|
418
|
+
);
|
|
419
|
+
console.log("");
|
|
420
|
+
console.log(" " + gray("// Unified — all books at once"));
|
|
421
|
+
console.log(" " + gray("const sunnah = useSunnah();"));
|
|
422
|
+
console.log(" " + gray("if (!sunnah) return <p>Loading...</p>;"));
|
|
423
|
+
if (books[0])
|
|
424
|
+
console.log(" " + gray(`sunnah.${books[0].cmd}.getRandom()`));
|
|
425
|
+
console.log("");
|
|
426
|
+
console.log(" " + gray("// Per-book hooks still work"));
|
|
427
|
+
if (books[0]) {
|
|
428
|
+
console.log(" " + gray(`const ${books[0].cmd} = ${books[0].hook}();`));
|
|
429
|
+
console.log(" " + gray(`${books[0].cmd}.get(1).english.text`));
|
|
430
|
+
}
|
|
431
|
+
console.log("\n" + div + "\n");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── Personalized suggestions based on what's installed ───────────────────────
|
|
435
|
+
function getPersonalizedSuggestions() {
|
|
436
|
+
const installed = PACKAGES.filter((p) => isInstalled(p.name));
|
|
437
|
+
const missing = PACKAGES.filter((p) => !isInstalled(p.name));
|
|
438
|
+
const tips = [];
|
|
439
|
+
|
|
440
|
+
if (installed.length === 0) {
|
|
441
|
+
tips.push(
|
|
442
|
+
cyan(" ▸") +
|
|
443
|
+
" Run " +
|
|
444
|
+
bold("sunnah") +
|
|
445
|
+
" to open the interactive installer",
|
|
446
|
+
);
|
|
447
|
+
tips.push(
|
|
448
|
+
cyan(" ▸") + " Or install directly: " + bold("sunnah install bukhari"),
|
|
449
|
+
);
|
|
450
|
+
} else {
|
|
451
|
+
installed.forEach((p) => {
|
|
452
|
+
tips.push(
|
|
453
|
+
cyan(" ▸") +
|
|
454
|
+
" " +
|
|
455
|
+
bold(p.cmd + " --random") +
|
|
456
|
+
gray(" — read a random " + p.label + " hadith"),
|
|
457
|
+
);
|
|
458
|
+
tips.push(
|
|
459
|
+
cyan(" ▸") +
|
|
460
|
+
" " +
|
|
461
|
+
bold(p.cmd + ' --search "prayer"') +
|
|
462
|
+
gray(" — search " + p.label),
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
if (installed.length > 1) {
|
|
466
|
+
tips.push(
|
|
467
|
+
cyan(" ▸") +
|
|
468
|
+
" " +
|
|
469
|
+
bold("sunnah --react") +
|
|
470
|
+
gray(" — generate useSunnah() hook for all installed books"),
|
|
471
|
+
);
|
|
472
|
+
} else if (installed.length === 1) {
|
|
473
|
+
tips.push(
|
|
474
|
+
cyan(" ▸") +
|
|
475
|
+
" " +
|
|
476
|
+
bold("sunnah --react " + installed[0].cmd) +
|
|
477
|
+
gray(" — generate React hook for " + installed[0].label),
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
if (missing.length > 0) {
|
|
481
|
+
tips.push(
|
|
482
|
+
cyan(" ▸") +
|
|
483
|
+
" " +
|
|
484
|
+
bold("sunnah") +
|
|
485
|
+
gray(
|
|
486
|
+
" — install more books (" +
|
|
487
|
+
missing.map((p) => p.label).join(", ") +
|
|
488
|
+
")",
|
|
489
|
+
),
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
tips.push(
|
|
493
|
+
cyan(" ▸") +
|
|
494
|
+
" " +
|
|
495
|
+
bold("sunnah --update") +
|
|
496
|
+
gray(" — check for updates"),
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return tips;
|
|
501
|
+
}
|
|
502
|
+
|
|
242
503
|
// ── Modes ─────────────────────────────────────────────────────────────────────
|
|
243
504
|
const MODE = { LIST: "list", CONFIRM_UNINSTALL: "confirm_uninstall" };
|
|
244
505
|
|
|
245
|
-
// ──
|
|
506
|
+
// ── Interactive render ────────────────────────────────────────────────────────
|
|
246
507
|
function render(state) {
|
|
247
508
|
const { cursor, selected, mode, confirmTarget, statusMsg } = state;
|
|
248
509
|
const divW = Math.min(W() - 2, 72);
|
|
@@ -250,10 +511,8 @@ function render(state) {
|
|
|
250
511
|
const div2 = gray("═".repeat(divW));
|
|
251
512
|
|
|
252
513
|
clearScreen();
|
|
253
|
-
|
|
254
514
|
let row = 1;
|
|
255
515
|
|
|
256
|
-
// Header
|
|
257
516
|
writeLine(row++, div2);
|
|
258
517
|
writeLine(
|
|
259
518
|
row++,
|
|
@@ -262,7 +521,7 @@ function render(state) {
|
|
|
262
521
|
writeLine(
|
|
263
522
|
row++,
|
|
264
523
|
gray(" ↑↓") +
|
|
265
|
-
"
|
|
524
|
+
" nav " +
|
|
266
525
|
gray("space") +
|
|
267
526
|
" select " +
|
|
268
527
|
gray("a") +
|
|
@@ -277,9 +536,8 @@ function render(state) {
|
|
|
277
536
|
" quit",
|
|
278
537
|
);
|
|
279
538
|
writeLine(row++, div2);
|
|
280
|
-
row++;
|
|
539
|
+
row++;
|
|
281
540
|
|
|
282
|
-
// Package list
|
|
283
541
|
PACKAGES.forEach((p, i) => {
|
|
284
542
|
const isCursor = i === cursor;
|
|
285
543
|
const isSel = selected.has(i);
|
|
@@ -305,16 +563,15 @@ function render(state) {
|
|
|
305
563
|
row++,
|
|
306
564
|
` ${gray("Hadiths: ")}${yellow(p.hadiths)}` +
|
|
307
565
|
` ${gray("CLI: ")}${cyan(p.cmd + " --help")}` +
|
|
308
|
-
(inst ? ` ${gray("
|
|
566
|
+
(inst ? ` ${gray("try: ")}${cyan(p.cmd + " --random")}` : ""),
|
|
309
567
|
);
|
|
310
|
-
row++;
|
|
568
|
+
row++;
|
|
311
569
|
}
|
|
312
570
|
});
|
|
313
571
|
|
|
314
|
-
row++;
|
|
572
|
+
row++;
|
|
315
573
|
writeLine(row++, div);
|
|
316
574
|
|
|
317
|
-
// Status / selection footer
|
|
318
575
|
if (statusMsg) {
|
|
319
576
|
writeLine(row++, ` ${yellow("⚠")} ${yellow(statusMsg)}`);
|
|
320
577
|
} else if (selected.size > 0) {
|
|
@@ -323,20 +580,16 @@ function render(state) {
|
|
|
323
580
|
row++,
|
|
324
581
|
` ${green("●")} ${bold(String(selected.size))} selected: ${names}`,
|
|
325
582
|
);
|
|
326
|
-
writeLine(
|
|
327
|
-
row++,
|
|
328
|
-
` ${dim("Press enter to install, u to uninstall selected")}`,
|
|
329
|
-
);
|
|
583
|
+
writeLine(row++, ` ${dim("enter = install u = uninstall i = info")}`);
|
|
330
584
|
} else {
|
|
331
585
|
writeLine(
|
|
332
586
|
row++,
|
|
333
|
-
` ${gray("Nothing selected — press space to select
|
|
587
|
+
` ${gray("Nothing selected — press space to select, enter to install focused")}`,
|
|
334
588
|
);
|
|
335
589
|
}
|
|
336
590
|
|
|
337
591
|
writeLine(row++, div);
|
|
338
592
|
|
|
339
|
-
// Confirm uninstall overlay
|
|
340
593
|
if (mode === MODE.CONFIRM_UNINSTALL && confirmTarget !== null) {
|
|
341
594
|
const p = PACKAGES[confirmTarget];
|
|
342
595
|
row++;
|
|
@@ -351,37 +604,45 @@ function render(state) {
|
|
|
351
604
|
}
|
|
352
605
|
}
|
|
353
606
|
|
|
354
|
-
// ──
|
|
607
|
+
// ── --list ────────────────────────────────────────────────────────────────────
|
|
355
608
|
function cmdList() {
|
|
356
609
|
installedCache = buildInstalledCache();
|
|
357
610
|
const div = gray("─".repeat(60));
|
|
358
|
-
console.log("");
|
|
359
|
-
console.log(div);
|
|
611
|
+
console.log("\n" + div);
|
|
360
612
|
console.log(bold(cyan(" Available Sunnah Packages")));
|
|
361
613
|
console.log(div);
|
|
362
614
|
PACKAGES.forEach((p) => {
|
|
363
|
-
const inst = isInstalled(p.name)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
615
|
+
const inst = isInstalled(p.name);
|
|
616
|
+
const badge = inst ? green(" ✓ installed") : red(" ✗ not installed");
|
|
617
|
+
const version = inst
|
|
618
|
+
? dim(gray(" " + (getInstalledVersion(p.name) || "")))
|
|
619
|
+
: "";
|
|
620
|
+
console.log(`\n ${bold(white(p.label))}${badge}${version}`);
|
|
368
621
|
console.log(` ${cyan("npm install -g " + p.name)}`);
|
|
369
622
|
console.log(` ${dim(p.desc)}`);
|
|
370
623
|
console.log(
|
|
371
624
|
` ${gray("Hadiths: ")}${yellow(p.hadiths)} ${gray("Author: ")}${magenta(p.author)}`,
|
|
372
625
|
);
|
|
626
|
+
if (inst) {
|
|
627
|
+
console.log(
|
|
628
|
+
` ${gray("CLI: ")}${cyan(p.cmd + " --help")} ${gray("React: ")}${cyan("sunnah --react " + p.cmd)}`,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
373
631
|
});
|
|
374
|
-
console.log("");
|
|
375
|
-
console.log(div);
|
|
376
|
-
console.log("");
|
|
632
|
+
console.log("\n" + div + "\n");
|
|
377
633
|
}
|
|
378
634
|
|
|
379
|
-
// ──
|
|
635
|
+
// ── --update ──────────────────────────────────────────────────────────────────
|
|
380
636
|
function cmdUpdate() {
|
|
381
637
|
installedCache = buildInstalledCache();
|
|
382
638
|
const installed = PACKAGES.filter((p) => isInstalled(p.name));
|
|
383
639
|
if (!installed.length) {
|
|
384
|
-
console.log(
|
|
640
|
+
console.log(
|
|
641
|
+
"\n " +
|
|
642
|
+
yellow("No sunnah packages installed. Run ") +
|
|
643
|
+
bold("sunnah") +
|
|
644
|
+
yellow(" to install.\n"),
|
|
645
|
+
);
|
|
385
646
|
return;
|
|
386
647
|
}
|
|
387
648
|
const div = gray("─".repeat(60));
|
|
@@ -389,11 +650,14 @@ function cmdUpdate() {
|
|
|
389
650
|
console.log(bold(cyan(" Checking for updates…")));
|
|
390
651
|
console.log(div + "\n");
|
|
391
652
|
|
|
653
|
+
let hasUpdates = false;
|
|
392
654
|
for (const p of installed) {
|
|
393
655
|
const current = getInstalledVersion(p.name);
|
|
394
656
|
const latest = getLatestVersion(p.name);
|
|
395
657
|
if (!current || !latest) {
|
|
396
|
-
console.log(
|
|
658
|
+
console.log(
|
|
659
|
+
` ${yellow("?")} ${bold(p.label)} ${gray("(could not check)")}`,
|
|
660
|
+
);
|
|
397
661
|
continue;
|
|
398
662
|
}
|
|
399
663
|
if (current === latest) {
|
|
@@ -401,105 +665,387 @@ function cmdUpdate() {
|
|
|
401
665
|
` ${green("✓")} ${bold(p.label)} ${gray(current + " — up to date")}`,
|
|
402
666
|
);
|
|
403
667
|
} else {
|
|
668
|
+
hasUpdates = true;
|
|
404
669
|
console.log(
|
|
405
|
-
` ${yellow("↑")} ${bold(p.label)} ${gray(current)}
|
|
670
|
+
` ${yellow("↑")} ${bold(p.label)} ${gray(current)} ${gray("→")} ${green(latest)}`,
|
|
406
671
|
);
|
|
672
|
+
console.log(` ${dim("sunnah install " + p.cmd)}`);
|
|
407
673
|
}
|
|
408
674
|
}
|
|
675
|
+
|
|
676
|
+
if (hasUpdates) {
|
|
677
|
+
console.log(
|
|
678
|
+
"\n " +
|
|
679
|
+
yellow("Updates available. Run ") +
|
|
680
|
+
bold("sunnah install <name>") +
|
|
681
|
+
yellow(" to update."),
|
|
682
|
+
);
|
|
683
|
+
}
|
|
409
684
|
console.log("\n" + div + "\n");
|
|
410
685
|
}
|
|
411
686
|
|
|
412
|
-
// ──
|
|
413
|
-
async function
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
687
|
+
// ── sunnah install <cmd> ──────────────────────────────────────────────────────
|
|
688
|
+
async function cmdInstall(targets) {
|
|
689
|
+
if (!targets.length) {
|
|
690
|
+
console.error(
|
|
691
|
+
red(
|
|
692
|
+
"\n ✗ Usage: sunnah install <name> (e.g. sunnah install bukhari)\n",
|
|
693
|
+
),
|
|
694
|
+
);
|
|
420
695
|
console.log(
|
|
421
|
-
" " +
|
|
696
|
+
" Available: " + PACKAGES.map((p) => cyan(p.cmd)).join(", ") + "\n",
|
|
422
697
|
);
|
|
423
|
-
process.exit(
|
|
698
|
+
process.exit(1);
|
|
424
699
|
}
|
|
425
700
|
|
|
426
|
-
//
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
701
|
+
// Resolve names — accept cmd alias or full npm name
|
|
702
|
+
const toInstall = [];
|
|
703
|
+
for (const t of targets) {
|
|
704
|
+
const byCmd = CMD_MAP[t.toLowerCase()];
|
|
705
|
+
const byName = PACKAGES.find((p) => p.name === t);
|
|
706
|
+
const found = byCmd || byName;
|
|
707
|
+
if (!found) {
|
|
708
|
+
console.log(yellow(`\n ⚠ Unknown package: "${t}"`));
|
|
709
|
+
console.log(
|
|
710
|
+
" Available: " +
|
|
711
|
+
PACKAGES.map((p) => cyan(p.cmd) + gray(" (" + p.name + ")")).join(
|
|
712
|
+
", ",
|
|
713
|
+
),
|
|
714
|
+
);
|
|
715
|
+
process.exit(1);
|
|
716
|
+
}
|
|
717
|
+
toInstall.push(found);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const divW = Math.min(W() - 2, 72);
|
|
721
|
+
const div2 = gray("═".repeat(divW));
|
|
722
|
+
|
|
723
|
+
console.log("\n" + div2);
|
|
724
|
+
console.log(
|
|
725
|
+
bold(cyan(" Installing ")) +
|
|
726
|
+
bold(yellow(String(toInstall.length))) +
|
|
727
|
+
bold(cyan(" package" + (toInstall.length > 1 ? "s" : "") + "…")),
|
|
728
|
+
);
|
|
729
|
+
console.log(div2);
|
|
730
|
+
|
|
731
|
+
for (let i = 0; i < toInstall.length; i++) {
|
|
732
|
+
const p = toInstall[i];
|
|
430
733
|
console.log(
|
|
431
|
-
|
|
734
|
+
"\n " +
|
|
735
|
+
cyan("[" + (i + 1) + "/" + toInstall.length + "]") +
|
|
736
|
+
" " +
|
|
737
|
+
bold(white(p.label)),
|
|
432
738
|
);
|
|
433
|
-
console.log(
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
739
|
+
console.log(" " + dim("npm install -g " + p.name) + "\n");
|
|
740
|
+
await animateInstall(p.name);
|
|
741
|
+
installedCache.set(p.name, true);
|
|
742
|
+
console.log(" " + green("✓") + " " + bold(green(p.label)) + " installed");
|
|
743
|
+
console.log(" " + gray("Usage: ") + cyan(p.cmd + " --help"));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
console.log("\n" + div2);
|
|
747
|
+
console.log(
|
|
748
|
+
" " +
|
|
749
|
+
green("✓ Done! ") +
|
|
750
|
+
toInstall.map((p) => bold(cyan(p.cmd))).join(", ") +
|
|
751
|
+
gray(" ready."),
|
|
752
|
+
);
|
|
753
|
+
console.log(div2 + "\n");
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// ── sunnah uninstall <cmd> ────────────────────────────────────────────────────
|
|
757
|
+
function cmdUninstall(targets) {
|
|
758
|
+
if (!targets.length) {
|
|
759
|
+
console.error(
|
|
760
|
+
red(
|
|
761
|
+
"\n ✗ Usage: sunnah uninstall <name> (e.g. sunnah uninstall bukhari)\n",
|
|
762
|
+
),
|
|
437
763
|
);
|
|
764
|
+
process.exit(1);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
installedCache = buildInstalledCache();
|
|
768
|
+
const divW = Math.min(W() - 2, 72);
|
|
769
|
+
const div2 = gray("═".repeat(divW));
|
|
770
|
+
|
|
771
|
+
console.log("\n" + div2);
|
|
772
|
+
|
|
773
|
+
for (const t of targets) {
|
|
774
|
+
const p =
|
|
775
|
+
CMD_MAP[t.toLowerCase()] || PACKAGES.find((pkg) => pkg.name === t);
|
|
776
|
+
if (!p) {
|
|
777
|
+
console.log(yellow(` ⚠ Unknown: "${t}"`) + "\n");
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
if (!isInstalled(p.name)) {
|
|
781
|
+
console.log(gray(` ○ ${p.label} is not installed, skipping.`));
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
console.log(yellow(" Uninstalling ") + bold(white(p.label)) + yellow("…"));
|
|
785
|
+
try {
|
|
786
|
+
execSync(`${NPM} uninstall -g ${p.name}`, {
|
|
787
|
+
stdio: "inherit",
|
|
788
|
+
shell: isWin,
|
|
789
|
+
});
|
|
790
|
+
installedCache.set(p.name, false);
|
|
791
|
+
console.log(
|
|
792
|
+
green(" ✓ ") + bold(green(p.label)) + green(" uninstalled.\n"),
|
|
793
|
+
);
|
|
794
|
+
} catch {
|
|
795
|
+
console.log(red(" ✗ Failed to uninstall " + p.label + "\n"));
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
console.log(div2 + "\n");
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// ── --version (personalized) ──────────────────────────────────────────────────
|
|
803
|
+
function cmdVersion() {
|
|
804
|
+
installedCache = buildInstalledCache();
|
|
805
|
+
const installed = PACKAGES.filter((p) => isInstalled(p.name));
|
|
806
|
+
const div = gray("─".repeat(60));
|
|
807
|
+
|
|
808
|
+
console.log("\n" + div);
|
|
809
|
+
console.log(bold(cyan(" 📿 sunnah")) + gray(" v" + pkg.version));
|
|
810
|
+
console.log(div);
|
|
811
|
+
console.log(
|
|
812
|
+
" " + gray("Available packages : ") + yellow(String(PACKAGES.length)),
|
|
813
|
+
);
|
|
814
|
+
console.log(
|
|
815
|
+
" " +
|
|
816
|
+
gray("Installed : ") +
|
|
817
|
+
green(String(installed.length)) +
|
|
818
|
+
gray(" / " + PACKAGES.length),
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
if (installed.length > 0) {
|
|
438
822
|
console.log(
|
|
439
|
-
"
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
gray(" List all packages + install status"),
|
|
823
|
+
" " +
|
|
824
|
+
gray("Your collection : ") +
|
|
825
|
+
installed.map((p) => cyan(p.label)).join(gray(", ")),
|
|
443
826
|
);
|
|
444
|
-
|
|
445
|
-
"
|
|
446
|
-
|
|
447
|
-
green(" --update") +
|
|
448
|
-
gray(" Check all installed packages for updates"),
|
|
827
|
+
const totalHadiths = installed.reduce(
|
|
828
|
+
(acc, p) => acc + parseInt(p.hadiths.replace(/,/g, "")),
|
|
829
|
+
0,
|
|
449
830
|
);
|
|
450
831
|
console.log(
|
|
451
|
-
"
|
|
832
|
+
" " +
|
|
833
|
+
gray("Total hadiths : ") +
|
|
834
|
+
bold(yellow(totalHadiths.toLocaleString())),
|
|
452
835
|
);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
console.log("\n" + div + "\n");
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// ── --help (personalized) ─────────────────────────────────────────────────────
|
|
842
|
+
function cmdHelp() {
|
|
843
|
+
installedCache = buildInstalledCache();
|
|
844
|
+
const div = gray("─".repeat(60));
|
|
845
|
+
|
|
846
|
+
console.log("\n" + div);
|
|
847
|
+
console.log(
|
|
848
|
+
bold(cyan(" 📿 Sunnah Package Manager")) + gray(" v" + pkg.version),
|
|
849
|
+
);
|
|
850
|
+
console.log(div);
|
|
851
|
+
|
|
852
|
+
console.log("\n " + bold("Commands:"));
|
|
853
|
+
console.log(
|
|
854
|
+
" " +
|
|
855
|
+
cyan("sunnah") +
|
|
856
|
+
gray(" Open interactive installer UI"),
|
|
857
|
+
);
|
|
858
|
+
console.log(
|
|
859
|
+
" " +
|
|
860
|
+
cyan("sunnah install") +
|
|
861
|
+
yellow(" <name>") +
|
|
862
|
+
gray(" Install a package directly"),
|
|
863
|
+
);
|
|
864
|
+
console.log(
|
|
865
|
+
" " +
|
|
866
|
+
cyan("sunnah uninstall") +
|
|
867
|
+
yellow(" <name>") +
|
|
868
|
+
gray(" Uninstall a package"),
|
|
869
|
+
);
|
|
870
|
+
console.log(
|
|
871
|
+
" " +
|
|
872
|
+
cyan("sunnah --react") +
|
|
873
|
+
gray(" Generate unified useSunnah() React hook"),
|
|
874
|
+
);
|
|
875
|
+
console.log(
|
|
876
|
+
" " +
|
|
877
|
+
cyan("sunnah --react") +
|
|
878
|
+
yellow(" <books>") +
|
|
879
|
+
gray(" Generate hook for specific books"),
|
|
880
|
+
);
|
|
881
|
+
console.log(
|
|
882
|
+
" " +
|
|
883
|
+
cyan("sunnah --list") +
|
|
884
|
+
gray(" List all packages with install status"),
|
|
885
|
+
);
|
|
886
|
+
console.log(
|
|
887
|
+
" " +
|
|
888
|
+
cyan("sunnah --update") +
|
|
889
|
+
gray(" Check installed packages for updates"),
|
|
890
|
+
);
|
|
891
|
+
console.log(
|
|
892
|
+
" " +
|
|
893
|
+
cyan("sunnah -v") +
|
|
894
|
+
gray(" Version + your collection stats"),
|
|
895
|
+
);
|
|
896
|
+
console.log(
|
|
897
|
+
" " +
|
|
898
|
+
cyan("sunnah -h") +
|
|
899
|
+
gray(" This help + personalized tips"),
|
|
900
|
+
);
|
|
901
|
+
|
|
902
|
+
console.log("\n " + bold("Package names (use any form):"));
|
|
903
|
+
PACKAGES.forEach((p) => {
|
|
453
904
|
console.log(
|
|
454
905
|
" " +
|
|
455
|
-
cyan(
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
);
|
|
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"),
|
|
906
|
+
cyan(p.cmd.padEnd(12)) +
|
|
907
|
+
gray(p.name.padEnd(24)) +
|
|
908
|
+
yellow(p.hadiths + " hadiths"),
|
|
464
909
|
);
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
console.log("\n " + bold("Examples:"));
|
|
913
|
+
console.log(" " + dim("sunnah install bukhari"));
|
|
914
|
+
console.log(" " + dim("sunnah install bukhari muslim tirmidhi"));
|
|
915
|
+
console.log(" " + dim("sunnah uninstall dawud"));
|
|
916
|
+
console.log(" " + dim("sunnah --react"));
|
|
917
|
+
console.log(" " + dim("sunnah --react bukhari muslim"));
|
|
918
|
+
console.log(" " + dim("sunnah --update"));
|
|
919
|
+
|
|
920
|
+
console.log("\n " + bold("Interactive UI controls:"));
|
|
921
|
+
console.log(" " + green("↑ ↓") + gray(" Navigate packages"));
|
|
922
|
+
console.log(" " + green("space") + gray(" Toggle select"));
|
|
923
|
+
console.log(" " + green("a") + gray(" Select all / deselect all"));
|
|
924
|
+
console.log(
|
|
925
|
+
" " + green("i") + gray(" Show info + installed version"),
|
|
926
|
+
);
|
|
927
|
+
console.log(" " + green("u") + gray(" Uninstall selected"));
|
|
928
|
+
console.log(
|
|
929
|
+
" " +
|
|
930
|
+
green("enter") +
|
|
931
|
+
gray(" Install selected (or focused if none selected)"),
|
|
932
|
+
);
|
|
933
|
+
console.log(" " + green("q") + gray(" Quit"));
|
|
934
|
+
|
|
935
|
+
// Personalized tips
|
|
936
|
+
const tips = getPersonalizedSuggestions();
|
|
937
|
+
if (tips.length) {
|
|
938
|
+
console.log("\n" + div);
|
|
939
|
+
console.log(bold(" 💡 Suggested for you:"));
|
|
940
|
+
tips.forEach((t) => console.log(t));
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
console.log("\n" + div + "\n");
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
947
|
+
async function main() {
|
|
948
|
+
const rawArgs = process.argv.slice(2);
|
|
949
|
+
const flags = rawArgs.filter((a) => a.startsWith("-"));
|
|
950
|
+
const positional = rawArgs.filter((a) => !a.startsWith("-"));
|
|
951
|
+
|
|
952
|
+
// sunnah -v / --version
|
|
953
|
+
if (flags.some((f) => f === "-v" || f === "--version")) {
|
|
954
|
+
cmdVersion();
|
|
470
955
|
process.exit(0);
|
|
471
956
|
}
|
|
472
957
|
|
|
473
|
-
// --
|
|
958
|
+
// sunnah -h / --help
|
|
959
|
+
if (flags.some((f) => f === "-h" || f === "--help")) {
|
|
960
|
+
cmdHelp();
|
|
961
|
+
process.exit(0);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// sunnah --list / -l
|
|
474
965
|
if (flags.some((f) => f === "--list" || f === "-l")) {
|
|
475
966
|
cmdList();
|
|
476
967
|
process.exit(0);
|
|
477
968
|
}
|
|
478
969
|
|
|
479
|
-
// --update
|
|
970
|
+
// sunnah --update
|
|
480
971
|
if (flags.some((f) => f === "--update")) {
|
|
481
972
|
cmdUpdate();
|
|
482
973
|
process.exit(0);
|
|
483
974
|
}
|
|
484
975
|
|
|
976
|
+
// sunnah install <names...>
|
|
977
|
+
if (positional[0] === "install") {
|
|
978
|
+
await cmdInstall(positional.slice(1));
|
|
979
|
+
process.exit(0);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// sunnah uninstall <names...>
|
|
983
|
+
if (positional[0] === "uninstall") {
|
|
984
|
+
cmdUninstall(positional.slice(1));
|
|
985
|
+
process.exit(0);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// sunnah --react [book1 book2 ...]
|
|
989
|
+
if (flags.some((f) => f === "--react")) {
|
|
990
|
+
installedCache = buildInstalledCache();
|
|
991
|
+
// If specific books given after --react, use those; else use all installed
|
|
992
|
+
const requestedCmds = positional; // e.g. ["bukhari", "muslim"]
|
|
993
|
+
let books;
|
|
994
|
+
if (requestedCmds.length > 0) {
|
|
995
|
+
books = requestedCmds
|
|
996
|
+
.map((cmd) => CMD_MAP[cmd.toLowerCase()])
|
|
997
|
+
.filter(Boolean);
|
|
998
|
+
const unknown = requestedCmds.filter(
|
|
999
|
+
(cmd) => !CMD_MAP[cmd.toLowerCase()],
|
|
1000
|
+
);
|
|
1001
|
+
if (unknown.length) {
|
|
1002
|
+
console.log(yellow("\n ⚠ Unknown books: " + unknown.join(", ")));
|
|
1003
|
+
console.log(" Available: " + Object.keys(CMD_MAP).join(", ") + "\n");
|
|
1004
|
+
}
|
|
1005
|
+
} else {
|
|
1006
|
+
books = PACKAGES.filter((p) => isInstalled(p.name));
|
|
1007
|
+
if (!books.length) {
|
|
1008
|
+
console.log(yellow("\n ⚠ No sunnah packages installed yet."));
|
|
1009
|
+
console.log(
|
|
1010
|
+
" Run " +
|
|
1011
|
+
bold("sunnah") +
|
|
1012
|
+
" to install some first, or specify books explicitly:",
|
|
1013
|
+
);
|
|
1014
|
+
console.log(" " + dim("sunnah --react bukhari muslim") + "\n");
|
|
1015
|
+
process.exit(1);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (!books.length) {
|
|
1019
|
+
console.log(red("\n ✗ No valid books specified.\n"));
|
|
1020
|
+
process.exit(1);
|
|
1021
|
+
}
|
|
1022
|
+
generateUnifiedHook(books);
|
|
1023
|
+
process.exit(0);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
485
1026
|
// ── Interactive mode ────────────────────────────────────────────────────────
|
|
486
1027
|
if (!process.stdin.isTTY) {
|
|
487
1028
|
console.error(red("\n ✗ Interactive mode requires a TTY terminal.\n"));
|
|
488
1029
|
process.exit(1);
|
|
489
1030
|
}
|
|
490
1031
|
|
|
491
|
-
// Build cache
|
|
1032
|
+
// Build cache
|
|
492
1033
|
process.stdout.write("\n " + gray("Checking installed packages…"));
|
|
493
1034
|
installedCache = buildInstalledCache();
|
|
494
1035
|
process.stdout.write("\r\x1b[K");
|
|
495
1036
|
|
|
496
|
-
//
|
|
1037
|
+
// Load persisted selection
|
|
1038
|
+
const persisted = loadPersistedState();
|
|
1039
|
+
const lastSelected = new Set(
|
|
1040
|
+
(persisted.lastSelected || []).filter((i) => i < PACKAGES.length),
|
|
1041
|
+
);
|
|
1042
|
+
|
|
497
1043
|
enterAltScreen();
|
|
498
1044
|
hideCursor();
|
|
499
1045
|
|
|
500
1046
|
const state = {
|
|
501
1047
|
cursor: 0,
|
|
502
|
-
selected:
|
|
1048
|
+
selected: lastSelected, // pre-check last session's selections
|
|
503
1049
|
mode: MODE.LIST,
|
|
504
1050
|
confirmTarget: null,
|
|
505
1051
|
statusMsg: "",
|
|
@@ -507,7 +1053,7 @@ async function main() {
|
|
|
507
1053
|
|
|
508
1054
|
let statusTimer = null;
|
|
509
1055
|
|
|
510
|
-
function setStatus(msg, ms =
|
|
1056
|
+
function setStatus(msg, ms = 2500) {
|
|
511
1057
|
state.statusMsg = msg;
|
|
512
1058
|
render(state);
|
|
513
1059
|
if (statusTimer) clearTimeout(statusTimer);
|
|
@@ -520,6 +1066,8 @@ async function main() {
|
|
|
520
1066
|
render(state);
|
|
521
1067
|
|
|
522
1068
|
const cleanup = () => {
|
|
1069
|
+
// Persist current selection before exiting
|
|
1070
|
+
savePersistedState({ lastSelected: [...state.selected] });
|
|
523
1071
|
showCursor();
|
|
524
1072
|
leaveAltScreen();
|
|
525
1073
|
try {
|
|
@@ -539,13 +1087,12 @@ async function main() {
|
|
|
539
1087
|
process.stdin.on("keypress", async (str, key) => {
|
|
540
1088
|
if (!key) return;
|
|
541
1089
|
|
|
542
|
-
// ── Confirm uninstall mode
|
|
1090
|
+
// ── Confirm uninstall mode ────────────────────────────────────────────────
|
|
543
1091
|
if (state.mode === MODE.CONFIRM_UNINSTALL) {
|
|
544
1092
|
if (str === "y" || str === "Y") {
|
|
545
1093
|
const p = PACKAGES[state.confirmTarget];
|
|
546
1094
|
state.mode = MODE.LIST;
|
|
547
1095
|
state.confirmTarget = null;
|
|
548
|
-
|
|
549
1096
|
cleanup();
|
|
550
1097
|
console.log(
|
|
551
1098
|
"\n " +
|
|
@@ -553,7 +1100,6 @@ async function main() {
|
|
|
553
1100
|
bold(white(p.label)) +
|
|
554
1101
|
yellow("…\n"),
|
|
555
1102
|
);
|
|
556
|
-
|
|
557
1103
|
try {
|
|
558
1104
|
execSync(`${NPM} uninstall -g ${p.name}`, {
|
|
559
1105
|
stdio: "inherit",
|
|
@@ -568,12 +1114,9 @@ async function main() {
|
|
|
568
1114
|
green(" uninstalled.\n"),
|
|
569
1115
|
);
|
|
570
1116
|
} catch {
|
|
571
|
-
console.log("\n " + red("✗ Failed to uninstall " + p.label + "\n")
|
|
1117
|
+
console.log("\n " + red("✗ Failed to uninstall " + p.label) + "\n");
|
|
572
1118
|
}
|
|
573
|
-
|
|
574
1119
|
await sleep(1200);
|
|
575
|
-
|
|
576
|
-
// Re-enter interactive UI
|
|
577
1120
|
enterAltScreen();
|
|
578
1121
|
hideCursor();
|
|
579
1122
|
readline.emitKeypressEvents(process.stdin);
|
|
@@ -587,16 +1130,14 @@ async function main() {
|
|
|
587
1130
|
return;
|
|
588
1131
|
}
|
|
589
1132
|
|
|
590
|
-
// ── Normal
|
|
1133
|
+
// ── Normal mode ───────────────────────────────────────────────────────────
|
|
591
1134
|
|
|
592
|
-
// Quit
|
|
593
1135
|
if (key.name === "q" || (key.ctrl && key.name === "c")) {
|
|
594
1136
|
cleanup();
|
|
595
1137
|
console.log("\n " + gray("Goodbye.\n"));
|
|
596
1138
|
process.exit(0);
|
|
597
1139
|
}
|
|
598
1140
|
|
|
599
|
-
// Navigate
|
|
600
1141
|
if (key.name === "up") {
|
|
601
1142
|
state.cursor = (state.cursor - 1 + PACKAGES.length) % PACKAGES.length;
|
|
602
1143
|
render(state);
|
|
@@ -608,7 +1149,6 @@ async function main() {
|
|
|
608
1149
|
return;
|
|
609
1150
|
}
|
|
610
1151
|
|
|
611
|
-
// Toggle select
|
|
612
1152
|
if (str === " ") {
|
|
613
1153
|
if (state.selected.has(state.cursor)) state.selected.delete(state.cursor);
|
|
614
1154
|
else state.selected.add(state.cursor);
|
|
@@ -616,7 +1156,6 @@ async function main() {
|
|
|
616
1156
|
return;
|
|
617
1157
|
}
|
|
618
1158
|
|
|
619
|
-
// Select all / deselect all
|
|
620
1159
|
if (str === "a" || str === "A") {
|
|
621
1160
|
if (state.selected.size === PACKAGES.length) state.selected.clear();
|
|
622
1161
|
else PACKAGES.forEach((_, i) => state.selected.add(i));
|
|
@@ -624,42 +1163,39 @@ async function main() {
|
|
|
624
1163
|
return;
|
|
625
1164
|
}
|
|
626
1165
|
|
|
627
|
-
//
|
|
1166
|
+
// i = info
|
|
628
1167
|
if (str === "i" || str === "I") {
|
|
629
1168
|
const p = PACKAGES[state.cursor];
|
|
630
1169
|
const inst = isInstalled(p.name);
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
1170
|
+
const ver = inst ? getInstalledVersion(p.name) : null;
|
|
1171
|
+
setStatus(
|
|
1172
|
+
`${p.label} | ${p.hadiths} hadiths | ${p.author} | ${inst ? "v" + ver + " installed CLI: " + p.cmd + " --help" : "not installed"}`,
|
|
1173
|
+
4000,
|
|
1174
|
+
);
|
|
634
1175
|
return;
|
|
635
1176
|
}
|
|
636
1177
|
|
|
637
|
-
//
|
|
1178
|
+
// u = uninstall
|
|
638
1179
|
if (str === "u" || str === "U") {
|
|
639
1180
|
const targets =
|
|
640
1181
|
state.selected.size > 0 ? [...state.selected] : [state.cursor];
|
|
641
|
-
|
|
642
|
-
// Only uninstall packages that are actually installed
|
|
643
1182
|
const toRemove = targets.filter((i) => isInstalled(PACKAGES[i].name));
|
|
644
1183
|
if (!toRemove.length) {
|
|
645
|
-
setStatus("No installed packages selected
|
|
1184
|
+
setStatus("No installed packages selected.");
|
|
646
1185
|
return;
|
|
647
1186
|
}
|
|
648
|
-
|
|
649
|
-
// Confirm one by one
|
|
650
1187
|
state.mode = MODE.CONFIRM_UNINSTALL;
|
|
651
1188
|
state.confirmTarget = toRemove[0];
|
|
652
1189
|
render(state);
|
|
653
1190
|
return;
|
|
654
1191
|
}
|
|
655
1192
|
|
|
656
|
-
//
|
|
1193
|
+
// enter = install
|
|
657
1194
|
if (key.name === "return") {
|
|
658
1195
|
const targets =
|
|
659
1196
|
state.selected.size > 0
|
|
660
1197
|
? [...state.selected].map((i) => PACKAGES[i])
|
|
661
1198
|
: [PACKAGES[state.cursor]];
|
|
662
|
-
|
|
663
1199
|
const toInstall = targets.filter((p) => !isInstalled(p.name));
|
|
664
1200
|
|
|
665
1201
|
if (!toInstall.length) {
|
|
@@ -668,8 +1204,6 @@ async function main() {
|
|
|
668
1204
|
}
|
|
669
1205
|
|
|
670
1206
|
cleanup();
|
|
671
|
-
|
|
672
|
-
const total = toInstall.length;
|
|
673
1207
|
const divW = Math.min(W() - 2, 72);
|
|
674
1208
|
const div = gray("─".repeat(divW));
|
|
675
1209
|
const div2 = gray("═".repeat(divW));
|
|
@@ -677,8 +1211,8 @@ async function main() {
|
|
|
677
1211
|
console.log("\n" + div2);
|
|
678
1212
|
console.log(
|
|
679
1213
|
bold(cyan(" Installing ")) +
|
|
680
|
-
bold(yellow(String(
|
|
681
|
-
bold(cyan(" package" + (
|
|
1214
|
+
bold(yellow(String(toInstall.length))) +
|
|
1215
|
+
bold(cyan(" package" + (toInstall.length > 1 ? "s" : "") + "…")),
|
|
682
1216
|
);
|
|
683
1217
|
console.log(div2);
|
|
684
1218
|
|
|
@@ -686,15 +1220,13 @@ async function main() {
|
|
|
686
1220
|
const p = toInstall[i];
|
|
687
1221
|
console.log(
|
|
688
1222
|
"\n " +
|
|
689
|
-
cyan("[" + (i + 1) + "/" +
|
|
1223
|
+
cyan("[" + (i + 1) + "/" + toInstall.length + "]") +
|
|
690
1224
|
" " +
|
|
691
1225
|
bold(white(p.label)),
|
|
692
1226
|
);
|
|
693
1227
|
console.log(" " + dim("npm install -g " + p.name) + "\n");
|
|
694
|
-
|
|
695
1228
|
await animateInstall(p.name);
|
|
696
1229
|
installedCache.set(p.name, true);
|
|
697
|
-
|
|
698
1230
|
console.log(
|
|
699
1231
|
" " + green("✓") + " " + bold(green(p.label)) + " installed",
|
|
700
1232
|
);
|
|
@@ -705,10 +1237,10 @@ async function main() {
|
|
|
705
1237
|
console.log(
|
|
706
1238
|
" " +
|
|
707
1239
|
green("✓ All done! ") +
|
|
708
|
-
bold(yellow(String(
|
|
1240
|
+
bold(yellow(String(toInstall.length))) +
|
|
709
1241
|
" package" +
|
|
710
|
-
(
|
|
711
|
-
" installed.",
|
|
1242
|
+
(toInstall.length > 1 ? "s" : "") +
|
|
1243
|
+
" installed globally.",
|
|
712
1244
|
);
|
|
713
1245
|
console.log("");
|
|
714
1246
|
toInstall.forEach((p) =>
|
|
@@ -722,8 +1254,19 @@ async function main() {
|
|
|
722
1254
|
dim(p.label),
|
|
723
1255
|
),
|
|
724
1256
|
);
|
|
725
|
-
console.log(div2 + "\n");
|
|
726
1257
|
|
|
1258
|
+
// Personalized next steps
|
|
1259
|
+
const installed = PACKAGES.filter((p) => isInstalled(p.name));
|
|
1260
|
+
if (installed.length > 1) {
|
|
1261
|
+
console.log(
|
|
1262
|
+
"\n " +
|
|
1263
|
+
dim("Tip: run ") +
|
|
1264
|
+
bold("sunnah --react") +
|
|
1265
|
+
dim(" to generate a unified React hook for all your books"),
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
console.log(div2 + "\n");
|
|
727
1270
|
showCursor();
|
|
728
1271
|
process.exit(0);
|
|
729
1272
|
}
|