@emaxe/tuigram 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,72 @@
1
+ import { cyan, green, yellow, magenta, gray, bold, blue, red } from "colorette";
2
+ import { formatFullDateTime } from "../utils/time.js";
3
+
4
+ /**
5
+ * Форматирует строку диалога для консольного вывода таблицы.
6
+ * @param {object} d
7
+ * @returns {string}
8
+ */
9
+ export function formatDialogRow(d) {
10
+ const typeColor =
11
+ d.type === "channel" ? magenta :
12
+ d.type === "supergroup" || d.type === "group" ? cyan :
13
+ d.type === "bot" ? yellow :
14
+ d.type === "saved" ? green : blue;
15
+
16
+ const pin = d.pinned ? "📌 " : " ";
17
+ const type = typeColor(`[${d.type.padEnd(10)}]`);
18
+ const title = bold((d.title || "Без названия").slice(0, 32).padEnd(32));
19
+ const unread = d.unreadCount > 0 ? green(` (+${d.unreadCount})`) : "";
20
+ const id = gray(`id=${d.id}`);
21
+
22
+ return `${pin}${type} ${title} ${id}${unread}`;
23
+ }
24
+
25
+ /**
26
+ * Форматирует сообщение для вывода истории в консоль.
27
+ * @param {object} m
28
+ * @returns {string}
29
+ */
30
+ export function formatHistoryMessage(m) {
31
+ const time = gray(`[${formatFullDateTime(m.date)}]`);
32
+ const id = gray(`#${m.id}`);
33
+ const author = m.out ? green(bold("Вы:")) : cyan(bold(`${m.senderName || "Собеседник"}:`));
34
+ const text = m.text || (m.mediaDescription ? m.mediaDescription.replace(/\{[a-z0-9\-]+\}/gi, "") : "");
35
+ const reply = m.replyToMsgId ? gray(` (в ответ на #${m.replyToMsgId})`) : "";
36
+ const edited = m.editDate ? gray(" (изменено)") : "";
37
+
38
+ return `${time} ${id} ${author}${reply} ${text}${edited}`;
39
+ }
40
+
41
+ /**
42
+ * Форматирует живое событие для CLI потока.
43
+ * @param {string} kind
44
+ * @param {object} data
45
+ * @returns {string}
46
+ */
47
+ export function formatStreamEvent(kind, data) {
48
+ const time = gray(`[${new Date().toLocaleTimeString()}]`);
49
+ switch (kind) {
50
+ case "new_message": {
51
+ const m = data.message;
52
+ const peer = yellow(`[${data.peerId}]`);
53
+ const author = m.out ? green("Вы") : cyan(m.senderName || "Собеседник");
54
+ const text = m.text || "[медиа]";
55
+ return `${time} ${green("+ НОВОЕ")} ${peer} ${author}: ${text}`;
56
+ }
57
+ case "edited_message": {
58
+ const m = data.message;
59
+ const peer = yellow(`[${data.peerId}]`);
60
+ return `${time} ${yellow("~ ИЗМЕНЕНО")} ${peer} #${m.id}: ${m.text}`;
61
+ }
62
+ case "deleted_messages": {
63
+ const peer = yellow(`[${data.peerId}]`);
64
+ return `${time} ${red("- УДАЛЕНО")} ${peer} IDs: ${data.deletedIds.join(", ")}`;
65
+ }
66
+ case "typing": {
67
+ return `${time} ${gray("✍️ ПЕЧАТАЕТ")} чат: ${data.chatId}`;
68
+ }
69
+ default:
70
+ return `${time} ${gray(kind)}`;
71
+ }
72
+ }
@@ -0,0 +1,197 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import input from "input";
4
+ import { bold, cyan, green, yellow, dim } from "colorette";
5
+ import { config } from "../config.js";
6
+
7
+ /** Сколько раз переспрашивать значение, прежде чем сдаться. */
8
+ const MAX_ATTEMPTS = 3;
9
+
10
+ /**
11
+ * Экранирует значение для записи в .env: переносы строк и кавычки
12
+ * ломают формат, поэтому оборачиваем в кавычки при необходимости.
13
+ * @param {string} value
14
+ * @returns {string}
15
+ */
16
+ export function escapeEnvValue(value) {
17
+ const clean = String(value ?? "").trim();
18
+ if (/^[A-Za-z0-9_.\-/]*$/.test(clean)) return clean;
19
+ return `"${clean.replace(/(["\\])/g, "\\$1").replace(/\n/g, "\\n")}"`;
20
+ }
21
+
22
+ /**
23
+ * Обновляет (или добавляет) переменные в тексте .env, сохраняя остальные строки
24
+ * и комментарии пользователя.
25
+ * @param {string} original
26
+ * @param {Record<string, string>} values
27
+ * @returns {string}
28
+ */
29
+ export function upsertEnv(original, values) {
30
+ let text = String(original ?? "");
31
+ for (const [key, value] of Object.entries(values)) {
32
+ const line = `${key}=${escapeEnvValue(value)}`;
33
+ const pattern = new RegExp(`^[ \\t]*${key}[ \\t]*=.*$`, "m");
34
+ text = pattern.test(text) ? text.replace(pattern, line) : `${text.replace(/\s*$/, "")}\n${line}\n`;
35
+ }
36
+ return text.replace(/^\n+/, "");
37
+ }
38
+
39
+ /** Заголовок, который пишется в свежесозданный конфиг. */
40
+ const HEADER = `# TuiGram — настройки пользователя
41
+ # Ключи API выдаются на https://my.telegram.org (API development tools).
42
+ `;
43
+
44
+ /** @param {unknown} value @returns {boolean} */
45
+ export const isValidApiId = (value) => /^\d+$/.test(String(value ?? "").trim());
46
+ /** @param {unknown} value @returns {boolean} */
47
+ export const isValidApiHash = (value) => /^[a-f0-9]{32}$/i.test(String(value ?? "").trim());
48
+
49
+ /**
50
+ * Записывает ключи в пользовательский .env с правами 0600, сохраняя
51
+ * остальное содержимое файла.
52
+ * @param {string} apiId
53
+ * @param {string} apiHash
54
+ * @param {string} [targetPath=config.configEnvPath]
55
+ * @returns {string} путь к записанному файлу
56
+ */
57
+ export function saveCredentials(apiId, apiHash, targetPath = config.configEnvPath) {
58
+ if (!isValidApiId(apiId)) throw new Error(`Некорректный api_id: ${apiId}`);
59
+ if (!isValidApiHash(apiHash)) throw new Error("Некорректный api_hash: ожидается 32 hex-символа");
60
+
61
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
62
+
63
+ let existingText = HEADER;
64
+ try {
65
+ existingText = fs.readFileSync(targetPath, "utf8");
66
+ } catch {
67
+ // файла ещё нет — пишем с заголовком
68
+ }
69
+
70
+ const text = upsertEnv(existingText, {
71
+ TELEGRAM_API_ID: String(apiId).trim(),
72
+ TELEGRAM_API_HASH: String(apiHash).trim()
73
+ });
74
+
75
+ fs.writeFileSync(targetPath, text, { encoding: "utf8", mode: 0o600 });
76
+ // Если файл уже существовал, mode в writeFileSync игнорируется — выставляем явно.
77
+ fs.chmodSync(targetPath, 0o600);
78
+ return targetPath;
79
+ }
80
+
81
+ /**
82
+ * Спрашивает значение с валидацией. Отдельно обрабатывает EOF (закрытый stdin,
83
+ * запуск в CI или из пайпа): без этого цикл переспрашивания молча вырождается
84
+ * в «успешный» выход без сохранения настроек.
85
+ * @param {string} label
86
+ * @param {(value: unknown) => boolean} validate
87
+ * @param {string} hint
88
+ * @returns {Promise<string>}
89
+ */
90
+ async function promptValidated(label, validate, hint) {
91
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
92
+ const answer = await input.text(label);
93
+
94
+ if (answer === undefined || answer === null) {
95
+ throw new Error(
96
+ "Ввод недоступен (stdin закрыт). Запустите tuigram init в интерактивном терминале\n" +
97
+ " либо задайте ключи без диалога:\n" +
98
+ " tuigram init --api-id <ID> --api-hash <HASH>"
99
+ );
100
+ }
101
+
102
+ const value = String(answer).trim();
103
+ if (validate(value)) return value;
104
+
105
+ if (attempt < MAX_ATTEMPTS) {
106
+ console.log(yellow(` ${hint} Попробуйте ещё раз.`));
107
+ }
108
+ }
109
+ throw new Error(`Не удалось получить корректное значение (${MAX_ATTEMPTS} попытки). ${hint}`);
110
+ }
111
+
112
+ /**
113
+ * Интерактивная (или неинтерактивная — через флаги) первичная настройка.
114
+ * @param {Record<string, string|boolean>} [flags]
115
+ * @returns {Promise<void>}
116
+ */
117
+ export async function cmdInit(flags = {}) {
118
+ const flagApiId = flags["api-id"] ?? flags.apiId;
119
+ const flagApiHash = flags["api-hash"] ?? flags.apiHash;
120
+
121
+ // Неинтерактивный режим: оба ключа переданы флагами.
122
+ if (flagApiId || flagApiHash) {
123
+ if (!flagApiId || !flagApiHash) {
124
+ throw new Error("Нужны оба флага сразу: --api-id <ID> --api-hash <HASH>");
125
+ }
126
+ const saved = saveCredentials(String(flagApiId), String(flagApiHash));
127
+ console.log(green(`\n✓ Настройки сохранены: ${saved}`));
128
+ console.log(dim(" Права доступа: 0600 (только владелец).\n"));
129
+ return;
130
+ }
131
+
132
+ // Без TTY промис ввода не резолвится вовсе, и процесс молча выходит
133
+ // с кодом 0, ничего не сохранив. Отсекаем это заранее понятной ошибкой.
134
+ if (!process.stdin.isTTY) {
135
+ throw new Error(
136
+ "Интерактивный ввод недоступен (stdin не подключён к терминалу).\n" +
137
+ " Задайте ключи без диалога:\n" +
138
+ " tuigram init --api-id <ID> --api-hash <HASH>"
139
+ );
140
+ }
141
+
142
+ console.log(bold("\n🚀 Настройка TuiGram\n"));
143
+ console.log(`Файл настроек: ${cyan(config.configEnvPath)}`);
144
+ console.log(`Данные и сессия: ${cyan(config.dataDir)}\n`);
145
+ console.log(dim("Ключи API выдаются на https://my.telegram.org → API development tools.\n"));
146
+
147
+ if (config.apiId && config.apiHash) {
148
+ console.log(yellow(`Ключи уже заданы (api_id: ${config.apiId}).`));
149
+ const overwrite = await input.confirm("Перезаписать их?", { default: false });
150
+ if (!overwrite) {
151
+ console.log(dim("\nОтменено, настройки не изменены."));
152
+ return;
153
+ }
154
+ console.log("");
155
+ }
156
+
157
+ const apiId = await promptValidated(
158
+ "TELEGRAM_API_ID: ",
159
+ isValidApiId,
160
+ "api_id — это число, например 1234567."
161
+ );
162
+ const apiHash = await promptValidated(
163
+ "TELEGRAM_API_HASH: ",
164
+ isValidApiHash,
165
+ "api_hash — 32 шестнадцатеричных символа."
166
+ );
167
+
168
+ const saved = saveCredentials(apiId, apiHash);
169
+
170
+ console.log(green(`\n✓ Настройки сохранены: ${saved}`));
171
+ console.log(dim(" Права доступа: 0600 (только владелец).\n"));
172
+ console.log("Следующий шаг — авторизация:");
173
+ console.log(` ${bold("tuigram login")} вход по номеру телефона`);
174
+ console.log(` ${bold("tuigram")} запуск интерфейса\n`);
175
+ }
176
+
177
+ /**
178
+ * Печатает актуальные пути и состояние конфигурации — для диагностики.
179
+ */
180
+ export function cmdPaths() {
181
+ const sessionExists = fs.existsSync(config.sessionPath);
182
+ const configExists = fs.existsSync(config.configEnvPath);
183
+ const localEnv = path.join(config.packageRoot, ".env");
184
+
185
+ console.log(bold("\n📂 Пути TuiGram\n"));
186
+ console.log(` Версия: ${config.version}`);
187
+ console.log(` Корень пакета: ${config.packageRoot}`);
188
+ console.log(` Настройки: ${config.configEnvPath} ${configExists ? green("(есть)") : dim("(нет)")}`);
189
+ if (fs.existsSync(localEnv)) {
190
+ console.log(` Локальный .env: ${localEnv} ${green("(есть, имеет приоритет)")}`);
191
+ }
192
+ console.log(` Данные: ${config.dataDir}`);
193
+ console.log(` Сессия: ${config.sessionPath} ${sessionExists ? green("(есть)") : dim("(нет)")}`);
194
+ console.log(` Загрузки: ${config.downloadsDir}`);
195
+ console.log(` Ключи API: ${config.apiId && config.apiHash ? green("заданы") : yellow("не заданы — запустите tuigram init")}`);
196
+ console.log("");
197
+ }
package/src/config.js ADDED
@@ -0,0 +1,158 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import { fileURLToPath } from "node:url";
5
+ import dotenv from "dotenv";
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ /** Корень пакета (там, где лежит package.json). */
10
+ const packageRoot = path.resolve(__dirname, "..");
11
+
12
+ /** Имя приложения — используется в путях пользовательских директорий. */
13
+ const APP_DIR_NAME = "tuigram";
14
+
15
+ /**
16
+ * Директория пользовательских настроек.
17
+ * Пакет может быть установлен глобально в системную папку без прав на запись,
18
+ * поэтому ни настройки, ни сессия внутри самого пакета не хранятся.
19
+ * @returns {string}
20
+ */
21
+ function resolveConfigDir() {
22
+ if (process.env.TUIGRAM_CONFIG_DIR) {
23
+ return path.resolve(process.env.TUIGRAM_CONFIG_DIR);
24
+ }
25
+ if (process.platform === "win32") {
26
+ const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
27
+ return path.join(appData, APP_DIR_NAME);
28
+ }
29
+ const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
30
+ return path.join(xdg, APP_DIR_NAME);
31
+ }
32
+
33
+ /**
34
+ * Директория пользовательских данных (сессия, загрузки).
35
+ * @returns {string}
36
+ */
37
+ function resolveDataDir() {
38
+ if (process.env.TUIGRAM_DATA_DIR) {
39
+ return path.resolve(process.env.TUIGRAM_DATA_DIR);
40
+ }
41
+ if (process.env.DATA_DIR) {
42
+ // Обратная совместимость со старым .env: относительные пути
43
+ // разрешаются от корня пакета, как было раньше.
44
+ return path.resolve(packageRoot, process.env.DATA_DIR);
45
+ }
46
+ if (process.platform === "win32") {
47
+ const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
48
+ return path.join(localAppData, APP_DIR_NAME);
49
+ }
50
+ const xdg = process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share");
51
+ return path.join(xdg, APP_DIR_NAME);
52
+ }
53
+
54
+ const configDir = resolveConfigDir();
55
+ const configEnvPath = path.join(configDir, ".env");
56
+
57
+ // Порядок загрузки = приоритет: dotenv не перетирает уже заданные переменные.
58
+ // 1. Переменные окружения процесса (наивысший приоритет — ничего не грузим).
59
+ // 2. .env в корне пакета — рабочий режим при запуске из клона репозитория
60
+ // (в опубликованный пакет этот файл не попадает, см. "files" в package.json).
61
+ // 3. .env в пользовательской директории настроек — режим установленного CLI.
62
+ const localEnvPath = path.join(packageRoot, ".env");
63
+ if (fs.existsSync(localEnvPath)) {
64
+ dotenv.config({ path: localEnvPath, quiet: true });
65
+ }
66
+ if (fs.existsSync(configEnvPath)) {
67
+ dotenv.config({ path: configEnvPath, quiet: true });
68
+ }
69
+
70
+ const dataDir = resolveDataDir();
71
+ const sessionPath = path.join(dataDir, "session.txt");
72
+ const downloadsDir = path.join(dataDir, "downloads");
73
+
74
+ /** Путь к сессии в старом формате (внутри пакета) — для одноразовой миграции. */
75
+ const legacySessionPath = path.resolve(packageRoot, "data", "session.txt");
76
+
77
+ /**
78
+ * Переносит сессию из старого расположения (`<пакет>/data/session.txt`)
79
+ * в пользовательскую директорию. Выполняется один раз и молча:
80
+ * отсутствие прав или файла не должно ломать запуск.
81
+ * @returns {boolean} true, если сессия была перенесена
82
+ */
83
+ function migrateLegacySession() {
84
+ try {
85
+ if (sessionPath === legacySessionPath) return false;
86
+ if (!fs.existsSync(legacySessionPath)) return false;
87
+ if (fs.existsSync(sessionPath)) return false;
88
+
89
+ const session = fs.readFileSync(legacySessionPath, "utf8").trim();
90
+ if (!session) return false;
91
+
92
+ fs.mkdirSync(path.dirname(sessionPath), { recursive: true });
93
+ fs.writeFileSync(sessionPath, session, { encoding: "utf8", mode: 0o600 });
94
+ return true;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Версия из package.json — единственный источник правды для `--version`.
102
+ * @returns {string}
103
+ */
104
+ function readVersion() {
105
+ try {
106
+ const raw = fs.readFileSync(path.join(packageRoot, "package.json"), "utf8");
107
+ return JSON.parse(raw).version || "0.0.0";
108
+ } catch {
109
+ return "0.0.0";
110
+ }
111
+ }
112
+
113
+ export const config = {
114
+ packageRoot,
115
+ /** @deprecated оставлено для обратной совместимости, равно packageRoot */
116
+ rootDir: packageRoot,
117
+ configDir,
118
+ configEnvPath,
119
+ dataDir,
120
+ sessionPath,
121
+ downloadsDir,
122
+ legacySessionPath,
123
+ version: readVersion(),
124
+
125
+ apiId: parseInt(process.env.TELEGRAM_API_ID || "0", 10),
126
+ apiHash: process.env.TELEGRAM_API_HASH || "",
127
+
128
+ theme: process.env.TUI_THEME || "default",
129
+ autoScroll: String(process.env.AUTO_SCROLL || "true").toLowerCase() === "true",
130
+ showTyping: String(process.env.SHOW_TYPING || "true").toLowerCase() === "true",
131
+
132
+ /**
133
+ * Путь к файлу сессии.
134
+ * @returns {string}
135
+ */
136
+ getSessionFilePath() {
137
+ return sessionPath;
138
+ },
139
+
140
+ migrateLegacySession,
141
+
142
+ /**
143
+ * Проверяет наличие ключей API.
144
+ */
145
+ assertCredentials() {
146
+ if (!config.apiId || !config.apiHash) {
147
+ throw new Error(
148
+ "Не заданы TELEGRAM_API_ID / TELEGRAM_API_HASH.\n" +
149
+ " 1. Получите ключи на https://my.telegram.org (API development tools)\n" +
150
+ " 2. Запустите: tuigram init\n" +
151
+ ` (ключи будут сохранены в ${configEnvPath})\n` +
152
+ " Либо задайте их через переменные окружения TELEGRAM_API_ID и TELEGRAM_API_HASH."
153
+ );
154
+ }
155
+ }
156
+ };
157
+
158
+ migrateLegacySession();
package/src/index.js ADDED
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * TuiGram — Полнофункциональный TUI & CLI клиент Telegram.
4
+ */
5
+ import { checkAuthStatus, loginInteractive } from "./telegram/auth.js";
6
+ import { connectClient } from "./telegram/client.js";
7
+ import { startTui } from "./ui/app.js";
8
+ import {
9
+ cmdLogin,
10
+ cmdDialogs,
11
+ cmdHistory,
12
+ cmdSend,
13
+ cmdSendFile,
14
+ cmdListen
15
+ } from "./cli/cliCommands.js";
16
+ import { cmdInit, cmdPaths } from "./cli/init.js";
17
+ import { config } from "./config.js";
18
+ import { red, bold } from "colorette";
19
+
20
+ /**
21
+ * Простой парсер аргументов командной строки.
22
+ * @param {string[]} argv
23
+ * @returns {{ positional: string[], flags: Record<string, string|boolean> }}
24
+ */
25
+ function parseArgs(argv) {
26
+ const positional = [];
27
+ const flags = {};
28
+ for (let i = 0; i < argv.length; i++) {
29
+ const arg = argv[i];
30
+ if (arg.startsWith("--")) {
31
+ const key = arg.slice(2);
32
+ const next = argv[i + 1];
33
+ if (next && !next.startsWith("--")) {
34
+ flags[key] = next;
35
+ i++;
36
+ } else {
37
+ flags[key] = true;
38
+ }
39
+ } else if (arg.startsWith("-")) {
40
+ const key = arg.slice(1);
41
+ flags[key] = true;
42
+ } else {
43
+ positional.push(arg);
44
+ }
45
+ }
46
+ return { positional, flags };
47
+ }
48
+
49
+ const HELP_TEXT = `
50
+ ${bold("🚀 TuiGram — Терминальный клиент Telegram")}
51
+
52
+ ${bold("Запуск TUI (по умолчанию):")}
53
+ tuigram Запустить графический терминальный интерфейс (TUI)
54
+ tuigram tui То же самое
55
+
56
+ ${bold("Настройка:")}
57
+ tuigram init Ввести ключи Telegram API (api_id / api_hash)
58
+ --api-id <ID> --api-hash <HASH> без диалога
59
+ tuigram paths Показать пути к настройкам, сессии и загрузкам
60
+
61
+ ${bold("Консольные команды (CLI):")}
62
+ tuigram login Авторизация в аккаунте (телефон, код, 2FA)
63
+ tuigram dialogs [--limit 50] Список диалогов
64
+ tuigram history <peer> [--limit 30]
65
+ История сообщений (@username, ID, me)
66
+ tuigram send <peer> <текст> Отправить текстовое сообщение
67
+ tuigram sendfile <peer> <путь> [<путь> ...]
68
+ Отправить файл(ы); несколько путей уходят альбомом
69
+ --caption "текст" подпись
70
+ --as-file без сжатия, документом
71
+ tuigram listen Слушать обновления в реальном времени
72
+
73
+ ${bold("Примеры:")}
74
+ tuigram send me "Привет из TuiGram!"
75
+ tuigram history @durov --limit 20
76
+ tuigram sendfile @friend ./document.pdf
77
+ tuigram sendfile me ~/a.png ~/b.png --caption "Две картинки"
78
+ tuigram sendfile me ~/photo.png --as-file
79
+ `;
80
+
81
+ async function main() {
82
+ const { positional, flags } = parseArgs(process.argv.slice(2));
83
+
84
+ if (flags.help || flags.h) {
85
+ console.log(HELP_TEXT);
86
+ return;
87
+ }
88
+
89
+ if (flags.version || flags.v) {
90
+ console.log(`TuiGram v${config.version}`);
91
+ return;
92
+ }
93
+
94
+ const command = positional[0]?.toLowerCase();
95
+
96
+ switch (command) {
97
+ case "init":
98
+ case "setup":
99
+ await cmdInit(flags);
100
+ process.exit(0);
101
+ break;
102
+ case "paths":
103
+ case "config":
104
+ cmdPaths();
105
+ process.exit(0);
106
+ break;
107
+ case "login":
108
+ await cmdLogin();
109
+ process.exit(0);
110
+ break;
111
+ case "dialogs":
112
+ await cmdDialogs(flags);
113
+ process.exit(0);
114
+ break;
115
+ case "history":
116
+ await cmdHistory(positional[1], flags);
117
+ process.exit(0);
118
+ break;
119
+ case "send": {
120
+ const peer = positional[1];
121
+ const text = positional.slice(2).join(" ");
122
+ await cmdSend(peer, text, flags);
123
+ process.exit(0);
124
+ break;
125
+ }
126
+ case "sendfile":
127
+ case "send-file":
128
+ await cmdSendFile(positional[1], positional.slice(2), flags);
129
+ process.exit(0);
130
+ break;
131
+ case "listen":
132
+ await cmdListen();
133
+ break;
134
+ case "help":
135
+ console.log(HELP_TEXT);
136
+ process.exit(0);
137
+ break;
138
+ case "version":
139
+ console.log(`TuiGram v${config.version}`);
140
+ process.exit(0);
141
+ break;
142
+ case "tui":
143
+ default: {
144
+ if (command && command !== "tui") {
145
+ console.log(red(`\nНеизвестная команда: "${command}"`));
146
+ console.log(HELP_TEXT);
147
+ process.exit(1);
148
+ }
149
+
150
+ // Запуск TUI интерфейса
151
+ let { authorized, me, client } = await checkAuthStatus();
152
+
153
+ if (!authorized) {
154
+ console.log(bold("\n🔑 Сессия не найдена. Требуется авторизация для входа в TuiGram:\n"));
155
+ me = await loginInteractive();
156
+ client = await connectClient();
157
+ }
158
+
159
+ if (!client) {
160
+ client = await connectClient();
161
+ }
162
+
163
+ await startTui(client, me);
164
+ break;
165
+ }
166
+ }
167
+ }
168
+
169
+ main().catch((err) => {
170
+ console.error(red(`\nОшибка TuiGram: ${err?.message || err}`));
171
+ process.exit(1);
172
+ });