stella-coder 3.9.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.
@@ -0,0 +1,570 @@
1
+ import readline from "node:readline"
2
+ import { fullSystemScan, quickScan, scanPath, scanFile, scanRunningProcesses, scanBootSector, startRealTimeMonitor, stopRealTimeMonitor, getMonitorStatus, quarantineFile, listQuarantine, restoreFromQuarantine, loadExclusions, addExclusion, removeExclusion, getExclusions, generateReport, saveReport } from "./scanner.mjs"
3
+ import { QUARANTINE_DIR } from "./database.mjs"
4
+ import fs from "node:fs"
5
+ import path from "node:path"
6
+
7
+ // ═══════════════════════════════════════════════════════════════
8
+ // STELLAR ANTIVIRUS — Kaspersky-style UI
9
+ // ═══════════════════════════════════════════════════════════════
10
+
11
+ const COLORS = {
12
+ reset: "\x1b[0m",
13
+ bold: "\x1b[1m",
14
+ dim: "\x1b[2m",
15
+ italic: "\x1b[3m",
16
+ underline: "\x1b[4m",
17
+
18
+ red: "\x1b[31m",
19
+ green: "\x1b[32m",
20
+ yellow: "\x1b[33m",
21
+ blue: "\x1b[34m",
22
+ magenta: "\x1b[35m",
23
+ cyan: "\x1b[36m",
24
+ white: "\x1b[37m",
25
+ gray: "\x1b[90m",
26
+
27
+ bgRed: "\x1b[41m",
28
+ bgGreen: "\x1b[42m",
29
+ bgYellow: "\x1b[43m",
30
+ bgBlue: "\x1b[44m",
31
+ bgMagenta: "\x1b[45m",
32
+ bgCyan: "\x1b[46m",
33
+ }
34
+
35
+ function c(color, text) {
36
+ return `${COLORS[color]}${text}${COLORS.reset}`
37
+ }
38
+
39
+ function bold(text) { return c("bold", text) }
40
+ function dim(text) { return c("dim", text) }
41
+ function red(text) { return c("red", text) }
42
+ function green(text) { return c("green", text) }
43
+ function yellow(text) { return c("yellow", text) }
44
+ function cyan(text) { return c("cyan", text) }
45
+ function magenta(text) { return c("magenta", text) }
46
+
47
+ function box(title, lines, color = [0, 200, 255]) {
48
+ const [r, g, b] = color
49
+ const ansiColor = `\x1b[38;2;${r};${g};${b}m`
50
+ const reset = "\x1b[0m"
51
+ const maxLen = Math.max(title.length + 4, ...lines.map(l => l.length))
52
+ const width = maxLen + 4
53
+
54
+ const top = `${ansiColor}╭${"─".repeat(width)}╮${reset}`
55
+ const bottom = `${ansiColor}╰${"─".repeat(width)}╯${reset}`
56
+ const titleLine = `${ansiColor}│${reset} ${ansiColor}${COLORS.bold}${title}${reset}${" ".repeat(width - title.length - 2)}${ansiColor}│${reset}`
57
+
58
+ const contentLines = lines.map(l =>
59
+ `${ansiColor}│${reset} ${l}${" ".repeat(Math.max(0, width - l.length - 2))}${ansiColor}│${reset}`
60
+ )
61
+
62
+ return [top, titleLine, `${ansiColor}│${reset}${" ".repeat(width)}${ansiColor}│${reset}`, ...contentLines, bottom].join("\n")
63
+ }
64
+
65
+ function progressLine(current, total, width = 40) {
66
+ const pct = Math.floor((current / total) * 100)
67
+ const filled = Math.floor((current / total) * width)
68
+ const empty = width - filled
69
+ const bar = "█".repeat(filled) + "░".repeat(empty)
70
+ return ` ${cyan(bar)} ${pct}% (${current}/${total})`
71
+ }
72
+
73
+ function statusIcon(severity) {
74
+ switch (severity) {
75
+ case "critical": return red("●")
76
+ case "high": return yellow("●")
77
+ case "medium": return cyan("●")
78
+ case "low": return dim("●")
79
+ default: return green("●")
80
+ }
81
+ }
82
+
83
+ function severityColor(severity) {
84
+ switch (severity) {
85
+ case "critical": return red
86
+ case "high": return yellow
87
+ case "medium": return cyan
88
+ default: return dim
89
+ }
90
+ }
91
+
92
+ // ═══════════════════════════════════════════════════════════════
93
+ // BANNER
94
+ // ═══════════════════════════════════════════════════════════════
95
+
96
+ function printBanner() {
97
+ const gradient = [
98
+ [167, 139, 250], [165, 136, 250], [163, 133, 250], [161, 130, 249],
99
+ [159, 127, 249], [157, 124, 249], [155, 121, 248], [153, 118, 248],
100
+ [151, 115, 248], [149, 112, 248], [147, 109, 247], [145, 106, 247],
101
+ [143, 103, 247], [141, 100, 247], [139, 97, 246], [137, 94, 246],
102
+ [135, 97, 246], [133, 100, 246], [131, 103, 247], [129, 106, 247],
103
+ [127, 109, 247], [125, 112, 247], [123, 115, 246], [121, 118, 246],
104
+ [119, 121, 246], [117, 124, 245], [115, 127, 245], [113, 130, 245],
105
+ [111, 133, 245], [109, 136, 244], [107, 139, 244], [105, 142, 243],
106
+ ]
107
+
108
+ const logo = [
109
+ " ███████╗████████╗██████╗ ███████╗██████╗ ███╗ ███╗",
110
+ " ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██╔══██╗████╗ ████║",
111
+ " ███████╗ ██║ ██████╔╝█████╗ ██████╔╝██╔████╔██║",
112
+ " ╚════██║ ██║ ██╔══██╗██╔══╝ ██╔══██╗██║╚██╔╝██║",
113
+ " ███████║ ██║ ██║ ██║███████╗██║ ██║██║ ╚═╝ ██║",
114
+ " ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝",
115
+ ]
116
+
117
+ console.log()
118
+ for (const line of logo) {
119
+ let colored = ""
120
+ for (let i = 0; i < line.length; i++) {
121
+ const colorIdx = Math.floor((i / line.length) * gradient.length)
122
+ const [r, g, b] = gradient[Math.min(colorIdx, gradient.length - 1)]
123
+ colored += `\x1b[38;2;${r};${g};${b}m${line[i]}`
124
+ }
125
+ console.log(colored + "\x1b[0m")
126
+ }
127
+
128
+ console.log()
129
+ console.log(` ${magenta("✦")} ${bold("Stellar Antivirus")}${dim(" · powered by ")}${cyan("Stella")} ${dim("Security Engine")}`)
130
+ console.log()
131
+ }
132
+
133
+ function printStatusBanner(status, detail = "") {
134
+ const [r, g, b] = status === "safe" ? [0, 200, 100] : status === "warning" ? [255, 200, 0] : [255, 80, 80]
135
+ const ansi = `\x1b[38;2;${r};${g};${b}m`
136
+ const icon = status === "safe" ? "✓" : status === "warning" ? "⚠" : "✗"
137
+
138
+ console.log()
139
+ console.log(`${ansi} ╔══════════════════════════════════════════════════════════╗${COLORS.reset}`)
140
+ console.log(`${ansi} ║ ${icon} ${bold(detail || (status === "safe" ? "СИСТЕМА ЗАЩИЩЕНА" : "ОБНАРУЖЕНЫ УГРОЗЫ"))}${" ".repeat(Math.max(0, 52 - (detail || "").length))}║${COLORS.reset}`)
141
+ console.log(`${ansi} ╚══════════════════════════════════════════════════════════╝${COLORS.reset}`)
142
+ console.log()
143
+ }
144
+
145
+ // ═══════════════════════════════════════════════════════════════
146
+ // SCAN RESULTS DISPLAY
147
+ // ═══════════════════════════════════════════════════════════════
148
+
149
+ function displayScanResults(results, scanType = "full") {
150
+ const filtered = results.filter(r => !r.excluded)
151
+ const excludedCount = results.filter(r => r.excluded).length
152
+ const threats = filtered.filter(r => !r.clean && !r.error)
153
+ const warnings = filtered.filter(r => r.clean && r.warnings && r.warnings.length > 0)
154
+ const clean = filtered.filter(r => r.clean && (!r.warnings || r.warnings.length === 0))
155
+ const errors = filtered.filter(r => r.error)
156
+
157
+ const scanTypeNames = {
158
+ full: "ПОЛНОЕ СКАНИРОВАНИЕ",
159
+ quick: "БЫСТРОЕ СКАНИРОВАНИЕ",
160
+ custom: "СКАНИРОВАНИЕ КАТАЛОГА",
161
+ "boot-sector": "СКАНИРОВАНИЕ MBR",
162
+ }
163
+
164
+ console.log()
165
+ console.log(box(scanTypeNames[scanType] || "СКАНИРОВАНИЕ", [
166
+ `Файлов: ${results.length}`,
167
+ `Угрозы: ${threats.length > 0 ? red(String(threats.length)) : green(String(threats.length))}`,
168
+ `Предупрежд.: ${warnings.length > 0 ? yellow(String(warnings.length)) : green(String(warnings.length))}`,
169
+ `Чисто: ${green(String(clean.length))}`,
170
+ `Исключено: ${dim(String(excludedCount))}`,
171
+ `Ошибки: ${errors.length > 0 ? red(String(errors.length)) : green(String(errors.length))}`,
172
+ ], threats.length > 0 ? [255, 80, 80] : [0, 200, 100]))
173
+
174
+ if (threats.length > 0) {
175
+ console.log(`\n ${red(bold("══════ УГРОЗЫ ══════"))}`)
176
+ console.log()
177
+ for (const t of threats) {
178
+ console.log(` ${red("●")} ${bold(path.basename(t.filepath))}`)
179
+ console.log(` ${dim(t.filepath)}`)
180
+ if (t.threats && t.threats.length > 0) {
181
+ for (const d of t.threats) {
182
+ const color = severityColor(d.severity)
183
+ console.log(` ${color("▸")} ${d.message}`)
184
+ }
185
+ }
186
+ console.log()
187
+ }
188
+ }
189
+
190
+ if (warnings.length > 0) {
191
+ console.log(`\n ${yellow(bold("══════ ПРЕДУПРЕЖДЕНИЯ ══════"))}`)
192
+ console.log()
193
+ for (const w of warnings.slice(0, 20)) {
194
+ console.log(` ${yellow("●")} ${bold(path.basename(w.filepath))}`)
195
+ for (const d of w.warnings) {
196
+ const color = severityColor(d.severity)
197
+ console.log(` ${color("▸")} ${d.message}`)
198
+ }
199
+ }
200
+ if (warnings.length > 20) {
201
+ console.log(` ${dim(`... и ещё ${warnings.length - 20} предупреждений`)}`)
202
+ }
203
+ console.log()
204
+ }
205
+
206
+ if (threats.length === 0 && warnings.length === 0) {
207
+ printStatusBanner("safe")
208
+ } else {
209
+ printStatusBanner("danger")
210
+ }
211
+ }
212
+
213
+ // ═══════════════════════════════════════════════════════════════
214
+ // MAIN MENU
215
+ // ═══════════════════════════════════════════════════════════════
216
+
217
+ function printMenu() {
218
+ console.log(box("✦ Stellar AV — Команды", [
219
+ "",
220
+ ` ${cyan("1")} │ ${bold("Полное сканирование")} Сканировать весь компьютер (C:\\, D:\\, ...)`,
221
+ ` ${cyan("2")} │ ${bold("Быстрое сканирование")} Temp, Downloads, Desktop, Startup`,
222
+ ` ${cyan("3")} │ ${bold("Сканировать файл")} Проверить один файл`,
223
+ ` ${cyan("4")} │ ${bold("Сканировать папку")} Проверить директорию`,
224
+ ` ${cyan("5")} │ ${bold("Сканировать MBR")} Проверить загрузочный сектор`,
225
+ ` ${cyan("6")} │ ${bold("Процессы")} Проверить запущенные процессы`,
226
+ ` ${cyan("7")} │ ${bold("Мониторинг 24/7")} Реальное время — следить за файлами`,
227
+ ` ${cyan("8")} │ ${bold("Карантин")} Удалённые угрозы`,
228
+ ` ${cyan("9")} │ ${bold("Исключения")} Список исключений`,
229
+ ` ${cyan("10")}│ ${bold("Отчёт")} Сохранить отчёт в JSON`,
230
+ "",
231
+ ` ${cyan("/exit")} │ ${dim("Выход")}`,
232
+ ], [0, 200, 255]))
233
+ console.log()
234
+ }
235
+
236
+ // ═══════════════════════════════════════════════════════════════
237
+ // INTERACTIVE MODE
238
+ // ═══════════════════════════════════════════════════════════════
239
+
240
+ export function startInteractive() {
241
+ const rl = readline.createInterface({
242
+ input: process.stdin,
243
+ output: process.stdout,
244
+ })
245
+
246
+ printBanner()
247
+ printStatusBanner("safe", "Stellar Antivirus запущен — готов к сканированию")
248
+ printMenu()
249
+
250
+ const ask = () => {
251
+ rl.question(`${magenta("❯")} `, async (input) => {
252
+ const cmd = input.trim().toLowerCase()
253
+
254
+ switch (cmd) {
255
+ case "1":
256
+ case "full":
257
+ case "полное": {
258
+ console.log()
259
+ console.log(` ${cyan("▶")} ${bold("Полное сканирование системы...")}`)
260
+ console.log(` ${dim("Сканирование всех дисков C:\\, D:\\, ...")}`)
261
+ console.log()
262
+
263
+ let lastUpdate = Date.now()
264
+ const { results, totalFiles } = fullSystemScan({
265
+ onProgress: (count, file) => {
266
+ if (Date.now() - lastUpdate > 100) {
267
+ process.stdout.write(`\r ${progressLine(count, Math.max(count, 500))} ${dim(path.basename(file).substring(0, 30))}`)
268
+ lastUpdate = Date.now()
269
+ }
270
+ },
271
+ })
272
+
273
+ process.stdout.write("\r" + " ".repeat(80) + "\r")
274
+ displayScanResults(results, "full")
275
+ break
276
+ }
277
+
278
+ case "2":
279
+ case "quick":
280
+ case "быстрое": {
281
+ console.log()
282
+ console.log(` ${cyan("▶")} ${bold("Быстрое сканирование...")}`)
283
+ console.log(` ${dim("Temp, Downloads, Desktop, Startup, AppData")}`)
284
+ console.log()
285
+
286
+ let lastUpdate = Date.now()
287
+ const { results } = quickScan({
288
+ onProgress: (count, file) => {
289
+ if (Date.now() - lastUpdate > 100) {
290
+ process.stdout.write(`\r ${progressLine(count, Math.max(count, 200))} ${dim(path.basename(file).substring(0, 30))}`)
291
+ lastUpdate = Date.now()
292
+ }
293
+ },
294
+ })
295
+
296
+ process.stdout.write("\r" + " ".repeat(80) + "\r")
297
+ displayScanResults(results, "quick")
298
+ break
299
+ }
300
+
301
+ case "3":
302
+ case "file":
303
+ case "файл": {
304
+ rl.question(` Введите путь к файлу: `, (filepath) => {
305
+ if (!filepath.trim()) { ask(); return }
306
+ console.log()
307
+ const result = scanFile(filepath.trim())
308
+ if (result.error) {
309
+ console.log(` ${red("✗")} ${result.error}`)
310
+ } else {
311
+ displayScanResults([result], "custom")
312
+ }
313
+ ask()
314
+ })
315
+ return
316
+ }
317
+
318
+ case "4":
319
+ case "folder":
320
+ case "папка": {
321
+ rl.question(` Введите путь к папке: `, (dirpath) => {
322
+ if (!dirpath.trim()) { ask(); return }
323
+ console.log()
324
+ console.log(` ${cyan("▶")} ${bold("Сканирование:")} ${dirpath}`)
325
+ console.log()
326
+
327
+ let lastUpdate = Date.now()
328
+ const { results } = scanPath(dirpath.trim(), {
329
+ onProgress: (count, file) => {
330
+ if (Date.now() - lastUpdate > 100) {
331
+ process.stdout.write(`\r ${progressLine(count, Math.max(count, 100))} ${dim(path.basename(file).substring(0, 30))}`)
332
+ lastUpdate = Date.now()
333
+ }
334
+ },
335
+ })
336
+
337
+ process.stdout.write("\r" + " ".repeat(80) + "\r")
338
+ displayScanResults(results, "custom")
339
+ ask()
340
+ })
341
+ return
342
+ }
343
+
344
+ case "5":
345
+ case "mbr":
346
+ case "загрузчик": {
347
+ console.log()
348
+ console.log(` ${cyan("▶")} ${bold("Сканирование MBR (Master Boot Record)...")}`)
349
+ console.log()
350
+
351
+ const drives = ["C:", "D:", "E:"]
352
+ for (const drive of drives) {
353
+ if (fs.existsSync(drive + "\\")) {
354
+ console.log(` Проверка ${drive}\\ ...`)
355
+ const result = scanBootSector(drive)
356
+ if (result.error) {
357
+ console.log(` ${yellow("⚠")} ${result.error}`)
358
+ } else {
359
+ if (!result.clean) {
360
+ console.log(` ${red("✗")} ${drive} MBR: ОБНАРУЖЕНА УГРОЗА!`)
361
+ for (const t of result.threats) console.log(` ${red("▸")} ${t.message}`)
362
+ } else {
363
+ console.log(` ${green("✓")} ${drive} MBR: чисто`)
364
+ }
365
+ }
366
+ }
367
+ }
368
+ console.log()
369
+ break
370
+ }
371
+
372
+ case "6":
373
+ case "processes":
374
+ case "процессы": {
375
+ console.log()
376
+ console.log(` ${cyan("▶")} ${bold("Проверка запущенных процессов...")}`)
377
+ console.log()
378
+
379
+ const procs = scanRunningProcesses()
380
+ if (procs.length === 0) {
381
+ console.log(` ${yellow("⚠")} Не удалось получить список процессов`)
382
+ } else {
383
+ const susp = procs.filter(p => p.suspicious)
384
+ const normal = procs.filter(p => !p.suspicious)
385
+
386
+ console.log(` Всего процессов: ${procs.length}`)
387
+ console.log(` ${green("Чистых:")} ${normal.length}`)
388
+ if (susp.length > 0) {
389
+ console.log(` ${red("Подозрительных:")} ${susp.length}`)
390
+ console.log()
391
+ console.log(` ${red(bold("══════ ПОДОЗРИТЕЛЬНЫЕ ══════"))}`)
392
+ for (const p of susp) {
393
+ console.log(` ${red("●")} ${bold(p.name)} (PID: ${p.pid}, Память: ${p.memory})`)
394
+ }
395
+ }
396
+ }
397
+ console.log()
398
+ break
399
+ }
400
+
401
+ case "7":
402
+ case "monitor":
403
+ case "мониторинг": {
404
+ const status = getMonitorStatus()
405
+ if (status.active) {
406
+ console.log()
407
+ console.log(` ${green("✓")} Мониторинг активен`)
408
+ console.log(` ${dim("Отслеживаемых файлов:")} ${status.trackedFiles}`)
409
+ console.log()
410
+ rl.question(` Остановить мониторинг? (y/n): `, (ans) => {
411
+ if (ans.toLowerCase() === "y" || ans.toLowerCase() === "да") {
412
+ stopRealTimeMonitor()
413
+ console.log(` ${green("✓")} Мониторинг остановлен`)
414
+ }
415
+ ask()
416
+ })
417
+ return
418
+ } else {
419
+ rl.question(` Путь для мониторинга (Enter = текущая папка): `, (dirpath) => {
420
+ const dir = dirpath.trim() || "."
421
+ console.log()
422
+ console.log(` ${cyan("▶")} ${bold("Запуск мониторинга:")} ${dir}`)
423
+ console.log(` ${dim("Проверка каждые 5 секунд...")}`)
424
+ console.log(` ${dim("Нажмите Ctrl+C для остановки")}`)
425
+ console.log()
426
+
427
+ startRealTimeMonitor(dir, {
428
+ interval: 5000,
429
+ onThreat: (result) => {
430
+ console.log()
431
+ console.log(` ${red("⚠ ОБНАРУЖЕНА УГРОЗА!")}`)
432
+ console.log(` ${bold(path.basename(result.filepath))}`)
433
+ for (const t of result.threats) {
434
+ console.log(` ${red("▸")} ${t.message}`)
435
+ }
436
+ console.log()
437
+ },
438
+ onFileChange: (file, type) => {
439
+ const icon = type === "new" ? green("+") : type === "modified" ? yellow("~") : red("-")
440
+ process.stdout.write(`\r ${icon} ${dim(path.basename(file).substring(0, 50))}`)
441
+ },
442
+ })
443
+ console.log()
444
+ ask()
445
+ })
446
+ return
447
+ }
448
+ }
449
+
450
+ case "8":
451
+ case "quarantine":
452
+ case "карантин": {
453
+ const items = listQuarantine()
454
+ console.log()
455
+ if (items.length === 0) {
456
+ console.log(` ${green("✓")} Карантин пуст`)
457
+ } else {
458
+ console.log(box("Карантин", [`Файлов: ${items.length}`], [255, 200, 0]))
459
+ console.log()
460
+ for (const item of items) {
461
+ console.log(` ${yellow("●")} ${bold(item.originalPath)}`)
462
+ console.log(` ${dim("Дата:")} ${item.timestamp}`)
463
+ console.log()
464
+ }
465
+ rl.question(` Восстановить файл? (y/n): `, (ans) => {
466
+ if (ans.toLowerCase() === "y") {
467
+ rl.question(` Номер файла (1-${items.length}): `, (num) => {
468
+ const idx = parseInt(num) - 1
469
+ if (idx >= 0 && idx < items.length) {
470
+ const quarantinePath = path.join(QUARANTINE_DIR, items[idx].file)
471
+ const result = restoreFromQuarantine(quarantinePath)
472
+ if (result.error) {
473
+ console.log(` ${red("✗")} ${result.error}`)
474
+ } else {
475
+ console.log(` ${green("✓")} Восстановлено: ${result.restored}`)
476
+ }
477
+ }
478
+ ask()
479
+ })
480
+ return
481
+ }
482
+ ask()
483
+ })
484
+ return
485
+ }
486
+ break
487
+ }
488
+
489
+ case "9":
490
+ case "exclusions":
491
+ case "исключения": {
492
+ const excs = getExclusions()
493
+ console.log()
494
+ console.log(box("Исключения", [`Правил: ${excs.length}`], [0, 200, 255]))
495
+ console.log()
496
+ for (const exc of excs) {
497
+ console.log(` ${cyan("●")} ${exc}`)
498
+ }
499
+ console.log()
500
+ rl.question(` Добавить исключение (Enter = пропустить): `, (pattern) => {
501
+ if (pattern.trim()) {
502
+ addExclusion(pattern.trim())
503
+ console.log(` ${green("✓")} Добавлено: ${pattern.trim()}`)
504
+ }
505
+ ask()
506
+ })
507
+ return
508
+ }
509
+
510
+ case "10":
511
+ case "report":
512
+ case "отчёт": {
513
+ console.log()
514
+ rl.question(` Тип отчёта (full/quick): `, (type) => {
515
+ console.log(` ${cyan("▶")} ${bold("Создание отчёта...")}`)
516
+ console.log()
517
+
518
+ const scanFunc = type === "quick" ? quickScan : fullSystemScan
519
+ const { results } = scanFunc()
520
+ const report = generateReport(results, type || "full")
521
+
522
+ const filename = `stellar-report-${new Date().toISOString().slice(0, 10)}.json`
523
+ saveReport(report, filename)
524
+ console.log(` ${green("✓")} Отчёт сохранён: ${filename}`)
525
+ console.log()
526
+ ask()
527
+ })
528
+ return
529
+ }
530
+
531
+ case "/exit":
532
+ case "exit":
533
+ case "выход": {
534
+ console.log()
535
+ console.log(` ${magenta("✦")} ${dim("Stellar Antivirus — до встречи!")}`)
536
+ console.log()
537
+ rl.close()
538
+ return
539
+ }
540
+
541
+ case "/help":
542
+ case "help":
543
+ case "помощь": {
544
+ printMenu()
545
+ break
546
+ }
547
+
548
+ default: {
549
+ console.log(` ${yellow("⚠")} Неизвестная команда. Введите ${cyan("/help")} для справки.`)
550
+ console.log()
551
+ }
552
+ }
553
+
554
+ ask()
555
+ })
556
+ }
557
+
558
+ ask()
559
+ }
560
+
561
+ // ═══════════════════════════════════════════════════════════════
562
+ // EXPORTS
563
+ // ═══════════════════════════════════════════════════════════════
564
+
565
+ export { printBanner, displayScanResults, box, statusIcon, severityColor }
566
+
567
+ // Run if executed directly
568
+ if (process.argv[1] && process.argv[1].includes("ui.mjs")) {
569
+ startInteractive()
570
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "base-nova",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ },
20
+ "iconLibrary": "lucide"
21
+ }