@statuser/cli 0.0.0-stage → 0.1.1

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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +448 -2
  3. package/dist/client.js +163 -0
  4. package/dist/client.js.map +1 -0
  5. package/dist/commander-i18n.js +52 -0
  6. package/dist/commander-i18n.js.map +1 -0
  7. package/dist/commands/api.js +104 -0
  8. package/dist/commands/api.js.map +1 -0
  9. package/dist/commands/auth.js +104 -0
  10. package/dist/commands/auth.js.map +1 -0
  11. package/dist/commands/catalog.js +72 -0
  12. package/dist/commands/catalog.js.map +1 -0
  13. package/dist/commands/config.js +302 -0
  14. package/dist/commands/config.js.map +1 -0
  15. package/dist/commands/heartbeat.js +199 -0
  16. package/dist/commands/heartbeat.js.map +1 -0
  17. package/dist/commands/incidents.js +265 -0
  18. package/dist/commands/incidents.js.map +1 -0
  19. package/dist/commands/maintenance.js +211 -0
  20. package/dist/commands/maintenance.js.map +1 -0
  21. package/dist/commands/reports.js +170 -0
  22. package/dist/commands/reports.js.map +1 -0
  23. package/dist/commands/scenarios.js +361 -0
  24. package/dist/commands/scenarios.js.map +1 -0
  25. package/dist/commands/servers-add.js +152 -0
  26. package/dist/commands/servers-add.js.map +1 -0
  27. package/dist/commands/servers-checks.js +56 -0
  28. package/dist/commands/servers-checks.js.map +1 -0
  29. package/dist/commands/servers.js +424 -0
  30. package/dist/commands/servers.js.map +1 -0
  31. package/dist/config.js +107 -0
  32. package/dist/config.js.map +1 -0
  33. package/dist/confirm.js +34 -0
  34. package/dist/confirm.js.map +1 -0
  35. package/dist/context.js +27 -0
  36. package/dist/context.js.map +1 -0
  37. package/dist/duration.js +20 -0
  38. package/dist/duration.js.map +1 -0
  39. package/dist/errors.js +73 -0
  40. package/dist/errors.js.map +1 -0
  41. package/dist/files.js +36 -0
  42. package/dist/files.js.map +1 -0
  43. package/dist/generated/openapi.js +6 -0
  44. package/dist/generated/openapi.js.map +1 -0
  45. package/dist/i18n.js +757 -0
  46. package/dist/i18n.js.map +1 -0
  47. package/dist/index.js +58 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/json-error.js +10 -0
  50. package/dist/json-error.js.map +1 -0
  51. package/dist/output.js +43 -0
  52. package/dist/output.js.map +1 -0
  53. package/dist/scenarios/config.js +228 -0
  54. package/dist/scenarios/config.js.map +1 -0
  55. package/dist/scenarios/plan.js +159 -0
  56. package/dist/scenarios/plan.js.map +1 -0
  57. package/dist/servers-config/export.js +72 -0
  58. package/dist/servers-config/export.js.map +1 -0
  59. package/dist/servers-config/fields.js +172 -0
  60. package/dist/servers-config/fields.js.map +1 -0
  61. package/dist/servers-config/forbidden-host.js +75 -0
  62. package/dist/servers-config/forbidden-host.js.map +1 -0
  63. package/dist/servers-config/host-pattern.js +6 -0
  64. package/dist/servers-config/host-pattern.js.map +1 -0
  65. package/dist/servers-config/parse.js +260 -0
  66. package/dist/servers-config/parse.js.map +1 -0
  67. package/dist/servers-config/plan.js +492 -0
  68. package/dist/servers-config/plan.js.map +1 -0
  69. package/dist/servers-update.js +157 -0
  70. package/dist/servers-update.js.map +1 -0
  71. package/dist/status-page.js +15 -0
  72. package/dist/status-page.js.map +1 -0
  73. package/dist/text-length.js +8 -0
  74. package/dist/text-length.js.map +1 -0
  75. package/dist/write-failure.js +37 -0
  76. package/dist/write-failure.js.map +1 -0
  77. package/package.json +70 -4
package/dist/i18n.js ADDED
@@ -0,0 +1,757 @@
1
+ /**
2
+ * User-facing strings in two languages, picked from the system locale.
3
+ *
4
+ * Every message lives in both dictionaries: `en` is typed by `ru`, so a key
5
+ * forgotten in one of them fails `tsc`. Commands and flags stay English.
6
+ */
7
+ export function detectLang(env = process.env) {
8
+ // POSIX precedence: LC_ALL overrides LC_MESSAGES, which overrides LANG.
9
+ const locale = env.LC_ALL || env.LC_MESSAGES || env.LANG || '';
10
+ if (locale.toLowerCase().startsWith('ru'))
11
+ return 'ru';
12
+ if (!locale && process.platform === 'win32') {
13
+ const intl = Intl.DateTimeFormat().resolvedOptions().locale;
14
+ if (intl.toLowerCase().startsWith('ru'))
15
+ return 'ru';
16
+ }
17
+ return 'en';
18
+ }
19
+ const ru = {
20
+ // Program
21
+ programDescription: 'CLI Statuser: серверы и сценарии как код, плановые работы в CI, инциденты, heartbeat для крон-задач',
22
+ optJson: 'вывести ответ в JSON',
23
+ optApiUrl: 'адрес API (по умолчанию https://api.statuser.cloud)',
24
+ optDebug: 'печатать запросы в stderr (секреты скрыты)',
25
+ // Auth
26
+ cmdLogin: 'сохранить API-ключ на этом компьютере',
27
+ cmdLogout: 'удалить сохранённый API-ключ',
28
+ cmdWhoami: 'показать аккаунт, к которому относится ключ',
29
+ optWithToken: 'прочитать ключ из stdin, а не спрашивать',
30
+ promptApiKey: 'API-ключ (панель Statuser → Настройки аккаунта → API и MCP): ',
31
+ loginEmptyKey: 'Ключ пустой.',
32
+ loginNoTty: 'Нет терминала для ввода. Передайте ключ через stdin: statuser login --with-token < key.txt',
33
+ loginSaved: (email, path) => `Готово: ${email}. Ключ сохранён в ${path}`,
34
+ logoutDone: 'Ключ удалён.',
35
+ logoutNothing: 'Сохранённого ключа нет.',
36
+ whoami: (email, source) => `${email} (ключ из ${source})`,
37
+ keySourceEnv: 'STATUSER_API_KEY',
38
+ keySourceFile: (path) => `${path}`,
39
+ noApiKey: 'Нет API-ключа. Выполните `statuser login` или задайте STATUSER_API_KEY.',
40
+ keyOtherOrigin: (saved, current) => `Сохранённый ключ выдан для ${saved}, а запрос идёт на ${current}. Для другого адреса задайте STATUSER_API_KEY.`,
41
+ apiKeyMalformed: 'В ключе есть пробелы или переводы строки внутри. Скопируйте его из панели заново.',
42
+ unauthorized: 'Ключ не принят (401). Проверьте его в панели и выполните `statuser login` заново.',
43
+ // Client
44
+ apiUrlInvalid: (url) => `Неверный адрес API: ${url}`,
45
+ apiUrlInsecure: (url) => `Адрес API должен быть https (http разрешён только для localhost): ${url}`,
46
+ foreignOrigin: (url) => `Запрос на ${url} не отправлен: ключ уходит только на адрес API.`,
47
+ debugRetry: (reason) => `× ${reason}, повтор`,
48
+ networkError: (message) => `Сеть: ${message}`,
49
+ // api
50
+ cmdApi: 'отправить произвольный запрос к API (как `gh api`)',
51
+ optField: 'поле тела или query-параметр key=value (значение — JSON, если разбирается)',
52
+ optRawField: 'поле key=value, значение всегда строка',
53
+ optInput: 'тело запроса из файла, `-` — из stdin',
54
+ argInvalid: (flag, position) => `${flag} №${position}: ожидается key=value (значение не показано)`,
55
+ apiInputWithGet: 'У GET нет тела: для --input укажите метод, например -X POST.',
56
+ apiFieldsWithInput: 'Нельзя одновременно передавать поля и --input.',
57
+ apiMethodInvalid: (method) => `Неизвестный метод: ${method}`,
58
+ apiPathInvalid: 'Путь должен начинаться с /, например /v1/servers',
59
+ // servers
60
+ cmdServers: 'серверы мониторинга',
61
+ cmdServersList: 'список серверов',
62
+ cmdServersGet: 'сервер по id',
63
+ cmdServersPause: 'приостановить проверки',
64
+ cmdServersResume: 'возобновить проверки',
65
+ optProject: 'id проекта',
66
+ optProtocol: 'только с этим протоколом',
67
+ optFromFile: 'взять id из JSON-вывода `servers pause --json` (возобновить только то, что поставили на паузу)',
68
+ serversNoTargets: 'Укажите id серверов или --project.',
69
+ serversEmpty: 'Серверов нет.',
70
+ colId: 'ID',
71
+ colName: 'Имя',
72
+ colHost: 'Хост',
73
+ colProtocol: 'Протокол',
74
+ colStatus: 'Статус',
75
+ colProject: 'Проект',
76
+ pausedOne: (id) => `#${id}: на паузе`,
77
+ resumedOne: (id) => `#${id}: проверки возобновлены`,
78
+ alreadyPaused: (id) => `#${id}: уже был на паузе, пропущен`,
79
+ notPaused: (id) => `#${id}: не на паузе, пропущен`,
80
+ bulkFailed: (id, message) => `#${id}: ошибка — ${message}`,
81
+ bulkSummary: (ok, failed) => failed ? `Готово: ${ok}, ошибок: ${failed}` : `Готово: ${ok}`,
82
+ invalidId: (raw) => `Неверный id: ${raw}`,
83
+ notFound: (id) => `#${id} не найден`,
84
+ fromFileInvalid: (path) => `В ${path} нет списка серверов из \`servers pause --json\``,
85
+ // heartbeat / run
86
+ cmdHeartbeat: 'heartbeat-проверки',
87
+ cmdHeartbeatPing: 'отправить сигнал heartbeat',
88
+ optFail: 'сообщить об ошибке, а не об успехе',
89
+ cmdRun: 'выполнить команду и отправить heartbeat по её итогу',
90
+ optHeartbeatToken: 'токен heartbeat (или STATUSER_HEARTBEAT_TOKEN); API-ключ тогда не нужен',
91
+ optHeartbeatServer: 'id heartbeat-сервера — токен возьмётся по API-ключу',
92
+ heartbeatNoToken: 'Нужен токен: --heartbeat <токен>, STATUSER_HEARTBEAT_TOKEN или --server <id>.',
93
+ heartbeatBothSources: 'Укажите что-то одно: токен или --server.',
94
+ heartbeatNotHeartbeat: (id, protocol) => `Сервер #${id} — не heartbeat (протокол ${protocol}).`,
95
+ heartbeatSent: 'Сигнал отправлен.',
96
+ heartbeatFailSent: 'Ошибка зафиксирована.',
97
+ heartbeatSendFailed: (message) => `Не удалось отправить heartbeat: ${message}`,
98
+ heartbeatTokenRejected: 'Токен heartbeat не найден (404). Сверьте его с «URL запроса» на карточке сервера.',
99
+ runNoCommand: 'Укажите команду после --: statuser run --heartbeat <токен> -- ./backup.sh',
100
+ runSpawnFailed: (cmd, message) => `Не удалось запустить ${cmd}: ${message}`,
101
+ // maintenance
102
+ cmdMaintenance: 'плановые работы на странице статуса',
103
+ cmdMaintenanceStart: 'открыть плановые работы с текущего момента',
104
+ cmdMaintenanceEnd: 'завершить плановые работы сейчас',
105
+ optPage: 'id страницы статуса',
106
+ optTitle: 'заголовок',
107
+ optDescription: 'описание для посетителей страницы',
108
+ optFor: 'длительность окна: 30m, 2h, 1h30m',
109
+ optServers: 'id серверов страницы через запятую',
110
+ optAllServers: 'все серверы страницы',
111
+ optMessage: 'добавить сообщение в таймлайн работ (уведомит подписчиков)',
112
+ maintenanceNoServers: 'Укажите --servers <id,…> или --all-servers.',
113
+ maintenanceBothServers: 'Укажите что-то одно: --servers или --all-servers.',
114
+ maintenancePageEmpty: (id) => `На странице #${id} нет серверов.`,
115
+ maintenanceStarted: (id, until) => `Плановые работы #${id} открыты до ${until}.`,
116
+ maintenanceEnded: (id) => `Плановые работы #${id} завершены.`,
117
+ maintenanceAlreadyEnded: (id) => `Плановые работы #${id} уже закончились, дата окончания не менялась.`,
118
+ messageInvalid: (max) => `Сообщение пустое или длиннее ${max} символов.`,
119
+ maintenanceMessageFailed: (message) => `Окно закрыто, но сообщение в таймлайн не добавлено: ${message}`,
120
+ durationInvalid: (raw) => `Неверная длительность: ${raw}. Примеры: 30m, 2h, 1h30m`,
121
+ // scenarios
122
+ cmdScenarios: 'сценарные проверки (Playwright)',
123
+ cmdScenariosTest: 'прогнать файл сценария в песочнице Statuser, ничего не сохраняя',
124
+ cmdScenariosDeploy: 'привести сценарии проекта к конфигу statuser.json',
125
+ optConfig: 'путь к конфигу',
126
+ optDryRun: 'показать план, ничего не меняя',
127
+ optTimeout: 'таймаут прогона, секунд: 15, 30, 45, 60, 120',
128
+ optLocation: 'локация прогона: msk-1, spb-1, ala-1, nyc-1, ams-1',
129
+ optEnv: 'переменная окружения сценария KEY=VALUE (можно несколько)',
130
+ scenarioRunning: 'Прогоняю сценарий…',
131
+ scenarioPassed: (seconds) => `Сценарий прошёл за ${seconds} с.`,
132
+ scenarioFailed: (state) => state === 'timed_out'
133
+ ? 'Сценарий не уложился в таймаут.'
134
+ : 'Сценарий не прошёл.',
135
+ scenarioRunnerError: 'Песочница не смогла выполнить прогон — это сбой на стороне Statuser, а не сайта.',
136
+ scenarioFailedTest: (title) => `Тест: ${title}`,
137
+ scenarioError: (message) => `Ошибка: ${message}`,
138
+ scenarioArtifact: (kind, url) => `${{ screenshot: 'Скриншот', video: 'Видео', trace: 'Трейс' }[kind] ?? kind}: ${url}`,
139
+ configNotFound: (path) => `Не найден конфиг ${path}`,
140
+ configInvalid: (path, problem) => `${path}: ${problem}`,
141
+ envMissing: (name, scenario) => `Переменная окружения ${name} не задана (сценарий «${scenario}»).`,
142
+ deployPlanTitle: (project) => `План для проекта #${project}:`,
143
+ deployCreate: (name) => ` + создать «${name}»`,
144
+ deployUpdate: (name, id, fields) => ` ~ изменить «${name}» (#${id}): ${fields}`,
145
+ deployUnchanged: (name, id) => ` = без изменений «${name}» (#${id})`,
146
+ deployUnmanaged: (name, id) => ` ? в проекте, но не в конфиге: «${name}» (#${id}) — не трогаю`,
147
+ deployNothing: 'Изменений нет.',
148
+ deployDryRunDone: 'Это план (--dry-run), ничего не изменено.',
149
+ deployApplied: (name) => `Готово: «${name}»`,
150
+ deployStopped: (name, message) => `Остановлено на «${name}»: ${message}. Применённое выше остаётся.`,
151
+ deployDuplicateName: (name) => `В проекте несколько сценариев с именем «${name}» — укажите id в конфиге.`,
152
+ deployDuplicateScript: (a, b) => `У «${a}» и «${b}» одинаковый текст скрипта — Statuser не заводит дубли.`,
153
+ deployScriptTaken: (name, id, project) => `Скрипт «${name}» уже стоит у сервера #${id} в проекте #${project}.`,
154
+ deployIdNotFound: (name, id) => `Сценарий «${name}»: сервер #${id} не найден среди сценариев аккаунта.`,
155
+ deployIdOtherProject: (name, id, project) => `Сценарий «${name}»: сервер #${id} в другом проекте (#${project}).`,
156
+ deployEnvAlwaysSent: 'Значения переменных окружения API не возвращает, поэтому они отправляются при каждом deploy.',
157
+ // incidents
158
+ cmdIncidents: 'инциденты',
159
+ cmdIncidentsList: 'список инцидентов',
160
+ cmdIncidentsGet: 'инцидент по id',
161
+ cmdIncidentsAi: 'разбор инцидента Statuser AI',
162
+ cmdIncidentsPdf: 'скачать PDF-отчёт по инциденту',
163
+ optOpen: 'только идущие',
164
+ optIncidentStatus: 'статусы через запятую: ongoing, resolved, dismissed, auto_closed_changed, auto_closed_timeout',
165
+ optServerIds: 'id серверов через запятую',
166
+ optSince: 'начавшиеся не раньше: 24h, 7d или дата ISO',
167
+ optUntil: 'начавшиеся не позже: дата ISO',
168
+ optOutput: 'куда сохранить файл',
169
+ optForce: 'перезаписать существующий файл',
170
+ incidentsOpenWithStatus: 'Укажите что-то одно: --open или --status.',
171
+ incidentStatusInvalid: (status) => `Неизвестный статус: ${status}`,
172
+ whenInvalid: (raw) => `Неверная дата: ${raw}. Примеры: 24h, 7d, 2026-11-01`,
173
+ incidentsEmpty: 'Инцидентов нет.',
174
+ colServer: 'Сервер',
175
+ colStarted: 'Начало',
176
+ colDuration: 'Длительность',
177
+ colError: 'Ошибка',
178
+ incidentStatus: (status) => ({
179
+ ongoing: 'идёт',
180
+ resolved: 'завершён',
181
+ dismissed: 'отклонён',
182
+ auto_closed_changed: 'закрыт после правки сервера',
183
+ auto_closed_timeout: 'закрыт по сроку',
184
+ })[status] ?? status,
185
+ aiNoData: 'Для разбора нет данных: у инцидента нет диагностики (она есть на платных тарифах).',
186
+ incidentTitle: (id) => `Инцидент #${id}`,
187
+ incidentServer: (name) => `Сервер: ${name}`,
188
+ incidentStatusLine: (status) => `Статус: ${status}`,
189
+ incidentStarted: (time) => `Начало: ${time}`,
190
+ incidentEnded: (time) => `Конец: ${time}`,
191
+ incidentDurationLine: (duration) => `Длительность: ${duration}`,
192
+ incidentError: (error) => `Ошибка: ${error}`,
193
+ incidentDiagnostics: (locations) => `Диагностика: ${locations} (подробно — с --json)`,
194
+ duration: (days, hours, minutes) => days
195
+ ? `${days} д ${hours} ч`
196
+ : hours
197
+ ? `${hours} ч ${minutes} мин`
198
+ : `${minutes} мин`,
199
+ unexpectedContentType: (type) => `API вернул не PDF, а ${type || 'неизвестный тип'}.`,
200
+ fileSaved: (path) => `Сохранено: ${path}`,
201
+ fileExists: (path) => `Файл ${path} уже есть. Чтобы перезаписать, добавьте --force.`,
202
+ // reports
203
+ cmdReports: 'отчёты об инцидентах на странице статуса',
204
+ cmdReportsList: 'отчёты страницы',
205
+ cmdReportsCreate: 'опубликовать отчёт об инциденте',
206
+ cmdReportsUpdate: 'добавить сообщение в отчёт',
207
+ cmdReportsResolve: 'закрыть отчёт: все серверы — resolved',
208
+ optReportMessage: 'сообщение для посетителей и подписчиков страницы (на языке страницы)',
209
+ optImpact: 'как затронуты серверы: downtime или degraded',
210
+ optUpdateStatus: 'новый статус всех серверов отчёта: downtime, degraded или resolved',
211
+ optLinkedIncidents: 'id инцидентов Statuser через запятую',
212
+ reportsEmpty: 'Отчётов нет.',
213
+ colTitle: 'Заголовок',
214
+ serverImpact: (status) => ({
215
+ not_affected: 'не затронут',
216
+ downtime: 'недоступен',
217
+ degraded: 'работает с ошибками',
218
+ resolved: 'решено',
219
+ })[status] ?? status,
220
+ statusChoice: (allowed) => `Статус — один из: ${allowed}`,
221
+ reportCreated: (id) => `Отчёт #${id} опубликован.`,
222
+ reportUpdated: (id) => `Сообщение добавлено в отчёт #${id}.`,
223
+ reportResolved: (id) => `Отчёт #${id} закрыт.`,
224
+ reportAlreadyResolved: (id) => `Отчёт #${id} уже закрыт, сообщение не отправлено.`,
225
+ reportNoServers: (id) => `В отчёте #${id} нет затронутых серверов — обновлять нечего.`,
226
+ // servers update
227
+ cmdServersUpdate: 'изменить настройки серверов',
228
+ optInterval: 'интервал проверки, секунд',
229
+ optRequestTimeout: 'таймаут проверки, секунд',
230
+ optRegions: 'локации через запятую: msk-1, spb-1, ala-1, nyc-1, ams-1',
231
+ optConfirmDelay: 'задержка подтверждения сбоя, секунд (0 — сразу)',
232
+ optSsl: 'включить проверку SSL-сертификата',
233
+ optNoSsl: 'выключить проверку SSL-сертификата',
234
+ optDomain: 'включить проверку срока домена',
235
+ optNoDomain: 'выключить проверку срока домена',
236
+ optBlocklist: 'включить проверку реестра блокировок',
237
+ optNoBlocklist: 'выключить проверку реестра блокировок',
238
+ optLatencyAlert: 'уведомлять о медленном ответе дольше N мс',
239
+ optNoLatencyAlert: 'не уведомлять о медленном ответе',
240
+ regionsEmpty: 'Список локаций пустой.',
241
+ regionInvalid: (region) => `Неизвестная локация: ${region}`,
242
+ numberInvalid: (raw) => `Ожидается целое число: ${raw}`,
243
+ numberRange: (min, max) => `Допустимо целое число от ${min} до ${max}.`,
244
+ updateNothing: 'Укажите, что изменить: --interval, --regions, --ssl и т. д.',
245
+ updateValueInvalid: (id, problem) => `#${id}: значение не подходит серверу (${problem}) — ничего не изменено.`,
246
+ updateUnchanged: (id) => `#${id}: уже так, пропущен`,
247
+ updateNotApplicable: (id, fields) => `#${id}: к протоколу сервера не относится (${fields}), пропущен`,
248
+ noChanges: 'без изменений',
249
+ notApplicableNote: (fields) => `не относится к протоколу: ${fields}`,
250
+ // export / diff / apply
251
+ cmdExport: 'выгрузить серверы проекта в statuser.json',
252
+ cmdDiff: 'показать, что изменит apply',
253
+ cmdApply: 'привести проект к statuser.json',
254
+ optExportOutput: 'записать в файл, а не в stdout',
255
+ exportHeaderVariables: (names) => `Значения секретных заголовков заменены ссылками на переменные окружения: ${names}. Задайте их перед apply.`,
256
+ exportSecretsOmitted: (names) => `Пароли, токены и ключи API не отдаёт, в файле их нет: ${names}. Для создания такого сервера заново добавьте их в файл как "$ПЕРЕМЕННАЯ".`,
257
+ exportPaused: (count) => `На паузе серверов: ${count}. Пауза в файл не попадает, apply её не меняет.`,
258
+ exportScenarios: (count) => `Сценариев в проекте: ${count}. Их скрипты не выгружаются — ведите их в секции scenarios.`,
259
+ exportReview: 'Проверьте файл перед коммитом: адрес, тело запроса и логин могут содержать секреты. Файл доступен только владельцу.',
260
+ applyNothingToManage: 'нет ни секции servers, ни секции scenarios',
261
+ applyClaimedTwice: (at, id) => `${at}: сервер #${id} уже указан в другой записи.`,
262
+ applyProtocolChange: (at, id, protocol) => `${at}: у сервера #${id} протокол ${protocol}. Смена протокола стирает его настройки (авторизацию, ключ, токен) — сделайте это в панели.`,
263
+ applyIdNotFound: (at, id) => `${at}: сервер #${id} не найден.`,
264
+ applyIdOtherProject: (at, id, project) => `${at}: сервер #${id} в другом проекте (#${project}).`,
265
+ applyAmbiguousName: (at, name) => `${at}: в проекте несколько серверов с именем «${name}» — укажите id.`,
266
+ applyFieldRequired: (at, field) => `${at}: для создания нужно поле ${field}.`,
267
+ applyLlmAvailability: (name) => `«${name}»: без llm_api_key проверяется только доступность эндпоинта, без запроса к модели.`,
268
+ applyDuplicate: (a, b) => `«${a}» совпадёт с «${b}» — Statuser не заводит одинаковые серверы.`,
269
+ applyServersLimit: (limit, existing, creating) => `Тариф разрешает серверов: ${limit}. Уже есть ${existing}, файл добавляет ${creating}.`,
270
+ applyIntervalTooLow: (at, min) => `${at}: интервал на этом тарифе — не меньше ${min} с.`,
271
+ applyLocationsUnavailable: (at, locations) => `${at}: на этом тарифе недоступны локации ${locations}.`,
272
+ applyDuplicateName: (at, other, name) => `${at}: имя «${name}» уже есть в ${other} — имена в файле должны быть разными.`,
273
+ applyAuthSecretMismatch: (at, field, type) => `${at}: ${field} не подходит к http_auth_type ${type} — сервер его не применит.`,
274
+ applyAuthHeaderConflict: (at) => `${at}: при http_auth_type заголовок Authorization задавать нельзя.`,
275
+ exportOtherProject: (path, project) => `${path} описывает проект #${project}, и его сценарии нельзя перенести в другой. Выгрузите в другой файл.`,
276
+ applyFeatureUnavailable: (at, feature) => `${at}: на этом тарифе нет функции ${feature}.`,
277
+ applyScenariosLimit: (limit, existing, creating) => `Тариф разрешает сценариев: ${limit}. Уже есть ${existing}, файл добавляет ${creating}.`,
278
+ applyScenarioTimeoutTooHigh: (name, max) => `«${name}»: таймаут сценария на этом тарифе — не больше ${max} с.`,
279
+ applyRequestTooLarge: (at, max) => `${at}: запрос больше ${max / 1024 / 1024} МБ — API его не примет`,
280
+ applyUnknownOutcome: (name, message) => `Нет ответа на запись «${name}» (${message}): неизвестно, применилась ли она. Запустите statuser diff и повторите apply.`,
281
+ // statuser.json problems: «where: what»
282
+ cfgObject: 'нужен объект',
283
+ cfgArray: 'нужен массив',
284
+ cfgNonEmptyArray: 'нужен непустой массив',
285
+ cfgRequired: 'обязательное поле',
286
+ cfgPositiveInt: 'нужно целое положительное число',
287
+ cfgString: 'нужна строка',
288
+ cfgNonEmptyString: 'нужна непустая строка или null',
289
+ cfgBoolean: 'нужно true или false',
290
+ cfgOneOf: (list) => `одно из: ${list}`,
291
+ cfgListOf: (list) => `непустой список из: ${list}`,
292
+ cfgStrings: 'нужен список строк',
293
+ cfgRange: (min, max) => `целое число от ${min} до ${max}`,
294
+ cfgMaxLength: (max) => `не длиннее ${max} символов`,
295
+ cfgDuplicate: (name) => `повторяется «${name}»`,
296
+ cfgNameOrId: 'нужно name или id',
297
+ cfgNotUsedBy: (protocol) => `не используется протоколом ${protocol}`,
298
+ cfgProtocol: (list) => `одно из: ${list} (сценарии — в секции scenarios)`,
299
+ cfgHeaders: 'нужен список { "key", "value" }',
300
+ cfgHeaderName: (name) => `недопустимое имя заголовка «${name}»: только латиница, цифры и !#$%&'*+.^_\`|~-`,
301
+ cfgHeaderValue: (name) => `в значении заголовка «${name}» управляющие символы`,
302
+ cfgCannotRead: (file) => `не удалось прочитать ${file}`,
303
+ cfgFileEmpty: (file) => `${file} пустой`,
304
+ cfgFileTooLarge: 'больше 64 КБ',
305
+ cfgNoTest: (file) => `в ${file} нет ни одного test(...) — такой сценарий ничего не проверял бы`,
306
+ cfgIntervalBelowTimeout: 'не меньше таймаута',
307
+ cfgEnvTooMany: (max) => `не больше ${max} переменных`,
308
+ cfgEnvName: (name) => `${name}: имя — ЗАГЛАВНЫМИ латинскими буквами и не из служебных`,
309
+ cfgEnvValue: (name) => `${name}: значение больше 4 КБ`,
310
+ cfgHostInternal: 'внутренний адрес: проверки идут снаружи, и API его не примет',
311
+ cfgHostInvalid: 'не похоже на адрес: домен, IP или URL',
312
+ cfgUsernameColon: 'в логине не может быть двоеточия',
313
+ cfgControlChars: 'управляющие символы недопустимы',
314
+ cfgSuccessCodes: (max) => `от 1 до ${max} кодов вида 200 или 2xx`,
315
+ cfgLlmModelWithoutKey: 'без llm_api_key модель не используется и не сохраняется — уберите llm_model или добавьте ключ',
316
+ cfgJson: (position) => position ? `некорректный JSON (позиция ${position})` : 'некорректный JSON',
317
+ // commander
318
+ helpUsage: 'Использование:',
319
+ helpArguments: 'Аргументы:',
320
+ helpOptions: 'Опции:',
321
+ helpGlobalOptions: 'Общие опции:',
322
+ helpCommands: 'Команды:',
323
+ helpDefault: 'по умолчанию',
324
+ helpOption: 'показать справку',
325
+ helpCommand: 'справка по команде',
326
+ versionOption: 'показать версию',
327
+ errUnknownOption: (option) => `Неизвестная опция: ${option}`,
328
+ errUnknownCommand: (command) => `Неизвестная команда: ${command}`,
329
+ errMissingOption: (option) => `Не указана обязательная опция ${option}`,
330
+ errMissingArgument: (argument) => `Не указан обязательный аргумент ${argument}`,
331
+ errOptionValue: (option) => `У опции ${option} нет значения`,
332
+ errTooManyArguments: 'Лишние аргументы.',
333
+ cmdServersAdd: 'добавить сервер в мониторинг',
334
+ cmdServersDelete: 'удалить серверы из мониторинга',
335
+ cmdServersTestNotify: 'отправить тестовый алёрт туда же, куда ушёл бы настоящий',
336
+ argServerHost: 'адрес: URL, домен или IP (у heartbeat не нужен)',
337
+ optServerName: 'имя сервера',
338
+ optServerHost: 'новый адрес сервера',
339
+ optServerDescription: 'описание; пустая строка убирает его',
340
+ optAddProtocol: 'протокол: http, keyword, ping, tcp, dns, heartbeat (по умолчанию http для URL и ping для IP)',
341
+ optPort: 'порт (tcp)',
342
+ optKeyword: 'слово на странице (keyword)',
343
+ optKeywordAbsent: 'алёрт, если слово появилось, а не пропало (keyword)',
344
+ optDnsTypes: 'типы DNS-записей через запятую, например A,AAAA (dns)',
345
+ optYes: 'не спрашивать подтверждение',
346
+ addProtocolNeeded: (host) => `Не понять протокол по адресу «${host}»: укажите --protocol (http, keyword, ping, tcp, dns).`,
347
+ addHostNeeded: 'Укажите адрес сервера.',
348
+ addHostNotUsed: 'У heartbeat нет адреса: уберите его, задайте --name.',
349
+ addCreated: (id, label) => `Сервер «${label}» добавлен: #${id}.`,
350
+ addDryRun: (label) => `Будет добавлен сервер «${label}».`,
351
+ deletedOne: (id) => `#${id}: удалён`,
352
+ confirmDeleteServers: (list) => `Удалить из мониторинга: ${list}? История проверок и инциденты удалятся вместе с ними. [y/N] `,
353
+ confirmDeleteIncidents: (list) => `Удалить инциденты ${list}? [y/N] `,
354
+ confirmNeedsYes: 'Удаление без терминала требует --yes: спросить подтверждение некому.',
355
+ cancelled: 'Отменено, ничего не изменено.',
356
+ testNotifySent: (id) => `Тестовый алёрт по серверу #${id} отправлен в каналы его проекта.`,
357
+ updateSingleOnly: '--name, --host и --description меняют один сервер: укажите ровно один id.',
358
+ updateHostNotApplicable: (id, protocol) => `#${id}: у ${protocol} адрес системный, его не меняют.`,
359
+ updateDuplicate: (id, other) => `#${id}: такой сервер уже есть — #${other}.`,
360
+ cmdIncidentsDelete: 'удалить инциденты',
361
+ cmdIncidentsComment: 'добавить комментарий к инциденту',
362
+ optCommentText: 'текст комментария',
363
+ commentInvalid: (max) => `Комментарий не может быть пустым или длиннее ${max} символов.`,
364
+ commentAdded: (id) => `Комментарий добавлен к инциденту #${id}.`,
365
+ cmdProjects: 'проекты аккаунта',
366
+ cmdProjectsList: 'список проектов',
367
+ projectsEmpty: 'Проектов нет.',
368
+ cmdPages: 'страницы статуса',
369
+ cmdPagesList: 'список страниц статуса',
370
+ pagesEmpty: 'Страниц статуса нет.',
371
+ colSlug: 'Путь',
372
+ colPublished: 'Опубликована',
373
+ yes: 'да',
374
+ no: 'нет',
375
+ cmdScenariosRun: 'прогнать сохранённый сценарий сейчас — как по расписанию, с инцидентом при сбое',
376
+ scenarioEnvUnmasked: (names) => `Значения переменных сценария (${names}) CLI не знает и в выводе не скроет: задайте их в окружении под теми же именами.`,
377
+ scenarioNotScenario: (id, protocol) => `#${id} — не сценарий, а ${protocol}.`,
378
+ cmdServersChecks: 'проверки сервера за период',
379
+ optChecksSince: 'с какого момента: 24h, 7d или дата (по умолчанию сутки)',
380
+ optChecksUntil: 'по какой момент: 24h, 7d или дата',
381
+ checksEmpty: 'Проверок за этот период нет.',
382
+ colTime: 'Время',
383
+ colLocations: 'Локации',
384
+ cmdMaintenanceSchedule: 'объявить плановые работы заранее',
385
+ cmdMaintenanceList: 'плановые работы страницы',
386
+ cmdMaintenanceDelete: 'удалить плановые работы со страницы',
387
+ optAt: 'начало: дата и время, например 2026-11-20T03:00+03:00',
388
+ maintenanceAtInvalid: (raw) => `--at: не дата или уже прошло: ${raw}. Работы, которые идут сейчас, открывает maintenance start.`,
389
+ maintenanceScheduled: (id, start, end) => `Плановые работы #${id} объявлены: ${start} — ${end}.`,
390
+ maintenanceEmpty: 'Плановых работ нет.',
391
+ confirmDeleteMaintenance: (id, title) => `Удалить плановые работы «${title}» (#${id}) со страницы? [y/N] `,
392
+ colEnd: 'Конец',
393
+ };
394
+ const en = {
395
+ programDescription: 'Statuser CLI: servers and scenario checks as code, maintenance windows in CI, incidents, heartbeat for cron jobs',
396
+ optJson: 'print the response as JSON',
397
+ optApiUrl: 'API address (default https://api.statuser.cloud)',
398
+ optDebug: 'print requests to stderr (secrets hidden)',
399
+ cmdLogin: 'save an API key on this machine',
400
+ cmdLogout: 'remove the saved API key',
401
+ cmdWhoami: 'show the account the key belongs to',
402
+ optWithToken: 'read the key from stdin instead of prompting',
403
+ promptApiKey: 'API key (Statuser panel → Account settings → API and MCP): ',
404
+ loginEmptyKey: 'The key is empty.',
405
+ loginNoTty: 'No terminal to prompt on. Pass the key via stdin: statuser login --with-token < key.txt',
406
+ loginSaved: (email, path) => `Done: ${email}. Key saved to ${path}`,
407
+ logoutDone: 'Key removed.',
408
+ logoutNothing: 'No saved key.',
409
+ whoami: (email, source) => `${email} (key from ${source})`,
410
+ keySourceEnv: 'STATUSER_API_KEY',
411
+ keySourceFile: (path) => `${path}`,
412
+ noApiKey: 'No API key. Run `statuser login` or set STATUSER_API_KEY.',
413
+ keyOtherOrigin: (saved, current) => `The saved key was issued for ${saved}, but the request goes to ${current}. Set STATUSER_API_KEY for another address.`,
414
+ apiKeyMalformed: 'The key contains spaces or line breaks inside. Copy it from the panel again.',
415
+ unauthorized: 'The key was rejected (401). Check it in the panel and run `statuser login` again.',
416
+ apiUrlInvalid: (url) => `Invalid API address: ${url}`,
417
+ apiUrlInsecure: (url) => `The API address must be https (http only for localhost): ${url}`,
418
+ foreignOrigin: (url) => `Request to ${url} not sent: the key only goes to the API address.`,
419
+ debugRetry: (reason) => `× ${reason}, retrying`,
420
+ networkError: (message) => `Network: ${message}`,
421
+ cmdApi: 'send an arbitrary API request (like `gh api`)',
422
+ optField: 'body field or query parameter key=value (value parsed as JSON when possible)',
423
+ optRawField: 'field key=value, value is always a string',
424
+ optInput: 'request body from a file, `-` for stdin',
425
+ argInvalid: (flag, position) => `${flag} #${position}: key=value expected (the value is not shown)`,
426
+ apiInputWithGet: 'GET has no body: give --input a method, e.g. -X POST.',
427
+ apiFieldsWithInput: 'Fields and --input cannot be combined.',
428
+ apiMethodInvalid: (method) => `Unknown method: ${method}`,
429
+ apiPathInvalid: 'The path must start with /, e.g. /v1/servers',
430
+ cmdServers: 'monitored servers',
431
+ cmdServersList: 'list servers',
432
+ cmdServersGet: 'server by id',
433
+ cmdServersPause: 'pause checks',
434
+ cmdServersResume: 'resume checks',
435
+ optProject: 'project id',
436
+ optProtocol: 'only this protocol',
437
+ optFromFile: 'take ids from the JSON output of `servers pause --json` (resume only what was paused)',
438
+ serversNoTargets: 'Pass server ids or --project.',
439
+ serversEmpty: 'No servers.',
440
+ colId: 'ID',
441
+ colName: 'Name',
442
+ colHost: 'Host',
443
+ colProtocol: 'Protocol',
444
+ colStatus: 'Status',
445
+ colProject: 'Project',
446
+ pausedOne: (id) => `#${id}: paused`,
447
+ resumedOne: (id) => `#${id}: checks resumed`,
448
+ alreadyPaused: (id) => `#${id}: already paused, skipped`,
449
+ notPaused: (id) => `#${id}: not paused, skipped`,
450
+ bulkFailed: (id, message) => `#${id}: error — ${message}`,
451
+ bulkSummary: (ok, failed) => failed ? `Done: ${ok}, errors: ${failed}` : `Done: ${ok}`,
452
+ invalidId: (raw) => `Invalid id: ${raw}`,
453
+ notFound: (id) => `#${id} not found`,
454
+ fromFileInvalid: (path) => `${path} has no server list from \`servers pause --json\``,
455
+ cmdHeartbeat: 'heartbeat checks',
456
+ cmdHeartbeatPing: 'send a heartbeat signal',
457
+ optFail: 'report a failure instead of success',
458
+ cmdRun: 'run a command and send a heartbeat based on its result',
459
+ optHeartbeatToken: 'heartbeat token (or STATUSER_HEARTBEAT_TOKEN); no API key needed then',
460
+ optHeartbeatServer: 'heartbeat server id — the token is fetched with the API key',
461
+ heartbeatNoToken: 'A token is required: --heartbeat <token>, STATUSER_HEARTBEAT_TOKEN or --server <id>.',
462
+ heartbeatBothSources: 'Pass either a token or --server, not both.',
463
+ heartbeatNotHeartbeat: (id, protocol) => `Server #${id} is not a heartbeat (protocol ${protocol}).`,
464
+ heartbeatSent: 'Signal sent.',
465
+ heartbeatFailSent: 'Failure recorded.',
466
+ heartbeatSendFailed: (message) => `Could not send the heartbeat: ${message}`,
467
+ heartbeatTokenRejected: 'Heartbeat token not found (404). Compare it with the request URL on the server card.',
468
+ runNoCommand: 'Pass a command after --: statuser run --heartbeat <token> -- ./backup.sh',
469
+ runSpawnFailed: (cmd, message) => `Could not start ${cmd}: ${message}`,
470
+ cmdMaintenance: 'planned maintenance on a status page',
471
+ cmdMaintenanceStart: 'open planned maintenance starting now',
472
+ cmdMaintenanceEnd: 'end planned maintenance now',
473
+ optPage: 'status page id',
474
+ optTitle: 'title',
475
+ optDescription: 'description for page visitors',
476
+ optFor: 'window length: 30m, 2h, 1h30m',
477
+ optServers: 'comma-separated ids of the page servers',
478
+ optAllServers: 'all servers of the page',
479
+ optMessage: 'add a message to the maintenance timeline (notifies subscribers)',
480
+ maintenanceNoServers: 'Pass --servers <id,…> or --all-servers.',
481
+ maintenanceBothServers: 'Pass either --servers or --all-servers, not both.',
482
+ maintenancePageEmpty: (id) => `Status page #${id} has no servers.`,
483
+ maintenanceStarted: (id, until) => `Planned maintenance #${id} is open until ${until}.`,
484
+ maintenanceEnded: (id) => `Planned maintenance #${id} ended.`,
485
+ maintenanceAlreadyEnded: (id) => `Planned maintenance #${id} has already ended; the end date was left as is.`,
486
+ messageInvalid: (max) => `The message is empty or longer than ${max} characters.`,
487
+ maintenanceMessageFailed: (message) => `The window is closed, but the timeline message was not added: ${message}`,
488
+ durationInvalid: (raw) => `Invalid duration: ${raw}. Examples: 30m, 2h, 1h30m`,
489
+ cmdScenarios: 'scenario checks (Playwright)',
490
+ cmdScenariosTest: 'run a scenario file in the Statuser sandbox without saving it',
491
+ cmdScenariosDeploy: 'bring the project scenarios in line with statuser.json',
492
+ optConfig: 'config path',
493
+ optDryRun: 'show the plan without changing anything',
494
+ optTimeout: 'run timeout, seconds: 15, 30, 45, 60, 120',
495
+ optLocation: 'run location: msk-1, spb-1, ala-1, nyc-1, ams-1',
496
+ optEnv: 'scenario environment variable KEY=VALUE (repeatable)',
497
+ scenarioRunning: 'Running the scenario…',
498
+ scenarioPassed: (seconds) => `Scenario passed in ${seconds} s.`,
499
+ scenarioFailed: (state) => state === 'timed_out'
500
+ ? 'The scenario ran out of time.'
501
+ : 'The scenario did not pass.',
502
+ scenarioRunnerError: 'The sandbox could not run the scenario — a Statuser-side failure, not a site one.',
503
+ scenarioFailedTest: (title) => `Test: ${title}`,
504
+ scenarioError: (message) => `Error: ${message}`,
505
+ scenarioArtifact: (kind, url) => `${{ screenshot: 'Screenshot', video: 'Video', trace: 'Trace' }[kind] ?? kind}: ${url}`,
506
+ configNotFound: (path) => `Config not found: ${path}`,
507
+ configInvalid: (path, problem) => `${path}: ${problem}`,
508
+ envMissing: (name, scenario) => `Environment variable ${name} is not set (scenario “${scenario}”).`,
509
+ deployPlanTitle: (project) => `Plan for project #${project}:`,
510
+ deployCreate: (name) => ` + create “${name}”`,
511
+ deployUpdate: (name, id, fields) => ` ~ update “${name}” (#${id}): ${fields}`,
512
+ deployUnchanged: (name, id) => ` = unchanged “${name}” (#${id})`,
513
+ deployUnmanaged: (name, id) => ` ? in the project but not in the config: “${name}” (#${id}) — left alone`,
514
+ deployNothing: 'Nothing to change.',
515
+ deployDryRunDone: 'This is a plan (--dry-run); nothing was changed.',
516
+ deployApplied: (name) => `Done: “${name}”`,
517
+ deployStopped: (name, message) => `Stopped at “${name}”: ${message}. Changes applied above remain.`,
518
+ deployDuplicateName: (name) => `Several scenarios in the project are named “${name}” — set its id in the config.`,
519
+ deployDuplicateScript: (a, b) => `“${a}” and “${b}” have the same script text — Statuser does not allow duplicates.`,
520
+ deployScriptTaken: (name, id, project) => `The script of “${name}” is already used by server #${id} in project #${project}.`,
521
+ deployIdNotFound: (name, id) => `Scenario “${name}”: server #${id} is not among the account scenarios.`,
522
+ deployIdOtherProject: (name, id, project) => `Scenario “${name}”: server #${id} is in another project (#${project}).`,
523
+ deployEnvAlwaysSent: 'The API does not return environment values, so they are sent on every deploy.',
524
+ cmdIncidents: 'incidents',
525
+ cmdIncidentsList: 'list incidents',
526
+ cmdIncidentsGet: 'incident by id',
527
+ cmdIncidentsAi: 'Statuser AI summary of an incident',
528
+ cmdIncidentsPdf: 'download the PDF report of an incident',
529
+ optOpen: 'ongoing only',
530
+ optIncidentStatus: 'comma-separated statuses: ongoing, resolved, dismissed, auto_closed_changed, auto_closed_timeout',
531
+ optServerIds: 'comma-separated server ids',
532
+ optSince: 'started no earlier than: 24h, 7d or an ISO date',
533
+ optUntil: 'started no later than: an ISO date',
534
+ optOutput: 'where to save the file',
535
+ optForce: 'overwrite an existing file',
536
+ incidentsOpenWithStatus: 'Pass either --open or --status, not both.',
537
+ incidentStatusInvalid: (status) => `Unknown status: ${status}`,
538
+ whenInvalid: (raw) => `Invalid date: ${raw}. Examples: 24h, 7d, 2026-11-01`,
539
+ incidentsEmpty: 'No incidents.',
540
+ colServer: 'Server',
541
+ colStarted: 'Started',
542
+ colDuration: 'Duration',
543
+ colError: 'Error',
544
+ incidentStatus: (status) => ({
545
+ ongoing: 'ongoing',
546
+ resolved: 'resolved',
547
+ dismissed: 'dismissed',
548
+ auto_closed_changed: 'closed after a server edit',
549
+ auto_closed_timeout: 'closed by timeout',
550
+ })[status] ?? status,
551
+ aiNoData: 'Nothing to analyse: the incident has no diagnostics (available on paid plans).',
552
+ incidentTitle: (id) => `Incident #${id}`,
553
+ incidentServer: (name) => `Server: ${name}`,
554
+ incidentStatusLine: (status) => `Status: ${status}`,
555
+ incidentStarted: (time) => `Started: ${time}`,
556
+ incidentEnded: (time) => `Ended: ${time}`,
557
+ incidentDurationLine: (duration) => `Duration: ${duration}`,
558
+ incidentError: (error) => `Error: ${error}`,
559
+ incidentDiagnostics: (locations) => `Diagnostics: ${locations} (details with --json)`,
560
+ duration: (days, hours, minutes) => days
561
+ ? `${days}d ${hours}h`
562
+ : hours
563
+ ? `${hours}h ${minutes}m`
564
+ : `${minutes}m`,
565
+ unexpectedContentType: (type) => `The API returned ${type || 'an unknown type'} instead of a PDF.`,
566
+ fileSaved: (path) => `Saved: ${path}`,
567
+ fileExists: (path) => `${path} already exists. Add --force to overwrite it.`,
568
+ cmdReports: 'incident reports on a status page',
569
+ cmdReportsList: 'reports of a page',
570
+ cmdReportsCreate: 'publish an incident report',
571
+ cmdReportsUpdate: 'add a message to a report',
572
+ cmdReportsResolve: 'close a report: all servers resolved',
573
+ optReportMessage: 'message for page visitors and subscribers (in the page language)',
574
+ optImpact: 'how servers are affected: downtime or degraded',
575
+ optUpdateStatus: 'new status of all report servers: downtime, degraded or resolved',
576
+ optLinkedIncidents: 'comma-separated Statuser incident ids',
577
+ reportsEmpty: 'No reports.',
578
+ colTitle: 'Title',
579
+ serverImpact: (status) => ({
580
+ not_affected: 'not affected',
581
+ downtime: 'down',
582
+ degraded: 'degraded',
583
+ resolved: 'resolved',
584
+ })[status] ?? status,
585
+ statusChoice: (allowed) => `Status must be one of: ${allowed}`,
586
+ reportCreated: (id) => `Report #${id} published.`,
587
+ reportUpdated: (id) => `Message added to report #${id}.`,
588
+ reportResolved: (id) => `Report #${id} closed.`,
589
+ reportAlreadyResolved: (id) => `Report #${id} is already closed; no message sent.`,
590
+ reportNoServers: (id) => `Report #${id} has no affected servers — nothing to update.`,
591
+ cmdServersUpdate: 'change server settings',
592
+ optInterval: 'check interval, seconds',
593
+ optRequestTimeout: 'check timeout, seconds',
594
+ optRegions: 'comma-separated locations: msk-1, spb-1, ala-1, nyc-1, ams-1',
595
+ optConfirmDelay: 'failure confirmation delay, seconds (0 — immediately)',
596
+ optSsl: 'turn the SSL certificate check on',
597
+ optNoSsl: 'turn the SSL certificate check off',
598
+ optDomain: 'turn the domain expiry check on',
599
+ optNoDomain: 'turn the domain expiry check off',
600
+ optBlocklist: 'turn the blocklist check on',
601
+ optNoBlocklist: 'turn the blocklist check off',
602
+ optLatencyAlert: 'alert on responses slower than N ms',
603
+ optNoLatencyAlert: 'do not alert on slow responses',
604
+ regionsEmpty: 'The location list is empty.',
605
+ regionInvalid: (region) => `Unknown location: ${region}`,
606
+ numberInvalid: (raw) => `Expected a whole number: ${raw}`,
607
+ numberRange: (min, max) => `A whole number from ${min} to ${max} is allowed.`,
608
+ updateNothing: 'Pass what to change: --interval, --regions, --ssl, etc.',
609
+ updateValueInvalid: (id, problem) => `#${id}: the value does not fit this server (${problem}) — nothing was changed.`,
610
+ updateUnchanged: (id) => `#${id}: already set, skipped`,
611
+ updateNotApplicable: (id, fields) => `#${id}: not applicable to the server protocol (${fields}), skipped`,
612
+ noChanges: 'no changes',
613
+ notApplicableNote: (fields) => `not applicable to the protocol: ${fields}`,
614
+ cmdExport: "export the project's servers to statuser.json",
615
+ cmdDiff: 'show what apply would change',
616
+ cmdApply: 'bring the project in line with statuser.json',
617
+ optExportOutput: 'write to a file instead of stdout',
618
+ exportHeaderVariables: (names) => `Secret header values were replaced with environment variable references: ${names}. Set them before apply.`,
619
+ exportSecretsOmitted: (names) => `The API does not return passwords, tokens and keys, so the file has none: ${names}. To create such a server anew, add them to the file as "$VARIABLE".`,
620
+ exportPaused: (count) => `Paused servers: ${count}. Pausing is not in the file and apply does not change it.`,
621
+ exportScenarios: (count) => `Scenarios in the project: ${count}. Their scripts are not exported — keep them in the scenarios section.`,
622
+ exportReview: 'Review the file before committing: the address, request body and username may hold secrets. The file is readable by its owner only.',
623
+ applyNothingToManage: 'neither a servers nor a scenarios section',
624
+ applyClaimedTwice: (at, id) => `${at}: server #${id} is already used by another entry.`,
625
+ applyProtocolChange: (at, id, protocol) => `${at}: server #${id} uses ${protocol}. Changing the protocol wipes its settings (credentials, key, token) — do it in the panel.`,
626
+ applyIdNotFound: (at, id) => `${at}: server #${id} not found.`,
627
+ applyIdOtherProject: (at, id, project) => `${at}: server #${id} is in another project (#${project}).`,
628
+ applyAmbiguousName: (at, name) => `${at}: several servers in the project are named “${name}” — set an id.`,
629
+ applyFieldRequired: (at, field) => `${at}: ${field} is required to create it.`,
630
+ applyLlmAvailability: (name) => `“${name}”: without llm_api_key only the endpoint availability is checked, no model request.`,
631
+ applyDuplicate: (a, b) => `“${a}” would equal “${b}” — Statuser does not allow identical servers.`,
632
+ applyServersLimit: (limit, existing, creating) => `The plan allows ${limit} servers. There are ${existing}; the file adds ${creating}.`,
633
+ applyIntervalTooLow: (at, min) => `${at}: the interval on this plan is at least ${min} s.`,
634
+ applyLocationsUnavailable: (at, locations) => `${at}: locations ${locations} are not available on this plan.`,
635
+ applyDuplicateName: (at, other, name) => `${at}: the name “${name}” is already used by ${other} — names in the file must differ.`,
636
+ applyAuthSecretMismatch: (at, field, type) => `${at}: ${field} does not match http_auth_type ${type} — the server would ignore it.`,
637
+ applyAuthHeaderConflict: (at) => `${at}: an Authorization header cannot be set together with http_auth_type.`,
638
+ exportOtherProject: (path, project) => `${path} describes project #${project}; its scenarios cannot move to another one. Export to another file.`,
639
+ applyFeatureUnavailable: (at, feature) => `${at}: the plan has no ${feature}.`,
640
+ applyScenariosLimit: (limit, existing, creating) => `The plan allows ${limit} scenarios. There are ${existing}; the file adds ${creating}.`,
641
+ applyScenarioTimeoutTooHigh: (name, max) => `“${name}”: the scenario timeout on this plan is at most ${max} s.`,
642
+ applyRequestTooLarge: (at, max) => `${at}: the request is over ${max / 1024 / 1024} MB, the API refuses it`,
643
+ applyUnknownOutcome: (name, message) => `No response to the write of “${name}” (${message}): whether it was applied is unknown. Run statuser diff and repeat apply.`,
644
+ cfgObject: 'object expected',
645
+ cfgArray: 'array expected',
646
+ cfgNonEmptyArray: 'non-empty array expected',
647
+ cfgRequired: 'required',
648
+ cfgPositiveInt: 'positive integer expected',
649
+ cfgString: 'string expected',
650
+ cfgNonEmptyString: 'non-empty string or null expected',
651
+ cfgBoolean: 'true or false expected',
652
+ cfgOneOf: (list) => `one of: ${list}`,
653
+ cfgListOf: (list) => `non-empty list of: ${list}`,
654
+ cfgStrings: 'list of strings expected',
655
+ cfgRange: (min, max) => `whole number from ${min} to ${max}`,
656
+ cfgMaxLength: (max) => `at most ${max} characters`,
657
+ cfgDuplicate: (name) => `duplicate “${name}”`,
658
+ cfgNameOrId: 'name or id required',
659
+ cfgNotUsedBy: (protocol) => `not used by protocol ${protocol}`,
660
+ cfgProtocol: (list) => `one of: ${list} (scenarios go to the scenarios section)`,
661
+ cfgHeaders: 'list of { "key", "value" } expected',
662
+ cfgHeaderName: (name) => `invalid header name “${name}”: only ASCII letters, digits and !#$%&'*+.^_\`|~-`,
663
+ cfgHeaderValue: (name) => `control characters in the value of header “${name}”`,
664
+ cfgCannotRead: (file) => `cannot read ${file}`,
665
+ cfgFileEmpty: (file) => `${file} is empty`,
666
+ cfgFileTooLarge: 'larger than 64 KB',
667
+ cfgNoTest: (file) => `${file} has no test(...) call — such a scenario would check nothing`,
668
+ cfgIntervalBelowTimeout: 'must not be less than the timeout',
669
+ cfgEnvTooMany: (max) => `no more than ${max} variables`,
670
+ cfgEnvName: (name) => `${name}: the name must be UPPER_CASE and not reserved`,
671
+ cfgEnvValue: (name) => `${name}: value larger than 4 KB`,
672
+ cfgHostInternal: 'an internal address: checks run from outside, and the API refuses it',
673
+ cfgHostInvalid: 'not an address: a domain, IP or URL expected',
674
+ cfgUsernameColon: 'the username cannot contain a colon',
675
+ cfgControlChars: 'control characters are not allowed',
676
+ cfgSuccessCodes: (max) => `1 to ${max} codes like 200 or 2xx`,
677
+ cfgLlmModelWithoutKey: 'without llm_api_key the model is neither used nor stored — drop llm_model or add the key',
678
+ cfgJson: (position) => position ? `invalid JSON (position ${position})` : 'invalid JSON',
679
+ helpUsage: 'Usage:',
680
+ helpArguments: 'Arguments:',
681
+ helpOptions: 'Options:',
682
+ helpGlobalOptions: 'Global Options:',
683
+ helpCommands: 'Commands:',
684
+ helpDefault: 'default',
685
+ helpOption: 'display help for command',
686
+ helpCommand: 'display help for command',
687
+ versionOption: 'output the version number',
688
+ errUnknownOption: (option) => `Unknown option: ${option}`,
689
+ errUnknownCommand: (command) => `Unknown command: ${command}`,
690
+ errMissingOption: (option) => `Required option ${option} not specified`,
691
+ errMissingArgument: (argument) => `Missing required argument ${argument}`,
692
+ errOptionValue: (option) => `Option ${option} has no value`,
693
+ errTooManyArguments: 'Too many arguments.',
694
+ cmdServersAdd: 'add a server to monitoring',
695
+ cmdServersDelete: 'delete servers from monitoring',
696
+ cmdServersTestNotify: 'send a test alert where a real one would go',
697
+ argServerHost: 'address: URL, domain or IP (not used by heartbeat)',
698
+ optServerName: 'server name',
699
+ optServerHost: 'new server address',
700
+ optServerDescription: 'description; an empty string removes it',
701
+ optAddProtocol: 'protocol: http, keyword, ping, tcp, dns, heartbeat (default: http for a URL, ping for an IP)',
702
+ optPort: 'port (tcp)',
703
+ optKeyword: 'word on the page (keyword)',
704
+ optKeywordAbsent: 'alert when the word appears, not when it is gone (keyword)',
705
+ optDnsTypes: 'DNS record types, comma-separated, e.g. A,AAAA (dns)',
706
+ optYes: 'do not ask for confirmation',
707
+ addProtocolNeeded: (host) => `Cannot tell the protocol from "${host}": pass --protocol (http, keyword, ping, tcp, dns).`,
708
+ addHostNeeded: 'Give the server address.',
709
+ addHostNotUsed: 'A heartbeat has no address: drop it and pass --name.',
710
+ addCreated: (id, label) => `Server "${label}" added: #${id}.`,
711
+ addDryRun: (label) => `Server "${label}" will be added.`,
712
+ deletedOne: (id) => `#${id}: deleted`,
713
+ confirmDeleteServers: (list) => `Delete from monitoring: ${list}? Their check history and incidents go too. [y/N] `,
714
+ confirmDeleteIncidents: (list) => `Delete incidents ${list}? [y/N] `,
715
+ confirmNeedsYes: 'Deleting without a terminal needs --yes: there is no one to confirm.',
716
+ cancelled: 'Cancelled, nothing changed.',
717
+ testNotifySent: (id) => `A test alert for server #${id} went to the channels of its project.`,
718
+ updateSingleOnly: '--name, --host and --description change one server: give exactly one id.',
719
+ updateHostNotApplicable: (id, protocol) => `#${id}: a ${protocol} address is system-made and is not changed.`,
720
+ updateDuplicate: (id, other) => `#${id}: such a server already exists — #${other}.`,
721
+ cmdIncidentsDelete: 'delete incidents',
722
+ cmdIncidentsComment: 'add a comment to an incident',
723
+ optCommentText: 'comment text',
724
+ commentInvalid: (max) => `A comment cannot be empty or longer than ${max} characters.`,
725
+ commentAdded: (id) => `Comment added to incident #${id}.`,
726
+ cmdProjects: 'projects of the account',
727
+ cmdProjectsList: 'list projects',
728
+ projectsEmpty: 'No projects.',
729
+ cmdPages: 'status pages',
730
+ cmdPagesList: 'list status pages',
731
+ pagesEmpty: 'No status pages.',
732
+ colSlug: 'Path',
733
+ colPublished: 'Published',
734
+ yes: 'yes',
735
+ no: 'no',
736
+ cmdScenariosRun: 'run a saved scenario now — as on schedule, with an incident on failure',
737
+ scenarioEnvUnmasked: (names) => `The CLI does not know the values of the scenario variables (${names}) and cannot hide them in the output: set them in the environment under the same names.`,
738
+ scenarioNotScenario: (id, protocol) => `#${id} is not a scenario but ${protocol}.`,
739
+ cmdServersChecks: 'checks of a server over a period',
740
+ optChecksSince: 'from: 24h, 7d or a date (default: a day)',
741
+ optChecksUntil: 'until: 24h, 7d or a date',
742
+ checksEmpty: 'No checks in this period.',
743
+ colTime: 'Time',
744
+ colLocations: 'Locations',
745
+ cmdMaintenanceSchedule: 'announce planned maintenance in advance',
746
+ cmdMaintenanceList: 'planned maintenance of a page',
747
+ cmdMaintenanceDelete: 'remove planned maintenance from a page',
748
+ optAt: 'start: a date and time, e.g. 2026-11-20T03:00+03:00',
749
+ maintenanceAtInvalid: (raw) => `--at: not a date or already past: ${raw}. Maintenance going on now is opened by maintenance start.`,
750
+ maintenanceScheduled: (id, start, end) => `Planned maintenance #${id} announced: ${start} — ${end}.`,
751
+ maintenanceEmpty: 'No planned maintenance.',
752
+ confirmDeleteMaintenance: (id, title) => `Remove planned maintenance "${title}" (#${id}) from the page? [y/N] `,
753
+ colEnd: 'End',
754
+ };
755
+ const dictionaries = { ru, en };
756
+ export const t = dictionaries[detectLang()];
757
+ //# sourceMappingURL=i18n.js.map