sunnah 1.3.6
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.
- package/README.md +254 -0
- package/bin/index.js +1604 -0
- package/package.json +27 -0
package/bin/index.js
ADDED
|
@@ -0,0 +1,1604 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import { execSync, spawnSync, spawn } from "child_process";
|
|
8
|
+
import readline from "readline";
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
const pkg = JSON.parse(
|
|
13
|
+
fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
// ── Windows compatibility ─────────────────────────────────────────────────────
|
|
17
|
+
const isWin = process.platform === "win32";
|
|
18
|
+
|
|
19
|
+
// ── Safe npm runner — zero warnings on all platforms ─────────────────────────
|
|
20
|
+
// DEP0190 fires when shell:true is combined with a separate args array because
|
|
21
|
+
// Node concatenates them unsafely. EINVAL fires on Windows when you try to
|
|
22
|
+
// spawn npm.cmd without a shell.
|
|
23
|
+
//
|
|
24
|
+
// Solution: on Windows pass a single pre-joined command string to shell:true
|
|
25
|
+
// (no array = no concatenation = no DEP0190). On Unix use an args array with
|
|
26
|
+
// no shell at all. pkgName values come from our own PACKAGES constant so
|
|
27
|
+
// there is no injection risk.
|
|
28
|
+
function npmSync(args, opts = {}) {
|
|
29
|
+
if (isWin) {
|
|
30
|
+
// Single string → cmd.exe handles it; no args array → no DEP0190
|
|
31
|
+
const result = spawnSync("npm " + args.join(" "), [], {
|
|
32
|
+
encoding: "utf8",
|
|
33
|
+
timeout: opts.timeout ?? 15000,
|
|
34
|
+
stdio: opts.stdio ?? ["ignore", "pipe", "pipe"],
|
|
35
|
+
shell: true,
|
|
36
|
+
});
|
|
37
|
+
if (result.error) throw result.error;
|
|
38
|
+
return result.stdout ?? "";
|
|
39
|
+
} else {
|
|
40
|
+
const result = spawnSync("npm", args, {
|
|
41
|
+
encoding: "utf8",
|
|
42
|
+
timeout: opts.timeout ?? 15000,
|
|
43
|
+
stdio: opts.stdio ?? ["ignore", "pipe", "pipe"],
|
|
44
|
+
});
|
|
45
|
+
if (result.error) throw result.error;
|
|
46
|
+
return result.stdout ?? "";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Persistence: remember last selections ────────────────────────────────────
|
|
51
|
+
const STATE_FILE = path.join(os.homedir(), ".sunnah-state.json");
|
|
52
|
+
|
|
53
|
+
function loadPersistedState() {
|
|
54
|
+
try {
|
|
55
|
+
const raw = fs.readFileSync(STATE_FILE, "utf8");
|
|
56
|
+
return JSON.parse(raw);
|
|
57
|
+
} catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function savePersistedState(data) {
|
|
63
|
+
try {
|
|
64
|
+
const existing = loadPersistedState();
|
|
65
|
+
fs.writeFileSync(
|
|
66
|
+
STATE_FILE,
|
|
67
|
+
JSON.stringify({ ...existing, ...data }, null, 2),
|
|
68
|
+
);
|
|
69
|
+
} catch (_e) {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Colors ────────────────────────────────────────────────────────────────────
|
|
73
|
+
const c = {
|
|
74
|
+
reset: "\x1b[0m",
|
|
75
|
+
bold: "\x1b[1m",
|
|
76
|
+
dim: "\x1b[2m",
|
|
77
|
+
green: "\x1b[32m",
|
|
78
|
+
yellow: "\x1b[33m",
|
|
79
|
+
cyan: "\x1b[36m",
|
|
80
|
+
magenta: "\x1b[35m",
|
|
81
|
+
blue: "\x1b[34m",
|
|
82
|
+
red: "\x1b[31m",
|
|
83
|
+
gray: "\x1b[90m",
|
|
84
|
+
white: "\x1b[97m",
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const clr = (color, text) => `${color}${text}${c.reset}`;
|
|
88
|
+
const bold = (t) => clr(c.bold, t);
|
|
89
|
+
const green = (t) => clr(c.green, t);
|
|
90
|
+
const yellow = (t) => clr(c.yellow, t);
|
|
91
|
+
const cyan = (t) => clr(c.cyan, t);
|
|
92
|
+
const magenta = (t) => clr(c.magenta, t);
|
|
93
|
+
const gray = (t) => clr(c.gray, t);
|
|
94
|
+
const red = (t) => clr(c.red, t);
|
|
95
|
+
const dim = (t) => clr(c.dim, t);
|
|
96
|
+
const white = (t) => clr(c.white, t);
|
|
97
|
+
const blue = (t) => clr(c.blue, t);
|
|
98
|
+
|
|
99
|
+
// ── Available packages ────────────────────────────────────────────────────────
|
|
100
|
+
const PACKAGES = [
|
|
101
|
+
{
|
|
102
|
+
name: "sahih-al-bukhari",
|
|
103
|
+
label: "Sahih al-Bukhari",
|
|
104
|
+
author: "Imam Muhammad ibn Ismail al-Bukhari",
|
|
105
|
+
desc: "The most authentic collection of hadith, widely regarded as the most sahih after the Quran.",
|
|
106
|
+
hadiths: "7,563",
|
|
107
|
+
cmd: "bukhari",
|
|
108
|
+
hook: "useBukhari",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "sahih-muslim",
|
|
112
|
+
label: "Sahih Muslim",
|
|
113
|
+
author: "Imam Muslim ibn al-Hajjaj",
|
|
114
|
+
desc: "Second most authentic hadith collection, known for its strict methodology and chain verification.",
|
|
115
|
+
hadiths: "7,470",
|
|
116
|
+
cmd: "muslim",
|
|
117
|
+
hook: "useMuslim",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: "sunan-abi-dawud",
|
|
121
|
+
label: "Sunan Abi Dawud",
|
|
122
|
+
author: "Imam Abu Dawud Sulayman ibn al-Ash'ath",
|
|
123
|
+
desc: "One of the six canonical hadith collections, focused on legal rulings and jurisprudence.",
|
|
124
|
+
hadiths: "5,274",
|
|
125
|
+
cmd: "dawud",
|
|
126
|
+
hook: "useDawud",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "jami-al-tirmidhi",
|
|
130
|
+
label: "Jami al-Tirmidhi",
|
|
131
|
+
author: "Imam Abu Isa Muhammad al-Tirmidhi",
|
|
132
|
+
desc: "Part of the six major hadith collections, unique for grading each hadith's authenticity.",
|
|
133
|
+
hadiths: "3,956",
|
|
134
|
+
cmd: "tirmidhi",
|
|
135
|
+
hook: "useTirmidhi",
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
// cmd → package lookup
|
|
140
|
+
const CMD_MAP = Object.fromEntries(PACKAGES.map((p) => [p.cmd, p]));
|
|
141
|
+
|
|
142
|
+
// ── Terminal: alternate screen buffer = no scroll ─────────────────────────────
|
|
143
|
+
const W = () => process.stdout.columns || 80;
|
|
144
|
+
const H = () => process.stdout.rows || 24;
|
|
145
|
+
|
|
146
|
+
function enterAltScreen() {
|
|
147
|
+
process.stdout.write("\x1b[?1049h");
|
|
148
|
+
}
|
|
149
|
+
function leaveAltScreen() {
|
|
150
|
+
process.stdout.write("\x1b[?1049l");
|
|
151
|
+
}
|
|
152
|
+
function clearScreen() {
|
|
153
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
154
|
+
}
|
|
155
|
+
function moveTo(r, col) {
|
|
156
|
+
process.stdout.write(`\x1b[${r};${col}H`);
|
|
157
|
+
}
|
|
158
|
+
function hideCursor() {
|
|
159
|
+
process.stdout.write("\x1b[?25l");
|
|
160
|
+
}
|
|
161
|
+
function showCursor() {
|
|
162
|
+
process.stdout.write("\x1b[?25h");
|
|
163
|
+
}
|
|
164
|
+
function clearToEOL() {
|
|
165
|
+
process.stdout.write("\x1b[K");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function writeLine(row, text) {
|
|
169
|
+
moveTo(row, 1);
|
|
170
|
+
clearToEOL();
|
|
171
|
+
process.stdout.write(text);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Progress bar ──────────────────────────────────────────────────────────────
|
|
175
|
+
function drawBar(label, percent, barWidth = 40) {
|
|
176
|
+
const filled = Math.round((percent / 100) * barWidth);
|
|
177
|
+
const empty = barWidth - filled;
|
|
178
|
+
const bar =
|
|
179
|
+
c.green + "█".repeat(filled) + c.gray + "░".repeat(empty) + c.reset;
|
|
180
|
+
const pct = cyan(String(Math.round(percent)).padStart(3) + "%");
|
|
181
|
+
return ` ${bar} ${pct} ${dim(label)}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Animate install with real npm running behind ──────────────────────────────
|
|
185
|
+
function animateInstall(pkgName) {
|
|
186
|
+
return new Promise((resolve) => {
|
|
187
|
+
const stages = [
|
|
188
|
+
{ label: "Resolving packages…", end: 12, ms: 80 },
|
|
189
|
+
{ label: "Fetching metadata…", end: 30, ms: 60 },
|
|
190
|
+
{ label: "Downloading tarball…", end: 75, ms: 22 },
|
|
191
|
+
{ label: "Extracting files…", end: 90, ms: 50 },
|
|
192
|
+
{ label: "Linking binaries…", end: 98, ms: 80 },
|
|
193
|
+
];
|
|
194
|
+
let percent = 0;
|
|
195
|
+
let stageIdx = 0;
|
|
196
|
+
let npmDone = false;
|
|
197
|
+
|
|
198
|
+
process.stdout.write(drawBar(stages[0].label, 0));
|
|
199
|
+
|
|
200
|
+
const tick = () => {
|
|
201
|
+
const stage = stages[stageIdx];
|
|
202
|
+
if (!stage) return;
|
|
203
|
+
const prevEnd = stageIdx > 0 ? stages[stageIdx - 1].end : 0;
|
|
204
|
+
const step = (stage.end - prevEnd) / 24;
|
|
205
|
+
percent = Math.min(percent + step, stage.end);
|
|
206
|
+
process.stdout.write("\r\x1b[K" + drawBar(stage.label, percent));
|
|
207
|
+
if (percent >= stage.end) {
|
|
208
|
+
stageIdx++;
|
|
209
|
+
if (stageIdx >= stages.length) {
|
|
210
|
+
const poll = setInterval(() => {
|
|
211
|
+
if (npmDone) {
|
|
212
|
+
clearInterval(poll);
|
|
213
|
+
process.stdout.write(
|
|
214
|
+
"\r\x1b[K" + drawBar("Complete!", 100) + "\n",
|
|
215
|
+
);
|
|
216
|
+
resolve();
|
|
217
|
+
}
|
|
218
|
+
}, 80);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
setTimeout(tick, stages[stageIdx]?.ms ?? 50);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
setTimeout(tick, stages[0].ms);
|
|
226
|
+
// Windows: shell:true required for npm.cmd, but passing an args array alongside
|
|
227
|
+
// shell:true triggers DEP0190. Fix: pass a single pre-joined string so Node
|
|
228
|
+
// hands it to cmd.exe as-is — no concatenation, no warning, no EINVAL.
|
|
229
|
+
// Unix: spawn directly without shell — clean and safe.
|
|
230
|
+
const proc = isWin
|
|
231
|
+
? spawn("npm install -g " + pkgName, [], {
|
|
232
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
233
|
+
shell: true,
|
|
234
|
+
})
|
|
235
|
+
: spawn("npm", ["install", "-g", pkgName], {
|
|
236
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
237
|
+
});
|
|
238
|
+
proc.on("error", () => {
|
|
239
|
+
npmDone = true;
|
|
240
|
+
});
|
|
241
|
+
proc.on("close", () => {
|
|
242
|
+
npmDone = true;
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── Installed cache — ONE npm call at startup, Map lookup during render ───────
|
|
248
|
+
function buildInstalledCache() {
|
|
249
|
+
const cache = new Map();
|
|
250
|
+
// Pre-mark all as false so the UI never hangs waiting
|
|
251
|
+
for (const p of PACKAGES) cache.set(p.name, false);
|
|
252
|
+
let out = "";
|
|
253
|
+
try {
|
|
254
|
+
// Use --json for faster, reliable parsing; short timeout so UI opens fast
|
|
255
|
+
out = npmSync(["list", "-g", "--depth=0", "--json"], { timeout: 6000 });
|
|
256
|
+
const parsed = JSON.parse(out);
|
|
257
|
+
const deps = parsed?.dependencies ?? {};
|
|
258
|
+
for (const p of PACKAGES) {
|
|
259
|
+
cache.set(p.name, p.name in deps);
|
|
260
|
+
}
|
|
261
|
+
} catch (e) {
|
|
262
|
+
// Fallback: plain text parse on error/timeout
|
|
263
|
+
const text = (e && e.stdout) || out || "";
|
|
264
|
+
for (const p of PACKAGES) {
|
|
265
|
+
cache.set(p.name, text.includes(p.name));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return cache;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function getLatestVersion(name) {
|
|
272
|
+
try {
|
|
273
|
+
// Use --json for cleaner parsing
|
|
274
|
+
const out = npmSync(["show", name, "version", "--json"], { timeout: 6000 });
|
|
275
|
+
return JSON.parse(out.trim());
|
|
276
|
+
} catch {
|
|
277
|
+
// Fallback: plain text
|
|
278
|
+
try {
|
|
279
|
+
return npmSync(["show", name, "version"], { timeout: 6000 }).trim();
|
|
280
|
+
} catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function getInstalledVersion(name) {
|
|
287
|
+
try {
|
|
288
|
+
// Use --json for reliable, fast parsing
|
|
289
|
+
const out = npmSync(["list", "-g", name, "--depth=0", "--json"], {
|
|
290
|
+
timeout: 6000,
|
|
291
|
+
});
|
|
292
|
+
const parsed = JSON.parse(out);
|
|
293
|
+
return parsed?.dependencies?.[name]?.version ?? null;
|
|
294
|
+
} catch (e) {
|
|
295
|
+
// Fallback: text parse
|
|
296
|
+
const text = (e && e.stdout) || "";
|
|
297
|
+
const match = text.match(new RegExp(name + "@([\\d.]+)"));
|
|
298
|
+
return match ? match[1] : null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let installedCache = new Map();
|
|
303
|
+
const isInstalled = (name) => installedCache.get(name) ?? false;
|
|
304
|
+
|
|
305
|
+
// ── Update cache — fetched async so UI stays responsive ───────────────────────
|
|
306
|
+
// Map<pkgName, { current: string|null, latest: string|null, hasUpdate: boolean }>
|
|
307
|
+
let updateCache = new Map();
|
|
308
|
+
let updateCacheReady = false;
|
|
309
|
+
|
|
310
|
+
async function prefetchUpdateCache() {
|
|
311
|
+
const installed = PACKAGES.filter((p) => isInstalled(p.name));
|
|
312
|
+
if (!installed.length) {
|
|
313
|
+
updateCacheReady = true;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Step 1: get ALL installed versions in ONE npm call (fast)
|
|
318
|
+
const currentVersions = new Map();
|
|
319
|
+
await new Promise((resolve) => {
|
|
320
|
+
let out = "";
|
|
321
|
+
const proc = isWin
|
|
322
|
+
? spawn("npm list -g --depth=0 --json", [], {
|
|
323
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
324
|
+
shell: true,
|
|
325
|
+
})
|
|
326
|
+
: spawn("npm", ["list", "-g", "--depth=0", "--json"], {
|
|
327
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
328
|
+
});
|
|
329
|
+
proc.stdout?.on("data", (d) => {
|
|
330
|
+
out += d;
|
|
331
|
+
});
|
|
332
|
+
proc.on("error", () => resolve());
|
|
333
|
+
proc.on("close", () => {
|
|
334
|
+
try {
|
|
335
|
+
const deps = JSON.parse(out)?.dependencies ?? {};
|
|
336
|
+
for (const p of installed) {
|
|
337
|
+
currentVersions.set(p.name, deps[p.name]?.version ?? null);
|
|
338
|
+
}
|
|
339
|
+
} catch (_e) {}
|
|
340
|
+
resolve();
|
|
341
|
+
});
|
|
342
|
+
setTimeout(() => {
|
|
343
|
+
try {
|
|
344
|
+
proc.kill();
|
|
345
|
+
} catch (_e) {}
|
|
346
|
+
resolve();
|
|
347
|
+
}, 6000);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Step 2: get latest versions — one spawn per package, all in parallel
|
|
351
|
+
await Promise.all(
|
|
352
|
+
installed.map(
|
|
353
|
+
(p) =>
|
|
354
|
+
new Promise((resolve) => {
|
|
355
|
+
let out = "";
|
|
356
|
+
const proc = isWin
|
|
357
|
+
? spawn("npm show " + p.name + " version", [], {
|
|
358
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
359
|
+
shell: true,
|
|
360
|
+
})
|
|
361
|
+
: spawn("npm", ["show", p.name, "version"], {
|
|
362
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
363
|
+
});
|
|
364
|
+
proc.stdout?.on("data", (d) => {
|
|
365
|
+
out += d;
|
|
366
|
+
});
|
|
367
|
+
proc.on("error", () => resolve());
|
|
368
|
+
proc.on("close", () => {
|
|
369
|
+
const latest = out.trim() || null;
|
|
370
|
+
const current = currentVersions.get(p.name) ?? null;
|
|
371
|
+
updateCache.set(p.name, {
|
|
372
|
+
current,
|
|
373
|
+
latest,
|
|
374
|
+
hasUpdate: !!(current && latest && current !== latest),
|
|
375
|
+
});
|
|
376
|
+
resolve();
|
|
377
|
+
});
|
|
378
|
+
setTimeout(() => {
|
|
379
|
+
try {
|
|
380
|
+
proc.kill();
|
|
381
|
+
} catch (_e) {}
|
|
382
|
+
resolve();
|
|
383
|
+
}, 6000);
|
|
384
|
+
}),
|
|
385
|
+
),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
updateCacheReady = true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ── Clipboard ─────────────────────────────────────────────────────────────────
|
|
392
|
+
function copyToClipboard(text) {
|
|
393
|
+
try {
|
|
394
|
+
if (isWin) {
|
|
395
|
+
execSync("clip", { input: text, shell: true });
|
|
396
|
+
} else if (process.platform === "darwin") {
|
|
397
|
+
execSync("pbcopy", { input: text });
|
|
398
|
+
} else {
|
|
399
|
+
execSync("xclip -selection clipboard || xsel --clipboard --input", {
|
|
400
|
+
input: text,
|
|
401
|
+
shell: true,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
return true;
|
|
405
|
+
} catch {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ── React hook generator ──────────────────────────────────────────────────────
|
|
411
|
+
function generateUnifiedHook(books) {
|
|
412
|
+
const cwd = process.cwd();
|
|
413
|
+
const srcDir = path.join(cwd, "src");
|
|
414
|
+
const hooksDir = path.join(srcDir, "hooks");
|
|
415
|
+
|
|
416
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
417
|
+
if (!fs.existsSync(pkgPath)) {
|
|
418
|
+
console.error(
|
|
419
|
+
red("\n ✗ No package.json found. Run inside your React project.\n"),
|
|
420
|
+
);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
const projectPkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
424
|
+
const deps = { ...projectPkg.dependencies, ...projectPkg.devDependencies };
|
|
425
|
+
if (!deps["react"]) {
|
|
426
|
+
console.error(red("\n ✗ React not found in package.json.\n"));
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
if (!fs.existsSync(srcDir)) {
|
|
430
|
+
console.error(red("\n ✗ No src/ directory found.\n"));
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
if (!fs.existsSync(hooksDir)) {
|
|
434
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
435
|
+
console.log(green(" ✓ Created src/hooks/"));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Build per-book loader blocks
|
|
439
|
+
const loaderBlocks = books
|
|
440
|
+
.map((p) => {
|
|
441
|
+
const CDN = `https://cdn.jsdelivr.net/npm/${p.name}@latest/chapters`;
|
|
442
|
+
return `
|
|
443
|
+
// ── ${p.label} ──
|
|
444
|
+
const _${p.cmd}CDN = '${CDN}';
|
|
445
|
+
let _${p.cmd}Cache = null;
|
|
446
|
+
let _${p.cmd}Prom = null;
|
|
447
|
+
const _${p.cmd}Subs = new Set();
|
|
448
|
+
|
|
449
|
+
function _load${p.hook.replace("use", "")}() {
|
|
450
|
+
if (_${p.cmd}Cache) return Promise.resolve(_${p.cmd}Cache);
|
|
451
|
+
if (_${p.cmd}Prom) return _${p.cmd}Prom;
|
|
452
|
+
_${p.cmd}Prom = fetch(_${p.cmd}CDN + '/meta.json')
|
|
453
|
+
.then(r => r.json())
|
|
454
|
+
.then(meta => Promise.all(
|
|
455
|
+
meta.chapters.map(c => fetch(_${p.cmd}CDN + '/' + c.id + '.json').then(r => r.json()))
|
|
456
|
+
).then(results => {
|
|
457
|
+
const hadiths = results.flat();
|
|
458
|
+
const _byId = new Map();
|
|
459
|
+
hadiths.forEach(h => _byId.set(h.id, h));
|
|
460
|
+
_${p.cmd}Cache = Object.assign([], hadiths, {
|
|
461
|
+
metadata: meta.metadata,
|
|
462
|
+
chapters: meta.chapters,
|
|
463
|
+
get: (id) => _byId.get(id),
|
|
464
|
+
getByChapter: (id) => hadiths.filter(h => h.chapterId === id),
|
|
465
|
+
search: (q, limit = 0) => {
|
|
466
|
+
const ql = q.toLowerCase();
|
|
467
|
+
const r = hadiths.filter(h =>
|
|
468
|
+
h.english?.text?.toLowerCase().includes(ql) ||
|
|
469
|
+
h.english?.narrator?.toLowerCase().includes(ql)
|
|
470
|
+
);
|
|
471
|
+
return limit > 0 ? r.slice(0, limit) : r;
|
|
472
|
+
},
|
|
473
|
+
getRandom: () => hadiths[Math.floor(Math.random() * hadiths.length)],
|
|
474
|
+
});
|
|
475
|
+
_${p.cmd}Subs.forEach(fn => fn(_${p.cmd}Cache));
|
|
476
|
+
_${p.cmd}Subs.clear();
|
|
477
|
+
return _${p.cmd}Cache;
|
|
478
|
+
}));
|
|
479
|
+
return _${p.cmd}Prom;
|
|
480
|
+
}
|
|
481
|
+
_load${p.hook.replace("use", "")}();
|
|
482
|
+
|
|
483
|
+
export function ${p.hook}() {
|
|
484
|
+
const [data, setData] = useState(_${p.cmd}Cache);
|
|
485
|
+
useEffect(() => {
|
|
486
|
+
if (_${p.cmd}Cache) { setData(_${p.cmd}Cache); }
|
|
487
|
+
else { _${p.cmd}Subs.add(setData); return () => _${p.cmd}Subs.delete(setData); }
|
|
488
|
+
}, []);
|
|
489
|
+
return data;
|
|
490
|
+
}`;
|
|
491
|
+
})
|
|
492
|
+
.join("\n");
|
|
493
|
+
|
|
494
|
+
const hookNames = books.map((p) => p.hook).join(", ");
|
|
495
|
+
|
|
496
|
+
const hookSrc = `// Auto-generated by: sunnah --react
|
|
497
|
+
// Re-run to regenerate after installing more sunnah packages.
|
|
498
|
+
//
|
|
499
|
+
// Included books: ${books.map((p) => p.label).join(", ")}
|
|
500
|
+
//
|
|
501
|
+
// Usage:
|
|
502
|
+
${books.map((p) => `// import { ${p.hook} } from '../hooks/useSunnah';`).join("\n")}
|
|
503
|
+
//
|
|
504
|
+
// const bukhari = useBukhari();
|
|
505
|
+
// if (!bukhari) return <p>Loading...</p>;
|
|
506
|
+
// bukhari.get(1) // hadith by ID
|
|
507
|
+
// bukhari.search('prayer', 5) // top 5 results
|
|
508
|
+
// bukhari.getRandom() // random hadith
|
|
509
|
+
// bukhari.getByChapter(1) // all hadiths in chapter
|
|
510
|
+
|
|
511
|
+
import { useState, useEffect } from 'react';
|
|
512
|
+
${loaderBlocks}
|
|
513
|
+
|
|
514
|
+
// Unified hook — loads all books in parallel
|
|
515
|
+
export function useSunnah() {
|
|
516
|
+
const hooks = [${books.map((p) => `${p.hook}()`).join(", ")}];
|
|
517
|
+
if (hooks.some(h => !h)) return null;
|
|
518
|
+
return { ${books.map((p) => p.cmd + ": hooks[" + books.indexOf(p) + "]").join(", ")} };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
export default useSunnah;
|
|
522
|
+
`;
|
|
523
|
+
|
|
524
|
+
const hookFile = path.join(hooksDir, "useSunnah.js");
|
|
525
|
+
fs.writeFileSync(hookFile, hookSrc, "utf8");
|
|
526
|
+
|
|
527
|
+
const div = gray("─".repeat(60));
|
|
528
|
+
console.log("\n" + div);
|
|
529
|
+
console.log(bold(cyan(" ✓ Generated: src/hooks/useSunnah.js")));
|
|
530
|
+
console.log(div);
|
|
531
|
+
console.log("\n " + gray("Included books:"));
|
|
532
|
+
books.forEach((p) =>
|
|
533
|
+
console.log(
|
|
534
|
+
" " + green("▸") + " " + bold(p.label) + gray(" " + p.hook + "()"),
|
|
535
|
+
),
|
|
536
|
+
);
|
|
537
|
+
console.log("\n " + gray("Usage:"));
|
|
538
|
+
console.log(
|
|
539
|
+
" " +
|
|
540
|
+
cyan("import { useSunnah") +
|
|
541
|
+
(books.length > 1
|
|
542
|
+
? ", " +
|
|
543
|
+
books
|
|
544
|
+
.slice(0, 2)
|
|
545
|
+
.map((p) => p.hook)
|
|
546
|
+
.join(", ")
|
|
547
|
+
: "") +
|
|
548
|
+
cyan(" } from '../hooks/useSunnah';"),
|
|
549
|
+
);
|
|
550
|
+
console.log("");
|
|
551
|
+
console.log(" " + gray("// Unified — all books at once"));
|
|
552
|
+
console.log(" " + gray("const sunnah = useSunnah();"));
|
|
553
|
+
console.log(" " + gray("if (!sunnah) return <p>Loading...</p>;"));
|
|
554
|
+
if (books[0])
|
|
555
|
+
console.log(" " + gray(`sunnah.${books[0].cmd}.getRandom()`));
|
|
556
|
+
console.log("");
|
|
557
|
+
console.log(" " + gray("// Per-book hooks still work"));
|
|
558
|
+
if (books[0]) {
|
|
559
|
+
console.log(" " + gray(`const ${books[0].cmd} = ${books[0].hook}();`));
|
|
560
|
+
console.log(" " + gray(`${books[0].cmd}.get(1).english.text`));
|
|
561
|
+
}
|
|
562
|
+
console.log("\n" + div + "\n");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ── Personalized suggestions based on what's installed ───────────────────────
|
|
566
|
+
function getPersonalizedSuggestions() {
|
|
567
|
+
const installed = PACKAGES.filter((p) => isInstalled(p.name));
|
|
568
|
+
const missing = PACKAGES.filter((p) => !isInstalled(p.name));
|
|
569
|
+
const tips = [];
|
|
570
|
+
|
|
571
|
+
if (installed.length === 0) {
|
|
572
|
+
tips.push(
|
|
573
|
+
cyan(" ▸") +
|
|
574
|
+
" Run " +
|
|
575
|
+
bold("sunnah") +
|
|
576
|
+
" to open the interactive installer",
|
|
577
|
+
);
|
|
578
|
+
tips.push(
|
|
579
|
+
cyan(" ▸") + " Or install directly: " + bold("sunnah install bukhari"),
|
|
580
|
+
);
|
|
581
|
+
} else {
|
|
582
|
+
installed.forEach((p) => {
|
|
583
|
+
tips.push(
|
|
584
|
+
cyan(" ▸") +
|
|
585
|
+
" " +
|
|
586
|
+
bold(p.cmd + " --random") +
|
|
587
|
+
gray(" — read a random " + p.label + " hadith"),
|
|
588
|
+
);
|
|
589
|
+
tips.push(
|
|
590
|
+
cyan(" ▸") +
|
|
591
|
+
" " +
|
|
592
|
+
bold(p.cmd + ' --search "prayer"') +
|
|
593
|
+
gray(" — search " + p.label),
|
|
594
|
+
);
|
|
595
|
+
});
|
|
596
|
+
if (installed.length > 1) {
|
|
597
|
+
tips.push(
|
|
598
|
+
cyan(" ▸") +
|
|
599
|
+
" " +
|
|
600
|
+
bold("sunnah --react") +
|
|
601
|
+
gray(" — generate useSunnah() hook for all installed books"),
|
|
602
|
+
);
|
|
603
|
+
} else if (installed.length === 1) {
|
|
604
|
+
tips.push(
|
|
605
|
+
cyan(" ▸") +
|
|
606
|
+
" " +
|
|
607
|
+
bold("sunnah --react " + installed[0].cmd) +
|
|
608
|
+
gray(" — generate React hook for " + installed[0].label),
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
if (missing.length > 0) {
|
|
612
|
+
tips.push(
|
|
613
|
+
cyan(" ▸") +
|
|
614
|
+
" " +
|
|
615
|
+
bold("sunnah") +
|
|
616
|
+
gray(
|
|
617
|
+
" — install more books (" +
|
|
618
|
+
missing.map((p) => p.label).join(", ") +
|
|
619
|
+
")",
|
|
620
|
+
),
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
tips.push(
|
|
624
|
+
cyan(" ▸") +
|
|
625
|
+
" " +
|
|
626
|
+
bold("sunnah --update") +
|
|
627
|
+
gray(" — check for updates"),
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return tips;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// ── Modes ─────────────────────────────────────────────────────────────────────
|
|
635
|
+
const MODE = {
|
|
636
|
+
LIST: "list",
|
|
637
|
+
CONFIRM_UNINSTALL: "confirm_uninstall",
|
|
638
|
+
UPDATE: "update",
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
// ── Interactive render ────────────────────────────────────────────────────────
|
|
642
|
+
function render(state) {
|
|
643
|
+
const { cursor, selected, mode, confirmTarget, statusMsg } = state;
|
|
644
|
+
const divW = Math.min(W() - 2, 72);
|
|
645
|
+
const div = gray("─".repeat(divW));
|
|
646
|
+
const div2 = gray("═".repeat(divW));
|
|
647
|
+
|
|
648
|
+
clearScreen();
|
|
649
|
+
let row = 1;
|
|
650
|
+
|
|
651
|
+
writeLine(row++, div2);
|
|
652
|
+
writeLine(
|
|
653
|
+
row++,
|
|
654
|
+
bold(cyan(" 📚 Sunnah Package Manager")) + gray(" v" + pkg.version),
|
|
655
|
+
);
|
|
656
|
+
writeLine(
|
|
657
|
+
row++,
|
|
658
|
+
gray(" ↑↓") +
|
|
659
|
+
" nav " +
|
|
660
|
+
gray("space") +
|
|
661
|
+
" select " +
|
|
662
|
+
gray("a") +
|
|
663
|
+
" all " +
|
|
664
|
+
gray("i") +
|
|
665
|
+
" info " +
|
|
666
|
+
gray("u") +
|
|
667
|
+
" uninstall " +
|
|
668
|
+
gray("U") +
|
|
669
|
+
" update " +
|
|
670
|
+
gray("enter") +
|
|
671
|
+
" install " +
|
|
672
|
+
gray("q") +
|
|
673
|
+
" quit",
|
|
674
|
+
);
|
|
675
|
+
writeLine(row++, div2);
|
|
676
|
+
row++;
|
|
677
|
+
|
|
678
|
+
PACKAGES.forEach((p, i) => {
|
|
679
|
+
const isCursor = i === cursor;
|
|
680
|
+
const isSel = selected.has(i);
|
|
681
|
+
const inst = isInstalled(p.name);
|
|
682
|
+
|
|
683
|
+
const checkbox = isSel ? green("[✓]") : gray("[ ]");
|
|
684
|
+
const arrow = isCursor ? cyan("▶") : " ";
|
|
685
|
+
const label = isCursor
|
|
686
|
+
? bold(white(p.label))
|
|
687
|
+
: isSel
|
|
688
|
+
? green(p.label)
|
|
689
|
+
: white(p.label);
|
|
690
|
+
const badge = inst
|
|
691
|
+
? dim(green(" ● installed")) +
|
|
692
|
+
(updateCache.get(p.name)?.hasUpdate
|
|
693
|
+
? " " + yellow("↑ update available")
|
|
694
|
+
: updateCacheReady && inst
|
|
695
|
+
? " " + dim(gray("(up to date)"))
|
|
696
|
+
: "")
|
|
697
|
+
: dim(gray(" ○ not installed"));
|
|
698
|
+
|
|
699
|
+
writeLine(row++, ` ${arrow} ${checkbox} ${label}${badge}`);
|
|
700
|
+
|
|
701
|
+
if (isCursor) {
|
|
702
|
+
writeLine(row++, ` ${dim(p.author)}`);
|
|
703
|
+
writeLine(row++, ` ${gray(p.desc)}`);
|
|
704
|
+
const uc = updateCache.get(p.name);
|
|
705
|
+
const verStr = inst
|
|
706
|
+
? uc
|
|
707
|
+
? uc.hasUpdate
|
|
708
|
+
? gray(" v") + yellow(uc.current) + gray(" → ") + green(uc.latest)
|
|
709
|
+
: gray(" v") + cyan(uc.current || "?")
|
|
710
|
+
: gray(" (checking…)")
|
|
711
|
+
: "";
|
|
712
|
+
writeLine(
|
|
713
|
+
row++,
|
|
714
|
+
` ${gray("Hadiths: ")}${yellow(p.hadiths)}` +
|
|
715
|
+
` ${gray("CLI: ")}${cyan(p.cmd + " --help")}` +
|
|
716
|
+
verStr +
|
|
717
|
+
(inst ? ` ${gray("try: ")}${cyan(p.cmd + " --random")}` : ""),
|
|
718
|
+
);
|
|
719
|
+
row++;
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
row++;
|
|
724
|
+
writeLine(row++, div);
|
|
725
|
+
|
|
726
|
+
if (statusMsg) {
|
|
727
|
+
writeLine(row++, ` ${yellow("⚠")} ${yellow(statusMsg)}`);
|
|
728
|
+
} else if (selected.size > 0) {
|
|
729
|
+
const names = [...selected].map((i) => cyan(PACKAGES[i].name)).join(", ");
|
|
730
|
+
writeLine(
|
|
731
|
+
row++,
|
|
732
|
+
` ${green("●")} ${bold(String(selected.size))} selected: ${names}`,
|
|
733
|
+
);
|
|
734
|
+
writeLine(row++, ` ${dim("enter = install u = uninstall i = info")}`);
|
|
735
|
+
} else {
|
|
736
|
+
writeLine(
|
|
737
|
+
row++,
|
|
738
|
+
` ${gray("Nothing selected — press space to select, enter to install focused")}`,
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
writeLine(row++, div);
|
|
743
|
+
|
|
744
|
+
if (mode === MODE.CONFIRM_UNINSTALL && confirmTarget !== null) {
|
|
745
|
+
const p = PACKAGES[confirmTarget];
|
|
746
|
+
row++;
|
|
747
|
+
writeLine(
|
|
748
|
+
row++,
|
|
749
|
+
` ${red("⚠ Uninstall ")}${bold(white(p.label))}${red("?")}`,
|
|
750
|
+
);
|
|
751
|
+
writeLine(
|
|
752
|
+
row++,
|
|
753
|
+
` ${green("y")} ${gray("confirm")} ${red("n")} ${gray("cancel")}`,
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// ── --list ────────────────────────────────────────────────────────────────────
|
|
759
|
+
function cmdList() {
|
|
760
|
+
installedCache = buildInstalledCache();
|
|
761
|
+
const div = gray("─".repeat(60));
|
|
762
|
+
console.log("\n" + div);
|
|
763
|
+
console.log(bold(cyan(" Available Sunnah Packages")));
|
|
764
|
+
console.log(div);
|
|
765
|
+
PACKAGES.forEach((p) => {
|
|
766
|
+
const inst = isInstalled(p.name);
|
|
767
|
+
const badge = inst ? green(" ✓ installed") : red(" ✗ not installed");
|
|
768
|
+
let versionStr = "";
|
|
769
|
+
if (inst) {
|
|
770
|
+
const current = getInstalledVersion(p.name);
|
|
771
|
+
const latest = getLatestVersion(p.name);
|
|
772
|
+
if (current && latest) {
|
|
773
|
+
versionStr =
|
|
774
|
+
current === latest
|
|
775
|
+
? dim(gray(" v" + current + " (up to date)"))
|
|
776
|
+
: yellow(" v" + current) +
|
|
777
|
+
gray(" → ") +
|
|
778
|
+
green("v" + latest) +
|
|
779
|
+
yellow(" ↑ update available");
|
|
780
|
+
} else if (current) {
|
|
781
|
+
versionStr = dim(gray(" v" + current));
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
console.log(`\n ${bold(white(p.label))}${badge}${versionStr}`);
|
|
785
|
+
console.log(` ${cyan("npm install -g " + p.name)}`);
|
|
786
|
+
console.log(` ${dim(p.desc)}`);
|
|
787
|
+
console.log(
|
|
788
|
+
` ${gray("Hadiths: ")}${yellow(p.hadiths)} ${gray("Author: ")}${magenta(p.author)}`,
|
|
789
|
+
);
|
|
790
|
+
if (inst) {
|
|
791
|
+
console.log(
|
|
792
|
+
` ${gray("CLI: ")}${cyan(p.cmd + " --help")} ${gray("React: ")}${cyan("sunnah --react " + p.cmd)}`,
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
console.log("\n" + div + "\n");
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// ── --update ──────────────────────────────────────────────────────────────────
|
|
800
|
+
async function cmdUpdate(autoInstall = false) {
|
|
801
|
+
installedCache = buildInstalledCache();
|
|
802
|
+
const installed = PACKAGES.filter((p) => isInstalled(p.name));
|
|
803
|
+
if (!installed.length) {
|
|
804
|
+
console.log(
|
|
805
|
+
"\n " +
|
|
806
|
+
yellow("No sunnah packages installed. Run ") +
|
|
807
|
+
bold("sunnah") +
|
|
808
|
+
yellow(" to install.\n"),
|
|
809
|
+
);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
const div = gray("─".repeat(60));
|
|
813
|
+
const div2 = gray("═".repeat(60));
|
|
814
|
+
console.log("\n" + div2);
|
|
815
|
+
console.log(bold(cyan(" Checking for updates…")));
|
|
816
|
+
console.log(div2 + "\n");
|
|
817
|
+
|
|
818
|
+
const updates = [];
|
|
819
|
+
for (const p of installed) {
|
|
820
|
+
process.stdout.write(
|
|
821
|
+
" " + gray("Checking ") + white(p.label) + gray("…\r"),
|
|
822
|
+
);
|
|
823
|
+
const current = getInstalledVersion(p.name);
|
|
824
|
+
const latest = getLatestVersion(p.name);
|
|
825
|
+
process.stdout.write("\x1b[K");
|
|
826
|
+
if (!current || !latest) {
|
|
827
|
+
console.log(
|
|
828
|
+
` ${yellow("?")} ${bold(p.label)} ${gray("(could not check)")}`,
|
|
829
|
+
);
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (current === latest) {
|
|
833
|
+
console.log(
|
|
834
|
+
` ${green("✓")} ${bold(p.label)} ${gray("v" + current + " — up to date")}`,
|
|
835
|
+
);
|
|
836
|
+
} else {
|
|
837
|
+
updates.push({ p, current, latest });
|
|
838
|
+
console.log(
|
|
839
|
+
` ${yellow("↑")} ${bold(p.label)} ${gray("v" + current)} ${gray("→")} ${green("v" + latest)} ${yellow("(update available)")}`,
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
console.log("\n" + div);
|
|
845
|
+
|
|
846
|
+
if (!updates.length) {
|
|
847
|
+
console.log(" " + green("✓ All packages are up to date."));
|
|
848
|
+
console.log(div + "\n");
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
console.log(
|
|
853
|
+
" " +
|
|
854
|
+
yellow(String(updates.length)) +
|
|
855
|
+
" update" +
|
|
856
|
+
(updates.length > 1 ? "s" : "") +
|
|
857
|
+
" available.",
|
|
858
|
+
);
|
|
859
|
+
|
|
860
|
+
if (autoInstall) {
|
|
861
|
+
console.log(bold(cyan("\n Installing updates…")));
|
|
862
|
+
console.log(div + "\n");
|
|
863
|
+
for (let i = 0; i < updates.length; i++) {
|
|
864
|
+
const { p, current, latest } = updates[i];
|
|
865
|
+
console.log(
|
|
866
|
+
" " +
|
|
867
|
+
cyan("[" + (i + 1) + "/" + updates.length + "]") +
|
|
868
|
+
" " +
|
|
869
|
+
bold(white(p.label)) +
|
|
870
|
+
gray(" v" + current + " → v" + latest) +
|
|
871
|
+
"\n",
|
|
872
|
+
);
|
|
873
|
+
await animateInstall(p.name);
|
|
874
|
+
console.log(
|
|
875
|
+
" " +
|
|
876
|
+
green("✓") +
|
|
877
|
+
" " +
|
|
878
|
+
bold(green(p.label)) +
|
|
879
|
+
gray(" updated to v" + latest),
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
console.log("\n" + div2);
|
|
883
|
+
console.log(" " + green("✓ All updates installed."));
|
|
884
|
+
console.log(div2 + "\n");
|
|
885
|
+
} else {
|
|
886
|
+
console.log(
|
|
887
|
+
" Run " +
|
|
888
|
+
bold(cyan("sunnah --update --install")) +
|
|
889
|
+
gray(" to install all updates automatically."),
|
|
890
|
+
);
|
|
891
|
+
console.log(" Or update individually:");
|
|
892
|
+
updates.forEach(({ p }) =>
|
|
893
|
+
console.log(" " + dim("sunnah install " + p.cmd)),
|
|
894
|
+
);
|
|
895
|
+
console.log(div + "\n");
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// ── sunnah install <cmd> ──────────────────────────────────────────────────────
|
|
900
|
+
async function cmdInstall(targets) {
|
|
901
|
+
if (!targets.length) {
|
|
902
|
+
console.error(
|
|
903
|
+
red(
|
|
904
|
+
"\n ✗ Usage: sunnah install <name> (e.g. sunnah install bukhari)\n",
|
|
905
|
+
),
|
|
906
|
+
);
|
|
907
|
+
console.log(
|
|
908
|
+
" Available: " + PACKAGES.map((p) => cyan(p.cmd)).join(", ") + "\n",
|
|
909
|
+
);
|
|
910
|
+
process.exit(1);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Resolve names — accept cmd alias or full npm name
|
|
914
|
+
const toInstall = [];
|
|
915
|
+
for (const t of targets) {
|
|
916
|
+
const byCmd = CMD_MAP[t.toLowerCase()];
|
|
917
|
+
const byName = PACKAGES.find((p) => p.name === t);
|
|
918
|
+
const found = byCmd || byName;
|
|
919
|
+
if (!found) {
|
|
920
|
+
console.log(yellow(`\n ⚠ Unknown package: "${t}"`));
|
|
921
|
+
console.log(
|
|
922
|
+
" Available: " +
|
|
923
|
+
PACKAGES.map((p) => cyan(p.cmd) + gray(" (" + p.name + ")")).join(
|
|
924
|
+
", ",
|
|
925
|
+
),
|
|
926
|
+
);
|
|
927
|
+
process.exit(1);
|
|
928
|
+
}
|
|
929
|
+
toInstall.push(found);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const divW = Math.min(W() - 2, 72);
|
|
933
|
+
const div2 = gray("═".repeat(divW));
|
|
934
|
+
|
|
935
|
+
console.log("\n" + div2);
|
|
936
|
+
console.log(
|
|
937
|
+
bold(cyan(" Installing ")) +
|
|
938
|
+
bold(yellow(String(toInstall.length))) +
|
|
939
|
+
bold(cyan(" package" + (toInstall.length > 1 ? "s" : "") + "…")),
|
|
940
|
+
);
|
|
941
|
+
console.log(div2);
|
|
942
|
+
|
|
943
|
+
for (let i = 0; i < toInstall.length; i++) {
|
|
944
|
+
const p = toInstall[i];
|
|
945
|
+
console.log(
|
|
946
|
+
"\n " +
|
|
947
|
+
cyan("[" + (i + 1) + "/" + toInstall.length + "]") +
|
|
948
|
+
" " +
|
|
949
|
+
bold(white(p.label)),
|
|
950
|
+
);
|
|
951
|
+
console.log(" " + dim("npm install -g " + p.name) + "\n");
|
|
952
|
+
await animateInstall(p.name);
|
|
953
|
+
installedCache.set(p.name, true);
|
|
954
|
+
console.log(" " + green("✓") + " " + bold(green(p.label)) + " installed");
|
|
955
|
+
console.log(" " + gray("Usage: ") + cyan(p.cmd + " --help"));
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
console.log("\n" + div2);
|
|
959
|
+
console.log(
|
|
960
|
+
" " +
|
|
961
|
+
green("✓ Done! ") +
|
|
962
|
+
toInstall.map((p) => bold(cyan(p.cmd))).join(", ") +
|
|
963
|
+
gray(" ready."),
|
|
964
|
+
);
|
|
965
|
+
console.log(div2 + "\n");
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// ── sunnah uninstall <cmd> ────────────────────────────────────────────────────
|
|
969
|
+
function cmdUninstall(targets) {
|
|
970
|
+
if (!targets.length) {
|
|
971
|
+
console.error(
|
|
972
|
+
red(
|
|
973
|
+
"\n ✗ Usage: sunnah uninstall <name> (e.g. sunnah uninstall bukhari)\n",
|
|
974
|
+
),
|
|
975
|
+
);
|
|
976
|
+
process.exit(1);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
installedCache = buildInstalledCache();
|
|
980
|
+
const divW = Math.min(W() - 2, 72);
|
|
981
|
+
const div2 = gray("═".repeat(divW));
|
|
982
|
+
|
|
983
|
+
console.log("\n" + div2);
|
|
984
|
+
|
|
985
|
+
for (const t of targets) {
|
|
986
|
+
const p =
|
|
987
|
+
CMD_MAP[t.toLowerCase()] || PACKAGES.find((pkg) => pkg.name === t);
|
|
988
|
+
if (!p) {
|
|
989
|
+
console.log(yellow(` ⚠ Unknown: "${t}"`) + "\n");
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (!isInstalled(p.name)) {
|
|
993
|
+
console.log(gray(` ○ ${p.label} is not installed, skipping.`));
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
console.log(yellow(" Uninstalling ") + bold(white(p.label)) + yellow("…"));
|
|
997
|
+
try {
|
|
998
|
+
npmSync(["uninstall", "-g", p.name], { stdio: "inherit" });
|
|
999
|
+
installedCache.set(p.name, false);
|
|
1000
|
+
console.log(
|
|
1001
|
+
green(" ✓ ") + bold(green(p.label)) + green(" uninstalled.\n"),
|
|
1002
|
+
);
|
|
1003
|
+
} catch {
|
|
1004
|
+
console.log(red(" ✗ Failed to uninstall " + p.label + "\n"));
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
console.log(div2 + "\n");
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// ── --version (personalized) ──────────────────────────────────────────────────
|
|
1012
|
+
function cmdVersion() {
|
|
1013
|
+
installedCache = buildInstalledCache();
|
|
1014
|
+
const installed = PACKAGES.filter((p) => isInstalled(p.name));
|
|
1015
|
+
const div = gray("─".repeat(60));
|
|
1016
|
+
|
|
1017
|
+
console.log("\n" + div);
|
|
1018
|
+
console.log(bold(cyan(" 📿 sunnah")) + gray(" v" + pkg.version));
|
|
1019
|
+
console.log(div);
|
|
1020
|
+
console.log(
|
|
1021
|
+
" " + gray("Available packages : ") + yellow(String(PACKAGES.length)),
|
|
1022
|
+
);
|
|
1023
|
+
console.log(
|
|
1024
|
+
" " +
|
|
1025
|
+
gray("Installed : ") +
|
|
1026
|
+
green(String(installed.length)) +
|
|
1027
|
+
gray(" / " + PACKAGES.length),
|
|
1028
|
+
);
|
|
1029
|
+
|
|
1030
|
+
if (installed.length > 0) {
|
|
1031
|
+
console.log(
|
|
1032
|
+
" " +
|
|
1033
|
+
gray("Your collection : ") +
|
|
1034
|
+
installed.map((p) => cyan(p.label)).join(gray(", ")),
|
|
1035
|
+
);
|
|
1036
|
+
const totalHadiths = installed.reduce(
|
|
1037
|
+
(acc, p) => acc + parseInt(p.hadiths.replace(/,/g, "")),
|
|
1038
|
+
0,
|
|
1039
|
+
);
|
|
1040
|
+
console.log(
|
|
1041
|
+
" " +
|
|
1042
|
+
gray("Total hadiths : ") +
|
|
1043
|
+
bold(yellow(totalHadiths.toLocaleString())),
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
console.log("\n" + div + "\n");
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// ── --help (personalized) ─────────────────────────────────────────────────────
|
|
1051
|
+
function cmdHelp() {
|
|
1052
|
+
installedCache = buildInstalledCache();
|
|
1053
|
+
const div = gray("─".repeat(60));
|
|
1054
|
+
|
|
1055
|
+
console.log("\n" + div);
|
|
1056
|
+
console.log(
|
|
1057
|
+
bold(cyan(" 📿 Sunnah Package Manager")) + gray(" v" + pkg.version),
|
|
1058
|
+
);
|
|
1059
|
+
console.log(div);
|
|
1060
|
+
|
|
1061
|
+
console.log("\n " + bold("Commands:"));
|
|
1062
|
+
console.log(
|
|
1063
|
+
" " +
|
|
1064
|
+
cyan("sunnah") +
|
|
1065
|
+
gray(" Open interactive installer UI"),
|
|
1066
|
+
);
|
|
1067
|
+
console.log(
|
|
1068
|
+
" " +
|
|
1069
|
+
cyan("sunnah install") +
|
|
1070
|
+
yellow(" <name>") +
|
|
1071
|
+
gray(" Install a package directly"),
|
|
1072
|
+
);
|
|
1073
|
+
console.log(
|
|
1074
|
+
" " +
|
|
1075
|
+
cyan("sunnah uninstall") +
|
|
1076
|
+
yellow(" <name>") +
|
|
1077
|
+
gray(" Uninstall a package"),
|
|
1078
|
+
);
|
|
1079
|
+
console.log(
|
|
1080
|
+
" " +
|
|
1081
|
+
cyan("sunnah --react") +
|
|
1082
|
+
gray(" Generate unified useSunnah() React hook"),
|
|
1083
|
+
);
|
|
1084
|
+
console.log(
|
|
1085
|
+
" " +
|
|
1086
|
+
cyan("sunnah --react") +
|
|
1087
|
+
yellow(" <books>") +
|
|
1088
|
+
gray(" Generate hook for specific books"),
|
|
1089
|
+
);
|
|
1090
|
+
console.log(
|
|
1091
|
+
" " +
|
|
1092
|
+
cyan("sunnah --list") +
|
|
1093
|
+
gray(" List all packages with install status"),
|
|
1094
|
+
);
|
|
1095
|
+
console.log(
|
|
1096
|
+
" " +
|
|
1097
|
+
cyan("sunnah --update") +
|
|
1098
|
+
gray(" Check installed packages for updates"),
|
|
1099
|
+
);
|
|
1100
|
+
console.log(
|
|
1101
|
+
" " +
|
|
1102
|
+
cyan("sunnah --update --install") +
|
|
1103
|
+
gray(" Auto-install all available updates"),
|
|
1104
|
+
);
|
|
1105
|
+
console.log(
|
|
1106
|
+
" " +
|
|
1107
|
+
cyan("sunnah -v") +
|
|
1108
|
+
gray(" Version + your collection stats"),
|
|
1109
|
+
);
|
|
1110
|
+
console.log(
|
|
1111
|
+
" " +
|
|
1112
|
+
cyan("sunnah -h") +
|
|
1113
|
+
gray(" This help + personalized tips"),
|
|
1114
|
+
);
|
|
1115
|
+
|
|
1116
|
+
console.log("\n " + bold("Package names (use any form):"));
|
|
1117
|
+
PACKAGES.forEach((p) => {
|
|
1118
|
+
console.log(
|
|
1119
|
+
" " +
|
|
1120
|
+
cyan(p.cmd.padEnd(12)) +
|
|
1121
|
+
gray(p.name.padEnd(24)) +
|
|
1122
|
+
yellow(p.hadiths + " hadiths"),
|
|
1123
|
+
);
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
console.log("\n " + bold("Examples:"));
|
|
1127
|
+
console.log(" " + dim("sunnah install bukhari"));
|
|
1128
|
+
console.log(" " + dim("sunnah install bukhari muslim tirmidhi"));
|
|
1129
|
+
console.log(" " + dim("sunnah uninstall dawud"));
|
|
1130
|
+
console.log(" " + dim("sunnah --react"));
|
|
1131
|
+
console.log(" " + dim("sunnah --react bukhari muslim"));
|
|
1132
|
+
console.log(" " + dim("sunnah --update"));
|
|
1133
|
+
|
|
1134
|
+
console.log("\n " + bold("Interactive UI controls:"));
|
|
1135
|
+
console.log(" " + green("↑ ↓") + gray(" Navigate packages"));
|
|
1136
|
+
console.log(" " + green("space") + gray(" Toggle select"));
|
|
1137
|
+
console.log(" " + green("a") + gray(" Select all / deselect all"));
|
|
1138
|
+
console.log(
|
|
1139
|
+
" " + green("i") + gray(" Show info + installed version"),
|
|
1140
|
+
);
|
|
1141
|
+
console.log(" " + green("u") + gray(" Uninstall selected"));
|
|
1142
|
+
console.log(
|
|
1143
|
+
" " +
|
|
1144
|
+
green("U") +
|
|
1145
|
+
gray(" Update selected (if newer version available)"),
|
|
1146
|
+
);
|
|
1147
|
+
console.log(
|
|
1148
|
+
" " +
|
|
1149
|
+
green("enter") +
|
|
1150
|
+
gray(" Install selected (or focused if none selected)"),
|
|
1151
|
+
);
|
|
1152
|
+
console.log(" " + green("q") + gray(" Quit"));
|
|
1153
|
+
|
|
1154
|
+
// Personalized tips
|
|
1155
|
+
const tips = getPersonalizedSuggestions();
|
|
1156
|
+
if (tips.length) {
|
|
1157
|
+
console.log("\n" + div);
|
|
1158
|
+
console.log(bold(" 💡 Suggested for you:"));
|
|
1159
|
+
tips.forEach((t) => console.log(t));
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
console.log("\n" + div + "\n");
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
1166
|
+
async function main() {
|
|
1167
|
+
const rawArgs = process.argv.slice(2);
|
|
1168
|
+
const flags = rawArgs.filter((a) => a.startsWith("-"));
|
|
1169
|
+
const positional = rawArgs.filter((a) => !a.startsWith("-"));
|
|
1170
|
+
|
|
1171
|
+
// sunnah -v / --version
|
|
1172
|
+
if (flags.some((f) => f === "-v" || f === "--version")) {
|
|
1173
|
+
cmdVersion();
|
|
1174
|
+
process.exit(0);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// sunnah -h / --help
|
|
1178
|
+
if (flags.some((f) => f === "-h" || f === "--help")) {
|
|
1179
|
+
cmdHelp();
|
|
1180
|
+
process.exit(0);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// sunnah --list / -l
|
|
1184
|
+
if (flags.some((f) => f === "--list" || f === "-l")) {
|
|
1185
|
+
cmdList();
|
|
1186
|
+
process.exit(0);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// sunnah --update [--install]
|
|
1190
|
+
if (flags.some((f) => f === "--update")) {
|
|
1191
|
+
const autoInstall = flags.some((f) => f === "--install");
|
|
1192
|
+
await cmdUpdate(autoInstall);
|
|
1193
|
+
process.exit(0);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// sunnah install <names...>
|
|
1197
|
+
if (positional[0] === "install") {
|
|
1198
|
+
await cmdInstall(positional.slice(1));
|
|
1199
|
+
process.exit(0);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// sunnah uninstall <names...>
|
|
1203
|
+
if (positional[0] === "uninstall") {
|
|
1204
|
+
cmdUninstall(positional.slice(1));
|
|
1205
|
+
process.exit(0);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// sunnah --react [book1 book2 ...]
|
|
1209
|
+
if (flags.some((f) => f === "--react")) {
|
|
1210
|
+
installedCache = buildInstalledCache();
|
|
1211
|
+
// If specific books given after --react, use those; else use all installed
|
|
1212
|
+
const requestedCmds = positional; // e.g. ["bukhari", "muslim"]
|
|
1213
|
+
let books;
|
|
1214
|
+
if (requestedCmds.length > 0) {
|
|
1215
|
+
books = requestedCmds
|
|
1216
|
+
.map((cmd) => CMD_MAP[cmd.toLowerCase()])
|
|
1217
|
+
.filter(Boolean);
|
|
1218
|
+
const unknown = requestedCmds.filter(
|
|
1219
|
+
(cmd) => !CMD_MAP[cmd.toLowerCase()],
|
|
1220
|
+
);
|
|
1221
|
+
if (unknown.length) {
|
|
1222
|
+
console.log(yellow("\n ⚠ Unknown books: " + unknown.join(", ")));
|
|
1223
|
+
console.log(" Available: " + Object.keys(CMD_MAP).join(", ") + "\n");
|
|
1224
|
+
}
|
|
1225
|
+
} else {
|
|
1226
|
+
books = PACKAGES.filter((p) => isInstalled(p.name));
|
|
1227
|
+
if (!books.length) {
|
|
1228
|
+
console.log(yellow("\n ⚠ No sunnah packages installed yet."));
|
|
1229
|
+
console.log(
|
|
1230
|
+
" Run " +
|
|
1231
|
+
bold("sunnah") +
|
|
1232
|
+
" to install some first, or specify books explicitly:",
|
|
1233
|
+
);
|
|
1234
|
+
console.log(" " + dim("sunnah --react bukhari muslim") + "\n");
|
|
1235
|
+
process.exit(1);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
if (!books.length) {
|
|
1239
|
+
console.log(red("\n ✗ No valid books specified.\n"));
|
|
1240
|
+
process.exit(1);
|
|
1241
|
+
}
|
|
1242
|
+
generateUnifiedHook(books);
|
|
1243
|
+
process.exit(0);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// ── Interactive mode ────────────────────────────────────────────────────────
|
|
1247
|
+
if (!process.stdin.isTTY) {
|
|
1248
|
+
console.error(red("\n ✗ Interactive mode requires a TTY terminal.\n"));
|
|
1249
|
+
process.exit(1);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// Build cache
|
|
1253
|
+
process.stdout.write("\n " + gray("Checking installed packages…"));
|
|
1254
|
+
installedCache = buildInstalledCache();
|
|
1255
|
+
process.stdout.write("\r\x1b[K");
|
|
1256
|
+
|
|
1257
|
+
// Load persisted selection
|
|
1258
|
+
const persisted = loadPersistedState();
|
|
1259
|
+
const lastSelected = new Set(
|
|
1260
|
+
(persisted.lastSelected || []).filter((i) => i < PACKAGES.length),
|
|
1261
|
+
);
|
|
1262
|
+
|
|
1263
|
+
enterAltScreen();
|
|
1264
|
+
hideCursor();
|
|
1265
|
+
|
|
1266
|
+
const state = {
|
|
1267
|
+
cursor: 0,
|
|
1268
|
+
selected: lastSelected, // pre-check last session's selections
|
|
1269
|
+
mode: MODE.LIST,
|
|
1270
|
+
confirmTarget: null,
|
|
1271
|
+
statusMsg: "",
|
|
1272
|
+
};
|
|
1273
|
+
|
|
1274
|
+
let statusTimer = null;
|
|
1275
|
+
|
|
1276
|
+
function setStatus(msg, ms = 2500) {
|
|
1277
|
+
state.statusMsg = msg;
|
|
1278
|
+
render(state);
|
|
1279
|
+
if (statusTimer) clearTimeout(statusTimer);
|
|
1280
|
+
statusTimer = setTimeout(() => {
|
|
1281
|
+
state.statusMsg = "";
|
|
1282
|
+
render(state);
|
|
1283
|
+
}, ms);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
render(state);
|
|
1287
|
+
|
|
1288
|
+
// Prefetch update info in background — re-render when ready so badges appear
|
|
1289
|
+
prefetchUpdateCache().then(() => render(state));
|
|
1290
|
+
|
|
1291
|
+
const cleanup = () => {
|
|
1292
|
+
// Persist current selection before exiting
|
|
1293
|
+
savePersistedState({ lastSelected: [...state.selected] });
|
|
1294
|
+
showCursor();
|
|
1295
|
+
leaveAltScreen();
|
|
1296
|
+
try {
|
|
1297
|
+
process.stdin.setRawMode(false);
|
|
1298
|
+
} catch (_e) {}
|
|
1299
|
+
process.stdin.pause();
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1302
|
+
process.on("SIGINT", () => {
|
|
1303
|
+
cleanup();
|
|
1304
|
+
process.exit(0);
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
readline.emitKeypressEvents(process.stdin);
|
|
1308
|
+
process.stdin.setRawMode(true);
|
|
1309
|
+
|
|
1310
|
+
let busy = false; // prevent keypress re-entry during install/uninstall/update
|
|
1311
|
+
|
|
1312
|
+
const keypressHandler = async (str, key) => {
|
|
1313
|
+
if (!key) return;
|
|
1314
|
+
// Q or Ctrl+C always exits immediately, even during operations
|
|
1315
|
+
if (str === "q" || str === "Q" || (key.ctrl && key.name === "c")) {
|
|
1316
|
+
cleanup();
|
|
1317
|
+
process.exit(0);
|
|
1318
|
+
}
|
|
1319
|
+
if (busy) return;
|
|
1320
|
+
|
|
1321
|
+
// ── Confirm uninstall mode ────────────────────────────────────────────────
|
|
1322
|
+
if (state.mode === MODE.CONFIRM_UNINSTALL) {
|
|
1323
|
+
if (str === "y" || str === "Y") {
|
|
1324
|
+
const p = PACKAGES[state.confirmTarget];
|
|
1325
|
+
state.mode = MODE.LIST;
|
|
1326
|
+
state.confirmTarget = null;
|
|
1327
|
+
busy = true;
|
|
1328
|
+
cleanup();
|
|
1329
|
+
console.log(
|
|
1330
|
+
"\n " +
|
|
1331
|
+
yellow("Uninstalling ") +
|
|
1332
|
+
bold(white(p.label)) +
|
|
1333
|
+
yellow("…\n"),
|
|
1334
|
+
);
|
|
1335
|
+
try {
|
|
1336
|
+
npmSync(["uninstall", "-g", p.name], { stdio: "inherit" });
|
|
1337
|
+
installedCache.set(p.name, false);
|
|
1338
|
+
state.selected.delete(PACKAGES.indexOf(p));
|
|
1339
|
+
console.log(
|
|
1340
|
+
"\n " +
|
|
1341
|
+
green("✓ ") +
|
|
1342
|
+
bold(green(p.label)) +
|
|
1343
|
+
green(" uninstalled.\n"),
|
|
1344
|
+
);
|
|
1345
|
+
} catch {
|
|
1346
|
+
console.log("\n " + red("✗ Failed to uninstall " + p.label) + "\n");
|
|
1347
|
+
}
|
|
1348
|
+
busy = false;
|
|
1349
|
+
reEnterMenu(state, render, prefetchUpdateCache, keypressHandler);
|
|
1350
|
+
} else {
|
|
1351
|
+
state.mode = MODE.LIST;
|
|
1352
|
+
state.confirmTarget = null;
|
|
1353
|
+
render(state);
|
|
1354
|
+
}
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// ── Normal mode ───────────────────────────────────────────────────────────
|
|
1359
|
+
|
|
1360
|
+
if (key.name === "up") {
|
|
1361
|
+
state.cursor = (state.cursor - 1 + PACKAGES.length) % PACKAGES.length;
|
|
1362
|
+
render(state);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
if (key.name === "down") {
|
|
1366
|
+
state.cursor = (state.cursor + 1) % PACKAGES.length;
|
|
1367
|
+
render(state);
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
if (str === " ") {
|
|
1372
|
+
if (state.selected.has(state.cursor)) state.selected.delete(state.cursor);
|
|
1373
|
+
else state.selected.add(state.cursor);
|
|
1374
|
+
render(state);
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
if (str === "a" || str === "A") {
|
|
1379
|
+
if (state.selected.size === PACKAGES.length) state.selected.clear();
|
|
1380
|
+
else PACKAGES.forEach((_, i) => state.selected.add(i));
|
|
1381
|
+
render(state);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// i = info
|
|
1386
|
+
if (str === "i" || str === "I") {
|
|
1387
|
+
const p = PACKAGES[state.cursor];
|
|
1388
|
+
const inst = isInstalled(p.name);
|
|
1389
|
+
const ver = inst ? getInstalledVersion(p.name) : null;
|
|
1390
|
+
setStatus(
|
|
1391
|
+
`${p.label} | ${p.hadiths} hadiths | ${p.author} | ${inst ? "v" + ver + " installed CLI: " + p.cmd + " --help" : "not installed"}`,
|
|
1392
|
+
4000,
|
|
1393
|
+
);
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// u = uninstall
|
|
1398
|
+
if (str === "u" || str === "U") {
|
|
1399
|
+
const targets =
|
|
1400
|
+
state.selected.size > 0 ? [...state.selected] : [state.cursor];
|
|
1401
|
+
const toRemove = targets.filter((i) => isInstalled(PACKAGES[i].name));
|
|
1402
|
+
if (!toRemove.length) {
|
|
1403
|
+
setStatus("No installed packages selected.");
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
state.mode = MODE.CONFIRM_UNINSTALL;
|
|
1407
|
+
state.confirmTarget = toRemove[0];
|
|
1408
|
+
render(state);
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// U = update (only packages with updates available)
|
|
1413
|
+
if (str === "U") {
|
|
1414
|
+
const targets =
|
|
1415
|
+
state.selected.size > 0 ? [...state.selected] : [state.cursor];
|
|
1416
|
+
const toUpdate = targets.filter((i) => {
|
|
1417
|
+
const p = PACKAGES[i];
|
|
1418
|
+
return isInstalled(p.name) && updateCache.get(p.name)?.hasUpdate;
|
|
1419
|
+
});
|
|
1420
|
+
if (!toUpdate.length) {
|
|
1421
|
+
setStatus(
|
|
1422
|
+
updateCacheReady
|
|
1423
|
+
? "All selected packages are up to date."
|
|
1424
|
+
: "Update info still loading — try again shortly.",
|
|
1425
|
+
);
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
busy = true;
|
|
1429
|
+
cleanup();
|
|
1430
|
+
const divW = Math.min(W() - 2, 72);
|
|
1431
|
+
const div2 = gray("═".repeat(divW));
|
|
1432
|
+
console.log("\n" + div2);
|
|
1433
|
+
console.log(
|
|
1434
|
+
bold(cyan(" Updating ")) +
|
|
1435
|
+
bold(yellow(String(toUpdate.length))) +
|
|
1436
|
+
bold(cyan(" package" + (toUpdate.length > 1 ? "s" : "") + "…")),
|
|
1437
|
+
);
|
|
1438
|
+
console.log(div2);
|
|
1439
|
+
for (let i = 0; i < toUpdate.length; i++) {
|
|
1440
|
+
const p = PACKAGES[toUpdate[i]];
|
|
1441
|
+
const uc = updateCache.get(p.name);
|
|
1442
|
+
console.log(
|
|
1443
|
+
"\n " +
|
|
1444
|
+
cyan("[" + (i + 1) + "/" + toUpdate.length + "]") +
|
|
1445
|
+
" " +
|
|
1446
|
+
bold(white(p.label)),
|
|
1447
|
+
);
|
|
1448
|
+
console.log(
|
|
1449
|
+
" " + dim(gray("v" + uc.current + " → v" + uc.latest)) + "\n",
|
|
1450
|
+
);
|
|
1451
|
+
await animateInstall(p.name);
|
|
1452
|
+
updateCache.set(p.name, {
|
|
1453
|
+
current: uc.latest,
|
|
1454
|
+
latest: uc.latest,
|
|
1455
|
+
hasUpdate: false,
|
|
1456
|
+
});
|
|
1457
|
+
console.log(
|
|
1458
|
+
" " +
|
|
1459
|
+
green("✓") +
|
|
1460
|
+
" " +
|
|
1461
|
+
bold(green(p.label)) +
|
|
1462
|
+
gray(" updated to v" + uc.latest),
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
console.log("\n" + div2 + "\n");
|
|
1466
|
+
busy = false;
|
|
1467
|
+
reEnterMenu(state, render, prefetchUpdateCache, keypressHandler);
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// enter = install
|
|
1472
|
+
if (key.name === "return") {
|
|
1473
|
+
const targets =
|
|
1474
|
+
state.selected.size > 0
|
|
1475
|
+
? [...state.selected].map((i) => PACKAGES[i])
|
|
1476
|
+
: [PACKAGES[state.cursor]];
|
|
1477
|
+
const toInstall = targets.filter((p) => !isInstalled(p.name));
|
|
1478
|
+
|
|
1479
|
+
if (!toInstall.length) {
|
|
1480
|
+
setStatus("All selected packages are already installed.");
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
busy = true;
|
|
1485
|
+
cleanup();
|
|
1486
|
+
const divW = Math.min(W() - 2, 72);
|
|
1487
|
+
const div = gray("─".repeat(divW));
|
|
1488
|
+
const div2 = gray("═".repeat(divW));
|
|
1489
|
+
|
|
1490
|
+
console.log("\n" + div2);
|
|
1491
|
+
console.log(
|
|
1492
|
+
bold(cyan(" Installing ")) +
|
|
1493
|
+
bold(yellow(String(toInstall.length))) +
|
|
1494
|
+
bold(cyan(" package" + (toInstall.length > 1 ? "s" : "") + "…")),
|
|
1495
|
+
);
|
|
1496
|
+
console.log(div2);
|
|
1497
|
+
|
|
1498
|
+
for (let i = 0; i < toInstall.length; i++) {
|
|
1499
|
+
const p = toInstall[i];
|
|
1500
|
+
console.log(
|
|
1501
|
+
"\n " +
|
|
1502
|
+
cyan("[" + (i + 1) + "/" + toInstall.length + "]") +
|
|
1503
|
+
" " +
|
|
1504
|
+
bold(white(p.label)),
|
|
1505
|
+
);
|
|
1506
|
+
console.log(" " + dim("npm install -g " + p.name) + "\n");
|
|
1507
|
+
await animateInstall(p.name);
|
|
1508
|
+
installedCache.set(p.name, true);
|
|
1509
|
+
console.log(
|
|
1510
|
+
" " + green("✓") + " " + bold(green(p.label)) + " installed",
|
|
1511
|
+
);
|
|
1512
|
+
// Show version from updateCache (fetched in background)
|
|
1513
|
+
const uc = updateCache.get(p.name);
|
|
1514
|
+
if (uc && uc.current) {
|
|
1515
|
+
console.log(
|
|
1516
|
+
" " +
|
|
1517
|
+
gray("Version: ") +
|
|
1518
|
+
cyan("v" + uc.current) +
|
|
1519
|
+
(uc.hasUpdate
|
|
1520
|
+
? " " +
|
|
1521
|
+
gray("Latest: ") +
|
|
1522
|
+
green("v" + uc.latest) +
|
|
1523
|
+
" " +
|
|
1524
|
+
yellow("↑ update available")
|
|
1525
|
+
: uc.latest
|
|
1526
|
+
? " " + dim(gray("(up to date)"))
|
|
1527
|
+
: ""),
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
console.log(" " + gray("Usage: ") + cyan(p.cmd + " --help"));
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
console.log("\n" + div2);
|
|
1534
|
+
console.log(
|
|
1535
|
+
" " +
|
|
1536
|
+
green("✓ All done! ") +
|
|
1537
|
+
bold(yellow(String(toInstall.length))) +
|
|
1538
|
+
" package" +
|
|
1539
|
+
(toInstall.length > 1 ? "s" : "") +
|
|
1540
|
+
" installed globally.",
|
|
1541
|
+
);
|
|
1542
|
+
console.log("");
|
|
1543
|
+
toInstall.forEach((p) =>
|
|
1544
|
+
console.log(
|
|
1545
|
+
" " +
|
|
1546
|
+
cyan("▸") +
|
|
1547
|
+
" " +
|
|
1548
|
+
bold(p.cmd) +
|
|
1549
|
+
gray(" --help") +
|
|
1550
|
+
" " +
|
|
1551
|
+
dim(p.label),
|
|
1552
|
+
),
|
|
1553
|
+
);
|
|
1554
|
+
|
|
1555
|
+
// Personalized next steps
|
|
1556
|
+
const installed = PACKAGES.filter((p) => isInstalled(p.name));
|
|
1557
|
+
if (installed.length > 1) {
|
|
1558
|
+
console.log(
|
|
1559
|
+
"\n " +
|
|
1560
|
+
dim("Tip: run ") +
|
|
1561
|
+
bold("sunnah --react") +
|
|
1562
|
+
dim(" to generate a unified React hook for all your books"),
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// Auto-return to menu after install
|
|
1567
|
+
await sleep(800);
|
|
1568
|
+
busy = false;
|
|
1569
|
+
reEnterMenu(state, render, prefetchUpdateCache, keypressHandler);
|
|
1570
|
+
}
|
|
1571
|
+
};
|
|
1572
|
+
process.stdin.on("keypress", keypressHandler);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
function sleep(ms) {
|
|
1576
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// ── Re-enter the interactive menu cleanly ───────────────────────────────────
|
|
1580
|
+
function reEnterMenu(state, render, prefetchUpdateCache, keypressHandler) {
|
|
1581
|
+
installedCache = buildInstalledCache();
|
|
1582
|
+
updateCacheReady = false;
|
|
1583
|
+
updateCache.clear();
|
|
1584
|
+
process.stdin.removeAllListeners("keypress");
|
|
1585
|
+
process.stdin.pause();
|
|
1586
|
+
enterAltScreen();
|
|
1587
|
+
hideCursor();
|
|
1588
|
+
render(state);
|
|
1589
|
+
prefetchUpdateCache().then(() => render(state));
|
|
1590
|
+
readline.emitKeypressEvents(process.stdin);
|
|
1591
|
+
try {
|
|
1592
|
+
process.stdin.setRawMode(true);
|
|
1593
|
+
} catch (_e) {}
|
|
1594
|
+
process.stdin.resume();
|
|
1595
|
+
// Re-attach the keypress handler — this is what was missing
|
|
1596
|
+
process.stdin.on("keypress", keypressHandler);
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
main().catch((err) => {
|
|
1600
|
+
showCursor();
|
|
1601
|
+
leaveAltScreen();
|
|
1602
|
+
console.error(red("\n ✗ " + err.message + "\n"));
|
|
1603
|
+
process.exit(1);
|
|
1604
|
+
});
|