sunnah 1.1.4 → 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 +978 -204
- 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",
|
|
@@ -41,6 +64,7 @@ const gray = (t) => clr(c.gray, t);
|
|
|
41
64
|
const red = (t) => clr(c.red, t);
|
|
42
65
|
const dim = (t) => clr(c.dim, t);
|
|
43
66
|
const white = (t) => clr(c.white, t);
|
|
67
|
+
const blue = (t) => clr(c.blue, t);
|
|
44
68
|
|
|
45
69
|
// ── Available packages ────────────────────────────────────────────────────────
|
|
46
70
|
const PACKAGES = [
|
|
@@ -51,6 +75,7 @@ const PACKAGES = [
|
|
|
51
75
|
desc: "The most authentic collection of hadith, widely regarded as the most sahih after the Quran.",
|
|
52
76
|
hadiths: "7,563",
|
|
53
77
|
cmd: "bukhari",
|
|
78
|
+
hook: "useBukhari",
|
|
54
79
|
},
|
|
55
80
|
{
|
|
56
81
|
name: "sahih-muslim",
|
|
@@ -59,6 +84,7 @@ const PACKAGES = [
|
|
|
59
84
|
desc: "Second most authentic hadith collection, known for its strict methodology and chain verification.",
|
|
60
85
|
hadiths: "7,470",
|
|
61
86
|
cmd: "muslim",
|
|
87
|
+
hook: "useMuslim",
|
|
62
88
|
},
|
|
63
89
|
{
|
|
64
90
|
name: "sunan-abi-dawud",
|
|
@@ -67,6 +93,7 @@ const PACKAGES = [
|
|
|
67
93
|
desc: "One of the six canonical hadith collections, focused on legal rulings and jurisprudence.",
|
|
68
94
|
hadiths: "5,274",
|
|
69
95
|
cmd: "dawud",
|
|
96
|
+
hook: "useDawud",
|
|
70
97
|
},
|
|
71
98
|
{
|
|
72
99
|
name: "jami-al-tirmidhi",
|
|
@@ -75,29 +102,47 @@ const PACKAGES = [
|
|
|
75
102
|
desc: "Part of the six major hadith collections, unique for grading each hadith's authenticity.",
|
|
76
103
|
hadiths: "3,956",
|
|
77
104
|
cmd: "tirmidhi",
|
|
105
|
+
hook: "useTirmidhi",
|
|
78
106
|
},
|
|
79
107
|
];
|
|
80
108
|
|
|
81
|
-
//
|
|
109
|
+
// cmd → package lookup
|
|
110
|
+
const CMD_MAP = Object.fromEntries(PACKAGES.map((p) => [p.cmd, p]));
|
|
111
|
+
|
|
112
|
+
// ── Terminal: alternate screen buffer = no scroll ─────────────────────────────
|
|
82
113
|
const W = () => process.stdout.columns || 80;
|
|
114
|
+
const H = () => process.stdout.rows || 24;
|
|
83
115
|
|
|
84
|
-
function
|
|
85
|
-
process.stdout.write("\
|
|
116
|
+
function enterAltScreen() {
|
|
117
|
+
process.stdout.write("\x1b[?1049h");
|
|
86
118
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
119
|
+
function leaveAltScreen() {
|
|
120
|
+
process.stdout.write("\x1b[?1049l");
|
|
121
|
+
}
|
|
122
|
+
function clearScreen() {
|
|
123
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
124
|
+
}
|
|
125
|
+
function moveTo(r, col) {
|
|
126
|
+
process.stdout.write(`\x1b[${r};${col}H`);
|
|
90
127
|
}
|
|
91
|
-
|
|
92
128
|
function hideCursor() {
|
|
93
129
|
process.stdout.write("\x1b[?25l");
|
|
94
130
|
}
|
|
95
131
|
function showCursor() {
|
|
96
132
|
process.stdout.write("\x1b[?25h");
|
|
97
133
|
}
|
|
134
|
+
function clearToEOL() {
|
|
135
|
+
process.stdout.write("\x1b[K");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function writeLine(row, text) {
|
|
139
|
+
moveTo(row, 1);
|
|
140
|
+
clearToEOL();
|
|
141
|
+
process.stdout.write(text);
|
|
142
|
+
}
|
|
98
143
|
|
|
99
144
|
// ── Progress bar ──────────────────────────────────────────────────────────────
|
|
100
|
-
function drawBar(label, percent, barWidth =
|
|
145
|
+
function drawBar(label, percent, barWidth = 40) {
|
|
101
146
|
const filled = Math.round((percent / 100) * barWidth);
|
|
102
147
|
const empty = barWidth - filled;
|
|
103
148
|
const bar =
|
|
@@ -106,75 +151,62 @@ function drawBar(label, percent, barWidth = 38) {
|
|
|
106
151
|
return ` ${bar} ${pct} ${dim(label)}`;
|
|
107
152
|
}
|
|
108
153
|
|
|
109
|
-
// ──
|
|
154
|
+
// ── Animate install with real npm running behind ──────────────────────────────
|
|
110
155
|
function animateInstall(pkgName) {
|
|
111
156
|
return new Promise((resolve) => {
|
|
112
157
|
const stages = [
|
|
113
|
-
{ label: "Resolving packages…", end: 12, ms:
|
|
114
|
-
{ label: "Fetching metadata…", end:
|
|
115
|
-
{ label: "Downloading tarball…", end:
|
|
116
|
-
{ label: "Extracting files…", end:
|
|
117
|
-
{ label: "Linking binaries…", end: 98, ms:
|
|
158
|
+
{ label: "Resolving packages…", end: 12, ms: 80 },
|
|
159
|
+
{ label: "Fetching metadata…", end: 30, ms: 60 },
|
|
160
|
+
{ label: "Downloading tarball…", end: 75, ms: 22 },
|
|
161
|
+
{ label: "Extracting files…", end: 90, ms: 50 },
|
|
162
|
+
{ label: "Linking binaries…", end: 98, ms: 80 },
|
|
118
163
|
];
|
|
119
|
-
|
|
120
164
|
let percent = 0;
|
|
121
165
|
let stageIdx = 0;
|
|
122
166
|
let npmDone = false;
|
|
123
167
|
|
|
124
|
-
process.stdout.write(drawBar(stages[0].label, 0)
|
|
168
|
+
process.stdout.write(drawBar(stages[0].label, 0));
|
|
125
169
|
|
|
126
170
|
const tick = () => {
|
|
127
171
|
const stage = stages[stageIdx];
|
|
128
172
|
if (!stage) return;
|
|
129
|
-
|
|
130
173
|
const prevEnd = stageIdx > 0 ? stages[stageIdx - 1].end : 0;
|
|
131
|
-
const step = (stage.end - prevEnd) /
|
|
174
|
+
const step = (stage.end - prevEnd) / 24;
|
|
132
175
|
percent = Math.min(percent + step, stage.end);
|
|
133
|
-
|
|
134
|
-
process.stdout.write("\r\x1b[K");
|
|
135
|
-
process.stdout.write(drawBar(stage.label, percent));
|
|
136
|
-
|
|
176
|
+
process.stdout.write("\r\x1b[K" + drawBar(stage.label, percent));
|
|
137
177
|
if (percent >= stage.end) {
|
|
138
178
|
stageIdx++;
|
|
139
179
|
if (stageIdx >= stages.length) {
|
|
140
180
|
const poll = setInterval(() => {
|
|
141
181
|
if (npmDone) {
|
|
142
182
|
clearInterval(poll);
|
|
143
|
-
process.stdout.write(
|
|
144
|
-
|
|
145
|
-
|
|
183
|
+
process.stdout.write(
|
|
184
|
+
"\r\x1b[K" + drawBar("Complete!", 100) + "\n",
|
|
185
|
+
);
|
|
146
186
|
resolve();
|
|
147
187
|
}
|
|
148
188
|
}, 80);
|
|
149
189
|
return;
|
|
150
190
|
}
|
|
151
191
|
}
|
|
152
|
-
|
|
153
192
|
setTimeout(tick, stages[stageIdx]?.ms ?? 50);
|
|
154
193
|
};
|
|
155
194
|
|
|
156
195
|
setTimeout(tick, stages[0].ms);
|
|
157
|
-
|
|
158
|
-
// spawn npm.cmd on Windows, npm on Unix
|
|
159
196
|
const proc = spawn(NPM, ["install", "-g", pkgName], {
|
|
160
197
|
stdio: ["ignore", "pipe", "pipe"],
|
|
161
198
|
shell: isWin,
|
|
162
199
|
});
|
|
163
|
-
|
|
164
200
|
proc.on("error", () => {
|
|
165
|
-
// resolve anyway so the UI doesn't hang on spawn failure
|
|
166
201
|
npmDone = true;
|
|
167
202
|
});
|
|
168
|
-
|
|
169
203
|
proc.on("close", () => {
|
|
170
204
|
npmDone = true;
|
|
171
205
|
});
|
|
172
206
|
});
|
|
173
207
|
}
|
|
174
208
|
|
|
175
|
-
// ──
|
|
176
|
-
// Calling npm on every render was freezing the terminal. We run one
|
|
177
|
-
// 'npm list -g' at startup, parse the output, and never shell out again.
|
|
209
|
+
// ── Installed cache — ONE npm call at startup, Map lookup during render ───────
|
|
178
210
|
function buildInstalledCache() {
|
|
179
211
|
const cache = new Map();
|
|
180
212
|
let out = "";
|
|
@@ -182,7 +214,7 @@ function buildInstalledCache() {
|
|
|
182
214
|
out = execSync(`${NPM} list -g --depth=0`, {
|
|
183
215
|
encoding: "utf8",
|
|
184
216
|
shell: isWin,
|
|
185
|
-
timeout:
|
|
217
|
+
timeout: 10000,
|
|
186
218
|
stdio: ["ignore", "pipe", "ignore"],
|
|
187
219
|
});
|
|
188
220
|
} catch (e) {
|
|
@@ -194,169 +226,800 @@ function buildInstalledCache() {
|
|
|
194
226
|
return cache;
|
|
195
227
|
}
|
|
196
228
|
|
|
229
|
+
function getLatestVersion(name) {
|
|
230
|
+
try {
|
|
231
|
+
return execSync(`${NPM} show ${name} version`, {
|
|
232
|
+
encoding: "utf8",
|
|
233
|
+
shell: isWin,
|
|
234
|
+
timeout: 8000,
|
|
235
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
236
|
+
}).trim();
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getInstalledVersion(name) {
|
|
243
|
+
try {
|
|
244
|
+
const out = execSync(`${NPM} list -g ${name} --depth=0`, {
|
|
245
|
+
encoding: "utf8",
|
|
246
|
+
shell: isWin,
|
|
247
|
+
timeout: 8000,
|
|
248
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
249
|
+
});
|
|
250
|
+
const match = out.match(new RegExp(name + "@([\\d.]+)"));
|
|
251
|
+
return match ? match[1] : null;
|
|
252
|
+
} catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
197
257
|
let installedCache = new Map();
|
|
198
258
|
const isInstalled = (name) => installedCache.get(name) ?? false;
|
|
199
259
|
|
|
200
|
-
// ──
|
|
201
|
-
|
|
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
|
+
|
|
503
|
+
// ── Modes ─────────────────────────────────────────────────────────────────────
|
|
504
|
+
const MODE = { LIST: "list", CONFIRM_UNINSTALL: "confirm_uninstall" };
|
|
505
|
+
|
|
506
|
+
// ── Interactive render ────────────────────────────────────────────────────────
|
|
507
|
+
function render(state) {
|
|
508
|
+
const { cursor, selected, mode, confirmTarget, statusMsg } = state;
|
|
509
|
+
const divW = Math.min(W() - 2, 72);
|
|
510
|
+
const div = gray("─".repeat(divW));
|
|
511
|
+
const div2 = gray("═".repeat(divW));
|
|
202
512
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const div2 = gray("═".repeat(DIV_W()));
|
|
206
|
-
const lines = [];
|
|
513
|
+
clearScreen();
|
|
514
|
+
let row = 1;
|
|
207
515
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
516
|
+
writeLine(row++, div2);
|
|
517
|
+
writeLine(
|
|
518
|
+
row++,
|
|
211
519
|
bold(cyan(" 📚 Sunnah Package Manager")) + gray(" v" + pkg.version),
|
|
212
520
|
);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
gray("
|
|
218
|
-
|
|
521
|
+
writeLine(
|
|
522
|
+
row++,
|
|
523
|
+
gray(" ↑↓") +
|
|
524
|
+
" nav " +
|
|
525
|
+
gray("space") +
|
|
526
|
+
" select " +
|
|
527
|
+
gray("a") +
|
|
528
|
+
" all " +
|
|
529
|
+
gray("i") +
|
|
530
|
+
" info " +
|
|
531
|
+
gray("u") +
|
|
532
|
+
" uninstall " +
|
|
533
|
+
gray("enter") +
|
|
534
|
+
" install " +
|
|
535
|
+
gray("q") +
|
|
536
|
+
" quit",
|
|
219
537
|
);
|
|
220
|
-
|
|
221
|
-
|
|
538
|
+
writeLine(row++, div2);
|
|
539
|
+
row++;
|
|
222
540
|
|
|
223
541
|
PACKAGES.forEach((p, i) => {
|
|
224
542
|
const isCursor = i === cursor;
|
|
225
|
-
const
|
|
226
|
-
const
|
|
543
|
+
const isSel = selected.has(i);
|
|
544
|
+
const inst = isInstalled(p.name);
|
|
227
545
|
|
|
228
|
-
const checkbox =
|
|
546
|
+
const checkbox = isSel ? green("[✓]") : gray("[ ]");
|
|
229
547
|
const arrow = isCursor ? cyan("▶") : " ";
|
|
230
548
|
const label = isCursor
|
|
231
549
|
? bold(white(p.label))
|
|
232
|
-
:
|
|
550
|
+
: isSel
|
|
233
551
|
? green(p.label)
|
|
234
552
|
: white(p.label);
|
|
235
|
-
const badge =
|
|
553
|
+
const badge = inst
|
|
554
|
+
? dim(green(" ● installed"))
|
|
555
|
+
: dim(gray(" ○ not installed"));
|
|
236
556
|
|
|
237
|
-
|
|
557
|
+
writeLine(row++, ` ${arrow} ${checkbox} ${label}${badge}`);
|
|
238
558
|
|
|
239
559
|
if (isCursor) {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
560
|
+
writeLine(row++, ` ${dim(p.author)}`);
|
|
561
|
+
writeLine(row++, ` ${gray(p.desc)}`);
|
|
562
|
+
writeLine(
|
|
563
|
+
row++,
|
|
564
|
+
` ${gray("Hadiths: ")}${yellow(p.hadiths)}` +
|
|
565
|
+
` ${gray("CLI: ")}${cyan(p.cmd + " --help")}` +
|
|
566
|
+
(inst ? ` ${gray("try: ")}${cyan(p.cmd + " --random")}` : ""),
|
|
244
567
|
);
|
|
245
|
-
|
|
568
|
+
row++;
|
|
246
569
|
}
|
|
247
570
|
});
|
|
248
571
|
|
|
249
|
-
|
|
250
|
-
|
|
572
|
+
row++;
|
|
573
|
+
writeLine(row++, div);
|
|
251
574
|
|
|
252
|
-
|
|
253
|
-
|
|
575
|
+
if (statusMsg) {
|
|
576
|
+
writeLine(row++, ` ${yellow("⚠")} ${yellow(statusMsg)}`);
|
|
577
|
+
} else if (selected.size > 0) {
|
|
254
578
|
const names = [...selected].map((i) => cyan(PACKAGES[i].name)).join(", ");
|
|
255
|
-
|
|
579
|
+
writeLine(
|
|
580
|
+
row++,
|
|
581
|
+
` ${green("●")} ${bold(String(selected.size))} selected: ${names}`,
|
|
582
|
+
);
|
|
583
|
+
writeLine(row++, ` ${dim("enter = install u = uninstall i = info")}`);
|
|
256
584
|
} else {
|
|
257
|
-
|
|
258
|
-
|
|
585
|
+
writeLine(
|
|
586
|
+
row++,
|
|
587
|
+
` ${gray("Nothing selected — press space to select, enter to install focused")}`,
|
|
259
588
|
);
|
|
260
589
|
}
|
|
261
|
-
lines.push(div);
|
|
262
|
-
lines.push("");
|
|
263
590
|
|
|
264
|
-
|
|
265
|
-
}
|
|
591
|
+
writeLine(row++, div);
|
|
266
592
|
|
|
267
|
-
|
|
268
|
-
|
|
593
|
+
if (mode === MODE.CONFIRM_UNINSTALL && confirmTarget !== null) {
|
|
594
|
+
const p = PACKAGES[confirmTarget];
|
|
595
|
+
row++;
|
|
596
|
+
writeLine(
|
|
597
|
+
row++,
|
|
598
|
+
` ${red("⚠ Uninstall ")}${bold(white(p.label))}${red("?")}`,
|
|
599
|
+
);
|
|
600
|
+
writeLine(
|
|
601
|
+
row++,
|
|
602
|
+
` ${green("y")} ${gray("confirm")} ${red("n")} ${gray("cancel")}`,
|
|
603
|
+
);
|
|
604
|
+
}
|
|
269
605
|
}
|
|
270
606
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
607
|
+
// ── --list ────────────────────────────────────────────────────────────────────
|
|
608
|
+
function cmdList() {
|
|
609
|
+
installedCache = buildInstalledCache();
|
|
610
|
+
const div = gray("─".repeat(60));
|
|
611
|
+
console.log("\n" + div);
|
|
612
|
+
console.log(bold(cyan(" Available Sunnah Packages")));
|
|
613
|
+
console.log(div);
|
|
614
|
+
PACKAGES.forEach((p) => {
|
|
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}`);
|
|
621
|
+
console.log(` ${cyan("npm install -g " + p.name)}`);
|
|
622
|
+
console.log(` ${dim(p.desc)}`);
|
|
623
|
+
console.log(
|
|
624
|
+
` ${gray("Hadiths: ")}${yellow(p.hadiths)} ${gray("Author: ")}${magenta(p.author)}`,
|
|
625
|
+
);
|
|
626
|
+
if (inst) {
|
|
627
|
+
console.log(
|
|
628
|
+
` ${gray("CLI: ")}${cyan(p.cmd + " --help")} ${gray("React: ")}${cyan("sunnah --react " + p.cmd)}`,
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
console.log("\n" + div + "\n");
|
|
277
633
|
}
|
|
278
634
|
|
|
279
|
-
// ──
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const
|
|
635
|
+
// ── --update ──────────────────────────────────────────────────────────────────
|
|
636
|
+
function cmdUpdate() {
|
|
637
|
+
installedCache = buildInstalledCache();
|
|
638
|
+
const installed = PACKAGES.filter((p) => isInstalled(p.name));
|
|
639
|
+
if (!installed.length) {
|
|
640
|
+
console.log(
|
|
641
|
+
"\n " +
|
|
642
|
+
yellow("No sunnah packages installed. Run ") +
|
|
643
|
+
bold("sunnah") +
|
|
644
|
+
yellow(" to install.\n"),
|
|
645
|
+
);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const div = gray("─".repeat(60));
|
|
649
|
+
console.log("\n" + div);
|
|
650
|
+
console.log(bold(cyan(" Checking for updates…")));
|
|
651
|
+
console.log(div + "\n");
|
|
652
|
+
|
|
653
|
+
let hasUpdates = false;
|
|
654
|
+
for (const p of installed) {
|
|
655
|
+
const current = getInstalledVersion(p.name);
|
|
656
|
+
const latest = getLatestVersion(p.name);
|
|
657
|
+
if (!current || !latest) {
|
|
658
|
+
console.log(
|
|
659
|
+
` ${yellow("?")} ${bold(p.label)} ${gray("(could not check)")}`,
|
|
660
|
+
);
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
if (current === latest) {
|
|
664
|
+
console.log(
|
|
665
|
+
` ${green("✓")} ${bold(p.label)} ${gray(current + " — up to date")}`,
|
|
666
|
+
);
|
|
667
|
+
} else {
|
|
668
|
+
hasUpdates = true;
|
|
669
|
+
console.log(
|
|
670
|
+
` ${yellow("↑")} ${bold(p.label)} ${gray(current)} ${gray("→")} ${green(latest)}`,
|
|
671
|
+
);
|
|
672
|
+
console.log(` ${dim("sunnah install " + p.cmd)}`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
283
675
|
|
|
284
|
-
|
|
285
|
-
if (flags.some((f) => f === "-v" || f === "--version")) {
|
|
286
|
-
console.log("");
|
|
287
|
-
console.log(" " + bold(cyan("sunnah")) + gray(" v" + pkg.version));
|
|
676
|
+
if (hasUpdates) {
|
|
288
677
|
console.log(
|
|
289
|
-
" " +
|
|
678
|
+
"\n " +
|
|
679
|
+
yellow("Updates available. Run ") +
|
|
680
|
+
bold("sunnah install <name>") +
|
|
681
|
+
yellow(" to update."),
|
|
290
682
|
);
|
|
291
|
-
console.log("");
|
|
292
|
-
process.exit(0);
|
|
293
683
|
}
|
|
684
|
+
console.log("\n" + div + "\n");
|
|
685
|
+
}
|
|
294
686
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
console.
|
|
299
|
-
|
|
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
|
+
);
|
|
300
695
|
console.log(
|
|
301
|
-
|
|
696
|
+
" Available: " + PACKAGES.map((p) => cyan(p.cmd)).join(", ") + "\n",
|
|
302
697
|
);
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
|
|
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];
|
|
306
733
|
console.log(
|
|
307
|
-
"
|
|
308
|
-
cyan("
|
|
309
|
-
|
|
734
|
+
"\n " +
|
|
735
|
+
cyan("[" + (i + 1) + "/" + toInstall.length + "]") +
|
|
736
|
+
" " +
|
|
737
|
+
bold(white(p.label)),
|
|
310
738
|
);
|
|
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
|
+
),
|
|
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) {
|
|
311
822
|
console.log(
|
|
312
|
-
"
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
823
|
+
" " +
|
|
824
|
+
gray("Your collection : ") +
|
|
825
|
+
installed.map((p) => cyan(p.label)).join(gray(", ")),
|
|
826
|
+
);
|
|
827
|
+
const totalHadiths = installed.reduce(
|
|
828
|
+
(acc, p) => acc + parseInt(p.hadiths.replace(/,/g, "")),
|
|
829
|
+
0,
|
|
316
830
|
);
|
|
317
831
|
console.log(
|
|
318
|
-
"
|
|
832
|
+
" " +
|
|
833
|
+
gray("Total hadiths : ") +
|
|
834
|
+
bold(yellow(totalHadiths.toLocaleString())),
|
|
319
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) => {
|
|
320
904
|
console.log(
|
|
321
905
|
" " +
|
|
322
|
-
cyan(
|
|
323
|
-
|
|
324
|
-
|
|
906
|
+
cyan(p.cmd.padEnd(12)) +
|
|
907
|
+
gray(p.name.padEnd(24)) +
|
|
908
|
+
yellow(p.hadiths + " hadiths"),
|
|
325
909
|
);
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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();
|
|
955
|
+
process.exit(0);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// sunnah -h / --help
|
|
959
|
+
if (flags.some((f) => f === "-h" || f === "--help")) {
|
|
960
|
+
cmdHelp();
|
|
336
961
|
process.exit(0);
|
|
337
962
|
}
|
|
338
963
|
|
|
339
|
-
// --list
|
|
964
|
+
// sunnah --list / -l
|
|
340
965
|
if (flags.some((f) => f === "--list" || f === "-l")) {
|
|
966
|
+
cmdList();
|
|
967
|
+
process.exit(0);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// sunnah --update
|
|
971
|
+
if (flags.some((f) => f === "--update")) {
|
|
972
|
+
cmdUpdate();
|
|
973
|
+
process.exit(0);
|
|
974
|
+
}
|
|
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")) {
|
|
341
990
|
installedCache = buildInstalledCache();
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
console.log(` ${cyan("npm install -g " + p.name)}`);
|
|
352
|
-
console.log(` ${dim(p.desc)}`);
|
|
353
|
-
console.log(
|
|
354
|
-
` ${gray("Hadiths: ")}${yellow(p.hadiths)} ${gray("Author: ")}${magenta(p.author)}`,
|
|
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()],
|
|
355
1000
|
);
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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);
|
|
360
1023
|
process.exit(0);
|
|
361
1024
|
}
|
|
362
1025
|
|
|
@@ -366,32 +1029,47 @@ async function main() {
|
|
|
366
1029
|
process.exit(1);
|
|
367
1030
|
}
|
|
368
1031
|
|
|
369
|
-
// Build
|
|
370
|
-
process.stdout.write(
|
|
371
|
-
"\n " + "\x1b[90m" + "Checking installed packages…" + "\x1b[0m",
|
|
372
|
-
);
|
|
1032
|
+
// Build cache
|
|
1033
|
+
process.stdout.write("\n " + gray("Checking installed packages…"));
|
|
373
1034
|
installedCache = buildInstalledCache();
|
|
374
1035
|
process.stdout.write("\r\x1b[K");
|
|
375
1036
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
1037
|
+
// Load persisted selection
|
|
1038
|
+
const persisted = loadPersistedState();
|
|
1039
|
+
const lastSelected = new Set(
|
|
1040
|
+
(persisted.lastSelected || []).filter((i) => i < PACKAGES.length),
|
|
1041
|
+
);
|
|
379
1042
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
let prevCount = 0;
|
|
1043
|
+
enterAltScreen();
|
|
1044
|
+
hideCursor();
|
|
383
1045
|
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
1046
|
+
const state = {
|
|
1047
|
+
cursor: 0,
|
|
1048
|
+
selected: lastSelected, // pre-check last session's selections
|
|
1049
|
+
mode: MODE.LIST,
|
|
1050
|
+
confirmTarget: null,
|
|
1051
|
+
statusMsg: "",
|
|
389
1052
|
};
|
|
390
1053
|
|
|
391
|
-
|
|
1054
|
+
let statusTimer = null;
|
|
1055
|
+
|
|
1056
|
+
function setStatus(msg, ms = 2500) {
|
|
1057
|
+
state.statusMsg = msg;
|
|
1058
|
+
render(state);
|
|
1059
|
+
if (statusTimer) clearTimeout(statusTimer);
|
|
1060
|
+
statusTimer = setTimeout(() => {
|
|
1061
|
+
state.statusMsg = "";
|
|
1062
|
+
render(state);
|
|
1063
|
+
}, ms);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
render(state);
|
|
392
1067
|
|
|
393
1068
|
const cleanup = () => {
|
|
1069
|
+
// Persist current selection before exiting
|
|
1070
|
+
savePersistedState({ lastSelected: [...state.selected] });
|
|
394
1071
|
showCursor();
|
|
1072
|
+
leaveAltScreen();
|
|
395
1073
|
try {
|
|
396
1074
|
process.stdin.setRawMode(false);
|
|
397
1075
|
} catch {}
|
|
@@ -400,112 +1078,208 @@ async function main() {
|
|
|
400
1078
|
|
|
401
1079
|
process.on("SIGINT", () => {
|
|
402
1080
|
cleanup();
|
|
403
|
-
console.log("");
|
|
404
1081
|
process.exit(0);
|
|
405
1082
|
});
|
|
406
1083
|
|
|
1084
|
+
readline.emitKeypressEvents(process.stdin);
|
|
1085
|
+
process.stdin.setRawMode(true);
|
|
1086
|
+
|
|
407
1087
|
process.stdin.on("keypress", async (str, key) => {
|
|
408
1088
|
if (!key) return;
|
|
409
1089
|
|
|
410
|
-
//
|
|
1090
|
+
// ── Confirm uninstall mode ────────────────────────────────────────────────
|
|
1091
|
+
if (state.mode === MODE.CONFIRM_UNINSTALL) {
|
|
1092
|
+
if (str === "y" || str === "Y") {
|
|
1093
|
+
const p = PACKAGES[state.confirmTarget];
|
|
1094
|
+
state.mode = MODE.LIST;
|
|
1095
|
+
state.confirmTarget = null;
|
|
1096
|
+
cleanup();
|
|
1097
|
+
console.log(
|
|
1098
|
+
"\n " +
|
|
1099
|
+
yellow("Uninstalling ") +
|
|
1100
|
+
bold(white(p.label)) +
|
|
1101
|
+
yellow("…\n"),
|
|
1102
|
+
);
|
|
1103
|
+
try {
|
|
1104
|
+
execSync(`${NPM} uninstall -g ${p.name}`, {
|
|
1105
|
+
stdio: "inherit",
|
|
1106
|
+
shell: isWin,
|
|
1107
|
+
});
|
|
1108
|
+
installedCache.set(p.name, false);
|
|
1109
|
+
state.selected.delete(PACKAGES.indexOf(p));
|
|
1110
|
+
console.log(
|
|
1111
|
+
"\n " +
|
|
1112
|
+
green("✓ ") +
|
|
1113
|
+
bold(green(p.label)) +
|
|
1114
|
+
green(" uninstalled.\n"),
|
|
1115
|
+
);
|
|
1116
|
+
} catch {
|
|
1117
|
+
console.log("\n " + red("✗ Failed to uninstall " + p.label) + "\n");
|
|
1118
|
+
}
|
|
1119
|
+
await sleep(1200);
|
|
1120
|
+
enterAltScreen();
|
|
1121
|
+
hideCursor();
|
|
1122
|
+
readline.emitKeypressEvents(process.stdin);
|
|
1123
|
+
process.stdin.setRawMode(true);
|
|
1124
|
+
render(state);
|
|
1125
|
+
} else {
|
|
1126
|
+
state.mode = MODE.LIST;
|
|
1127
|
+
state.confirmTarget = null;
|
|
1128
|
+
render(state);
|
|
1129
|
+
}
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// ── Normal mode ───────────────────────────────────────────────────────────
|
|
1134
|
+
|
|
411
1135
|
if (key.name === "q" || (key.ctrl && key.name === "c")) {
|
|
412
1136
|
cleanup();
|
|
413
|
-
if (prevCount > 0) eraseLines(prevCount);
|
|
414
1137
|
console.log("\n " + gray("Goodbye.\n"));
|
|
415
1138
|
process.exit(0);
|
|
416
1139
|
}
|
|
417
1140
|
|
|
418
|
-
// Navigate
|
|
419
1141
|
if (key.name === "up") {
|
|
420
|
-
cursor = (cursor - 1 + PACKAGES.length) % PACKAGES.length;
|
|
421
|
-
|
|
1142
|
+
state.cursor = (state.cursor - 1 + PACKAGES.length) % PACKAGES.length;
|
|
1143
|
+
render(state);
|
|
422
1144
|
return;
|
|
423
1145
|
}
|
|
424
1146
|
if (key.name === "down") {
|
|
425
|
-
cursor = (cursor + 1) % PACKAGES.length;
|
|
426
|
-
|
|
1147
|
+
state.cursor = (state.cursor + 1) % PACKAGES.length;
|
|
1148
|
+
render(state);
|
|
427
1149
|
return;
|
|
428
1150
|
}
|
|
429
1151
|
|
|
430
|
-
// Toggle selection
|
|
431
1152
|
if (str === " ") {
|
|
432
|
-
if (selected.has(cursor)) selected.delete(cursor);
|
|
433
|
-
else selected.add(cursor);
|
|
434
|
-
|
|
1153
|
+
if (state.selected.has(state.cursor)) state.selected.delete(state.cursor);
|
|
1154
|
+
else state.selected.add(state.cursor);
|
|
1155
|
+
render(state);
|
|
435
1156
|
return;
|
|
436
1157
|
}
|
|
437
1158
|
|
|
438
|
-
// Toggle all
|
|
439
1159
|
if (str === "a" || str === "A") {
|
|
440
|
-
if (selected.size === PACKAGES.length) selected.clear();
|
|
441
|
-
else PACKAGES.forEach((_, i) => selected.add(i));
|
|
442
|
-
|
|
1160
|
+
if (state.selected.size === PACKAGES.length) state.selected.clear();
|
|
1161
|
+
else PACKAGES.forEach((_, i) => state.selected.add(i));
|
|
1162
|
+
render(state);
|
|
443
1163
|
return;
|
|
444
1164
|
}
|
|
445
1165
|
|
|
446
|
-
//
|
|
1166
|
+
// i = info
|
|
1167
|
+
if (str === "i" || str === "I") {
|
|
1168
|
+
const p = PACKAGES[state.cursor];
|
|
1169
|
+
const inst = isInstalled(p.name);
|
|
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
|
+
);
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// u = uninstall
|
|
1179
|
+
if (str === "u" || str === "U") {
|
|
1180
|
+
const targets =
|
|
1181
|
+
state.selected.size > 0 ? [...state.selected] : [state.cursor];
|
|
1182
|
+
const toRemove = targets.filter((i) => isInstalled(PACKAGES[i].name));
|
|
1183
|
+
if (!toRemove.length) {
|
|
1184
|
+
setStatus("No installed packages selected.");
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
state.mode = MODE.CONFIRM_UNINSTALL;
|
|
1188
|
+
state.confirmTarget = toRemove[0];
|
|
1189
|
+
render(state);
|
|
1190
|
+
return;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// enter = install
|
|
447
1194
|
if (key.name === "return") {
|
|
448
|
-
|
|
1195
|
+
const targets =
|
|
1196
|
+
state.selected.size > 0
|
|
1197
|
+
? [...state.selected].map((i) => PACKAGES[i])
|
|
1198
|
+
: [PACKAGES[state.cursor]];
|
|
1199
|
+
const toInstall = targets.filter((p) => !isInstalled(p.name));
|
|
1200
|
+
|
|
1201
|
+
if (!toInstall.length) {
|
|
1202
|
+
setStatus("All selected packages are already installed.");
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
449
1205
|
|
|
450
1206
|
cleanup();
|
|
451
|
-
|
|
1207
|
+
const divW = Math.min(W() - 2, 72);
|
|
1208
|
+
const div = gray("─".repeat(divW));
|
|
1209
|
+
const div2 = gray("═".repeat(divW));
|
|
452
1210
|
|
|
453
|
-
|
|
454
|
-
const total = toInstall.length;
|
|
455
|
-
const div = gray("─".repeat(DIV_W()));
|
|
456
|
-
const div2 = gray("═".repeat(DIV_W()));
|
|
457
|
-
|
|
458
|
-
console.log("");
|
|
459
|
-
console.log(div2);
|
|
1211
|
+
console.log("\n" + div2);
|
|
460
1212
|
console.log(
|
|
461
1213
|
bold(cyan(" Installing ")) +
|
|
462
|
-
bold(yellow(String(
|
|
463
|
-
bold(cyan(" package" + (
|
|
1214
|
+
bold(yellow(String(toInstall.length))) +
|
|
1215
|
+
bold(cyan(" package" + (toInstall.length > 1 ? "s" : "") + "…")),
|
|
464
1216
|
);
|
|
465
1217
|
console.log(div2);
|
|
466
1218
|
|
|
467
1219
|
for (let i = 0; i < toInstall.length; i++) {
|
|
468
1220
|
const p = toInstall[i];
|
|
469
|
-
console.log("");
|
|
470
1221
|
console.log(
|
|
471
|
-
|
|
1222
|
+
"\n " +
|
|
1223
|
+
cyan("[" + (i + 1) + "/" + toInstall.length + "]") +
|
|
1224
|
+
" " +
|
|
1225
|
+
bold(white(p.label)),
|
|
472
1226
|
);
|
|
473
|
-
console.log(
|
|
474
|
-
console.log("");
|
|
475
|
-
|
|
1227
|
+
console.log(" " + dim("npm install -g " + p.name) + "\n");
|
|
476
1228
|
await animateInstall(p.name);
|
|
477
|
-
installedCache.set(p.name, true);
|
|
478
|
-
|
|
1229
|
+
installedCache.set(p.name, true);
|
|
479
1230
|
console.log(
|
|
480
|
-
|
|
1231
|
+
" " + green("✓") + " " + bold(green(p.label)) + " installed",
|
|
481
1232
|
);
|
|
482
|
-
console.log(
|
|
1233
|
+
console.log(" " + gray("Usage: ") + cyan(p.cmd + " --help"));
|
|
483
1234
|
}
|
|
484
1235
|
|
|
485
|
-
console.log("");
|
|
486
|
-
console.log(div2);
|
|
1236
|
+
console.log("\n" + div2);
|
|
487
1237
|
console.log(
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
1238
|
+
" " +
|
|
1239
|
+
green("✓ All done! ") +
|
|
1240
|
+
bold(yellow(String(toInstall.length))) +
|
|
1241
|
+
" package" +
|
|
1242
|
+
(toInstall.length > 1 ? "s" : "") +
|
|
1243
|
+
" installed globally.",
|
|
491
1244
|
);
|
|
492
1245
|
console.log("");
|
|
493
|
-
toInstall.forEach((p) =>
|
|
1246
|
+
toInstall.forEach((p) =>
|
|
1247
|
+
console.log(
|
|
1248
|
+
" " +
|
|
1249
|
+
cyan("▸") +
|
|
1250
|
+
" " +
|
|
1251
|
+
bold(p.cmd) +
|
|
1252
|
+
gray(" --help") +
|
|
1253
|
+
" " +
|
|
1254
|
+
dim(p.label),
|
|
1255
|
+
),
|
|
1256
|
+
);
|
|
1257
|
+
|
|
1258
|
+
// Personalized next steps
|
|
1259
|
+
const installed = PACKAGES.filter((p) => isInstalled(p.name));
|
|
1260
|
+
if (installed.length > 1) {
|
|
494
1261
|
console.log(
|
|
495
|
-
|
|
1262
|
+
"\n " +
|
|
1263
|
+
dim("Tip: run ") +
|
|
1264
|
+
bold("sunnah --react") +
|
|
1265
|
+
dim(" to generate a unified React hook for all your books"),
|
|
496
1266
|
);
|
|
497
|
-
}
|
|
498
|
-
console.log(div2);
|
|
499
|
-
console.log("");
|
|
1267
|
+
}
|
|
500
1268
|
|
|
1269
|
+
console.log(div2 + "\n");
|
|
501
1270
|
showCursor();
|
|
502
1271
|
process.exit(0);
|
|
503
1272
|
}
|
|
504
1273
|
});
|
|
505
1274
|
}
|
|
506
1275
|
|
|
1276
|
+
function sleep(ms) {
|
|
1277
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1278
|
+
}
|
|
1279
|
+
|
|
507
1280
|
main().catch((err) => {
|
|
508
1281
|
showCursor();
|
|
1282
|
+
leaveAltScreen();
|
|
509
1283
|
console.error(red("\n ✗ " + err.message + "\n"));
|
|
510
1284
|
process.exit(1);
|
|
511
1285
|
});
|