stella-coder 5.2.1 → 5.3.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.
@@ -26,6 +26,13 @@ import { ScreenMonitor } from "./screen-monitor.mjs"
26
26
  import { ChromeBrowser } from "./browser-control.mjs"
27
27
  import { GitAPI } from "./git-api.mjs"
28
28
  import { WebParser } from "./web-parser.mjs"
29
+ import { GmailClient } from "./gmail.mjs"
30
+ import { ChartGenerator } from "./charts.mjs"
31
+ import { YandexMaps } from "./yandex-maps.mjs"
32
+ import { GameEngine } from "./game-engine.mjs"
33
+ import { GDriveBackup } from "./gdrive-backup.mjs"
34
+ import { ADB } from "./adb.mjs"
35
+ import { HomeAssistant } from "./home-assistant.mjs"
29
36
  import {
30
37
  buildRepoMap, buildProjectContext, compressContext,
31
38
  loadSpec, generateSpecTemplate,
@@ -692,6 +699,78 @@ const COMMANDS = [
692
699
  ["/web-search", "поиск в интернете"],
693
700
  ["/web-batch", "загрузить несколько страниц"],
694
701
 
702
+ // Gmail
703
+ ["/gmail-setup", "настроить Gmail API (OAuth2)"],
704
+ ["/gmail-auth", "завершить OAuth2 авторизацию"],
705
+ ["/gmail-send", "отправить email"],
706
+ ["/gmail-inbox", "последние письма"],
707
+ ["/gmail-search", "поиск писем"],
708
+ ["/gmail-read", "прочитать письмо"],
709
+ ["/gmail-labels", "список меток"],
710
+ ["/gmail-read-all", "пометить все прочитанным"],
711
+
712
+ // Charts
713
+ ["/chart-bar", "столбчатая диаграмма"],
714
+ ["/chart-line", "линейный график"],
715
+ ["/chart-pie", "круговая диаграмма"],
716
+ ["/chart-doughnut", "кольцевая диаграмма"],
717
+ ["/chart-radar", "лепестковая диаграмма"],
718
+ ["/chart-scatter", "точечный график"],
719
+ ["/chart-csv", "график из CSV файла"],
720
+ ["/chart-list", "список сохранённых графиков"],
721
+
722
+ // Yandex Maps
723
+ ["/ymaps-key", "установить API ключ Яндекс.Карт"],
724
+ ["/ymaps-geo", "геокодирование (адрес → координаты)"],
725
+ ["/ymaps-show", "показать место на карте"],
726
+ ["/ymaps-route", "маршрут от A до B"],
727
+ ["/ymaps-places", "показать несколько мест"],
728
+ ["/ymaps-static", "скачать статическую карту"],
729
+
730
+ // Games
731
+ ["/snake", "игра Змейка"],
732
+ ["/tetris", "игра Тетрис"],
733
+ ["/minesweeper", "игра Сапёр"],
734
+ ["/2048", "игра 2048"],
735
+ ["/flappy", "игра Flappy Bird"],
736
+ ["/tictactoe", "Крестики-нолики"],
737
+ ["/sudoku", "игра Судоку"],
738
+
739
+ // Google Drive Backup
740
+ ["/gdrive-setup", "настроить Google Drive API"],
741
+ ["/gdrive-auth", "завершить OAuth2 авторизацию"],
742
+ ["/gdrive-backup", "создать бэкап проекта"],
743
+ ["/gdrive-list", "список файлов на Drive"],
744
+ ["/gdrive-quota", "использовано места"],
745
+ ["/gdrive-search", "поиск файлов"],
746
+ ["/gdrive-history", "история бэкапов"],
747
+
748
+ // ADB
749
+ ["/adb-devices", "список устройств"],
750
+ ["/adb-info", "информация об устройстве"],
751
+ ["/adb-screenshot", "скриншот телефона"],
752
+ ["/adb-shell", "выполнить shell команду"],
753
+ ["/adb-tap", "тап по координатам"],
754
+ ["/adb-swipe", "свайп"],
755
+ ["/adb-type", "набрать текст"],
756
+ ["/adb-battery", "информация о батарее"],
757
+ ["/adb-install", "установить APK"],
758
+ ["/adb-packages", "список пакетов"],
759
+
760
+ // Home Assistant
761
+ ["/ha-setup", "настроить Home Assistant"],
762
+ ["/ha-status", "статус HA"],
763
+ ["/ha-states", "все состояния"],
764
+ ["/ha-lights", "спискок лампочек"],
765
+ ["/ha-sensors", "список сенсоров"],
766
+ ["/ha-toggle", "переключить устройство"],
767
+ ["/ha-on", "включить"],
768
+ ["/ha-off", "выключить"],
769
+ ["/ha-climate", "информация о климате"],
770
+ ["/ha-media", "список медиа-плееров"],
771
+ ["/ha-volume", "громкость"],
772
+ ["/ha-cover", "управление шторами/воротами"],
773
+
695
774
  // Управление компьютером
696
775
  ["/open", "открыть приложение/файл/URL"],
697
776
  ["/app", "запустить приложение"],
@@ -821,6 +900,13 @@ async function handleCommand(line) {
821
900
  ["🌐 Browser Control", ["/browser", "/browser-click", "/browser-type", "/browser-scroll", "/browser-screenshot", "/browser-links", "/browser-forms", "/browser-close"]],
822
901
  ["🔗 Git API", ["/git-issues", "/git-issue", "/git-prs", "/git-pr-create", "/git-pr-review", "/git-pr-merge", "/git-branches", "/git-releases", "/git-release"]],
823
902
  ["🔍 Web Parser", ["/fetch", "/fetch-text", "/fetch-links", "/fetch-images", "/fetch-forms", "/fetch-seo", "/web-search", "/web-batch"]],
903
+ ["📧 Gmail", ["/gmail-setup", "/gmail-auth", "/gmail-send", "/gmail-inbox", "/gmail-search", "/gmail-read", "/gmail-labels", "/gmail-read-all"]],
904
+ ["📊 Charts", ["/chart-bar", "/chart-line", "/chart-pie", "/chart-doughnut", "/chart-radar", "/chart-scatter", "/chart-csv", "/chart-list"]],
905
+ ["🗺️ Яндекс.Карты", ["/ymaps-key", "/ymaps-geo", "/ymaps-show", "/ymaps-route", "/ymaps-places", "/ymaps-static"]],
906
+ ["🎮 Игры", ["/snake", "/tetris", "/minesweeper", "/2048", "/flappy", "/tictactoe", "/sudoku"]],
907
+ ["💾 Google Drive", ["/gdrive-setup", "/gdrive-auth", "/gdrive-backup", "/gdrive-list", "/gdrive-quota", "/gdrive-search", "/gdrive-history"]],
908
+ ["📱 ADB", ["/adb-devices", "/adb-info", "/adb-screenshot", "/adb-shell", "/adb-tap", "/adb-swipe", "/adb-type", "/adb-battery", "/adb-install", "/adb-packages"]],
909
+ ["🏠 Home Assistant", ["/ha-setup", "/ha-status", "/ha-states", "/ha-lights", "/ha-sensors", "/ha-toggle", "/ha-on", "/ha-off", "/ha-climate", "/ha-media", "/ha-volume", "/ha-cover"]],
824
910
  ["⚙️ Настройки", ["/help", "/model", "/clear", "/compact", "/cost", "/context", "/config", "/version", "/login", "/newkey", "/doctor", "/sessions", "/color", "/lang", "/shortcut"]],
825
911
  ]
826
912
  for (const [cat, cmds] of categories) {
@@ -4285,6 +4371,843 @@ If everything looks normal, set severity to "none" and issues to [].`
4285
4371
  return
4286
4372
  }
4287
4373
 
4374
+
4375
+ // ═══════════════════════════════════════════════════
4376
+ // GMAIL
4377
+ // ═══════════════════════════════════════════════════
4378
+ case "/gmail-setup": {
4379
+ console.log()
4380
+ const gmail = new GmailClient()
4381
+ const url = gmail.getSetupURL()
4382
+ if (!url) {
4383
+ console.log(box([
4384
+ yellow("Требуется файл credentials.json"),
4385
+ "",
4386
+ dim("1. Создай проект в https://console.cloud.google.com"),
4387
+ dim("2. Включи Gmail API"),
4388
+ dim("3. Скачай credentials.json (OAuth 2.0 Client ID)"),
4389
+ dim("4. Сохрани в ~/.stella/gmail/credentials.json"),
4390
+ ], { title: "📧 Gmail Setup", color: yellow, padding: 2 }))
4391
+ } else {
4392
+ console.log(box([
4393
+ green("Перейди по ссылке и авторизуйся:"),
4394
+ "",
4395
+ cyan(url),
4396
+ "",
4397
+ dim("После авторизации введи код:"),
4398
+ dim(" /gmail-auth <code>"),
4399
+ ], { title: "📧 Gmail OAuth2", color: cyan, padding: 2 }))
4400
+ }
4401
+ console.log()
4402
+ return
4403
+ }
4404
+
4405
+ case "/gmail-auth": {
4406
+ if (!arg) { console.log(dim("\n Usage: /gmail-auth <code>\n")); return }
4407
+ const gmail = new GmailClient()
4408
+ const result = await gmail.completeAuth(arg.trim())
4409
+ if (result.success) console.log(green(" ✓ Gmail авторизован!\n"))
4410
+ else console.log(red(` ✗ ${result.error}\n`))
4411
+ return
4412
+ }
4413
+
4414
+ case "/gmail-send": {
4415
+ if (!arg) { console.log(dim("\n Usage: /gmail-send <to> | <subject> | <body>\n")); return }
4416
+ const parts = arg.split("|").map(s => s.trim())
4417
+ const [to, subject = "(no subject)", ...bodyParts] = parts
4418
+ const body = bodyParts.join("|")
4419
+ const gmail = new GmailClient()
4420
+ startSpinner("Sending email")
4421
+ const result = await gmail.sendEmail({ to, subject, body, isHtml: body.includes("<") })
4422
+ stopSpinner()
4423
+ if (result.success) console.log(green(` ✓ Email sent: ${result.messageId}\n`))
4424
+ else console.log(red(` ✗ ${result.error}\n`))
4425
+ return
4426
+ }
4427
+
4428
+ case "/gmail-inbox": {
4429
+ console.log()
4430
+ const gmail = new GmailClient()
4431
+ startSpinner("Fetching inbox")
4432
+ const result = await gmail.listMessages({ labelIds: ["INBOX"] })
4433
+ stopSpinner()
4434
+ if (result.success && result.messages.length > 0) {
4435
+ console.log(box(result.messages.slice(0, 10).map(m =>
4436
+ `${m.from?.match(/<?(\S+@\S+)/)?.[1] || m.from} ${violet(m.subject?.slice(0, 40))} ${dim(m.date?.slice(0, 16) || "")}`
4437
+ ), { title: `📧 Inbox (${result.messages.length})`, color: cyan, padding: 2 }))
4438
+ } else {
4439
+ console.log(dim(" No messages\n"))
4440
+ }
4441
+ console.log()
4442
+ return
4443
+ }
4444
+
4445
+ case "/gmail-search": {
4446
+ if (!arg) { console.log(dim("\n Usage: /gmail-search <query>\n")); return }
4447
+ console.log()
4448
+ const gmail = new GmailClient()
4449
+ startSpinner("Searching")
4450
+ const result = await gmail.searchMessages(arg.trim())
4451
+ stopSpinner()
4452
+ if (result.success && result.messages.length > 0) {
4453
+ console.log(box(result.messages.slice(0, 10).map(m =>
4454
+ `${m.from?.match(/<?(\S+@\S+)/)?.[1] || m.from} ${violet(m.subject?.slice(0, 40))} ${dim(m.date?.slice(0, 16) || "")}`
4455
+ ), { title: `🔍 Search (${result.messages.length})`, color: violet, padding: 2 }))
4456
+ } else {
4457
+ console.log(dim(" No results\n"))
4458
+ }
4459
+ console.log()
4460
+ return
4461
+ }
4462
+
4463
+ case "/gmail-read": {
4464
+ if (!arg) { console.log(dim("\n Usage: /gmail-read <messageId>\n")); return }
4465
+ const gmail = new GmailClient()
4466
+ startSpinner("Reading message")
4467
+ const result = await gmail.getMessage(arg.trim())
4468
+ stopSpinner()
4469
+ if (result.success) {
4470
+ console.log(box([
4471
+ `${violet("From:")} ${result.message.from}`,
4472
+ `${violet("Subject:")} ${result.message.subject}`,
4473
+ `${violet("Date:")} ${result.message.date}`,
4474
+ "",
4475
+ result.message.textBody?.slice(0, 1000) || result.message.snippet || "(no text)",
4476
+ ], { title: "📧 Message", color: cyan, padding: 2 }))
4477
+ } else {
4478
+ console.log(red(` ✗ ${result.error}\n`))
4479
+ }
4480
+ console.log()
4481
+ return
4482
+ }
4483
+
4484
+ case "/gmail-labels": {
4485
+ console.log()
4486
+ const gmail = new GmailClient()
4487
+ const result = await gmail.listLabels()
4488
+ if (result.success) {
4489
+ console.log(box(result.labels.map(l => `${l.name}`), { title: "🏷️ Labels", color: violet, padding: 2 }))
4490
+ } else {
4491
+ console.log(dim(" No labels\n"))
4492
+ }
4493
+ console.log()
4494
+ return
4495
+ }
4496
+
4497
+ case "/gmail-read-all": {
4498
+ const gmail = new GmailClient()
4499
+ const result = await gmail.getUnreadCount()
4500
+ if (result === 0) { console.log(green(" ✓ All read\n")); return }
4501
+ console.log(dim(` Found ${result} unread\n`))
4502
+ return
4503
+ }
4504
+
4505
+ // ═══════════════════════════════════════════════════
4506
+ // CHARTS
4507
+ // ═══════════════════════════════════════════════════
4508
+ case "/chart-bar": {
4509
+ if (!arg) { console.log(dim("\n Usage: /chart-bar <title> | <label1:val1,label2:val2,...>\n")); return }
4510
+ const parts = arg.split("|").map(s => s.trim())
4511
+ const title = parts[0]
4512
+ const dataParts = (parts[1] || "").split(",").map(p => p.trim()).filter(Boolean)
4513
+ const labels = dataParts.map(p => p.split(":")[0])
4514
+ const data = dataParts.map(p => parseFloat(p.split(":")[1]) || 0)
4515
+ const chart = new ChartGenerator()
4516
+ const result = await chart.bar(title, labels, data)
4517
+ if (result.success) console.log(green(` ✓ Chart: ${result.path}\n`))
4518
+ return
4519
+ }
4520
+
4521
+ case "/chart-line": {
4522
+ if (!arg) { console.log(dim("\n Usage: /chart-line <title> | <label1:val1,label2:val2,...>\n")); return }
4523
+ const parts = arg.split("|").map(s => s.trim())
4524
+ const title = parts[0]
4525
+ const dataParts = (parts[1] || "").split(",").map(p => p.trim()).filter(Boolean)
4526
+ const labels = dataParts.map(p => p.split(":")[0])
4527
+ const data = dataParts.map(p => parseFloat(p.split(":")[1]) || 0)
4528
+ const chart = new ChartGenerator()
4529
+ const result = await chart.line(title, labels, [{ label: title, data }])
4530
+ if (result.success) console.log(green(` ✓ Chart: ${result.path}\n`))
4531
+ return
4532
+ }
4533
+
4534
+ case "/chart-pie": {
4535
+ if (!arg) { console.log(dim("\n Usage: /chart-pie <title> | <label1:val1,label2:val2,...>\n")); return }
4536
+ const parts = arg.split("|").map(s => s.trim())
4537
+ const title = parts[0]
4538
+ const dataParts = (parts[1] || "").split(",").map(p => p.trim()).filter(Boolean)
4539
+ const labels = dataParts.map(p => p.split(":")[0])
4540
+ const data = dataParts.map(p => parseFloat(p.split(":")[1]) || 0)
4541
+ const chart = new ChartGenerator()
4542
+ const result = await chart.pie(title, labels, data)
4543
+ if (result.success) console.log(green(` ✓ Chart: ${result.path}\n`))
4544
+ return
4545
+ }
4546
+
4547
+ case "/chart-doughnut": {
4548
+ if (!arg) { console.log(dim("\n Usage: /chart-doughnut <title> | <label1:val1,...>\n")); return }
4549
+ const parts = arg.split("|").map(s => s.trim())
4550
+ const title = parts[0]
4551
+ const dataParts = (parts[1] || "").split(",").map(p => p.trim()).filter(Boolean)
4552
+ const labels = dataParts.map(p => p.split(":")[0])
4553
+ const data = dataParts.map(p => parseFloat(p.split(":")[1]) || 0)
4554
+ const chart = new ChartGenerator()
4555
+ const result = await chart.doughnut(title, labels, data)
4556
+ if (result.success) console.log(green(` ✓ Chart: ${result.path}\n`))
4557
+ return
4558
+ }
4559
+
4560
+ case "/chart-radar": {
4561
+ if (!arg) { console.log(dim("\n Usage: /chart-radar <title> | <label1:val1,...>\n")); return }
4562
+ const parts = arg.split("|").map(s => s.trim())
4563
+ const title = parts[0]
4564
+ const dataParts = (parts[1] || "").split(",").map(p => p.trim()).filter(Boolean)
4565
+ const labels = dataParts.map(p => p.split(":")[0])
4566
+ const data = dataParts.map(p => parseFloat(p.split(":")[1]) || 0)
4567
+ const chart = new ChartGenerator()
4568
+ const result = await chart.radar(title, labels, [{ label: title, data }])
4569
+ if (result.success) console.log(green(` ✓ Chart: ${result.path}\n`))
4570
+ return
4571
+ }
4572
+
4573
+ case "/chart-scatter": {
4574
+ if (!arg) { console.log(dim("\n Usage: /chart-scatter <title> | <x1,y1,x2,y2,...>\n")); return }
4575
+ const parts = arg.split("|").map(s => s.trim())
4576
+ const title = parts[0]
4577
+ const nums = (parts[1] || "").split(",").map(parseFloat).filter(n => !isNaN(n))
4578
+ const points = []
4579
+ for (let i = 0; i < nums.length - 1; i += 2) { points.push({ x: nums[i], y: nums[i + 1] }) }
4580
+ const chart = new ChartGenerator()
4581
+ const result = await chart.scatter(title, points)
4582
+ if (result.success) console.log(green(` ✓ Chart: ${result.path}\n`))
4583
+ return
4584
+ }
4585
+
4586
+ case "/chart-csv": {
4587
+ if (!arg) { console.log(dim("\n Usage: /chart-csv <path.csv>\n")); return }
4588
+ const chart = new ChartGenerator()
4589
+ const result = await chart.fromCSV(arg.trim())
4590
+ if (result.success) console.log(green(` ✓ Chart: ${result.path}\n`))
4591
+ else console.log(red(` ✗ ${result.error}\n`))
4592
+ return
4593
+ }
4594
+
4595
+ case "/chart-list": {
4596
+ console.log()
4597
+ const chart = new ChartGenerator()
4598
+ const list = chart.listCharts()
4599
+ if (list.length > 0) {
4600
+ console.log(box(list.map(c => `${dim(c.created?.toLocaleString() || "")} ${violet(c.name)}`), { title: "📊 Charts", color: cyan, padding: 2 }))
4601
+ } else {
4602
+ console.log(dim(" No charts yet\n"))
4603
+ }
4604
+ console.log()
4605
+ return
4606
+ }
4607
+
4608
+ // ═══════════════════════════════════════════════════
4609
+ // YANDEX MAPS
4610
+ // ═══════════════════════════════════════════════════
4611
+ case "/ymaps-key": {
4612
+ if (!arg) { console.log(dim("\n Usage: /ymaps-key <api_key>\n")); return }
4613
+ state.config.yandexMapsKey = arg.trim()
4614
+ console.log(green(" ✓ API key set\n"))
4615
+ return
4616
+ }
4617
+
4618
+ case "/ymaps-geo": {
4619
+ if (!arg) { console.log(dim("\n Usage: /ymaps-geo <address>\n")); return }
4620
+ console.log()
4621
+ const key = state.config.yandexMapsKey || process.env.YANDEX_MAPS_API_KEY
4622
+ const ymaps = new YandexMaps(key)
4623
+ if (!ymaps.isConfigured()) { console.log(yellow(" ⚠ API key not set. Use /ymaps-key <key>\n")); return }
4624
+ startSpinner("Geocoding")
4625
+ const result = await ymaps.geocode(arg.trim())
4626
+ stopSpinner()
4627
+ if (result.success) {
4628
+ console.log(box([
4629
+ `${violet("Name:")} ${result.name}`,
4630
+ `${violet("Address:")} ${result.address || result.description}`,
4631
+ `${violet("Coordinates:")} ${result.coordinates.lat}, ${result.coordinates.lng}`,
4632
+ ], { title: "🗺️ Geocode", color: cyan, padding: 2 }))
4633
+ } else {
4634
+ console.log(red(` ✗ ${result.error}\n`))
4635
+ }
4636
+ console.log()
4637
+ return
4638
+ }
4639
+
4640
+ case "/ymaps-show": {
4641
+ if (!arg) { console.log(dim("\n Usage: /ymaps-show <address>\n")); return }
4642
+ console.log()
4643
+ const key = state.config.yandexMapsKey || process.env.YANDEX_MAPS_API_KEY
4644
+ const ymaps = new YandexMaps(key)
4645
+ if (!ymaps.isConfigured()) { console.log(yellow(" ⚠ API key not set\n")); return }
4646
+ startSpinner("Loading map")
4647
+ const result = await ymaps.showLocation(arg.trim())
4648
+ stopSpinner()
4649
+ if (result.success) console.log(green(` ✓ Map: ${result.path}\n`))
4650
+ else console.log(red(` ✗ ${result.error}\n`))
4651
+ return
4652
+ }
4653
+
4654
+ case "/ymaps-route": {
4655
+ if (!arg) { console.log(dim("\n Usage: /ymaps-route <from> | <to>\n")); return }
4656
+ const [from, to] = arg.split("|").map(s => s.trim())
4657
+ if (!from || !to) { console.log(dim("\n Usage: /ymaps-route <from> | <to>\n")); return }
4658
+ console.log()
4659
+ const key = state.config.yandexMapsKey || process.env.YANDEX_MAPS_API_KEY
4660
+ const ymaps = new YandexMaps(key)
4661
+ if (!ymaps.isConfigured()) { console.log(yellow(" ⚠ API key not set\n")); return }
4662
+ startSpinner("Building route")
4663
+ const result = await ymaps.showRoute(from, to)
4664
+ stopSpinner()
4665
+ if (result.success) console.log(green(` ✓ Route: ${result.path}\n`))
4666
+ else console.log(red(` ✗ ${result.error}\n`))
4667
+ return
4668
+ }
4669
+
4670
+ case "/ymaps-places": {
4671
+ if (!arg) { console.log(dim("\n Usage: /ymaps-places <addr1> | <addr2> | ...\n")); return }
4672
+ const places = arg.split("|").map(s => s.trim()).filter(Boolean)
4673
+ console.log()
4674
+ const key = state.config.yandexMapsKey || process.env.YANDEX_MAPS_API_KEY
4675
+ const ymaps = new YandexMaps(key)
4676
+ if (!ymaps.isConfigured()) { console.log(yellow(" ⚠ API key not set\n")); return }
4677
+ startSpinner("Loading places")
4678
+ const result = await ymaps.showMultiplePlaces(places)
4679
+ stopSpinner()
4680
+ if (result.success) console.log(green(` ✓ Map with ${places.length} places: ${result.path}\n`))
4681
+ else console.log(red(` ✗ ${result.error}\n`))
4682
+ return
4683
+ }
4684
+
4685
+ case "/ymaps-static": {
4686
+ if (!arg) { console.log(dim("\n Usage: /ymaps-static <address>\n")); return }
4687
+ console.log()
4688
+ const key = state.config.yandexMapsKey || process.env.YANDEX_MAPS_API_KEY
4689
+ const ymaps = new YandexMaps(key)
4690
+ if (!ymaps.isConfigured()) { console.log(yellow(" ⚠ API key not set\n")); return }
4691
+ startSpinner("Geocoding")
4692
+ const geo = await ymaps.geocode(arg.trim())
4693
+ stopSpinner()
4694
+ if (!geo.success) { console.log(red(` ✗ ${geo.error}\n`)); return }
4695
+ startSpinner("Downloading map")
4696
+ const result = await ymaps.getStaticMap({ center: [geo.coordinates.lat, geo.coordinates.lng], zoom: 15, markers: [{ lat: geo.coordinates.lat, lng: geo.coordinates.lng }] })
4697
+ stopSpinner()
4698
+ if (result.success) console.log(green(` ✓ Map saved: ${result.path} (${result.size} bytes)\n`))
4699
+ else console.log(red(` ✗ ${result.error}\n`))
4700
+ return
4701
+ }
4702
+
4703
+ // ═══════════════════════════════════════════════════
4704
+ // GAMES
4705
+ // ═══════════════════════════════════════════════════
4706
+ case "/snake": {
4707
+ const game = new GameEngine()
4708
+ const result = await game.playSnake()
4709
+ if (result.success) console.log(green(` ✓ Snake: ${result.path}\n`))
4710
+ return
4711
+ }
4712
+
4713
+ case "/tetris": {
4714
+ const game = new GameEngine()
4715
+ const result = await game.playTetris()
4716
+ if (result.success) console.log(green(` ✓ Tetris: ${result.path}\n`))
4717
+ return
4718
+ }
4719
+
4720
+ case "/minesweeper": {
4721
+ const game = new GameEngine()
4722
+ const result = await game.playMinesweeper()
4723
+ if (result.success) console.log(green(` ✓ Minesweeper: ${result.path}\n`))
4724
+ return
4725
+ }
4726
+
4727
+ case "/2048": {
4728
+ const game = new GameEngine()
4729
+ const result = await game.play2048()
4730
+ if (result.success) console.log(green(` ✓ 2048: ${result.path}\n`))
4731
+ return
4732
+ }
4733
+
4734
+ case "/flappy": {
4735
+ const game = new GameEngine()
4736
+ const result = await game.playFlappyBird()
4737
+ if (result.success) console.log(green(` ✓ Flappy Bird: ${result.path}\n`))
4738
+ return
4739
+ }
4740
+
4741
+ case "/tictactoe": {
4742
+ const game = new GameEngine()
4743
+ const result = await game.playTicTacToe()
4744
+ if (result.success) console.log(green(` ✓ Tic Tac Toe: ${result.path}\n`))
4745
+ return
4746
+ }
4747
+
4748
+ case "/sudoku": {
4749
+ const game = new GameEngine()
4750
+ const result = await game.playSudoku()
4751
+ if (result.success) console.log(green(` ✓ Sudoku: ${result.path}\n`))
4752
+ return
4753
+ }
4754
+
4755
+ // ═══════════════════════════════════════════════════
4756
+ // GOOGLE DRIVE BACKUP
4757
+ // ═══════════════════════════════════════════════════
4758
+ case "/gdrive-setup": {
4759
+ console.log()
4760
+ const gdrive = new GDriveBackup()
4761
+ const url = gdrive.getSetupURL()
4762
+ if (!url) {
4763
+ console.log(box([
4764
+ yellow("Требуется credentials.json"),
4765
+ "",
4766
+ dim("1. https://console.cloud.google.com"),
4767
+ dim("2. Включи Google Drive API"),
4768
+ dim("3. Скачай credentials.json (OAuth 2.0)"),
4769
+ dim("4. Сохрани в ~/.stella/gdrive/credentials.json"),
4770
+ ], { title: "💾 Google Drive Setup", color: yellow, padding: 2 }))
4771
+ } else {
4772
+ console.log(box([
4773
+ green("Перейди по ссылке и авторизуйся:"), "",
4774
+ cyan(url), "",
4775
+ dim("После авторизации введи код:"),
4776
+ dim(" /gdrive-auth <code>"),
4777
+ ], { title: "💾 GDrive OAuth2", color: cyan, padding: 2 }))
4778
+ }
4779
+ console.log()
4780
+ return
4781
+ }
4782
+
4783
+ case "/gdrive-auth": {
4784
+ if (!arg) { console.log(dim("\n Usage: /gdrive-auth <code>\n")); return }
4785
+ const gdrive = new GDriveBackup()
4786
+ const result = await gdrive.completeAuth(arg.trim())
4787
+ if (result.success) console.log(green(" ✓ Google Drive авторизован!\n"))
4788
+ else console.log(red(` ✗ ${result.error}\n`))
4789
+ return
4790
+ }
4791
+
4792
+ case "/gdrive-backup": {
4793
+ if (!arg) { console.log(dim("\n Usage: /gdrive-backup <path> [name]\n")); return }
4794
+ const parts = arg.split("|").map(s => s.trim())
4795
+ const targetPath = parts[0]
4796
+ const name = parts[1] || `backup_${path.basename(targetPath)}`
4797
+ if (!fs.existsSync(targetPath)) { console.log(red(` ✗ Path not found: ${targetPath}\n`)); return }
4798
+ const gdrive = new GDriveBackup()
4799
+ startSpinner("Backing up")
4800
+ const result = await gdrive.backupProject(targetPath, name)
4801
+ stopSpinner()
4802
+ if (result.success) {
4803
+ const size = result.size ? ` (${(result.size / 1024 / 1024).toFixed(2)} MB)` : ""
4804
+ console.log(green(` ✓ Backup "${name}" uploaded${size}\n`))
4805
+ } else {
4806
+ console.log(red(` ✗ ${result.error}\n`))
4807
+ }
4808
+ return
4809
+ }
4810
+
4811
+ case "/gdrive-list": {
4812
+ console.log()
4813
+ const gdrive = new GDriveBackup()
4814
+ startSpinner("Fetching files")
4815
+ const result = await gdrive.listBackups()
4816
+ stopSpinner()
4817
+ if (result.success && result.files.length > 0) {
4818
+ console.log(box(result.files.slice(0, 20).map(f =>
4819
+ `${violet(f.name)} ${dim(new Date(f.created).toLocaleDateString())} ${dim((f.size / 1024 / 1024).toFixed(1) + " MB")}`
4820
+ ), { title: "💾 Google Drive Files", color: cyan, padding: 2 }))
4821
+ } else {
4822
+ console.log(dim(" No files found\n"))
4823
+ }
4824
+ console.log()
4825
+ return
4826
+ }
4827
+
4828
+ case "/gdrive-quota": {
4829
+ console.log()
4830
+ const gdrive = new GDriveBackup()
4831
+ const result = await gdrive.getQuota()
4832
+ if (result.success) {
4833
+ const pct = result.limit > 0 ? ((result.used / result.limit) * 100).toFixed(1) : "?"
4834
+ console.log(box([
4835
+ `${green("Used:")} ${result.usedFormatted}`,
4836
+ `${yellow("Limit:")} ${result.limitFormatted}`,
4837
+ `${violet("Usage:")} ${pct}%`,
4838
+ ], { title: "💾 Storage Quota", color: cyan, padding: 2 }))
4839
+ }
4840
+ console.log()
4841
+ return
4842
+ }
4843
+
4844
+ case "/gdrive-search": {
4845
+ if (!arg) { console.log(dim("\n Usage: /gdrive-search <query>\n")); return }
4846
+ console.log()
4847
+ const gdrive = new GDriveBackup()
4848
+ startSpinner("Searching")
4849
+ const result = await gdrive.searchFiles(arg.trim())
4850
+ stopSpinner()
4851
+ if (result.success && result.files.length > 0) {
4852
+ console.log(box(result.files.map(f => `${violet(f.name)} (${f.id})`), { title: "🔍 Search", color: violet, padding: 2 }))
4853
+ } else {
4854
+ console.log(dim(" No results\n"))
4855
+ }
4856
+ console.log()
4857
+ return
4858
+ }
4859
+
4860
+ case "/gdrive-history": {
4861
+ console.log()
4862
+ const gdrive = new GDriveBackup()
4863
+ const history = gdrive.getHistory()
4864
+ if (history.length > 0) {
4865
+ console.log(box(history.slice(-10).reverse().map(h =>
4866
+ `${dim(new Date(h.date).toLocaleString())} ${violet(h.name)} ${dim((h.size / 1024 / 1024).toFixed(1) + " MB")}`
4867
+ ), { title: "📋 Backup History", color: cyan, padding: 2 }))
4868
+ } else {
4869
+ console.log(dim(" No backup history\n"))
4870
+ }
4871
+ console.log()
4872
+ return
4873
+ }
4874
+
4875
+ // ═══════════════════════════════════════════════════
4876
+ // ADB (ANDROID DEBUG BRIDGE)
4877
+ // ═══════════════════════════════════════════════════
4878
+ case "/adb-devices": {
4879
+ console.log()
4880
+ const adb = new ADB()
4881
+ if (!adb.isAvailable()) { console.log(yellow(" ⚠ ADB not found. Install Android SDK platform-tools\n")); return }
4882
+ const result = adb.getDevices()
4883
+ if (result.success && result.devices.length > 0) {
4884
+ console.log(box(result.devices.map(d => `${d.status === "device" ? green("✓") : yellow("?")} ${d.id} (${d.status})`), { title: "📱 ADB Devices", color: cyan, padding: 2 }))
4885
+ } else {
4886
+ console.log(dim(" No devices connected\n"))
4887
+ }
4888
+ console.log()
4889
+ return
4890
+ }
4891
+
4892
+ case "/adb-info": {
4893
+ console.log()
4894
+ const adb = new ADB()
4895
+ if (!adb.isAvailable()) { console.log(yellow(" ⚠ ADB not found\n")); return }
4896
+ const devices = adb.getDevices()
4897
+ const serial = devices.success && devices.devices[0]?.id
4898
+ if (!serial) { console.log(dim(" No device connected\n")); return }
4899
+ const info = adb.getInfo(serial)
4900
+ if (info.success) {
4901
+ console.log(box([
4902
+ `${violet("Model:")} ${info.model}`,
4903
+ `${violet("Brand:")} ${info.brand}`,
4904
+ `${violet("Android:")} ${info.android} (SDK ${info.sdk})`,
4905
+ `${violet("Resolution:")} ${info.resolution}`,
4906
+ `${violet("Density:")} ${info.density}`,
4907
+ ], { title: `📱 ${serial}`, color: cyan, padding: 2 }))
4908
+ }
4909
+ console.log()
4910
+ return
4911
+ }
4912
+
4913
+ case "/adb-screenshot": {
4914
+ const adb = new ADB()
4915
+ if (!adb.isAvailable()) { console.log(yellow(" ⚠ ADB not found\n")); return }
4916
+ const devices = adb.getDevices()
4917
+ const serial = devices.success && devices.devices[0]?.id
4918
+ startSpinner("Taking screenshot")
4919
+ const result = adb.screenshot(serial)
4920
+ stopSpinner()
4921
+ if (result.success) console.log(green(` ✓ Screenshot: ${result.path}\n`))
4922
+ else console.log(red(` ✗ ${result.error}\n`))
4923
+ return
4924
+ }
4925
+
4926
+ case "/adb-shell": {
4927
+ if (!arg) { console.log(dim("\n Usage: /adb-shell <command>\n")); return }
4928
+ const adb = new ADB()
4929
+ if (!adb.isAvailable()) { console.log(yellow(" ⚠ ADB not found\n")); return }
4930
+ const devices = adb.getDevices()
4931
+ const serial = devices.success && devices.devices[0]?.id
4932
+ const result = adb.shell(serial, arg.trim())
4933
+ if (result.success) console.log(result.output + "\n")
4934
+ else console.log(red(` ✗ ${result.error}\n`))
4935
+ return
4936
+ }
4937
+
4938
+ case "/adb-tap": {
4939
+ if (!arg) { console.log(dim("\n Usage: /adb-tap <x> <y>\n")); return }
4940
+ const [x, y] = arg.split(" ").map(parseFloat)
4941
+ if (isNaN(x) || isNaN(y)) { console.log(dim("\n Usage: /adb-tap <x> <y>\n")); return }
4942
+ const adb = new ADB()
4943
+ if (!adb.isAvailable()) { console.log(yellow(" ⚠ ADB not found\n")); return }
4944
+ const devices = adb.getDevices()
4945
+ const serial = devices.success && devices.devices[0]?.id
4946
+ const result = adb.tap(serial, x, y)
4947
+ if (result.success) console.log(green(` ✓ Tapped at (${x}, ${y})\n`))
4948
+ else console.log(red(` ✗ ${result.error}\n`))
4949
+ return
4950
+ }
4951
+
4952
+ case "/adb-swipe": {
4953
+ if (!arg) { console.log(dim("\n Usage: /adb-swipe <x1> <y1> <x2> <y2>\n")); return }
4954
+ const [x1, y1, x2, y2] = arg.split(" ").map(parseFloat)
4955
+ if ([x1, y1, x2, y2].some(n => isNaN(n))) { console.log(dim("\n Usage: /adb-swipe <x1> <y1> <x2> <y2>\n")); return }
4956
+ const adb = new ADB()
4957
+ const devices = adb.getDevices()
4958
+ const serial = devices.success && devices.devices[0]?.id
4959
+ const result = adb.swipe(serial, x1, y1, x2, y2)
4960
+ if (result.success) console.log(green(" ✓ Swiped\n"))
4961
+ else console.log(red(` ✗ ${result.error}\n`))
4962
+ return
4963
+ }
4964
+
4965
+ case "/adb-type": {
4966
+ if (!arg) { console.log(dim("\n Usage: /adb-type <text>\n")); return }
4967
+ const adb = new ADB()
4968
+ const devices = adb.getDevices()
4969
+ const serial = devices.success && devices.devices[0]?.id
4970
+ const result = adb.text(serial, arg)
4971
+ if (result.success) console.log(green(` ✓ Typed "${arg.slice(0, 30)}"\n`))
4972
+ else console.log(red(` ✗ ${result.error}\n`))
4973
+ return
4974
+ }
4975
+
4976
+ case "/adb-battery": {
4977
+ console.log()
4978
+ const adb = new ADB()
4979
+ const devices = adb.getDevices()
4980
+ const serial = devices.success && devices.devices[0]?.id
4981
+ if (!serial) { console.log(dim(" No device\n")); return }
4982
+ const result = adb.getBattery(serial)
4983
+ if (result.success) {
4984
+ console.log(box([
4985
+ `${green("Level:")} ${result.level}%`,
4986
+ `${yellow("Temperature:")} ${result.temperature}°C`,
4987
+ `${violet("Status:")} ${result.status}`,
4988
+ ], { title: "🔋 Battery", color: cyan, padding: 2 }))
4989
+ }
4990
+ console.log()
4991
+ return
4992
+ }
4993
+
4994
+ case "/adb-install": {
4995
+ if (!arg) { console.log(dim("\n Usage: /adb-install <path.apk>\n")); return }
4996
+ if (!fs.existsSync(arg.trim())) { console.log(red(` ✗ File not found: ${arg.trim()}\n`)); return }
4997
+ const adb = new ADB()
4998
+ const devices = adb.getDevices()
4999
+ const serial = devices.success && devices.devices[0]?.id
5000
+ startSpinner("Installing")
5001
+ const result = adb.installApp(serial, arg.trim())
5002
+ stopSpinner()
5003
+ if (result.success) console.log(green(" ✓ Installed\n"))
5004
+ else console.log(red(` ✗ ${result.error}\n`))
5005
+ return
5006
+ }
5007
+
5008
+ case "/adb-packages": {
5009
+ console.log()
5010
+ const adb = new ADB()
5011
+ const devices = adb.getDevices()
5012
+ const serial = devices.success && devices.devices[0]?.id
5013
+ if (!serial) { console.log(dim(" No device\n")); return }
5014
+ const filter = arg || ""
5015
+ const result = adb.listPackages(serial, filter)
5016
+ if (result.success && result.packages.length > 0) {
5017
+ console.log(box(result.packages.slice(0, 30).map(p => dim(p)), { title: `📦 Packages (${result.packages.length})`, color: violet, padding: 2 }))
5018
+ } else {
5019
+ console.log(dim(" No packages\n"))
5020
+ }
5021
+ console.log()
5022
+ return
5023
+ }
5024
+
5025
+ // ═══════════════════════════════════════════════════
5026
+ // HOME ASSISTANT
5027
+ // ═══════════════════════════════════════════════════
5028
+ case "/ha-setup": {
5029
+ if (!arg) { console.log(dim("\n Usage: /ha-setup <url> | <token>\n")); return }
5030
+ const [url, token] = arg.split("|").map(s => s.trim())
5031
+ if (!url || !token) { console.log(dim("\n Usage: /ha-setup <url> | <token>\n")); return }
5032
+ const ha = new HomeAssistant(url, token)
5033
+ startSpinner("Connecting")
5034
+ const ping = await ha.ping()
5035
+ stopSpinner()
5036
+ if (ping.success) {
5037
+ ha.configure(url, token)
5038
+ console.log(green(" ✓ Home Assistant configured!\n"))
5039
+ } else {
5040
+ console.log(red(` ✗ Connection failed\n`))
5041
+ }
5042
+ return
5043
+ }
5044
+
5045
+ case "/ha-status": {
5046
+ console.log()
5047
+ const ha = new HomeAssistant()
5048
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured. Use /ha-setup\n")); return }
5049
+ startSpinner("Getting info")
5050
+ const info = await ha.getAutoInfo()
5051
+ stopSpinner()
5052
+ if (info.success) {
5053
+ console.log(box([
5054
+ `${violet("HA Version:")} ${info.haVersion || "?"}`,
5055
+ `${violet("Location:")} ${info.locationName || "?"}`,
5056
+ `${violet("Timezone:")} ${info.timezone || "?"}`,
5057
+ ], { title: "🏠 Home Assistant", color: cyan, padding: 2 }))
5058
+ } else {
5059
+ console.log(red(` ✗ ${info.error}\n`))
5060
+ }
5061
+ console.log()
5062
+ return
5063
+ }
5064
+
5065
+ case "/ha-states": {
5066
+ console.log()
5067
+ const ha = new HomeAssistant()
5068
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5069
+ startSpinner("Fetching states")
5070
+ const result = await ha.getStates()
5071
+ stopSpinner()
5072
+ if (result.success) {
5073
+ const entries = result.data.map(s => `${s.state === "on" ? green("●") : s.state === "off" ? red("●") : dim("○")} ${dim(s.entity_id)}: ${white(s.state)}`)
5074
+ console.log(box(entries.slice(0, 25), { title: `🏠 States (${result.data.length})`, color: cyan, padding: 2 }))
5075
+ if (result.data.length > 25) console.log(dim(` ... and ${result.data.length - 25} more\n`))
5076
+ }
5077
+ console.log()
5078
+ return
5079
+ }
5080
+
5081
+ case "/ha-lights": {
5082
+ console.log()
5083
+ const ha = new HomeAssistant()
5084
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5085
+ const result = await ha.getLights()
5086
+ if (result.success && result.lights.length > 0) {
5087
+ console.log(box(result.lights.map(l =>
5088
+ `${l.state === "on" ? green("●") : red("●")} ${l.friendlyName || l.entity} ${dim(l.brightness ? `(${Math.round(l.brightness / 2.55)}%)` : "")}`
5089
+ ), { title: `💡 Lights (${result.lights.length})`, color: yellow, padding: 2 }))
5090
+ } else {
5091
+ console.log(dim(" No lights\n"))
5092
+ }
5093
+ console.log()
5094
+ return
5095
+ }
5096
+
5097
+ case "/ha-sensors": {
5098
+ console.log()
5099
+ const ha = new HomeAssistant()
5100
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5101
+ const result = await ha.getSensors()
5102
+ if (result.success && result.sensors.length > 0) {
5103
+ console.log(box(result.sensors.slice(0, 20).map(s =>
5104
+ `${s.friendlyName || s.entity}: ${white(s.state)} ${dim(s.unit || "")}`
5105
+ ), { title: `📡 Sensors (${result.sensors.length})`, color: cyan, padding: 2 }))
5106
+ } else {
5107
+ console.log(dim(" No sensors\n"))
5108
+ }
5109
+ console.log()
5110
+ return
5111
+ }
5112
+
5113
+ case "/ha-toggle": {
5114
+ if (!arg) { console.log(dim("\n Usage: /ha-toggle <entity_id>\n")); return }
5115
+ const ha = new HomeAssistant()
5116
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5117
+ const result = await ha.toggle(arg.trim())
5118
+ if (result.success) console.log(green(` ✓ Toggled ${arg.trim()}\n`))
5119
+ else console.log(red(` ✗ ${result.error}\n`))
5120
+ return
5121
+ }
5122
+
5123
+ case "/ha-on": {
5124
+ if (!arg) { console.log(dim("\n Usage: /ha-on <entity_id>\n")); return }
5125
+ const ha = new HomeAssistant()
5126
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5127
+ const result = await ha.turnOn(arg.trim())
5128
+ if (result.success) console.log(green(` ✓ Turned on ${arg.trim()}\n`))
5129
+ else console.log(red(` ✗ ${result.error}\n`))
5130
+ return
5131
+ }
5132
+
5133
+ case "/ha-off": {
5134
+ if (!arg) { console.log(dim("\n Usage: /ha-off <entity_id>\n")); return }
5135
+ const ha = new HomeAssistant()
5136
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5137
+ const result = await ha.turnOff(arg.trim())
5138
+ if (result.success) console.log(green(` ✓ Turned off ${arg.trim()}\n`))
5139
+ else console.log(red(` ✗ ${result.error}\n`))
5140
+ return
5141
+ }
5142
+
5143
+ case "/ha-climate": {
5144
+ console.log()
5145
+ const ha = new HomeAssistant()
5146
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5147
+ const result = await ha.getClimate()
5148
+ if (result.success && result.devices.length > 0) {
5149
+ console.log(box(result.devices.map(c =>
5150
+ `${c.friendlyName || c.entity}: ${c.state} ${dim(c.currentTemp ? `(${c.currentTemp}°C → ${c.temp}°C)` : "")}`
5151
+ ), { title: "🌡️ Climate", color: cyan, padding: 2 }))
5152
+ } else {
5153
+ console.log(dim(" No climate devices\n"))
5154
+ }
5155
+ console.log()
5156
+ return
5157
+ }
5158
+
5159
+ case "/ha-media": {
5160
+ console.log()
5161
+ const ha = new HomeAssistant()
5162
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5163
+ const result = await ha.getMediaPlayers()
5164
+ if (result.success && result.players.length > 0) {
5165
+ console.log(box(result.players.map(p =>
5166
+ `${p.state === "playing" ? green("▶") : p.state === "paused" ? yellow("⏸") : dim("⏹")} ${p.friendlyName || p.entity} ${dim(p.volume ? `(${Math.round(p.volume * 100)}%)` : "")}`
5167
+ ), { title: "📺 Media Players", color: violet, padding: 2 }))
5168
+ } else {
5169
+ console.log(dim(" No media players\n"))
5170
+ }
5171
+ console.log()
5172
+ return
5173
+ }
5174
+
5175
+ case "/ha-volume": {
5176
+ if (!arg) { console.log(dim("\n Usage: /ha-volume <entity_id> | <level 0-100>\n")); return }
5177
+ const [entity, levelStr] = arg.split("|").map(s => s.trim())
5178
+ const level = parseFloat(levelStr) / 100
5179
+ if (isNaN(level)) { console.log(dim("\n Usage: /ha-volume <entity_id> | <level 0-100>\n")); return }
5180
+ const ha = new HomeAssistant()
5181
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5182
+ const result = await ha.setVolume(entity, level)
5183
+ if (result.success) console.log(green(` ✓ Volume set to ${levelStr}%\n`))
5184
+ else console.log(red(` ✗ ${result.error}\n`))
5185
+ return
5186
+ }
5187
+
5188
+ case "/ha-cover": {
5189
+ if (!arg) { console.log(dim("\n Usage: /ha-cover <entity_id> | <open|close|stop|position>\n")); return }
5190
+ const [entityId, action] = arg.split("|").map(s => s.trim())
5191
+ const ha = new HomeAssistant()
5192
+ if (!ha.isConfigured()) { console.log(yellow(" ⚠ Not configured\n")); return }
5193
+ const actions = { open: () => ha.openCover(entityId), close: () => ha.closeCover(entityId), stop: () => ha.stopCover(entityId) }
5194
+ const fn = actions[action]
5195
+ if (!fn) {
5196
+ const pos = parseInt(action)
5197
+ if (!isNaN(pos)) {
5198
+ const result = await ha.setCoverPosition(entityId, pos)
5199
+ if (result.success) console.log(green(` ✓ Cover position set to ${pos}%\n`))
5200
+ else console.log(red(` ✗ ${result.error}\n`))
5201
+ return
5202
+ }
5203
+ console.log(dim("\n Usage: /ha-cover <entity_id> | <open|close|stop|0-100>\n")); return
5204
+ }
5205
+ const result = await fn()
5206
+ if (result.success) console.log(green(` ✓ Cover ${action}: ${entityId}\n`))
5207
+ else console.log(red(` ✗ ${result.error}\n`))
5208
+ return
5209
+ }
5210
+
4288
5211
  default:
4289
5212
  console.log(dim(`\n Неизвестная команда: ${cmd}. Смотри /help\n`))
4290
5213
  }