izteamslots 1.1.6 → 1.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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [1.3.0](https://github.com/izzzzzi/izTeamSlots/compare/v1.2.0...v1.3.0) (2026-03-06)
2
+
3
+
4
+ ### Features
5
+
6
+ * add Settings menu for API keys and mail providers ([e026db7](https://github.com/izzzzzi/izTeamSlots/commit/e026db7dd01db0544ad7993dfb6ab020490f2ae8))
7
+
8
+ # [1.2.0](https://github.com/izzzzzi/izTeamSlots/compare/v1.1.6...v1.2.0) (2026-03-06)
9
+
10
+
11
+ ### Features
12
+
13
+ * load config from ~/.izteamslots/.env, update README install docs ([99ca787](https://github.com/izzzzzi/izTeamSlots/commit/99ca787207d65cfde29a9b0bd173dc870ca59ee6))
14
+
1
15
  ## [1.1.6](https://github.com/izzzzzi/izTeamSlots/compare/v1.1.5...v1.1.6) (2026-03-06)
2
16
 
3
17
 
package/README.md CHANGED
@@ -83,14 +83,12 @@ izTeamSlots/
83
83
  ### npm (рекомендуется)
84
84
 
85
85
  ```bash
86
- npm install -g izteamslots
86
+ npm install -g izteamslots@latest
87
87
  ```
88
88
 
89
- Или запуск без установки:
89
+ Установщик автоматически поставит Python-зависимости (через [uv](https://docs.astral.sh/uv/)), [Bun](https://bun.sh) и всё остальное.
90
90
 
91
- ```bash
92
- npx izteamslots
93
- ```
91
+ > Chrome и chromedriver скачиваются автоматически при первом запуске через SeleniumBase.
94
92
 
95
93
  ### Из исходников
96
94
 
@@ -100,32 +98,27 @@ cd izTeamSlots
100
98
  npm install
101
99
  ```
102
100
 
103
- `npm install` / `postinstall` автоматически:
104
- - Проверит Python 3.11+
105
- - Установит [uv](https://docs.astral.sh/uv/) если его нет (быстрый Python package manager)
106
- - Установит Python-зависимости через uv (`seleniumbase`, `requests`)
107
- - Установит [Bun](https://bun.sh) если его нет + UI-зависимости
108
- - Создаст `.env` из `.env.example` если его нет
101
+ ### Настройка
109
102
 
110
- > Chrome и chromedriver скачиваются автоматически при первом запуске через SeleniumBase.
111
-
112
- Если нужно перезапустить setup вручную:
103
+ Создайте файл конфигурации `~/.izteamslots/.env`:
113
104
 
114
105
  ```bash
115
- npm run setup
116
- ```
106
+ # Linux / macOS
107
+ mkdir -p ~/.izteamslots
108
+ echo "BOOMLIFY_API_KEY=your_api_key" > ~/.izteamslots/.env
117
109
 
118
- ### Настройка .env
119
-
120
- ```bash
121
- # Temp mail API (обязательно для создания слотов)
122
- BOOMLIFY_API_KEY=your_api_key
110
+ # Windows (PowerShell)
111
+ mkdir "$env:USERPROFILE\.izteamslots" -Force
112
+ echo "BOOMLIFY_API_KEY=your_api_key" > "$env:USERPROFILE\.izteamslots\.env"
123
113
  ```
124
114
 
115
+ Конфиг загружается в порядке приоритета: `~/.izteamslots/.env` > `./.env` > встроенный.
116
+
125
117
  Опциональные переменные:
126
118
 
127
119
  | Переменная | По умолчанию | Описание |
128
120
  |-----------|:------------:|----------|
121
+ | `BOOMLIFY_API_KEY` | — | API-ключ Boomlify (обязательно для слотов) |
129
122
  | `BOOMLIFY_DOMAIN` | авто | Домен для временных почт |
130
123
  | `BOOMLIFY_TIME` | `permanent` | Время жизни ящика |
131
124
  | `SLOT_MAIL_PROVIDER` | `boomlify` | Провайдер почты для слотов |
@@ -134,13 +127,11 @@ BOOMLIFY_API_KEY=your_api_key
134
127
  ## Запуск
135
128
 
136
129
  ```bash
137
- # Глобальная установка
138
130
  izteamslots
139
-
140
- # Из исходников
141
- npm start
142
131
  ```
143
132
 
133
+ Из исходников: `npm start`
134
+
144
135
  Это запустит OpenTUI frontend через **Bun**, который поднимет Python RPC backend (`python -m backend`) по `stdio`.
145
136
 
146
137
  ---
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import os
4
5
  import sys
5
6
  import threading
6
7
  import traceback
8
+ from pathlib import Path
7
9
  from typing import Any
8
10
 
9
11
  from .file_logger import FileLogger
@@ -51,6 +53,59 @@ class RPCServer:
51
53
  def _run_job(self, title: str, fn):
52
54
  return self.jobs.start(title, fn)
53
55
 
56
+ _SETTINGS_KEYS = [
57
+ ("BOOMLIFY_API_KEY", "Boomlify API ключ"),
58
+ ("BOOMLIFY_DOMAIN", "Домен временной почты"),
59
+ ("BOOMLIFY_TIME", "Время жизни ящика"),
60
+ ("SLOT_MAIL_PROVIDER", "Провайдер почты для слотов"),
61
+ ("MAIL_PROVIDER", "Провайдер почты для админов"),
62
+ ]
63
+
64
+ @staticmethod
65
+ def _settings_path() -> Path:
66
+ return Path.home() / ".izteamslots" / ".env"
67
+
68
+ def _get_settings(self) -> dict[str, Any]:
69
+ items = []
70
+ for key, label in self._SETTINGS_KEYS:
71
+ value = os.environ.get(key, "")
72
+ masked = ""
73
+ if value:
74
+ masked = value[:4] + "***" + value[-4:] if len(value) > 12 else "***"
75
+ items.append({"key": key, "label": label, "value": value, "masked": masked})
76
+ return {"items": items, "path": str(self._settings_path())}
77
+
78
+ def _set_setting(self, key: str, value: str) -> None:
79
+ allowed = {k for k, _ in self._SETTINGS_KEYS}
80
+ if key not in allowed:
81
+ raise RPCError(-32602, "Unknown setting", {"key": key})
82
+
83
+ env_path = self._settings_path()
84
+ env_path.parent.mkdir(parents=True, exist_ok=True)
85
+
86
+ # Read existing lines
87
+ lines: list[str] = []
88
+ if env_path.is_file():
89
+ lines = env_path.read_text(encoding="utf-8").splitlines()
90
+
91
+ # Update or append
92
+ found = False
93
+ for i, line in enumerate(lines):
94
+ if line.startswith(f"{key}=") or line.startswith(f"{key} ="):
95
+ lines[i] = f"{key}={value}"
96
+ found = True
97
+ break
98
+ if not found:
99
+ lines.append(f"{key}={value}")
100
+
101
+ env_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
102
+
103
+ # Update runtime env
104
+ if value:
105
+ os.environ[key] = value
106
+ elif key in os.environ:
107
+ del os.environ[key]
108
+
54
109
  def _handle_request(self, req: RPCRequest) -> dict[str, Any]:
55
110
  m = req.method
56
111
  p = req.params
@@ -168,6 +223,15 @@ class RPCServer:
168
223
  )
169
224
  return make_success_response(req.request_id, {"job_id": job_id})
170
225
 
226
+ if m == "settings.get":
227
+ return make_success_response(req.request_id, self._get_settings())
228
+
229
+ if m == "settings.set":
230
+ key = self._as_str_param(p, "key")
231
+ value = self._as_str_param(p, "value")
232
+ self._set_setting(key, value)
233
+ return make_success_response(req.request_id, {"ok": True})
234
+
171
235
  if m == "shutdown":
172
236
  self.facade.shutdown()
173
237
  return make_success_response(req.request_id, {"ok": True})
@@ -222,7 +286,15 @@ def main() -> int:
222
286
 
223
287
  from dotenv import load_dotenv
224
288
 
225
- load_dotenv(Path(__file__).resolve().parent.parent / ".env")
289
+ # Priority: ~/.izteamslots/.env > CWD/.env > package .env
290
+ home_env = Path.home() / ".izteamslots" / ".env"
291
+ cwd_env = Path.cwd() / ".env"
292
+ pkg_env = Path(__file__).resolve().parent.parent / ".env"
293
+
294
+ for env_path in (home_env, cwd_env, pkg_env):
295
+ if env_path.is_file():
296
+ load_dotenv(env_path)
297
+ break
226
298
 
227
299
  server = RPCServer()
228
300
  server.serve()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "izteamslots",
3
- "version": "1.1.6",
3
+ "version": "1.3.0",
4
4
  "description": "ChatGPT Team slot management — automated invite, register, OAuth login & codex token pipeline",
5
5
  "bin": {
6
6
  "izteamslots": "bin/izteamslots.mjs"
@@ -6,6 +6,7 @@ export function getMenuOptions(menuName: MenuName, state: AppState): MenuOption[
6
6
  { id: "menu_admins", label: "Админы", hint: "Доступы, токены, браузерные профили" },
7
7
  { id: "menu_slots", label: "Слоты", hint: "Создание, перелогин и обслуживание" },
8
8
  { id: "menu_mail", label: "Почта", hint: "Ящики и входящие письма" },
9
+ { id: "menu_settings", label: "Настройки", hint: "API-ключи и провайдеры почты" },
9
10
  { id: "menu_exit", label: "Выход", hint: "Закрыть приложение" },
10
11
  ]
11
12
  }
@@ -62,6 +63,16 @@ export function getMenuOptions(menuName: MenuName, state: AppState): MenuOption[
62
63
  }))
63
64
  }
64
65
 
66
+ if (menuName === "settings") {
67
+ return [
68
+ ...(state.settings ?? []).map((s: { key: string; label: string; masked: string }) => ({
69
+ id: `setting:${s.key}`,
70
+ label: s.label,
71
+ hint: s.masked || "не задан",
72
+ })),
73
+ ]
74
+ }
75
+
65
76
  if (menuName === "confirm") {
66
77
  return [
67
78
  { id: "confirm_yes", label: "Да, удалить", hint: "Подтвердить необратимое действие", destructive: true },
@@ -110,6 +121,7 @@ export function getScreenTitle(menuName: MenuName, ctx: MenuContext): string {
110
121
  if (menuName === "admins") return "Администраторы"
111
122
  if (menuName === "slots") return "Слоты"
112
123
  if (menuName === "mail") return "Почтовые ящики"
124
+ if (menuName === "settings") return "Настройки"
113
125
  if (menuName === "pick_admin") return ctx.title ?? "Выбор администратора"
114
126
  if (menuName === "pick_worker") return ctx.title ?? "Выбор слота"
115
127
  if (menuName === "pick_account") return ctx.title ?? "Выбор аккаунта"
@@ -140,6 +152,8 @@ export function getScreenDescription(menuName: MenuName, state: AppState, ctx: M
140
152
  : "Выберите аккаунт слева, чтобы забрать входящие письма."
141
153
  }
142
154
 
155
+ if (menuName === "settings") return "Настройте API-ключи и провайдеры почты. Enter для редактирования."
156
+
143
157
  if (menuName === "pick_admin") return ctx.title ?? "Выберите администратора для следующего действия."
144
158
  if (menuName === "pick_worker") return ctx.title ?? "Выберите слот для следующего действия."
145
159
  if (menuName === "pick_account") return ctx.title ?? "Выберите аккаунт."
@@ -200,7 +214,7 @@ export function getTable(menuName: MenuName, state: AppState, ctx: MenuContext):
200
214
  }
201
215
 
202
216
  export function getHint(menuName: MenuName, _ctx: MenuContext): string {
203
- if (menuName === "main") return "↑↓ или 1-4: выбор Enter: открыть r: обновить q: выход"
217
+ if (menuName === "main") return "↑↓ или 1-5: выбор Enter: открыть r: обновить q: выход"
204
218
  if (menuName === "confirm") return "Enter: подтвердить Esc: отмена"
205
219
  return "↑↓ или 1-9: выбор Enter: действие Esc: назад r: обновить y: копировать лог"
206
220
  }
@@ -212,6 +226,7 @@ export function parentMenu(menuName: MenuName, ctx: MenuContext): MenuName {
212
226
  admins: "main",
213
227
  slots: "main",
214
228
  mail: "main",
229
+ settings: "main",
215
230
  pick_admin: "admins",
216
231
  pick_worker: "slots",
217
232
  pick_account: "mail",
@@ -3,6 +3,7 @@ export type MenuName =
3
3
  | "admins"
4
4
  | "slots"
5
5
  | "mail"
6
+ | "settings"
6
7
  | "pick_admin"
7
8
  | "pick_worker"
8
9
  | "pick_account"
@@ -46,10 +47,18 @@ export interface MailAccountRow {
46
47
  email: string
47
48
  }
48
49
 
50
+ export interface SettingItem {
51
+ key: string
52
+ label: string
53
+ value: string
54
+ masked: string
55
+ }
56
+
49
57
  export interface AppState {
50
58
  admins: AdminRow[]
51
59
  workers: WorkerRow[]
52
60
  accounts: MailAccountRow[]
61
+ settings?: SettingItem[]
53
62
  }
54
63
 
55
64
  export interface MenuContext {
@@ -127,6 +127,7 @@ function getMenuOptionDescription(option: MenuOption | undefined): string {
127
127
  if (optionId === "menu_admins") return "Добавление, логин и обслуживание администраторов."
128
128
  if (optionId === "menu_slots") return "Создание слотов, перелогин и работа с профилями."
129
129
  if (optionId === "menu_mail") return "Проверка входящих писем админов и слотов."
130
+ if (optionId === "menu_settings") return "API-ключи и настройки провайдеров почты."
130
131
  if (optionId === "menu_exit") return "Аккуратно завершить интерфейс."
131
132
  if (optionId === "adm_add") return "Создать нового админа: в авто-режиме ввод в TUI, в ручном режиме вход сразу в браузере."
132
133
  if (optionId === "adm_relogin") return "Выбрать админа и режим перелогина: автоматический или ручной."
@@ -372,6 +373,17 @@ export class MainScreen {
372
373
  this.render()
373
374
  }
374
375
 
376
+ private async loadSettings() {
377
+ try {
378
+ const result = await this.rpc.request<{ items: Array<{ key: string; label: string; value: string; masked: string }> }>("settings.get")
379
+ this.state = { ...this.state, settings: result.items }
380
+ } catch (error) {
381
+ this.pushLog(`Ошибка загрузки настроек: ${String(error)}`)
382
+ }
383
+ this.rebuildMenuOptions()
384
+ this.render()
385
+ }
386
+
375
387
  private rebuildMenuOptions() {
376
388
  this.menuOptions = getMenuOptions(this.menuName, this.state)
377
389
  if (this.selectedIndex >= this.menuOptions.length) {
@@ -833,6 +845,10 @@ export class MainScreen {
833
845
  if (optionId === "menu_admins") this.menuName = "admins"
834
846
  else if (optionId === "menu_slots") this.menuName = "slots"
835
847
  else if (optionId === "menu_mail") this.menuName = "mail"
848
+ else if (optionId === "menu_settings") {
849
+ this.menuName = "settings"
850
+ await this.loadSettings()
851
+ }
836
852
  else if (optionId === "menu_exit") { await this.exit(); return }
837
853
  this.menuCtx = {}
838
854
  this.selectedIndex = 0
@@ -902,6 +918,22 @@ export class MainScreen {
902
918
  }
903
919
  }
904
920
 
921
+ if (this.menuName === "settings" && optionId.startsWith("setting:")) {
922
+ const key = optionId.split(":", 2)[1]
923
+ const current = this.state.settings?.find(s => s.key === key)
924
+ const newValue = await this.promptInput(`${current?.label ?? key}`, key.includes("KEY"))
925
+ if (newValue === null) return
926
+ try {
927
+ await this.rpc.request("settings.set", { key, value: newValue })
928
+ this.pushLog(`Настройка ${key} обновлена`)
929
+ } catch (error) {
930
+ this.pushLog(`Ошибка: ${String(error)}`)
931
+ }
932
+ await this.loadSettings()
933
+ this.render()
934
+ return
935
+ }
936
+
905
937
  if (this.menuName === "mail" && optionId.startsWith("mail_pick:")) {
906
938
  const index = Number(optionId.split(":", 2)[1])
907
939
  if (!Number.isInteger(index) || index < 0 || index >= this.state.accounts.length) return