stella-coder 5.2.0 → 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 +2 -1
- package/stella-cli/adb.mjs +200 -0
- package/stella-cli/browser-control.mjs +274 -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/git-api.mjs +407 -0
- package/stella-cli/gmail.mjs +415 -0
- package/stella-cli/home-assistant.mjs +168 -0
- package/stella-cli/index.mjs +1364 -0
- package/stella-cli/web-parser.mjs +229 -0
- package/stella-cli/yandex-maps.mjs +426 -0
- package/capture.ps1 +0 -14
- package/test_screen.png +0 -0
package/stella-cli/index.mjs
CHANGED
|
@@ -23,6 +23,16 @@ import { mcp, MCP_COMMANDS } from "./mcp.mjs"
|
|
|
23
23
|
import { generatePresentation, createPresentationFromTopic, AVAILABLE_THEMES, exportToPDF } from "./presentations.mjs"
|
|
24
24
|
import { AutonomousAgent } from "./autonomous-agent.mjs"
|
|
25
25
|
import { ScreenMonitor } from "./screen-monitor.mjs"
|
|
26
|
+
import { ChromeBrowser } from "./browser-control.mjs"
|
|
27
|
+
import { GitAPI } from "./git-api.mjs"
|
|
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"
|
|
26
36
|
import {
|
|
27
37
|
buildRepoMap, buildProjectContext, compressContext,
|
|
28
38
|
loadSpec, generateSpecTemplate,
|
|
@@ -658,6 +668,109 @@ const COMMANDS = [
|
|
|
658
668
|
["/monitor-alerts", "показать обнаруженные проблемы"],
|
|
659
669
|
["/monitor-screenshot", "сделать скриншот и проанализировать"],
|
|
660
670
|
|
|
671
|
+
// Chrome DevTools Protocol
|
|
672
|
+
["/browser", "открыть браузер и перейти на URL"],
|
|
673
|
+
["/browser-click", "кликнуть по элементу (CSS selector)"],
|
|
674
|
+
["/browser-type", "набрать текст в поле"],
|
|
675
|
+
["/browser-scroll", "прокрутить страницу"],
|
|
676
|
+
["/browser-screenshot", "скриншот текущей страницы"],
|
|
677
|
+
["/browser-links", "показать все ссылки"],
|
|
678
|
+
["/browser-forms", "показать формы на странице"],
|
|
679
|
+
["/browser-close", "закрыть браузер"],
|
|
680
|
+
|
|
681
|
+
// Git API (GitHub/GitLab)
|
|
682
|
+
["/git-issues", "список issues"],
|
|
683
|
+
["/git-issue", "создать issue"],
|
|
684
|
+
["/git-prs", "список Pull Requests"],
|
|
685
|
+
["/git-pr-create", "создать Pull Request"],
|
|
686
|
+
["/git-pr-review", "оставить ревью на PR"],
|
|
687
|
+
["/git-pr-merge", "слить PR"],
|
|
688
|
+
["/git-branches", "список веток"],
|
|
689
|
+
["/git-releases", "список релизов"],
|
|
690
|
+
["/git-release", "создать релиз"],
|
|
691
|
+
|
|
692
|
+
// Web Parser
|
|
693
|
+
["/fetch", "загрузить и показать страницу"],
|
|
694
|
+
["/fetch-text", "извлечь текст со страницы"],
|
|
695
|
+
["/fetch-links", "извлечь все ссылки"],
|
|
696
|
+
["/fetch-images", "извлечь все картинки"],
|
|
697
|
+
["/fetch-forms", "извлечь формы"],
|
|
698
|
+
["/fetch-seo", "SEO анализ страницы"],
|
|
699
|
+
["/web-search", "поиск в интернете"],
|
|
700
|
+
["/web-batch", "загрузить несколько страниц"],
|
|
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
|
+
|
|
661
774
|
// Управление компьютером
|
|
662
775
|
["/open", "открыть приложение/файл/URL"],
|
|
663
776
|
["/app", "запустить приложение"],
|
|
@@ -784,6 +897,16 @@ async function handleCommand(line) {
|
|
|
784
897
|
["🔀 Git Экосистема", ["/git-eco", "/git-pr", "/git-merge-auto"]],
|
|
785
898
|
["🤖 Autonomous Agent", ["/auto", "/auto-stop", "/auto-status", "/auto-dashboard", "/auto-history"]],
|
|
786
899
|
["👁️ Computer Vision", ["/monitor", "/monitor-stop", "/monitor-status", "/monitor-alerts", "/monitor-screenshot"]],
|
|
900
|
+
["🌐 Browser Control", ["/browser", "/browser-click", "/browser-type", "/browser-scroll", "/browser-screenshot", "/browser-links", "/browser-forms", "/browser-close"]],
|
|
901
|
+
["🔗 Git API", ["/git-issues", "/git-issue", "/git-prs", "/git-pr-create", "/git-pr-review", "/git-pr-merge", "/git-branches", "/git-releases", "/git-release"]],
|
|
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"]],
|
|
787
910
|
["⚙️ Настройки", ["/help", "/model", "/clear", "/compact", "/cost", "/context", "/config", "/version", "/login", "/newkey", "/doctor", "/sessions", "/color", "/lang", "/shortcut"]],
|
|
788
911
|
]
|
|
789
912
|
for (const [cat, cmds] of categories) {
|
|
@@ -3844,6 +3967,1247 @@ If everything looks normal, set severity to "none" and issues to [].`
|
|
|
3844
3967
|
return
|
|
3845
3968
|
}
|
|
3846
3969
|
|
|
3970
|
+
// ═══════════════════════════════════════════════════
|
|
3971
|
+
// CHROME DEVTOOLS PROTOCOL (BROWSER CONTROL)
|
|
3972
|
+
// ═══════════════════════════════════════════════════
|
|
3973
|
+
case "/browser": {
|
|
3974
|
+
console.log()
|
|
3975
|
+
const browser = new ChromeBrowser()
|
|
3976
|
+
const url = arg || "about:blank"
|
|
3977
|
+
startSpinner("Launching browser")
|
|
3978
|
+
const launch = await browser.launch(false)
|
|
3979
|
+
stopSpinner()
|
|
3980
|
+
if (!launch.success) { console.log(red(` ✗ ${launch.error}\n`)); return }
|
|
3981
|
+
startSpinner("Connecting")
|
|
3982
|
+
const conn = await browser.connect()
|
|
3983
|
+
stopSpinner()
|
|
3984
|
+
if (!conn.success) { console.log(red(` ✗ ${conn.error}\n`)); return }
|
|
3985
|
+
startSpinner(`Navigating to ${url}`)
|
|
3986
|
+
await browser.navigate(url)
|
|
3987
|
+
stopSpinner()
|
|
3988
|
+
const title = await browser.getTitle()
|
|
3989
|
+
console.log(box([
|
|
3990
|
+
green("✓ Browser launched!"), "",
|
|
3991
|
+
dim("URL: ") + cyan(url),
|
|
3992
|
+
dim("Title: ") + white(title.title || "—"), "",
|
|
3993
|
+
dim("Commands: /browser-click, /browser-type, /browser-scroll"),
|
|
3994
|
+
dim(" /browser-screenshot, /browser-links, /browser-close"),
|
|
3995
|
+
], { title: "🌐 Browser", color: cyan, padding: 2 }))
|
|
3996
|
+
console.log()
|
|
3997
|
+
return
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
case "/browser-click": {
|
|
4001
|
+
if (!arg) { console.log(dim("\n Usage: /browser-click <css-selector>\n")); return }
|
|
4002
|
+
const browser = new ChromeBrowser()
|
|
4003
|
+
const result = await browser.click(arg.trim())
|
|
4004
|
+
if (result.success) console.log(green(` ✓ Clicked at (${result.x}, ${result.y})\n`))
|
|
4005
|
+
else console.log(red(` ✗ ${result.error}\n`))
|
|
4006
|
+
return
|
|
4007
|
+
}
|
|
4008
|
+
|
|
4009
|
+
case "/browser-type": {
|
|
4010
|
+
if (!arg) { console.log(dim("\n Usage: /browser-type <selector> <text>\n")); return }
|
|
4011
|
+
const [sel, ...textParts] = arg.split(" ")
|
|
4012
|
+
const text = textParts.join(" ")
|
|
4013
|
+
const browser = new ChromeBrowser()
|
|
4014
|
+
const result = await browser.type(sel, text)
|
|
4015
|
+
if (result.success) console.log(green(` ✓ Typed "${text}" in ${sel}\n`))
|
|
4016
|
+
else console.log(red(` ✗ ${result.error}\n`))
|
|
4017
|
+
return
|
|
4018
|
+
}
|
|
4019
|
+
|
|
4020
|
+
case "/browser-scroll": {
|
|
4021
|
+
const browser = new ChromeBrowser()
|
|
4022
|
+
const delta = arg ? parseInt(arg) || 300 : 300
|
|
4023
|
+
await browser.scroll(0, delta)
|
|
4024
|
+
console.log(green(` ✓ Scrolled ${delta}px\n`))
|
|
4025
|
+
return
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4028
|
+
case "/browser-screenshot": {
|
|
4029
|
+
const browser = new ChromeBrowser()
|
|
4030
|
+
const ss = await browser.screenshot()
|
|
4031
|
+
if (ss.success) {
|
|
4032
|
+
console.log(green(` ✓ Screenshot saved: ${ss.path}\n`))
|
|
4033
|
+
try { execSync(`start "" "${ss.path}"`, { shell: "cmd.exe" }) } catch {}
|
|
4034
|
+
} else {
|
|
4035
|
+
console.log(red(` ✗ ${ss.error}\n`))
|
|
4036
|
+
}
|
|
4037
|
+
return
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
case "/browser-links": {
|
|
4041
|
+
const browser = new ChromeBrowser()
|
|
4042
|
+
const links = await browser.getLinks()
|
|
4043
|
+
if (links.success && links.links.length > 0) {
|
|
4044
|
+
console.log(box([
|
|
4045
|
+
...links.links.slice(0, 20).map(l => cyan(l.text.slice(0, 50)) + dim(` → ${l.href.slice(0, 60)}`)),
|
|
4046
|
+
links.links.length > 20 ? dim(`... and ${links.links.length - 20} more`) : "",
|
|
4047
|
+
].filter(Boolean), { title: `🔗 Links (${links.links.length})`, color: cyan, padding: 2 }))
|
|
4048
|
+
} else {
|
|
4049
|
+
console.log(dim(" No links found\n"))
|
|
4050
|
+
}
|
|
4051
|
+
console.log()
|
|
4052
|
+
return
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
case "/browser-forms": {
|
|
4056
|
+
const browser = new ChromeBrowser()
|
|
4057
|
+
const forms = await browser.getForms()
|
|
4058
|
+
if (forms.success && forms.forms.length > 0) {
|
|
4059
|
+
console.log(box([
|
|
4060
|
+
...forms.forms.map((f, i) => [
|
|
4061
|
+
violet(`Form ${i + 1}: ${f.method} ${f.action?.slice(0, 50)}`),
|
|
4062
|
+
...f.inputs.map(inp => dim(` ${inp.name || inp.type}: ${inp.placeholder || ""}`)),
|
|
4063
|
+
].join("\n")),
|
|
4064
|
+
], { title: `📝 Forms (${forms.forms.length})`, color: violet, padding: 2 }))
|
|
4065
|
+
} else {
|
|
4066
|
+
console.log(dim(" No forms found\n"))
|
|
4067
|
+
}
|
|
4068
|
+
console.log()
|
|
4069
|
+
return
|
|
4070
|
+
}
|
|
4071
|
+
|
|
4072
|
+
case "/browser-close": {
|
|
4073
|
+
const browser = new ChromeBrowser()
|
|
4074
|
+
browser.close()
|
|
4075
|
+
console.log(green(" ✓ Browser closed\n"))
|
|
4076
|
+
return
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4079
|
+
// ═══════════════════════════════════════════════════
|
|
4080
|
+
// GIT API (GitHub/GitLab)
|
|
4081
|
+
// ═══════════════════════════════════════════════════
|
|
4082
|
+
case "/git-issues": {
|
|
4083
|
+
console.log()
|
|
4084
|
+
const api = new GitAPI()
|
|
4085
|
+
if (!api.isConfigured()) { console.log(red(" ✗ No GitHub/GitLab remote found\n")); return }
|
|
4086
|
+
if (!api.hasToken()) { console.log(yellow(" ⚠ No token. Set GITHUB_TOKEN or GITLAB_TOKEN\n")); return }
|
|
4087
|
+
startSpinner("Fetching issues")
|
|
4088
|
+
const result = await api.listIssues()
|
|
4089
|
+
stopSpinner()
|
|
4090
|
+
if (result.success && result.issues.length > 0) {
|
|
4091
|
+
console.log(box([
|
|
4092
|
+
...result.issues.slice(0, 15).map(i => {
|
|
4093
|
+
const num = i.number || i.iid
|
|
4094
|
+
const state = i.state === "open" ? green("●") : red("●")
|
|
4095
|
+
return `${state} ${violet(`#${num}`)} ${white(i.title?.slice(0, 60))}`
|
|
4096
|
+
}),
|
|
4097
|
+
], { title: `📋 Issues (${result.issues.length})`, color: cyan, padding: 2 }))
|
|
4098
|
+
} else {
|
|
4099
|
+
console.log(dim(" No issues found\n"))
|
|
4100
|
+
}
|
|
4101
|
+
console.log()
|
|
4102
|
+
return
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
case "/git-issue": {
|
|
4106
|
+
if (!arg) { console.log(dim("\n Usage: /git-issue <title>\n")); return }
|
|
4107
|
+
const api = new GitAPI()
|
|
4108
|
+
if (!api.isConfigured() || !api.hasToken()) { console.log(red(" ✗ Not configured\n")); return }
|
|
4109
|
+
startSpinner("Creating issue")
|
|
4110
|
+
const result = await api.createIssue(arg.trim(), `Created by Stella Coder\n\n_${new Date().toISOString()}_`)
|
|
4111
|
+
stopSpinner()
|
|
4112
|
+
if (result.success) {
|
|
4113
|
+
console.log(green(` ✓ Issue created: ${result.url}\n`))
|
|
4114
|
+
} else {
|
|
4115
|
+
console.log(red(` ✗ Failed to create issue\n`))
|
|
4116
|
+
}
|
|
4117
|
+
return
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
case "/git-prs": {
|
|
4121
|
+
console.log()
|
|
4122
|
+
const api = new GitAPI()
|
|
4123
|
+
if (!api.isConfigured() || !api.hasToken()) { console.log(red(" ✗ Not configured\n")); return }
|
|
4124
|
+
startSpinner("Fetching PRs")
|
|
4125
|
+
const result = await api.listPRs()
|
|
4126
|
+
stopSpinner()
|
|
4127
|
+
if (result.success && result.prs.length > 0) {
|
|
4128
|
+
console.log(box([
|
|
4129
|
+
...result.prs.slice(0, 15).map(pr => {
|
|
4130
|
+
const num = pr.number || pr.iid
|
|
4131
|
+
const state = pr.merged_at ? violet("merged") : pr.state === "open" ? green("open") : red("closed")
|
|
4132
|
+
return `${state} ${violet(`#${num}`)} ${white(pr.title?.slice(0, 60))}`
|
|
4133
|
+
}),
|
|
4134
|
+
], { title: `🔀 Pull Requests (${result.prs.length})`, color: violet, padding: 2 }))
|
|
4135
|
+
} else {
|
|
4136
|
+
console.log(dim(" No PRs found\n"))
|
|
4137
|
+
}
|
|
4138
|
+
console.log()
|
|
4139
|
+
return
|
|
4140
|
+
}
|
|
4141
|
+
|
|
4142
|
+
case "/git-pr-create": {
|
|
4143
|
+
if (!arg) { console.log(dim("\n Usage: /git-pr-create <title>\n")); return }
|
|
4144
|
+
const api = new GitAPI()
|
|
4145
|
+
if (!api.isConfigured() || !api.hasToken()) { console.log(red(" ✗ Not configured\n")); return }
|
|
4146
|
+
startSpinner("Creating PR")
|
|
4147
|
+
const result = await api.createPR(arg.trim(), `Created by Stella Coder\n\n_${new Date().toISOString()}_`)
|
|
4148
|
+
stopSpinner()
|
|
4149
|
+
if (result.success) {
|
|
4150
|
+
console.log(green(` ✓ PR created: ${result.url}\n`))
|
|
4151
|
+
} else {
|
|
4152
|
+
console.log(red(` ✗ Failed to create PR\n`))
|
|
4153
|
+
}
|
|
4154
|
+
return
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
case "/git-pr-review": {
|
|
4158
|
+
if (!arg) { console.log(dim("\n Usage: /git-pr-review <number> <APPROVE|REQUEST_CHANGES|COMMENT>\n")); return }
|
|
4159
|
+
const [prNum, ...reviewParts] = arg.split(" ")
|
|
4160
|
+
const reviewEvent = reviewParts.join(" ") || "COMMENT"
|
|
4161
|
+
const api = new GitAPI()
|
|
4162
|
+
if (!api.isConfigured() || !api.hasToken()) { console.log(red(" ✗ Not configured\n")); return }
|
|
4163
|
+
startSpinner("Submitting review")
|
|
4164
|
+
const result = await api.reviewPR(parseInt(prNum), reviewEvent, `Review by Stella Coder`)
|
|
4165
|
+
stopSpinner()
|
|
4166
|
+
if (result.success) console.log(green(` ✓ Review submitted on #${prNum}\n`))
|
|
4167
|
+
else console.log(red(` ✗ Failed\n`))
|
|
4168
|
+
return
|
|
4169
|
+
}
|
|
4170
|
+
|
|
4171
|
+
case "/git-pr-merge": {
|
|
4172
|
+
if (!arg) { console.log(dim("\n Usage: /git-pr-merge <number>\n")); return }
|
|
4173
|
+
const api = new GitAPI()
|
|
4174
|
+
if (!api.isConfigured() || !api.hasToken()) { console.log(red(" ✗ Not configured\n")); return }
|
|
4175
|
+
startSpinner("Merging PR")
|
|
4176
|
+
const result = await api.mergePR(parseInt(arg))
|
|
4177
|
+
stopSpinner()
|
|
4178
|
+
if (result.success) console.log(green(` ✓ PR #${arg} merged\n`))
|
|
4179
|
+
else console.log(red(` ✗ Failed\n`))
|
|
4180
|
+
return
|
|
4181
|
+
}
|
|
4182
|
+
|
|
4183
|
+
case "/git-branches": {
|
|
4184
|
+
console.log()
|
|
4185
|
+
const api = new GitAPI()
|
|
4186
|
+
if (!api.isConfigured()) { console.log(red(" ✗ Not configured\n")); return }
|
|
4187
|
+
startSpinner("Fetching branches")
|
|
4188
|
+
const result = await api.listBranches()
|
|
4189
|
+
stopSpinner()
|
|
4190
|
+
if (result.success) {
|
|
4191
|
+
console.log(box(result.branches.map(b => cyan(b)), { title: "🌿 Branches", color: cyan, padding: 2 }))
|
|
4192
|
+
}
|
|
4193
|
+
console.log()
|
|
4194
|
+
return
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
case "/git-releases": {
|
|
4198
|
+
console.log()
|
|
4199
|
+
const api = new GitAPI()
|
|
4200
|
+
if (!api.isConfigured()) { console.log(red(" ✗ Not configured\n")); return }
|
|
4201
|
+
startSpinner("Fetching releases")
|
|
4202
|
+
const result = await api.listReleases()
|
|
4203
|
+
stopSpinner()
|
|
4204
|
+
if (result.success && result.releases.length > 0) {
|
|
4205
|
+
console.log(box([
|
|
4206
|
+
...result.releases.slice(0, 10).map(r => green(r.tag_name) + dim(` — ${r.name || ""}`)),
|
|
4207
|
+
], { title: `📦 Releases (${result.releases.length})`, color: green, padding: 2 }))
|
|
4208
|
+
} else {
|
|
4209
|
+
console.log(dim(" No releases found\n"))
|
|
4210
|
+
}
|
|
4211
|
+
console.log()
|
|
4212
|
+
return
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
case "/git-release": {
|
|
4216
|
+
if (!arg) { console.log(dim("\n Usage: /git-release <tag> [name]\n")); return }
|
|
4217
|
+
const [tag, ...nameParts] = arg.split(" ")
|
|
4218
|
+
const name = nameParts.join(" ") || tag
|
|
4219
|
+
const api = new GitAPI()
|
|
4220
|
+
if (!api.isConfigured() || !api.hasToken()) { console.log(red(" ✗ Not configured\n")); return }
|
|
4221
|
+
startSpinner("Creating release")
|
|
4222
|
+
const result = await api.createRelease(tag, name, `Release ${tag} created by Stella Coder`)
|
|
4223
|
+
stopSpinner()
|
|
4224
|
+
if (result.success) {
|
|
4225
|
+
console.log(green(` ✓ Release created: ${result.url}\n`))
|
|
4226
|
+
} else {
|
|
4227
|
+
console.log(red(` ✗ Failed\n`))
|
|
4228
|
+
}
|
|
4229
|
+
return
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
// ═══════════════════════════════════════════════════
|
|
4233
|
+
// WEB PARSER
|
|
4234
|
+
// ═══════════════════════════════════════════════════
|
|
4235
|
+
case "/fetch": {
|
|
4236
|
+
if (!arg) { console.log(dim("\n Usage: /fetch <url>\n")); return }
|
|
4237
|
+
console.log()
|
|
4238
|
+
const parser = new WebParser()
|
|
4239
|
+
startSpinner("Fetching page")
|
|
4240
|
+
const result = await parser.fetchAndParse(arg.trim())
|
|
4241
|
+
stopSpinner()
|
|
4242
|
+
if (result.success) {
|
|
4243
|
+
console.log(box([
|
|
4244
|
+
green(`✓ ${result.page.title || "No title"}`),
|
|
4245
|
+
dim(`URL: ${result.page.url}`),
|
|
4246
|
+
dim(`Length: ${result.page.text?.length || 0} chars`),
|
|
4247
|
+
"",
|
|
4248
|
+
result.page.text?.slice(0, 500) || "No text content",
|
|
4249
|
+
result.page.text?.length > 500 ? dim("\n... (truncated)") : "",
|
|
4250
|
+
], { title: "🌐 Fetched Page", color: cyan, padding: 2 }))
|
|
4251
|
+
} else {
|
|
4252
|
+
console.log(red(` ✗ ${result.error}\n`))
|
|
4253
|
+
}
|
|
4254
|
+
console.log()
|
|
4255
|
+
return
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
case "/fetch-text": {
|
|
4259
|
+
if (!arg) { console.log(dim("\n Usage: /fetch-text <url>\n")); return }
|
|
4260
|
+
const parser = new WebParser()
|
|
4261
|
+
const result = await parser.fetchAndParse(arg.trim())
|
|
4262
|
+
if (result.success) {
|
|
4263
|
+
console.log(result.page.text || "No text")
|
|
4264
|
+
} else {
|
|
4265
|
+
console.log(red(` ✗ ${result.error}`))
|
|
4266
|
+
}
|
|
4267
|
+
console.log()
|
|
4268
|
+
return
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
case "/fetch-links": {
|
|
4272
|
+
if (!arg) { console.log(dim("\n Usage: /fetch-links <url>\n")); return }
|
|
4273
|
+
const parser = new WebParser()
|
|
4274
|
+
const result = await parser.fetchAndParse(arg.trim())
|
|
4275
|
+
if (result.success && result.page.links?.length > 0) {
|
|
4276
|
+
console.log(box(result.page.links.slice(0, 30).map(l => cyan(l.text.slice(0, 50)) + dim(` → ${l.href}`)), { title: `🔗 Links (${result.page.links.length})`, color: cyan, padding: 2 }))
|
|
4277
|
+
} else {
|
|
4278
|
+
console.log(dim(" No links found\n"))
|
|
4279
|
+
}
|
|
4280
|
+
console.log()
|
|
4281
|
+
return
|
|
4282
|
+
}
|
|
4283
|
+
|
|
4284
|
+
case "/fetch-images": {
|
|
4285
|
+
if (!arg) { console.log(dim("\n Usage: /fetch-images <url>\n")); return }
|
|
4286
|
+
const parser = new WebParser()
|
|
4287
|
+
const result = await parser.fetchAndParse(arg.trim())
|
|
4288
|
+
if (result.success && result.page.images?.length > 0) {
|
|
4289
|
+
console.log(box(result.page.images.slice(0, 20).map(i => {
|
|
4290
|
+
const src = i.src?.length > 80 ? i.src.slice(0, 80) + "..." : i.src
|
|
4291
|
+
return violet(src) + dim(` alt="${i.alt || ""}"`)
|
|
4292
|
+
}), { title: `🖼️ Images (${result.page.images.length})`, color: violet, padding: 2 }))
|
|
4293
|
+
} else {
|
|
4294
|
+
console.log(dim(" No images found\n"))
|
|
4295
|
+
}
|
|
4296
|
+
console.log()
|
|
4297
|
+
return
|
|
4298
|
+
}
|
|
4299
|
+
|
|
4300
|
+
case "/fetch-forms": {
|
|
4301
|
+
if (!arg) { console.log(dim("\n Usage: /fetch-forms <url>\n")); return }
|
|
4302
|
+
const parser = new WebParser()
|
|
4303
|
+
const result = await parser.fetchAndParse(arg.trim())
|
|
4304
|
+
if (result.success && result.page.forms?.length > 0) {
|
|
4305
|
+
console.log(box(result.page.forms.map((f, i) => [
|
|
4306
|
+
violet(`Form ${i + 1}: ${f.method} ${f.action}`),
|
|
4307
|
+
...f.fields.map(field => dim(` ${field.name || field.type}: ${field.placeholder || ""}`)),
|
|
4308
|
+
].join("\n")), { title: `📝 Forms (${result.page.forms.length})`, color: violet, padding: 2 }))
|
|
4309
|
+
} else {
|
|
4310
|
+
console.log(dim(" No forms found\n"))
|
|
4311
|
+
}
|
|
4312
|
+
console.log()
|
|
4313
|
+
return
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4316
|
+
case "/fetch-seo": {
|
|
4317
|
+
if (!arg) { console.log(dim("\n Usage: /fetch-seo <url>\n")); return }
|
|
4318
|
+
console.log()
|
|
4319
|
+
const parser = new WebParser()
|
|
4320
|
+
const result = await parser.fetchAndParse(arg.trim())
|
|
4321
|
+
if (!result.success) { console.log(red(` ✗ ${result.error}\n`)); return }
|
|
4322
|
+
const seo = parser.analyzeSEO(result.page)
|
|
4323
|
+
console.log(box([
|
|
4324
|
+
`Score: ${seo.score}/100`,
|
|
4325
|
+
"",
|
|
4326
|
+
...seo.checks.map(c => `${c.pass ? green("✓") : red("✗")} ${c.label}: ${dim(c.detail)}`),
|
|
4327
|
+
"",
|
|
4328
|
+
...seo.recommendations.map(r => dim(`• ${r}`)),
|
|
4329
|
+
], { title: `🔍 SEO Analysis`, color: seo.score >= 70 ? green : yellow, padding: 2 }))
|
|
4330
|
+
console.log()
|
|
4331
|
+
return
|
|
4332
|
+
}
|
|
4333
|
+
|
|
4334
|
+
case "/web-search": {
|
|
4335
|
+
if (!arg) { console.log(dim("\n Usage: /web-search <query>\n")); return }
|
|
4336
|
+
console.log()
|
|
4337
|
+
const parser = new WebParser()
|
|
4338
|
+
startSpinner("Searching")
|
|
4339
|
+
const results = await parser.search(arg.trim())
|
|
4340
|
+
stopSpinner()
|
|
4341
|
+
if (results.length > 0) {
|
|
4342
|
+
console.log(box(results.slice(0, 8).map((r, i) => [
|
|
4343
|
+
violet(`${i + 1}. `) + white(r.title),
|
|
4344
|
+
dim(` ${r.url}`),
|
|
4345
|
+
dim(` ${r.snippet?.slice(0, 100) || ""}`),
|
|
4346
|
+
].join("\n")), { title: `🔍 Search Results (${results.length})`, color: violet, padding: 2 }))
|
|
4347
|
+
} else {
|
|
4348
|
+
console.log(dim(" No results found\n"))
|
|
4349
|
+
}
|
|
4350
|
+
console.log()
|
|
4351
|
+
return
|
|
4352
|
+
}
|
|
4353
|
+
|
|
4354
|
+
case "/web-batch": {
|
|
4355
|
+
if (!arg) { console.log(dim("\n Usage: /web-batch <url1> <url2> ...\n")); return }
|
|
4356
|
+
const urls = arg.split(/\s+/).filter(Boolean)
|
|
4357
|
+
console.log()
|
|
4358
|
+
const parser = new WebParser()
|
|
4359
|
+
startSpinner(`Fetching ${urls.length} pages`)
|
|
4360
|
+
const results = await parser.batchFetch(urls)
|
|
4361
|
+
stopSpinner()
|
|
4362
|
+
const ok = results.filter(r => r.success)
|
|
4363
|
+
const fail = results.filter(r => !r.success)
|
|
4364
|
+
console.log(box([
|
|
4365
|
+
green(`✓ Fetched: ${ok.length}`),
|
|
4366
|
+
fail.length > 0 ? red(`✗ Failed: ${fail.length}`) : "",
|
|
4367
|
+
"",
|
|
4368
|
+
...ok.map(r => dim(`• ${r.page?.title || r.page?.url}`)),
|
|
4369
|
+
].filter(Boolean), { title: "📦 Batch Fetch", color: cyan, padding: 2 }))
|
|
4370
|
+
console.log()
|
|
4371
|
+
return
|
|
4372
|
+
}
|
|
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
|
+
|
|
3847
5211
|
default:
|
|
3848
5212
|
console.log(dim(`\n Неизвестная команда: ${cmd}. Смотри /help\n`))
|
|
3849
5213
|
}
|