sunnah 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +258 -398
- package/bin/index.js +1022 -1604
- package/books/jami-al-tirmidhi/README.md +201 -0
- package/books/jami-al-tirmidhi/bin/index.js +165 -0
- package/books/jami-al-tirmidhi/examples/express/server.js +7 -0
- package/books/jami-al-tirmidhi/examples/node-commonjs/example.js +7 -0
- package/books/jami-al-tirmidhi/examples/node-esm/example.mjs +6 -0
- package/books/jami-al-tirmidhi/examples/react/HadithExample.jsx +17 -0
- package/books/jami-al-tirmidhi/package.json +58 -0
- package/books/jami-al-tirmidhi/src/index.cjs +52 -0
- package/books/jami-al-tirmidhi/src/index.js +35 -0
- package/books/jami-al-tirmidhi/src/index.node.js +18 -0
- package/books/jami-al-tirmidhi/types/index.d.ts +28 -0
- package/books/sahih-al-bukhari/README.md +551 -0
- package/books/sahih-al-bukhari/bin/index.js +306 -0
- package/books/sahih-al-bukhari/data/bukhari.json.gz +0 -0
- package/books/sahih-al-bukhari/examples/express/server.js +49 -0
- package/books/sahih-al-bukhari/examples/node-commonjs/example.js +21 -0
- package/books/sahih-al-bukhari/examples/node-esm/example.mjs +24 -0
- package/books/sahih-al-bukhari/examples/react/HadithExample.jsx +73 -0
- package/books/sahih-al-bukhari/package.json +54 -0
- package/books/sahih-al-bukhari/src/index.cjs +55 -0
- package/books/sahih-al-bukhari/src/index.js +35 -0
- package/books/sahih-al-bukhari/src/index.node.js +21 -0
- package/books/sahih-al-bukhari/types/index.d.ts +35 -0
- package/books/sahih-muslim/LICENSE +661 -0
- package/books/sahih-muslim/README.md +547 -0
- package/books/sahih-muslim/bin/index.js +183 -0
- package/books/sahih-muslim/data/muslim.json.gz +0 -0
- package/books/sahih-muslim/examples/express/server.js +16 -0
- package/books/sahih-muslim/examples/node-commonjs/example.js +11 -0
- package/books/sahih-muslim/examples/node-esm/example.mjs +9 -0
- package/books/sahih-muslim/examples/react/HadithExample.jsx +28 -0
- package/books/sahih-muslim/package.json +58 -0
- package/books/sahih-muslim/src/index.cjs +52 -0
- package/books/sahih-muslim/src/index.js +35 -0
- package/books/sahih-muslim/src/index.node.js +18 -0
- package/books/sahih-muslim/types/index.d.ts +28 -0
- package/books/sunan-abi-dawud/LICENSE +661 -0
- package/books/sunan-abi-dawud/README.md +149 -0
- package/books/sunan-abi-dawud/bin/index.js +183 -0
- package/books/sunan-abi-dawud/data/dawud.json.gz +0 -0
- package/books/sunan-abi-dawud/examples/express/server.js +7 -0
- package/books/sunan-abi-dawud/examples/node-commonjs/example.js +7 -0
- package/books/sunan-abi-dawud/examples/node-esm/example.mjs +6 -0
- package/books/sunan-abi-dawud/examples/react/HadithExample.jsx +17 -0
- package/books/sunan-abi-dawud/package.json +58 -0
- package/books/sunan-abi-dawud/src/index.cjs +52 -0
- package/books/sunan-abi-dawud/src/index.js +35 -0
- package/books/sunan-abi-dawud/src/index.node.js +18 -0
- package/books/sunan-abi-dawud/types/index.d.ts +28 -0
- package/books/sunan-ibn-majah/README.md +198 -0
- package/books/sunan-ibn-majah/bin/index.js +138 -0
- package/books/sunan-ibn-majah/examples/express/server.js +8 -0
- package/books/sunan-ibn-majah/examples/node-commonjs/example.js +7 -0
- package/books/sunan-ibn-majah/examples/node-esm/example.mjs +6 -0
- package/books/sunan-ibn-majah/examples/react/HadithExample.jsx +17 -0
- package/books/sunan-ibn-majah/package.json +58 -0
- package/books/sunan-ibn-majah/src/index.cjs +52 -0
- package/books/sunan-ibn-majah/src/index.js +35 -0
- package/books/sunan-ibn-majah/src/index.node.js +18 -0
- package/books/sunan-ibn-majah/types/index.d.ts +28 -0
- package/package.json +39 -27
- /package/{LICENSE → books/sahih-al-bukhari/LICENSE} +0 -0
package/bin/index.js
CHANGED
|
@@ -1,1604 +1,1022 @@
|
|
|
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
|
|
12
|
-
const pkg
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ──
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return
|
|
57
|
-
} catch {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
hadiths: "7,
|
|
116
|
-
|
|
117
|
-
hook: "
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
);
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
.map((
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
console.log(
|
|
530
|
-
|
|
531
|
-
console.log("\n
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
(
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
);
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
console.log("
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
console.log("
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
function
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
};
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
);
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
);
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
if (
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
)
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
);
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
console.log(bold(
|
|
764
|
-
console.log(div);
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
);
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
console.log(
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
const
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
if (
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
)
|
|
895
|
-
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
)
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
console.log(
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
);
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
" " +
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
);
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
"
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
-
});
|
|
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(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
|
|
13
|
+
|
|
14
|
+
// ── Windows compatibility ─────────────────────────────────────────────────────
|
|
15
|
+
const isWin = process.platform === "win32";
|
|
16
|
+
|
|
17
|
+
function npmSync(args, opts = {}) {
|
|
18
|
+
if (isWin) {
|
|
19
|
+
const r = spawnSync("npm " + args.join(" "), [], {
|
|
20
|
+
encoding: "utf8", timeout: opts.timeout ?? 15000,
|
|
21
|
+
stdio: opts.stdio ?? ["ignore", "pipe", "pipe"], shell: true,
|
|
22
|
+
});
|
|
23
|
+
if (r.error) throw r.error;
|
|
24
|
+
return r.stdout ?? "";
|
|
25
|
+
}
|
|
26
|
+
const r = spawnSync("npm", args, {
|
|
27
|
+
encoding: "utf8", timeout: opts.timeout ?? 15000,
|
|
28
|
+
stdio: opts.stdio ?? ["ignore", "pipe", "pipe"],
|
|
29
|
+
});
|
|
30
|
+
if (r.error) throw r.error;
|
|
31
|
+
return r.stdout ?? "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function pipSync(args, opts = {}) {
|
|
35
|
+
const cmd = isWin ? "pip" : "pip3";
|
|
36
|
+
if (isWin) {
|
|
37
|
+
const r = spawnSync(cmd + " " + args.join(" "), [], {
|
|
38
|
+
encoding: "utf8", timeout: opts.timeout ?? 15000,
|
|
39
|
+
stdio: opts.stdio ?? ["ignore", "pipe", "pipe"], shell: true,
|
|
40
|
+
});
|
|
41
|
+
if (r.error) return ""; return r.stdout ?? "";
|
|
42
|
+
}
|
|
43
|
+
const r = spawnSync(cmd, args, {
|
|
44
|
+
encoding: "utf8", timeout: opts.timeout ?? 15000,
|
|
45
|
+
stdio: opts.stdio ?? ["ignore", "pipe", "pipe"],
|
|
46
|
+
});
|
|
47
|
+
if (r.error) return ""; return r.stdout ?? "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Tool availability checks ─────────────────────────────────────────────────
|
|
51
|
+
function hasNpm() {
|
|
52
|
+
try {
|
|
53
|
+
const r = isWin
|
|
54
|
+
? spawnSync("npm --version", [], { encoding:"utf8", timeout:4000, stdio:["ignore","pipe","pipe"], shell:true })
|
|
55
|
+
: spawnSync("npm", ["--version"], { encoding:"utf8", timeout:4000, stdio:["ignore","pipe","pipe"] });
|
|
56
|
+
return !r.error && r.status === 0;
|
|
57
|
+
} catch { return false; }
|
|
58
|
+
}
|
|
59
|
+
function hasPip() {
|
|
60
|
+
try {
|
|
61
|
+
const cmd = isWin ? "pip" : "pip3";
|
|
62
|
+
const r = isWin
|
|
63
|
+
? spawnSync(cmd + " --version", [], { encoding:"utf8", timeout:4000, stdio:["ignore","pipe","pipe"], shell:true })
|
|
64
|
+
: spawnSync(cmd, ["--version"], { encoding:"utf8", timeout:4000, stdio:["ignore","pipe","pipe"] });
|
|
65
|
+
return !r.error && r.status === 0;
|
|
66
|
+
} catch { return false; }
|
|
67
|
+
}
|
|
68
|
+
let _npmAvail = null, _pipAvail = null;
|
|
69
|
+
const npmAvailable = () => { if (_npmAvail === null) _npmAvail = hasNpm(); return _npmAvail; };
|
|
70
|
+
const pipAvailable = () => { if (_pipAvail === null) _pipAvail = hasPip(); return _pipAvail; };
|
|
71
|
+
|
|
72
|
+
function warnNoNpm() {
|
|
73
|
+
const div = gray("─".repeat(60));
|
|
74
|
+
console.log("\n" + div);
|
|
75
|
+
console.log(yellow(" ⚠ npm is not installed or not found in PATH."));
|
|
76
|
+
console.log(gray(" npm packages require Node.js — install it from:"));
|
|
77
|
+
console.log(cyan(" https://nodejs.org"));
|
|
78
|
+
console.log(div + "\n");
|
|
79
|
+
}
|
|
80
|
+
function warnNoPip() {
|
|
81
|
+
const div = gray("─".repeat(60));
|
|
82
|
+
console.log("\n" + div);
|
|
83
|
+
console.log(yellow(" ⚠ pip / pip3 is not installed or not found in PATH."));
|
|
84
|
+
console.log(gray(" Python packages require Python — install it from:"));
|
|
85
|
+
console.log(cyan(" https://python.org"));
|
|
86
|
+
console.log(gray(" Then run: ") + cyan("pip install sunnah"));
|
|
87
|
+
console.log(div + "\n");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── Persistence ───────────────────────────────────────────────────────────────
|
|
91
|
+
const STATE_FILE = path.join(os.homedir(), ".sunnah-state.json");
|
|
92
|
+
function loadState() { try { return JSON.parse(fs.readFileSync(STATE_FILE, "utf8")); } catch { return {}; } }
|
|
93
|
+
function saveState(d) { try { fs.writeFileSync(STATE_FILE, JSON.stringify({ ...loadState(), ...d }, null, 2)); } catch {} }
|
|
94
|
+
|
|
95
|
+
// ── Colors ────────────────────────────────────────────────────────────────────
|
|
96
|
+
const c = {
|
|
97
|
+
reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
|
|
98
|
+
green: "\x1b[32m", yellow: "\x1b[33m", cyan: "\x1b[36m",
|
|
99
|
+
magenta: "\x1b[35m", blue: "\x1b[34m", red: "\x1b[31m",
|
|
100
|
+
gray: "\x1b[90m", white: "\x1b[97m",
|
|
101
|
+
};
|
|
102
|
+
const clr = (col, t) => `${col}${t}${c.reset}`;
|
|
103
|
+
const bold = t => clr(c.bold, t);
|
|
104
|
+
const green = t => clr(c.green, t);
|
|
105
|
+
const yellow = t => clr(c.yellow, t);
|
|
106
|
+
const cyan = t => clr(c.cyan, t);
|
|
107
|
+
const magenta = t => clr(c.magenta, t);
|
|
108
|
+
const gray = t => clr(c.gray, t);
|
|
109
|
+
const red = t => clr(c.red, t);
|
|
110
|
+
const dim = t => clr(c.dim, t);
|
|
111
|
+
const white = t => clr(c.white, t);
|
|
112
|
+
|
|
113
|
+
// ── Package registry ──────────────────────────────────────────────────────────
|
|
114
|
+
const PACKAGES = [
|
|
115
|
+
{ name: "sahih-al-bukhari", pip: "sahih-al-bukhari", label: "Sahih al-Bukhari", author: "Imam Muhammad ibn Ismail al-Bukhari", desc: "The most authentic collection of hadith, widely regarded as the most sahih after the Quran.", hadiths: "7,563", cmd: "bukhari", hook: "useBukhari", pyClass: "Bukhari", pyMod: "sahih_al_bukhari" },
|
|
116
|
+
{ name: "sahih-muslim", pip: "sahih-muslim", label: "Sahih Muslim", author: "Imam Muslim ibn al-Hajjaj", desc: "Second most authentic hadith collection, known for its strict methodology and chain verification.", hadiths: "7,470", cmd: "muslim", hook: "useMuslim", pyClass: "Muslim", pyMod: "sahih_muslim" },
|
|
117
|
+
{ name: "sunan-abi-dawud", pip: "sunan-abi-dawud", label: "Sunan Abi Dawud", author: "Imam Abu Dawud Sulayman ibn al-Ash'ath", desc: "One of the six canonical hadith collections, focused on legal rulings and jurisprudence.", hadiths: "5,274", cmd: "dawud", hook: "useDawud", pyClass: "Dawud", pyMod: "sunan_abi_dawud" },
|
|
118
|
+
{ name: "jami-al-tirmidhi", pip: "jami-al-tirmidhi", label: "Jami al-Tirmidhi", author: "Imam Abu Isa Muhammad al-Tirmidhi", desc: "Part of the six major hadith collections, unique for grading each hadith's authenticity.", hadiths: "3,956", cmd: "tirmidhi", hook: "useTirmidhi", pyClass: "Tirmidhi", pyMod: "jami_al_tirmidhi" },
|
|
119
|
+
{ name: "sunan-ibn-majah", pip: "sunan-ibn-majah", label: "Sunan Ibn Majah", author: "Imam Muhammad ibn Yazid Ibn Majah", desc: "Sixth of the six major canonical hadith collections.", hadiths: "4,341", cmd: "majah", hook: "useMajah", pyClass: "Majah", pyMod: "sunan_ibn_majah" },
|
|
120
|
+
{ name: "sunan-al-nasai", pip: "sunan-al-nasai", label: "Sunan al-Nasa'i", author: "Imam Ahmad ibn Shu'ayb al-Nasa'i", desc: "Known for its strict standards in accepting transmitters.", hadiths: "5,768", cmd: "nasai", hook: "useNasai", pyClass: "Nasai", pyMod: "sunan_al_nasai" },
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
const CMD_MAP = Object.fromEntries(PACKAGES.map(p => [p.cmd, p]));
|
|
124
|
+
const NAME_MAP = Object.fromEntries(PACKAGES.map(p => [p.name, p]));
|
|
125
|
+
|
|
126
|
+
// ── Terminal helpers ──────────────────────────────────────────────────────────
|
|
127
|
+
const W = () => process.stdout.columns || 80;
|
|
128
|
+
const H = () => process.stdout.rows || 24;
|
|
129
|
+
const enterAltScreen = () => process.stdout.write("\x1b[?1049h");
|
|
130
|
+
const leaveAltScreen = () => process.stdout.write("\x1b[?1049l");
|
|
131
|
+
const clearScreen = () => process.stdout.write("\x1b[2J\x1b[H");
|
|
132
|
+
const moveTo = (r, col) => process.stdout.write(`\x1b[${r};${col}H`);
|
|
133
|
+
const hideCursor = () => process.stdout.write("\x1b[?25l");
|
|
134
|
+
const showCursor = () => process.stdout.write("\x1b[?25h");
|
|
135
|
+
const clearToEOL = () => process.stdout.write("\x1b[K");
|
|
136
|
+
function writeLine(row, text) { moveTo(row, 1); clearToEOL(); process.stdout.write(text); }
|
|
137
|
+
|
|
138
|
+
// ── Progress bar ──────────────────────────────────────────────────────────────
|
|
139
|
+
function drawBar(label, percent, barWidth = 40) {
|
|
140
|
+
const filled = Math.round((percent / 100) * barWidth);
|
|
141
|
+
const empty = barWidth - filled;
|
|
142
|
+
const bar = c.green + "█".repeat(filled) + c.gray + "░".repeat(empty) + c.reset;
|
|
143
|
+
const pct = cyan(String(Math.round(percent)).padStart(3) + "%");
|
|
144
|
+
return ` ${bar} ${pct} ${dim(label)}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function animateInstall(pkgName, isPip = false) {
|
|
148
|
+
return new Promise(resolve => {
|
|
149
|
+
const stages = [
|
|
150
|
+
{ label: "Resolving packages…", end: 12, ms: 80 },
|
|
151
|
+
{ label: "Fetching metadata…", end: 30, ms: 60 },
|
|
152
|
+
{ label: "Downloading tarball…", end: 75, ms: 22 },
|
|
153
|
+
{ label: "Extracting files…", end: 90, ms: 50 },
|
|
154
|
+
{ label: "Linking binaries…", end: 98, ms: 80 },
|
|
155
|
+
];
|
|
156
|
+
let percent = 0, stageIdx = 0, npmDone = false;
|
|
157
|
+
process.stdout.write(drawBar(stages[0].label, 0));
|
|
158
|
+
const tick = () => {
|
|
159
|
+
const stage = stages[stageIdx]; if (!stage) return;
|
|
160
|
+
const prev = stageIdx > 0 ? stages[stageIdx - 1].end : 0;
|
|
161
|
+
percent = Math.min(percent + (stage.end - prev) / 24, stage.end);
|
|
162
|
+
process.stdout.write("\r\x1b[K" + drawBar(stage.label, percent));
|
|
163
|
+
if (percent >= stage.end) {
|
|
164
|
+
stageIdx++;
|
|
165
|
+
if (stageIdx >= stages.length) {
|
|
166
|
+
const poll = setInterval(() => {
|
|
167
|
+
if (npmDone) { clearInterval(poll); process.stdout.write("\r\x1b[K" + drawBar("Complete!", 100) + "\n"); resolve(); }
|
|
168
|
+
}, 80);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
setTimeout(tick, stages[stageIdx]?.ms ?? 50);
|
|
173
|
+
};
|
|
174
|
+
setTimeout(tick, stages[0].ms);
|
|
175
|
+
let proc;
|
|
176
|
+
if (isPip) {
|
|
177
|
+
const pipCmd = isWin ? "pip" : "pip3";
|
|
178
|
+
proc = isWin
|
|
179
|
+
? spawn(`${pipCmd} install ${pkgName}`, [], { stdio: ["ignore", "pipe", "pipe"], shell: true })
|
|
180
|
+
: spawn(pipCmd, ["install", pkgName], { stdio: ["ignore", "pipe", "pipe"] });
|
|
181
|
+
} else {
|
|
182
|
+
proc = isWin
|
|
183
|
+
? spawn("npm install -g " + pkgName, [], { stdio: ["ignore", "pipe", "pipe"], shell: true })
|
|
184
|
+
: spawn("npm", ["install", "-g", pkgName], { stdio: ["ignore", "pipe", "pipe"] });
|
|
185
|
+
}
|
|
186
|
+
proc.on("error", () => { npmDone = true; });
|
|
187
|
+
proc.on("close", () => { npmDone = true; });
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Installed caches ──────────────────────────────────────────────────────────
|
|
192
|
+
function buildInstalledCache() {
|
|
193
|
+
const cache = new Map();
|
|
194
|
+
for (const p of PACKAGES) cache.set(p.name, false);
|
|
195
|
+
if (!npmAvailable()) return cache;
|
|
196
|
+
let out = "";
|
|
197
|
+
try {
|
|
198
|
+
out = npmSync(["list", "-g", "--depth=0", "--json"], { timeout: 6000 });
|
|
199
|
+
const deps = JSON.parse(out)?.dependencies ?? {};
|
|
200
|
+
for (const p of PACKAGES) cache.set(p.name, p.name in deps);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
const text = (e && e.stdout) || out || "";
|
|
203
|
+
for (const p of PACKAGES) cache.set(p.name, text.includes(p.name));
|
|
204
|
+
}
|
|
205
|
+
return cache;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function buildPipCache() {
|
|
209
|
+
const cache = new Map();
|
|
210
|
+
for (const p of PACKAGES) cache.set(p.pip, false);
|
|
211
|
+
if (!pipAvailable()) return cache;
|
|
212
|
+
try {
|
|
213
|
+
const out = pipSync(["list", "--format=json"], { timeout: 8000 });
|
|
214
|
+
const inst = JSON.parse(out).map(x => x.name.toLowerCase());
|
|
215
|
+
for (const p of PACKAGES) cache.set(p.pip, inst.includes(p.pip.toLowerCase()));
|
|
216
|
+
} catch {
|
|
217
|
+
for (const p of PACKAGES) {
|
|
218
|
+
try { const o = pipSync(["show", p.pip], { timeout: 4000 }); cache.set(p.pip, o.includes("Name:")); }
|
|
219
|
+
catch { cache.set(p.pip, false); }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return cache;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function getVersion(name, pip = false) {
|
|
226
|
+
try {
|
|
227
|
+
if (pip) {
|
|
228
|
+
const o = pipSync(["show", name], { timeout: 4000 });
|
|
229
|
+
const m = o.match(/^Version:\s+([\d.]+)/m);
|
|
230
|
+
return m ? m[1] : null;
|
|
231
|
+
}
|
|
232
|
+
const o = npmSync(["list", "-g", name, "--depth=0", "--json"], { timeout: 6000 });
|
|
233
|
+
return JSON.parse(o)?.dependencies?.[name]?.version ?? null;
|
|
234
|
+
} catch { return null; }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function getLatest(name, pip = false) {
|
|
238
|
+
try {
|
|
239
|
+
if (pip) {
|
|
240
|
+
const o = pipSync(["index", "versions", name], { timeout: 8000 });
|
|
241
|
+
const m = o.match(/LATEST:\s+([\d.]+)/);
|
|
242
|
+
return m ? m[1] : null;
|
|
243
|
+
}
|
|
244
|
+
const o = npmSync(["show", name, "version", "--json"], { timeout: 6000 });
|
|
245
|
+
return JSON.parse(o.trim());
|
|
246
|
+
} catch {
|
|
247
|
+
try { return pip ? null : npmSync(["show", name, "version"], { timeout: 6000 }).trim(); }
|
|
248
|
+
catch { return null; }
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let installedCache = new Map(), pipCache = new Map();
|
|
253
|
+
const isInstalled = n => installedCache.get(n) ?? false;
|
|
254
|
+
const isPipInst = n => pipCache.get(n) ?? false;
|
|
255
|
+
|
|
256
|
+
// ── Update cache (background) ─────────────────────────────────────────────────
|
|
257
|
+
let updateCache = new Map(), updateReady = false;
|
|
258
|
+
|
|
259
|
+
async function prefetchUpdateCache() {
|
|
260
|
+
// refresh pip cache in background too
|
|
261
|
+
if (pipAvailable()) { const pc = buildPipCache(); for (const [k,v] of pc) pipCache.set(k,v); }
|
|
262
|
+
const installed = PACKAGES.filter(p => isInstalled(p.name));
|
|
263
|
+
if (!installed.length) { updateReady = true; return; }
|
|
264
|
+
let out = "";
|
|
265
|
+
await new Promise(resolve => {
|
|
266
|
+
const proc = isWin
|
|
267
|
+
? spawn("npm list -g --depth=0 --json", [], { stdio: ["ignore", "pipe", "pipe"], shell: true })
|
|
268
|
+
: spawn("npm", ["list", "-g", "--depth=0", "--json"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
269
|
+
proc.stdout?.on("data", d => { out += d; });
|
|
270
|
+
proc.on("error", () => resolve()); proc.on("close", () => resolve());
|
|
271
|
+
setTimeout(() => { try { proc.kill(); } catch {} resolve(); }, 6000);
|
|
272
|
+
});
|
|
273
|
+
const curVers = new Map();
|
|
274
|
+
try { const deps = JSON.parse(out)?.dependencies ?? {}; for (const p of installed) curVers.set(p.name, deps[p.name]?.version ?? null); } catch {}
|
|
275
|
+
// npm + pip share same version number — check all packages (npm or pip installed)
|
|
276
|
+
await Promise.all(PACKAGES.map(p => new Promise(resolve => {
|
|
277
|
+
const npmI = isInstalled(p.name), pipI = isPipInst(p.pip);
|
|
278
|
+
if (!npmI && !pipI) { resolve(); return; }
|
|
279
|
+
if (npmI) {
|
|
280
|
+
// fetch latest from npm registry
|
|
281
|
+
let v = "";
|
|
282
|
+
const proc = isWin
|
|
283
|
+
? spawn("npm show " + p.name + " version", [], { stdio: ["ignore", "pipe", "pipe"], shell: true })
|
|
284
|
+
: spawn("npm", ["show", p.name, "version"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
285
|
+
proc.stdout?.on("data", d => { v += d; });
|
|
286
|
+
proc.on("error", () => resolve());
|
|
287
|
+
proc.on("close", () => {
|
|
288
|
+
const latest = v.trim() || null, current = curVers.get(p.name) ?? null;
|
|
289
|
+
updateCache.set(p.name, { current, latest, hasUpdate: !!(current && latest && current !== latest) });
|
|
290
|
+
resolve();
|
|
291
|
+
});
|
|
292
|
+
setTimeout(() => { try { proc.kill(); } catch {} resolve(); }, 6000);
|
|
293
|
+
} else {
|
|
294
|
+
// pip-only: get installed version, mark for checking
|
|
295
|
+
const cur = getVersion(p.pip, true);
|
|
296
|
+
updateCache.set(p.name, { current: cur, latest: null, hasUpdate: false });
|
|
297
|
+
resolve();
|
|
298
|
+
}
|
|
299
|
+
})));
|
|
300
|
+
updateReady = true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ── Clipboard ─────────────────────────────────────────────────────────────────
|
|
304
|
+
function copyToClipboard(text) {
|
|
305
|
+
try {
|
|
306
|
+
if (isWin) execSync("clip", { input: text, shell: true });
|
|
307
|
+
else if (process.platform === "darwin") execSync("pbcopy", { input: text });
|
|
308
|
+
else execSync("xclip -selection clipboard || xsel --clipboard --input", { input: text, shell: true });
|
|
309
|
+
return true;
|
|
310
|
+
} catch { return false; }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── React hook generator ──────────────────────────────────────────────────────
|
|
314
|
+
function generateUnifiedHook(books) {
|
|
315
|
+
const cwd = process.cwd(), srcDir = path.join(cwd, "src"), hooksDir = path.join(srcDir, "hooks");
|
|
316
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
317
|
+
if (!fs.existsSync(pkgPath)) { console.error(red("\n ✗ No package.json found. Run inside your React project.\n")); process.exit(1); }
|
|
318
|
+
const deps = { ...JSON.parse(fs.readFileSync(pkgPath, "utf8")).dependencies, ...JSON.parse(fs.readFileSync(pkgPath, "utf8")).devDependencies };
|
|
319
|
+
if (!deps["react"]) { console.error(red("\n ✗ React not found in package.json.\n")); process.exit(1); }
|
|
320
|
+
if (!fs.existsSync(srcDir)) { console.error(red("\n ✗ No src/ directory found.\n")); process.exit(1); }
|
|
321
|
+
if (!fs.existsSync(hooksDir)) { fs.mkdirSync(hooksDir, { recursive: true }); console.log(green(" ✓ Created src/hooks/")); }
|
|
322
|
+
|
|
323
|
+
const loaderBlocks = books.map(p => {
|
|
324
|
+
const CDN = `https://cdn.jsdelivr.net/npm/${p.name}@latest/chapters`;
|
|
325
|
+
return `\n// ── ${p.label} ──\nconst _${p.cmd}CDN='${CDN}';let _${p.cmd}Cache=null,_${p.cmd}Prom=null;const _${p.cmd}Subs=new Set();\nfunction _load${p.hook.replace("use","")}(){if(_${p.cmd}Cache)return Promise.resolve(_${p.cmd}Cache);if(_${p.cmd}Prom)return _${p.cmd}Prom;_${p.cmd}Prom=fetch(_${p.cmd}CDN+'/meta.json').then(r=>r.json()).then(meta=>Promise.all(meta.chapters.map(c=>fetch(_${p.cmd}CDN+'/'+c.id+'.json').then(r=>r.json()))).then(results=>{const hadiths=results.flat();const _byId=new Map();hadiths.forEach(h=>_byId.set(h.id,h));_${p.cmd}Cache=Object.assign([],hadiths,{metadata:meta.metadata,chapters:meta.chapters,get:id=>_byId.get(id),getByChapter:id=>hadiths.filter(h=>h.chapterId===id),search:(q,limit=0)=>{const ql=q.toLowerCase();const r=hadiths.filter(h=>h.english?.text?.toLowerCase().includes(ql)||h.english?.narrator?.toLowerCase().includes(ql));return limit>0?r.slice(0,limit):r;},getRandom:()=>hadiths[Math.floor(Math.random()*hadiths.length)]});_${p.cmd}Subs.forEach(fn=>fn(_${p.cmd}Cache));_${p.cmd}Subs.clear();return _${p.cmd}Cache;}));return _${p.cmd}Prom;}_load${p.hook.replace("use","")}();\nexport function ${p.hook}(){const[data,setData]=useState(_${p.cmd}Cache);useEffect(()=>{if(_${p.cmd}Cache){setData(_${p.cmd}Cache);}else{_${p.cmd}Subs.add(setData);return()=>_${p.cmd}Subs.delete(setData);}},[]);return data;}`;
|
|
326
|
+
}).join("\n");
|
|
327
|
+
|
|
328
|
+
const hookSrc = `// Auto-generated by: sunnah --react\n// Books: ${books.map(p => p.label).join(", ")}\nimport { useState, useEffect } from 'react';\n${loaderBlocks}\nexport function useSunnah(){const hooks=[${books.map(p => `${p.hook}()`).join(",")}];if(hooks.some(h=>!h))return null;return{${books.map((p,i) => `${p.cmd}:hooks[${i}]`).join(",")}};}\nexport default useSunnah;\n`;
|
|
329
|
+
|
|
330
|
+
const hookFile = path.join(hooksDir, "useSunnah.js");
|
|
331
|
+
fs.writeFileSync(hookFile, hookSrc, "utf8");
|
|
332
|
+
const div = gray("─".repeat(60));
|
|
333
|
+
console.log("\n" + div);
|
|
334
|
+
console.log(bold(cyan(" ✓ Generated: src/hooks/useSunnah.js")));
|
|
335
|
+
console.log(div);
|
|
336
|
+
console.log("\n " + gray("Included books:"));
|
|
337
|
+
books.forEach(p => console.log(" " + green("▸") + " " + bold(p.label) + gray(" " + p.hook + "()")));
|
|
338
|
+
console.log("\n " + gray("Usage:") + "\n " + cyan("import { useSunnah } from '../hooks/useSunnah';"));
|
|
339
|
+
console.log(" " + gray("const sunnah = useSunnah();"));
|
|
340
|
+
if (books[0]) console.log(" " + gray(`sunnah.${books[0].cmd}.getRandom()`));
|
|
341
|
+
console.log("\n" + div + "\n");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── Personalized suggestions ──────────────────────────────────────────────────
|
|
345
|
+
function getPersonalizedSuggestions() {
|
|
346
|
+
const installed = PACKAGES.filter(p => isInstalled(p.name));
|
|
347
|
+
const missing = PACKAGES.filter(p => !isInstalled(p.name));
|
|
348
|
+
const tips = [];
|
|
349
|
+
if (installed.length === 0) {
|
|
350
|
+
tips.push(cyan(" ▸") + " Run " + bold("sunnah") + " to open the interactive installer");
|
|
351
|
+
tips.push(cyan(" ▸") + " Or install directly: " + bold("sunnah install bukhari"));
|
|
352
|
+
} else {
|
|
353
|
+
installed.slice(0, 2).forEach(p => {
|
|
354
|
+
tips.push(cyan(" ▸") + " " + bold(p.cmd + " --random") + gray(" — random " + p.label + " hadith"));
|
|
355
|
+
tips.push(cyan(" ▸") + " " + bold(`sunnah search "prayer"`) + gray(" — search all installed books"));
|
|
356
|
+
});
|
|
357
|
+
if (installed.length > 1) tips.push(cyan(" ▸") + " " + bold("sunnah --react") + gray(" — generate useSunnah() React hook"));
|
|
358
|
+
if (missing.length > 0) tips.push(cyan(" ▸") + " " + bold("sunnah") + gray(" — install more books (" + missing.map(p => p.label).join(", ") + ")"));
|
|
359
|
+
tips.push(cyan(" ▸") + " " + bold("sunnah --update") + gray(" — check for updates"));
|
|
360
|
+
}
|
|
361
|
+
return tips;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ── Interactive TUI mode ──────────────────────────────────────────────────────
|
|
365
|
+
const MODE = { LIST: "list", CONFIRM_UNINSTALL: "confirm_uninstall" };
|
|
366
|
+
|
|
367
|
+
function render(state) {
|
|
368
|
+
const { cursor, selected, mode, confirmTarget, statusMsg } = state;
|
|
369
|
+
const divW = Math.min(W() - 2, 72);
|
|
370
|
+
const div = gray("─".repeat(divW));
|
|
371
|
+
const div2 = gray("═".repeat(divW));
|
|
372
|
+
|
|
373
|
+
clearScreen();
|
|
374
|
+
let row = 1;
|
|
375
|
+
|
|
376
|
+
writeLine(row++, div2);
|
|
377
|
+
writeLine(row++, bold(cyan(" 📚 Sunnah Package Manager")) + gray(" v" + pkg.version));
|
|
378
|
+
writeLine(row++,
|
|
379
|
+
gray(" ↑↓") + " nav " + gray("space") + " select " + gray("a") + " all " +
|
|
380
|
+
gray("enter") + " npm " + gray("p") + " pip " +
|
|
381
|
+
gray("u") + " uninstall " + gray("U") + " update " +
|
|
382
|
+
gray("i") + " info " + gray("q") + " quit"
|
|
383
|
+
);
|
|
384
|
+
writeLine(row++, div2);
|
|
385
|
+
if (!npmAvailable()) writeLine(row++, " " + yellow("⚠ npm not found") + gray(" — npm packages unavailable ") + dim("nodejs.org"));
|
|
386
|
+
if (!pipAvailable()) writeLine(row++, " " + yellow("⚠ pip not found") + gray(" — pip packages unavailable ") + dim("python.org"));
|
|
387
|
+
row++;
|
|
388
|
+
|
|
389
|
+
PACKAGES.forEach((p, i) => {
|
|
390
|
+
const isCursor = i === cursor;
|
|
391
|
+
const isSel = selected.has(i);
|
|
392
|
+
const inst = isInstalled(p.name);
|
|
393
|
+
|
|
394
|
+
const checkbox = isSel ? green("[✓]") : gray("[ ]");
|
|
395
|
+
const arrow = isCursor ? cyan("▶") : " ";
|
|
396
|
+
const label = isCursor ? bold(white(p.label)) : isSel ? green(p.label) : white(p.label);
|
|
397
|
+
const npmInst = isInstalled(p.name);
|
|
398
|
+
const pipInst = isPipInst(p.pip);
|
|
399
|
+
const anyInst = npmInst || pipInst;
|
|
400
|
+
// build compact badge: only show what IS installed
|
|
401
|
+
const labels = [...(npmInst ? ["npm"] : []), ...(pipInst ? ["pip"] : [])];
|
|
402
|
+
const instStr = labels.length ? "(" + labels.join(" + ") + ")" : "";
|
|
403
|
+
const uc = updateCache.get(p.name);
|
|
404
|
+
const updStr = uc?.hasUpdate ? " " + yellow("↑ update available") : (updateReady && anyInst && uc ? " " + dim(gray("(up to date)")) : "");
|
|
405
|
+
const badge = anyInst
|
|
406
|
+
? dim(green(" ● ")) + dim(green(instStr)) + updStr
|
|
407
|
+
: dim(gray(" ○ not installed"));
|
|
408
|
+
|
|
409
|
+
writeLine(row++, ` ${arrow} ${checkbox} ${label}${badge}`);
|
|
410
|
+
|
|
411
|
+
if (isCursor) {
|
|
412
|
+
writeLine(row++, ` ${dim(p.author)}`);
|
|
413
|
+
writeLine(row++, ` ${gray(p.desc)}`);
|
|
414
|
+
const uc = updateCache.get(p.name);
|
|
415
|
+
// version — npm and pip share the same version number
|
|
416
|
+
const verInfo = anyInst
|
|
417
|
+
? (uc ? (uc.hasUpdate
|
|
418
|
+
? gray(" v") + yellow(uc.current) + gray(" → ") + green(uc.latest)
|
|
419
|
+
: gray(" v") + cyan(uc.current || "?"))
|
|
420
|
+
: gray(" (checking…)"))
|
|
421
|
+
: "";
|
|
422
|
+
const pkgDetail = [npmInst ? dim(gray("npm: " + p.name)) : "", pipInst ? dim(gray("pip: " + p.pip)) : ""].filter(Boolean).join(" ");
|
|
423
|
+
writeLine(row++,
|
|
424
|
+
` ${gray("Hadiths: ")}${yellow(p.hadiths)}` +
|
|
425
|
+
` ${gray("CLI: ")}${cyan(p.cmd + " --help")}` +
|
|
426
|
+
verInfo +
|
|
427
|
+
(pkgDetail ? ` ${pkgDetail}` : "") +
|
|
428
|
+
(anyInst ? ` ${gray("try: ")}${cyan(p.cmd + " --random")}` : "")
|
|
429
|
+
);
|
|
430
|
+
row++;
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
row++;
|
|
435
|
+
writeLine(row++, div);
|
|
436
|
+
|
|
437
|
+
if (statusMsg) {
|
|
438
|
+
writeLine(row++, ` ${yellow("⚠")} ${yellow(statusMsg)}`);
|
|
439
|
+
} else if (selected.size > 0) {
|
|
440
|
+
const names = [...selected].map(i => cyan(PACKAGES[i].name)).join(", ");
|
|
441
|
+
writeLine(row++, ` ${green("●")} ${bold(String(selected.size))} selected: ${names}`);
|
|
442
|
+
writeLine(row++, ` ${dim("enter = npm install p = pip install u = uninstall")}`);
|
|
443
|
+
} else {
|
|
444
|
+
writeLine(row++, ` ${gray("Nothing selected — press space to select, enter to install focused")}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
writeLine(row++, div);
|
|
448
|
+
|
|
449
|
+
if (mode === MODE.CONFIRM_UNINSTALL && confirmTarget !== null) {
|
|
450
|
+
const p = PACKAGES[confirmTarget];
|
|
451
|
+
row++;
|
|
452
|
+
writeLine(row++, ` ${red("⚠ Uninstall ")}${bold(white(p.label))}${red("?")}`);
|
|
453
|
+
writeLine(row++, ` ${green("y")} ${gray("confirm")} ${red("n")} ${gray("cancel")}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ── Non-interactive commands ──────────────────────────────────────────────────
|
|
458
|
+
|
|
459
|
+
function cmdList() {
|
|
460
|
+
installedCache = buildInstalledCache(); pipCache = buildPipCache();
|
|
461
|
+
const div = gray("─".repeat(60));
|
|
462
|
+
console.log("\n" + div);
|
|
463
|
+
console.log(bold(cyan(" Available Sunnah Packages")));
|
|
464
|
+
console.log(div);
|
|
465
|
+
PACKAGES.forEach(p => {
|
|
466
|
+
const n = isInstalled(p.name), pi = isPipInst(p.pip);
|
|
467
|
+
const nv = n ? getVersion(p.name) : null;
|
|
468
|
+
const nl = n ? getLatest(p.name) : null;
|
|
469
|
+
const vStr = n && nv && nl
|
|
470
|
+
? (nv === nl ? dim(gray(" v" + nv + " (up to date)")) : yellow(" v" + nv) + gray(" → ") + green("v" + nl) + yellow(" ↑"))
|
|
471
|
+
: (n && nv ? dim(gray(" v" + nv)) : "");
|
|
472
|
+
console.log(`\n ${bold(white(p.label))}${n ? green(" ✓ npm") : red(" ✗ npm")}${pi ? green(" ✓ pip") : gray(" ○ pip")}${vStr}`);
|
|
473
|
+
console.log(` ${cyan("npm install -g " + p.name)} ${dim("pip install " + p.pip)}`);
|
|
474
|
+
console.log(` ${dim(p.desc)}`);
|
|
475
|
+
console.log(` ${gray("Hadiths: ")}${yellow(p.hadiths)} ${gray("Author: ")}${magenta(p.author)}`);
|
|
476
|
+
if (n) console.log(` ${gray("CLI: ")}${cyan(p.cmd + " --help")} ${gray("React: ")}${cyan("sunnah --react " + p.cmd)}`);
|
|
477
|
+
if (pi) console.log(` ${gray("Python: ")}${dim("from " + p.pyMod + " import " + p.pyClass)}`);
|
|
478
|
+
});
|
|
479
|
+
console.log("\n" + div + "\n");
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function cmdInfo(target) {
|
|
483
|
+
const p = CMD_MAP[target?.toLowerCase()] || NAME_MAP[target];
|
|
484
|
+
if (!p) { console.log(red(`\n Unknown book: "${target}"\n`)); console.log(" Available: " + PACKAGES.map(x => cyan(x.cmd)).join(", ") + "\n"); process.exit(1); }
|
|
485
|
+
installedCache = buildInstalledCache(); pipCache = buildPipCache();
|
|
486
|
+
const n = isInstalled(p.name), pi = isPipInst(p.pip);
|
|
487
|
+
const nv = n ? getVersion(p.name) : null;
|
|
488
|
+
const pv = pi ? getVersion(p.pip, true) : null;
|
|
489
|
+
const div = gray("─".repeat(60)), div2 = gray("═".repeat(60));
|
|
490
|
+
console.log("\n" + div2 + "\n " + bold(cyan(p.label)) + "\n" + div2);
|
|
491
|
+
console.log(" " + gray("Author: ") + magenta(p.author));
|
|
492
|
+
console.log(" " + gray("Hadiths: ") + yellow(p.hadiths));
|
|
493
|
+
console.log(" " + gray("npm: ") + cyan(p.name) + (n ? green(" ✓ v" + nv) : red(" ✗ not installed")));
|
|
494
|
+
console.log(" " + gray("pip: ") + cyan(p.pip) + (pi ? green(" ✓ v" + pv) : red(" ✗ not installed")));
|
|
495
|
+
console.log(" " + gray("CLI: ") + cyan(p.cmd + " --help"));
|
|
496
|
+
console.log(" " + gray("React: ") + cyan("sunnah --react " + p.cmd));
|
|
497
|
+
console.log(" " + gray("Python: ") + dim("from " + p.pyMod + " import " + p.pyClass));
|
|
498
|
+
console.log(" " + gray("Desc: ") + p.desc);
|
|
499
|
+
console.log(div);
|
|
500
|
+
if (!n) console.log(" " + yellow("npm:") + " sunnah install " + p.cmd);
|
|
501
|
+
if (!pi) console.log(" " + yellow("pip:") + " sunnah pip install " + p.cmd);
|
|
502
|
+
console.log(div2 + "\n");
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function cmdRandom(bookCmd = null) {
|
|
506
|
+
installedCache = buildInstalledCache();
|
|
507
|
+
const inst = PACKAGES.filter(p => isInstalled(p.name));
|
|
508
|
+
if (!inst.length) { console.log(yellow("\n No packages installed. Run sunnah to install some.\n")); process.exit(0); }
|
|
509
|
+
const target = bookCmd ? CMD_MAP[bookCmd] : inst[Math.floor(Math.random() * inst.length)];
|
|
510
|
+
if (!target) { console.log(red(`\n Unknown book: ${bookCmd}\n`)); process.exit(1); }
|
|
511
|
+
try {
|
|
512
|
+
const mod = await import(target.name).catch(() => null);
|
|
513
|
+
if (!mod) { console.log(red("\n Could not load " + target.label + "\n")); process.exit(1); }
|
|
514
|
+
const h = mod.default.getRandom(), div2 = gray("═".repeat(60));
|
|
515
|
+
console.log("\n" + div2);
|
|
516
|
+
console.log(` ${bold(cyan("Hadith #" + h.id))} ${gray("|")} ${bold(target.label)}`);
|
|
517
|
+
console.log(div2);
|
|
518
|
+
if (h.english?.narrator) console.log(" " + bold(yellow("Narrator: ")) + magenta(h.english.narrator));
|
|
519
|
+
if (h.english?.text) { console.log(""); console.log(" " + h.english.text); }
|
|
520
|
+
console.log("\n" + div2 + "\n");
|
|
521
|
+
console.log(" " + gray("Try: ") + cyan(target.cmd + " " + h.id + " -b") + gray(" (Arabic + English)\n"));
|
|
522
|
+
} catch (_e) { console.log(red("\n Could not load " + target.label + ".\n")); }
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function cmdSearch(query, all = false) {
|
|
526
|
+
if (!query) { console.error(red('\n Usage: sunnah search "<query>" [--all]\n')); process.exit(1); }
|
|
527
|
+
installedCache = buildInstalledCache();
|
|
528
|
+
const inst = PACKAGES.filter(p => isInstalled(p.name));
|
|
529
|
+
if (!inst.length) { console.log(yellow("\n No packages installed.\n")); process.exit(0); }
|
|
530
|
+
const div = gray("─".repeat(60)), div2 = gray("═".repeat(60));
|
|
531
|
+
console.log("\n" + div2);
|
|
532
|
+
console.log(bold(cyan(" Searching across ")) + yellow(String(inst.length)) + bold(cyan(" book" + (inst.length > 1 ? "s" : "") + "…")));
|
|
533
|
+
console.log(div2 + "\n");
|
|
534
|
+
let total = 0;
|
|
535
|
+
for (const p of inst) {
|
|
536
|
+
process.stdout.write(" " + gray("Loading ") + white(p.label) + gray("…\r"));
|
|
537
|
+
try {
|
|
538
|
+
const mod = await import(p.name).catch(() => null);
|
|
539
|
+
if (!mod) { process.stdout.write("\x1b[K"); console.log(" " + gray("○ " + p.label + " — could not load")); continue; }
|
|
540
|
+
const results = mod.default.search ? mod.default.search(query, 0) : [];
|
|
541
|
+
process.stdout.write("\x1b[K");
|
|
542
|
+
if (!results.length) { console.log(" " + dim(gray("○ " + p.label + " — no results"))); continue; }
|
|
543
|
+
total += results.length;
|
|
544
|
+
const limit = all ? results.length : Math.min(3, results.length);
|
|
545
|
+
console.log(" " + green("▸") + " " + bold(p.label) + gray(" " + results.length + " results"));
|
|
546
|
+
console.log(div);
|
|
547
|
+
results.slice(0, limit).forEach((h, i) => {
|
|
548
|
+
console.log("\n " + bold(green("#" + (i + 1))) + gray(" Hadith " + h.id));
|
|
549
|
+
if (h.english?.narrator) console.log(" " + bold(yellow("Narrator: ")) + magenta(h.english.narrator));
|
|
550
|
+
if (h.english?.text) {
|
|
551
|
+
const txt = h.english.text.slice(0, 200) + (h.english.text.length > 200 ? "…" : "");
|
|
552
|
+
const hi = txt.replace(new RegExp("(" + query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "gi"), "\x1b[1m\x1b[33m$1\x1b[0m");
|
|
553
|
+
console.log(" " + hi);
|
|
554
|
+
}
|
|
555
|
+
console.log(dim(gray(" " + p.cmd + " " + h.id + " -b")));
|
|
556
|
+
});
|
|
557
|
+
if (!all && results.length > 3) console.log("\n " + dim("Showing 3 of " + results.length + ". ") + yellow(`sunnah search "${query}" --all`));
|
|
558
|
+
console.log(div);
|
|
559
|
+
} catch { process.stdout.write("\x1b[K"); console.log(" " + gray("○ " + p.label + " — skipped")); }
|
|
560
|
+
}
|
|
561
|
+
console.log("\n" + div2);
|
|
562
|
+
console.log(" " + green("✓") + " " + bold(String(total)) + gray(" total results across ") + yellow(String(inst.length)) + gray(" book" + (inst.length > 1 ? "s" : "") + "."));
|
|
563
|
+
console.log(div2 + "\n");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function cmdPipInstall(targets) {
|
|
567
|
+
if (!targets.length) { console.error(red("\n Usage: sunnah pip install <book>\n")); process.exit(1); }
|
|
568
|
+
if (!pipAvailable()) { warnNoPip(); process.exit(1); }
|
|
569
|
+
const to = [];
|
|
570
|
+
for (const t of targets) {
|
|
571
|
+
const p = CMD_MAP[t.toLowerCase()] || NAME_MAP[t];
|
|
572
|
+
if (!p) { console.log(yellow(`\n Unknown: "${t}"`)); process.exit(1); }
|
|
573
|
+
to.push(p);
|
|
574
|
+
}
|
|
575
|
+
const div2 = gray("═".repeat(60));
|
|
576
|
+
console.log("\n" + div2);
|
|
577
|
+
console.log(bold(cyan(" Installing ")) + bold(yellow(String(to.length))) + bold(cyan(" Python package" + (to.length > 1 ? "s" : "") + "…")));
|
|
578
|
+
console.log(div2);
|
|
579
|
+
for (let i = 0; i < to.length; i++) {
|
|
580
|
+
const p = to[i];
|
|
581
|
+
console.log(`\n ${cyan("[" + (i + 1) + "/" + to.length + "]")} ${bold(white(p.label))}`);
|
|
582
|
+
console.log(" " + dim("pip install " + p.pip) + "\n");
|
|
583
|
+
await animateInstall(p.pip, true);
|
|
584
|
+
console.log(" " + green("✓") + " " + bold(green(p.label)) + " (Python) installed");
|
|
585
|
+
console.log(" " + gray("Usage: ") + dim("from " + p.pyMod + " import " + p.pyClass));
|
|
586
|
+
}
|
|
587
|
+
console.log("\n" + div2 + "\n");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function cmdPipList() {
|
|
591
|
+
if (!pipAvailable()) { warnNoPip(); return; }
|
|
592
|
+
pipCache = buildPipCache(); const div = gray("─".repeat(60));
|
|
593
|
+
console.log("\n" + div + "\n " + bold(cyan(" Python (pip) Packages")) + "\n" + div);
|
|
594
|
+
PACKAGES.forEach(p => {
|
|
595
|
+
const i = isPipInst(p.pip), v = i ? getVersion(p.pip, true) : null;
|
|
596
|
+
console.log(`\n ${bold(white(p.label))}${i ? green(" ✓ v" + (v || "?")) : red(" ✗ not installed")}`);
|
|
597
|
+
console.log(` ${dim("pip install " + p.pip)}`);
|
|
598
|
+
if (i) console.log(` ${gray("from ")}${cyan(p.pyMod)}${gray(" import ")}${cyan(p.pyClass)}`);
|
|
599
|
+
});
|
|
600
|
+
console.log("\n" + div + "\n");
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function cmdPipUpdate() {
|
|
604
|
+
if (!pipAvailable()) { warnNoPip(); return; }
|
|
605
|
+
pipCache = buildPipCache();
|
|
606
|
+
const inst = PACKAGES.filter(p => isPipInst(p.pip));
|
|
607
|
+
if (!inst.length) { console.log(yellow("\n No pip packages installed. Run: sunnah pip install <book>\n")); return; }
|
|
608
|
+
const div2 = gray("═".repeat(60));
|
|
609
|
+
console.log("\n" + div2 + "\n " + bold(cyan(" Checking Python packages for updates…")) + "\n" + div2 + "\n");
|
|
610
|
+
const ups = [];
|
|
611
|
+
for (const p of inst) {
|
|
612
|
+
process.stdout.write(" " + gray("Checking ") + white(p.label) + gray("…\r"));
|
|
613
|
+
const cur = getVersion(p.pip, true), lat = getLatest(p.pip, true);
|
|
614
|
+
process.stdout.write("\x1b[K");
|
|
615
|
+
if (!cur || !lat) { console.log(` ${yellow("?")} ${bold(p.label)} ${gray("(could not check)")}`); continue; }
|
|
616
|
+
if (cur === lat) { console.log(` ${green("✓")} ${bold(p.label)} ${gray("v" + cur + " — up to date")}`); }
|
|
617
|
+
else { ups.push({ p, cur, lat }); console.log(` ${yellow("↑")} ${bold(p.label)} ${gray("v" + cur)} → ${green("v" + lat)} ${yellow("(update available)")}`); }
|
|
618
|
+
}
|
|
619
|
+
console.log("\n" + div2);
|
|
620
|
+
if (!ups.length) { console.log(" " + green("✓ All Python packages are up to date.")); console.log(div2 + "\n"); return; }
|
|
621
|
+
console.log(" " + yellow(String(ups.length)) + " update" + (ups.length > 1 ? "s" : "") + " available.");
|
|
622
|
+
console.log(" Run " + bold(cyan("sunnah pip update --install")) + gray(" to install all."));
|
|
623
|
+
console.log(div2 + "\n");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async function cmdUpdate(autoInstall = false) {
|
|
627
|
+
if (!npmAvailable()) { warnNoNpm(); return; }
|
|
628
|
+
installedCache = buildInstalledCache();
|
|
629
|
+
const inst = PACKAGES.filter(p => isInstalled(p.name));
|
|
630
|
+
if (!inst.length) { console.log("\n " + yellow("No sunnah packages installed. Run ") + bold("sunnah") + yellow(" to install.\n")); return; }
|
|
631
|
+
const div = gray("─".repeat(60)), div2 = gray("═".repeat(60));
|
|
632
|
+
console.log("\n" + div2 + "\n " + bold(cyan(" Checking for updates…")) + "\n" + div2 + "\n");
|
|
633
|
+
const ups = [];
|
|
634
|
+
for (const p of inst) {
|
|
635
|
+
process.stdout.write(" " + gray("Checking ") + white(p.label) + gray("…\r"));
|
|
636
|
+
const cur = getVersion(p.name), lat = getLatest(p.name);
|
|
637
|
+
process.stdout.write("\x1b[K");
|
|
638
|
+
if (!cur || !lat) { console.log(` ${yellow("?")} ${bold(p.label)} ${gray("(could not check)")}`); continue; }
|
|
639
|
+
if (cur === lat) { console.log(` ${green("✓")} ${bold(p.label)} ${gray("v" + cur + " — up to date")}`); }
|
|
640
|
+
else { ups.push({ p, cur, lat }); console.log(` ${yellow("↑")} ${bold(p.label)} ${gray("v" + cur)} → ${green("v" + lat)} ${yellow("(update available)")}`); }
|
|
641
|
+
}
|
|
642
|
+
console.log("\n" + div);
|
|
643
|
+
if (!ups.length) { console.log(" " + green("✓ All packages are up to date.")); console.log(div + "\n"); return; }
|
|
644
|
+
console.log(" " + yellow(String(ups.length)) + " update" + (ups.length > 1 ? "s" : "") + " available.");
|
|
645
|
+
if (autoInstall) {
|
|
646
|
+
console.log(bold(cyan("\n Installing updates…"))); console.log(div + "\n");
|
|
647
|
+
for (let i = 0; i < ups.length; i++) {
|
|
648
|
+
const { p, cur, lat } = ups[i];
|
|
649
|
+
console.log(" " + cyan("[" + (i + 1) + "/" + ups.length + "]") + " " + bold(white(p.label)) + gray(" v" + cur + " → v" + lat) + "\n");
|
|
650
|
+
await animateInstall(p.name);
|
|
651
|
+
console.log(" " + green("✓") + " " + bold(green(p.label)) + gray(" updated to v" + lat));
|
|
652
|
+
}
|
|
653
|
+
console.log("\n" + div2 + "\n " + green("✓ All updates installed.") + "\n" + div2 + "\n");
|
|
654
|
+
} else {
|
|
655
|
+
console.log(" Run " + bold(cyan("sunnah --update --install")) + gray(" to install all updates automatically."));
|
|
656
|
+
ups.forEach(({ p }) => console.log(" " + dim("sunnah install " + p.cmd)));
|
|
657
|
+
console.log(div + "\n");
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async function cmdInstall(targets) {
|
|
662
|
+
if (!targets.length) {
|
|
663
|
+
console.error(red("\n ✗ Usage: sunnah install <book> (e.g. sunnah install bukhari)\n"));
|
|
664
|
+
console.log(" Available: " + PACKAGES.map(p => cyan(p.cmd)).join(", ") + "\n");
|
|
665
|
+
process.exit(1);
|
|
666
|
+
}
|
|
667
|
+
if (!npmAvailable()) { warnNoNpm(); process.exit(1); }
|
|
668
|
+
const to = [];
|
|
669
|
+
for (const t of targets) {
|
|
670
|
+
const p = CMD_MAP[t.toLowerCase()] || NAME_MAP[t];
|
|
671
|
+
if (!p) { console.log(yellow(`\n ⚠ Unknown package: "${t}"`)); process.exit(1); }
|
|
672
|
+
to.push(p);
|
|
673
|
+
}
|
|
674
|
+
const div2 = gray("═".repeat(60));
|
|
675
|
+
console.log("\n" + div2);
|
|
676
|
+
console.log(bold(cyan(" Installing ")) + bold(yellow(String(to.length))) + bold(cyan(" package" + (to.length > 1 ? "s" : "") + "…")));
|
|
677
|
+
console.log(div2);
|
|
678
|
+
for (let i = 0; i < to.length; i++) {
|
|
679
|
+
const p = to[i];
|
|
680
|
+
console.log(`\n ${cyan("[" + (i + 1) + "/" + to.length + "]")} ${bold(white(p.label))}`);
|
|
681
|
+
console.log(" " + dim("npm install -g " + p.name) + "\n");
|
|
682
|
+
await animateInstall(p.name);
|
|
683
|
+
installedCache.set(p.name, true);
|
|
684
|
+
console.log(" " + green("✓") + " " + bold(green(p.label)) + " installed");
|
|
685
|
+
console.log(" " + gray("Usage: ") + cyan(p.cmd + " --help"));
|
|
686
|
+
console.log(" " + gray("Python: ") + dim("pip install " + p.pip));
|
|
687
|
+
}
|
|
688
|
+
console.log("\n" + div2);
|
|
689
|
+
console.log(" " + green("✓ Done! ") + to.map(p => bold(cyan(p.cmd))).join(", ") + gray(" ready."));
|
|
690
|
+
console.log(div2 + "\n");
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function cmdUninstall(targets) {
|
|
694
|
+
if (!targets.length) { console.error(red("\n ✗ Usage: sunnah uninstall <book>\n")); process.exit(1); }
|
|
695
|
+
if (!npmAvailable()) { warnNoNpm(); process.exit(1); }
|
|
696
|
+
installedCache = buildInstalledCache();
|
|
697
|
+
const div2 = gray("═".repeat(60));
|
|
698
|
+
console.log("\n" + div2);
|
|
699
|
+
for (const t of targets) {
|
|
700
|
+
const p = CMD_MAP[t.toLowerCase()] || NAME_MAP[t];
|
|
701
|
+
if (!p) { console.log(yellow(` ⚠ Unknown: "${t}"\n`)); continue; }
|
|
702
|
+
if (!isInstalled(p.name)) { console.log(gray(` ○ ${p.label} is not installed, skipping.`)); continue; }
|
|
703
|
+
console.log(yellow(" Uninstalling ") + bold(white(p.label)) + yellow("…"));
|
|
704
|
+
try { npmSync(["uninstall", "-g", p.name], { stdio: "inherit" }); installedCache.set(p.name, false); console.log(green(" ✓ ") + bold(green(p.label)) + green(" uninstalled.\n")); }
|
|
705
|
+
catch { console.log(red(" ✗ Failed to uninstall " + p.label + "\n")); }
|
|
706
|
+
}
|
|
707
|
+
console.log(div2 + "\n");
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function cmdVersion() {
|
|
711
|
+
installedCache = buildInstalledCache(); pipCache = buildPipCache();
|
|
712
|
+
const n = PACKAGES.filter(p => isInstalled(p.name)), pi = PACKAGES.filter(p => isPipInst(p.pip));
|
|
713
|
+
const div = gray("─".repeat(60));
|
|
714
|
+
console.log("\n" + div + "\n " + bold(cyan(" 📿 sunnah")) + gray(" v" + pkg.version) + "\n" + div);
|
|
715
|
+
console.log(" " + gray("Available : ") + yellow(String(PACKAGES.length)));
|
|
716
|
+
console.log(" " + gray("npm inst. : ") + green(String(n.length)) + gray(" / " + PACKAGES.length));
|
|
717
|
+
console.log(" " + gray("pip inst. : ") + (pi.length ? green(String(pi.length)) : gray("0")) + gray(" / " + PACKAGES.length));
|
|
718
|
+
if (n.length) {
|
|
719
|
+
console.log(" " + gray("Collection : ") + n.map(p => cyan(p.label)).join(gray(", ")));
|
|
720
|
+
const tot = n.reduce((a, p) => a + parseInt(p.hadiths.replace(/,/g, "")), 0);
|
|
721
|
+
console.log(" " + gray("Hadiths : ") + bold(yellow(tot.toLocaleString())));
|
|
722
|
+
}
|
|
723
|
+
console.log("\n" + div + "\n");
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function cmdHelp() {
|
|
727
|
+
installedCache = buildInstalledCache();
|
|
728
|
+
const div = gray("─".repeat(60));
|
|
729
|
+
console.log("\n" + div + "\n " + bold(cyan(" 📿 Sunnah Package Manager")) + gray(" v" + pkg.version) + "\n" + div);
|
|
730
|
+
console.log("\n " + bold("Commands:"));
|
|
731
|
+
[
|
|
732
|
+
["sunnah", "Open interactive installer UI ← arrow keys, space, enter"],
|
|
733
|
+
["sunnah install <book>", "Install npm package(s)"],
|
|
734
|
+
["sunnah uninstall <book>", "Uninstall npm package(s)"],
|
|
735
|
+
["sunnah pip install <book>", "Install Python (pip) package(s)"],
|
|
736
|
+
["sunnah pip list", "List Python package status"],
|
|
737
|
+
["sunnah pip update", "Check Python packages for updates"],
|
|
738
|
+
['sunnah search "<query>"', "Search across ALL installed books"],
|
|
739
|
+
["sunnah random [book]", "Random hadith from installed books"],
|
|
740
|
+
["sunnah info <book>", "Detailed info for a book"],
|
|
741
|
+
["sunnah --react [books]", "Generate unified useSunnah() React hook"],
|
|
742
|
+
["sunnah --list", "List all packages with install status"],
|
|
743
|
+
["sunnah --update", "Check npm packages for updates"],
|
|
744
|
+
["sunnah --update --install", "Auto-install all available updates"],
|
|
745
|
+
["sunnah -v", "Version + collection stats"],
|
|
746
|
+
["sunnah -h", "This help"],
|
|
747
|
+
].forEach(([cmd, desc]) => console.log(" " + cyan(cmd.padEnd(34)) + gray(desc)));
|
|
748
|
+
console.log("\n " + bold("Book names (cmd alias or full npm name):"));
|
|
749
|
+
PACKAGES.forEach(p => {
|
|
750
|
+
const inst = isInstalled(p.name);
|
|
751
|
+
console.log(" " + cyan(p.cmd.padEnd(12)) + gray(p.name.padEnd(26)) + yellow(p.hadiths + " hadiths") + (inst ? green(" ✓") : ""));
|
|
752
|
+
});
|
|
753
|
+
console.log("\n " + bold("Interactive UI controls:"));
|
|
754
|
+
[["↑ ↓", "Navigate"], ["space", "Toggle select"], ["a", "Select all / deselect all"],
|
|
755
|
+
["i", "Show info + installed version"], ["u", "Uninstall selected"],
|
|
756
|
+
["U", "Update selected"], ["enter", "Install selected"], ["q", "Quit"]
|
|
757
|
+
].forEach(([k, d]) => console.log(" " + green(k.padEnd(8)) + gray(d)));
|
|
758
|
+
console.log("\n " + bold("Examples:"));
|
|
759
|
+
["sunnah install bukhari", "sunnah install bukhari muslim nasai", "sunnah pip install bukhari",
|
|
760
|
+
'sunnah search "prayer"', "sunnah random", "sunnah --react", "sunnah --update"
|
|
761
|
+
].forEach(e => console.log(" " + dim(e)));
|
|
762
|
+
const tips = getPersonalizedSuggestions();
|
|
763
|
+
if (tips.length) { console.log("\n" + div + "\n " + bold("💡 Suggested for you:")); tips.forEach(t => console.log(t)); }
|
|
764
|
+
console.log("\n" + div + "\n");
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
768
|
+
async function main() {
|
|
769
|
+
const rawArgs = process.argv.slice(2);
|
|
770
|
+
const flags = rawArgs.filter(a => a.startsWith("-"));
|
|
771
|
+
const positional = rawArgs.filter(a => !a.startsWith("-"));
|
|
772
|
+
|
|
773
|
+
if (flags.some(f => f === "-v" || f === "--version")) { cmdVersion(); process.exit(0); }
|
|
774
|
+
if (flags.some(f => f === "-h" || f === "--help")) { cmdHelp(); process.exit(0); }
|
|
775
|
+
if (flags.some(f => f === "--list" || f === "-l")) { cmdList(); process.exit(0); }
|
|
776
|
+
if (flags.some(f => f === "--update")) { await cmdUpdate(flags.some(f => f === "--install")); process.exit(0); }
|
|
777
|
+
|
|
778
|
+
if (positional[0] === "install") { await cmdInstall(positional.slice(1)); process.exit(0); }
|
|
779
|
+
if (positional[0] === "uninstall") { cmdUninstall(positional.slice(1)); process.exit(0); }
|
|
780
|
+
if (positional[0] === "info") { cmdInfo(positional[1]); process.exit(0); }
|
|
781
|
+
if (positional[0] === "random") { await cmdRandom(positional[1] || null); process.exit(0); }
|
|
782
|
+
if (positional[0] === "search") { await cmdSearch(positional.slice(1).join(" "), flags.some(f => f === "--all")); process.exit(0); }
|
|
783
|
+
|
|
784
|
+
if (positional[0] === "pip") {
|
|
785
|
+
const sub = positional[1];
|
|
786
|
+
if (sub === "install") { await cmdPipInstall(positional.slice(2)); process.exit(0); }
|
|
787
|
+
if (sub === "list") { cmdPipList(); process.exit(0); }
|
|
788
|
+
if (sub === "update") { await cmdPipUpdate(); process.exit(0); }
|
|
789
|
+
console.error(red(`\n ✗ Unknown pip command: "${sub}". Use: install, list, update\n`)); process.exit(1);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (flags.some(f => f === "--react")) {
|
|
793
|
+
installedCache = buildInstalledCache();
|
|
794
|
+
const requestedCmds = positional;
|
|
795
|
+
let books;
|
|
796
|
+
if (requestedCmds.length > 0) {
|
|
797
|
+
books = requestedCmds.map(cmd => CMD_MAP[cmd.toLowerCase()]).filter(Boolean);
|
|
798
|
+
const unknown = requestedCmds.filter(cmd => !CMD_MAP[cmd.toLowerCase()]);
|
|
799
|
+
if (unknown.length) { console.log(yellow("\n ⚠ Unknown books: " + unknown.join(", "))); console.log(" Available: " + Object.keys(CMD_MAP).join(", ") + "\n"); }
|
|
800
|
+
} else {
|
|
801
|
+
books = PACKAGES.filter(p => isInstalled(p.name));
|
|
802
|
+
if (!books.length) { console.log(yellow("\n ⚠ No sunnah packages installed yet.\n Run ") + bold("sunnah") + yellow(" to install some first, or specify books:\n ") + dim("sunnah --react bukhari muslim") + "\n"); process.exit(1); }
|
|
803
|
+
}
|
|
804
|
+
if (!books.length) { console.log(red("\n ✗ No valid books specified.\n")); process.exit(1); }
|
|
805
|
+
generateUnifiedHook(books); process.exit(0);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ── Interactive TUI (default — no args given) ─────────────────────────────
|
|
809
|
+
process.stdout.write("\n " + gray("Checking installed packages…"));
|
|
810
|
+
installedCache = buildInstalledCache();
|
|
811
|
+
process.stdout.write("\r\x1b[K");
|
|
812
|
+
|
|
813
|
+
const persisted = loadState();
|
|
814
|
+
const lastSelected = new Set((persisted.lastSelected || []).filter(i => i < PACKAGES.length));
|
|
815
|
+
|
|
816
|
+
enterAltScreen();
|
|
817
|
+
hideCursor();
|
|
818
|
+
|
|
819
|
+
const state = {
|
|
820
|
+
cursor: 0,
|
|
821
|
+
selected: lastSelected,
|
|
822
|
+
mode: MODE.LIST,
|
|
823
|
+
confirmTarget: null,
|
|
824
|
+
statusMsg: "",
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
let statusTimer = null;
|
|
828
|
+
|
|
829
|
+
function setStatus(msg, ms = 2500) {
|
|
830
|
+
state.statusMsg = msg;
|
|
831
|
+
render(state);
|
|
832
|
+
if (statusTimer) clearTimeout(statusTimer);
|
|
833
|
+
statusTimer = setTimeout(() => { state.statusMsg = ""; render(state); }, ms);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
render(state);
|
|
837
|
+
prefetchUpdateCache().then(() => render(state));
|
|
838
|
+
|
|
839
|
+
const cleanup = () => {
|
|
840
|
+
saveState({ lastSelected: [...state.selected] });
|
|
841
|
+
showCursor();
|
|
842
|
+
leaveAltScreen();
|
|
843
|
+
try { process.stdin.setRawMode(false); } catch {}
|
|
844
|
+
process.stdin.pause();
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
process.on("SIGINT", () => { cleanup(); process.exit(0); });
|
|
848
|
+
|
|
849
|
+
// ── stdin setup — handles Windows PowerShell + cmd + Unix ─────────────────
|
|
850
|
+
process.stdin.resume();
|
|
851
|
+
readline.emitKeypressEvents(process.stdin);
|
|
852
|
+
try { process.stdin.setRawMode(true); } catch {}
|
|
853
|
+
|
|
854
|
+
let busy = false;
|
|
855
|
+
|
|
856
|
+
const keypressHandler = async (str, key) => {
|
|
857
|
+
if (!key) return;
|
|
858
|
+
if (str === "q" || str === "Q" || (key.ctrl && key.name === "c")) { cleanup(); process.exit(0); }
|
|
859
|
+
if (busy) return;
|
|
860
|
+
|
|
861
|
+
// ── Confirm uninstall mode ──────────────────────────────────────────────
|
|
862
|
+
if (state.mode === MODE.CONFIRM_UNINSTALL) {
|
|
863
|
+
if (str === "y" || str === "Y") {
|
|
864
|
+
const p = PACKAGES[state.confirmTarget];
|
|
865
|
+
state.mode = MODE.LIST; state.confirmTarget = null; busy = true;
|
|
866
|
+
cleanup();
|
|
867
|
+
console.log("\n " + yellow("Uninstalling ") + bold(white(p.label)) + yellow("…\n"));
|
|
868
|
+
try {
|
|
869
|
+
if (npmAvailable() && isInstalled(p.name)) { npmSync(["uninstall", "-g", p.name], { stdio: "inherit" }); installedCache.set(p.name, false); console.log(" " + green("✓") + " " + bold(p.label) + gray(" (npm) uninstalled")); }
|
|
870
|
+
if (pipAvailable() && isPipInst(p.pip)) { pipSync(["uninstall", "-y", p.pip], { stdio: "inherit" }); pipCache.set(p.pip, false); console.log(" " + green("✓") + " " + bold(p.label) + gray(" (pip) uninstalled")); }
|
|
871
|
+
state.selected.delete(PACKAGES.indexOf(p));
|
|
872
|
+
console.log("");
|
|
873
|
+
} catch { console.log("\n " + red("✗ Failed to uninstall " + p.label) + "\n"); }
|
|
874
|
+
busy = false;
|
|
875
|
+
reEnterMenu(state, render, prefetchUpdateCache, keypressHandler);
|
|
876
|
+
} else { state.mode = MODE.LIST; state.confirmTarget = null; render(state); }
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// ── Normal navigation ───────────────────────────────────────────────────
|
|
881
|
+
if (key.name === "up") { state.cursor = (state.cursor - 1 + PACKAGES.length) % PACKAGES.length; render(state); return; }
|
|
882
|
+
if (key.name === "down") { state.cursor = (state.cursor + 1) % PACKAGES.length; render(state); return; }
|
|
883
|
+
|
|
884
|
+
if (str === " ") {
|
|
885
|
+
state.selected.has(state.cursor) ? state.selected.delete(state.cursor) : state.selected.add(state.cursor);
|
|
886
|
+
render(state); return;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (str === "a" || str === "A") {
|
|
890
|
+
state.selected.size === PACKAGES.length ? state.selected.clear() : PACKAGES.forEach((_, i) => state.selected.add(i));
|
|
891
|
+
render(state); return;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (str === "i" || str === "I") {
|
|
895
|
+
const p = PACKAGES[state.cursor], inst = isInstalled(p.name), ver = inst ? getVersion(p.name) : null;
|
|
896
|
+
setStatus(`${p.label} | ${p.hadiths} hadiths | ${p.author} | ${inst ? "v" + ver + " installed CLI: " + p.cmd + " --help" : "not installed"}`, 4000);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// p = pip install
|
|
901
|
+
if (str === "p" || str === "P") {
|
|
902
|
+
if (!pipAvailable()) { setStatus("pip not found — install Python from https://python.org", 5000); return; }
|
|
903
|
+
const targets = state.selected.size > 0 ? [...state.selected].map(i => PACKAGES[i]) : [PACKAGES[state.cursor]];
|
|
904
|
+
const toInstall = targets.filter(p => !isPipInst(p.pip));
|
|
905
|
+
if (!toInstall.length) { setStatus("All selected already installed via pip."); return; }
|
|
906
|
+
busy = true; cleanup();
|
|
907
|
+
const div2 = gray("═".repeat(Math.min(W() - 2, 72)));
|
|
908
|
+
console.log("\n" + div2);
|
|
909
|
+
console.log(bold(cyan(" pip installing ")) + bold(yellow(String(toInstall.length))) + bold(cyan(" package" + (toInstall.length > 1 ? "s" : "") + "…")));
|
|
910
|
+
console.log(div2);
|
|
911
|
+
for (let i = 0; i < toInstall.length; i++) {
|
|
912
|
+
const p = toInstall[i];
|
|
913
|
+
console.log("\n " + cyan("[" + (i + 1) + "/" + toInstall.length + "]") + " " + bold(white(p.label)));
|
|
914
|
+
console.log(" " + dim("pip install " + p.pip) + "\n");
|
|
915
|
+
await animateInstall(p.pip, true);
|
|
916
|
+
pipCache.set(p.pip, true);
|
|
917
|
+
console.log(" " + green("✓") + " " + bold(green(p.label)) + " (pip) installed");
|
|
918
|
+
console.log(" " + gray("Python: ") + dim("from " + p.pyMod + " import " + p.pyClass));
|
|
919
|
+
}
|
|
920
|
+
console.log("\n" + div2 + "\n");
|
|
921
|
+
await sleep(600);
|
|
922
|
+
busy = false; reEnterMenu(state, render, prefetchUpdateCache, keypressHandler);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (str === "u") {
|
|
927
|
+
const targets = state.selected.size > 0 ? [...state.selected] : [state.cursor];
|
|
928
|
+
const toRemove = targets.filter(i => isInstalled(PACKAGES[i].name) || isPipInst(PACKAGES[i].pip));
|
|
929
|
+
if (!toRemove.length) { setStatus("No installed packages selected."); return; }
|
|
930
|
+
state.mode = MODE.CONFIRM_UNINSTALL; state.confirmTarget = toRemove[0]; render(state); return;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (str === "U") {
|
|
934
|
+
const targets = state.selected.size > 0 ? [...state.selected] : [state.cursor];
|
|
935
|
+
const toUpdate = targets.filter(i => (isInstalled(PACKAGES[i].name) || isPipInst(PACKAGES[i].pip)) && updateCache.get(PACKAGES[i].name)?.hasUpdate);
|
|
936
|
+
if (!toUpdate.length) { setStatus(updateReady ? "All selected packages are up to date." : "Update info still loading — try again shortly."); return; }
|
|
937
|
+
busy = true; cleanup();
|
|
938
|
+
const divW = Math.min(W() - 2, 72), div2 = gray("═".repeat(divW));
|
|
939
|
+
console.log("\n" + div2);
|
|
940
|
+
console.log(bold(cyan(" Updating ")) + bold(yellow(String(toUpdate.length))) + bold(cyan(" package" + (toUpdate.length > 1 ? "s" : "") + "…")));
|
|
941
|
+
console.log(div2);
|
|
942
|
+
for (let i = 0; i < toUpdate.length; i++) {
|
|
943
|
+
const p = PACKAGES[toUpdate[i]], uc = updateCache.get(p.name);
|
|
944
|
+
console.log("\n " + cyan("[" + (i + 1) + "/" + toUpdate.length + "]") + " " + bold(white(p.label)));
|
|
945
|
+
console.log(" " + dim(gray("v" + uc.current + " → v" + uc.latest)) + "\n");
|
|
946
|
+
await animateInstall(p.name);
|
|
947
|
+
updateCache.set(p.name, { current: uc.latest, latest: uc.latest, hasUpdate: false });
|
|
948
|
+
console.log(" " + green("✓") + " " + bold(green(p.label)) + gray(" updated to v" + uc.latest));
|
|
949
|
+
}
|
|
950
|
+
console.log("\n" + div2 + "\n");
|
|
951
|
+
busy = false; reEnterMenu(state, render, prefetchUpdateCache, keypressHandler);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (key.name === "return") {
|
|
956
|
+
if (!npmAvailable()) { setStatus("npm not found — install Node.js from https://nodejs.org", 5000); return; }
|
|
957
|
+
const targets = state.selected.size > 0 ? [...state.selected].map(i => PACKAGES[i]) : [PACKAGES[state.cursor]];
|
|
958
|
+
const toInstall = targets.filter(p => !isInstalled(p.name));
|
|
959
|
+
|
|
960
|
+
if (!toInstall.length) { setStatus("All selected already installed via npm. Press p to pip install."); return; }
|
|
961
|
+
|
|
962
|
+
busy = true; cleanup();
|
|
963
|
+
const divW = Math.min(W() - 2, 72);
|
|
964
|
+
const div = gray("─".repeat(divW)), div2 = gray("═".repeat(divW));
|
|
965
|
+
|
|
966
|
+
console.log("\n" + div2);
|
|
967
|
+
console.log(bold(cyan(" Installing ")) + bold(yellow(String(toInstall.length))) + bold(cyan(" package" + (toInstall.length > 1 ? "s" : "") + "…")));
|
|
968
|
+
console.log(div2);
|
|
969
|
+
|
|
970
|
+
for (let i = 0; i < toInstall.length; i++) {
|
|
971
|
+
const p = toInstall[i];
|
|
972
|
+
console.log("\n " + cyan("[" + (i + 1) + "/" + toInstall.length + "]") + " " + bold(white(p.label)));
|
|
973
|
+
console.log(" " + dim("npm install -g " + p.name) + "\n");
|
|
974
|
+
await animateInstall(p.name);
|
|
975
|
+
installedCache.set(p.name, true);
|
|
976
|
+
console.log(" " + green("✓") + " " + bold(green(p.label)) + " installed");
|
|
977
|
+
const uc = updateCache.get(p.name);
|
|
978
|
+
if (uc && uc.current) {
|
|
979
|
+
console.log(" " + gray("Version: ") + cyan("v" + uc.current) + (uc.hasUpdate ? " " + gray("Latest: ") + green("v" + uc.latest) + " " + yellow("↑ update available") : uc.latest ? " " + dim(gray("(up to date)")) : ""));
|
|
980
|
+
}
|
|
981
|
+
console.log(" " + gray("Usage: ") + cyan(p.cmd + " --help"));
|
|
982
|
+
console.log(" " + gray("Python: ") + dim("pip install " + p.pip));
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
console.log("\n" + div2);
|
|
986
|
+
console.log(" " + green("✓ All done! ") + bold(yellow(String(toInstall.length))) + " package" + (toInstall.length > 1 ? "s" : "") + " installed globally.");
|
|
987
|
+
console.log("");
|
|
988
|
+
toInstall.forEach(p => console.log(" " + cyan("▸") + " " + bold(p.cmd) + gray(" --help") + " " + dim(p.label)));
|
|
989
|
+
|
|
990
|
+
const allInstalled = PACKAGES.filter(p => isInstalled(p.name));
|
|
991
|
+
if (allInstalled.length > 1) {
|
|
992
|
+
console.log("\n " + dim("Tip: run ") + bold("sunnah --react") + dim(" to generate a unified React hook for all your books"));
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
await sleep(800);
|
|
996
|
+
busy = false;
|
|
997
|
+
reEnterMenu(state, render, prefetchUpdateCache, keypressHandler);
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
process.stdin.on("keypress", keypressHandler);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
1005
|
+
|
|
1006
|
+
function reEnterMenu(state, render, prefetchUpdateCache, keypressHandler) {
|
|
1007
|
+
installedCache = buildInstalledCache();
|
|
1008
|
+
updateReady = false;
|
|
1009
|
+
updateCache.clear();
|
|
1010
|
+
process.stdin.removeAllListeners("keypress");
|
|
1011
|
+
process.stdin.pause();
|
|
1012
|
+
enterAltScreen();
|
|
1013
|
+
hideCursor();
|
|
1014
|
+
render(state);
|
|
1015
|
+
prefetchUpdateCache().then(() => render(state));
|
|
1016
|
+
process.stdin.resume();
|
|
1017
|
+
readline.emitKeypressEvents(process.stdin);
|
|
1018
|
+
try { process.stdin.setRawMode(true); } catch {}
|
|
1019
|
+
process.stdin.on("keypress", keypressHandler);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
main().catch(err => { showCursor(); leaveAltScreen(); console.error(red("\n ✗ " + err.message + "\n")); process.exit(1); });
|