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.
- package/package.json +1 -1
- package/stella-cli/adb.mjs +200 -0
- package/stella-cli/charts.mjs +411 -0
- package/stella-cli/game-engine.mjs +708 -0
- package/stella-cli/gdrive-backup.mjs +338 -0
- package/stella-cli/gmail.mjs +415 -0
- package/stella-cli/home-assistant.mjs +168 -0
- package/stella-cli/index.mjs +923 -0
- package/stella-cli/yandex-maps.mjs +426 -0
package/stella-cli/index.mjs
CHANGED
|
@@ -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
|
}
|