bazis 0.99.5 → 0.100.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.
Files changed (50) hide show
  1. package/cli/build.ts +171 -36
  2. package/cli/codegen.ts +155 -4
  3. package/cli/generateProject.ts +8 -2
  4. package/cli/main.ts +134 -51
  5. package/cli/naming.ts +3 -0
  6. package/cli/ui.ts +86 -0
  7. package/core/agent/index.ts +8 -0
  8. package/core/app/runApp.ts +4 -1
  9. package/core/di/extensions/hosted-service.ts +8 -0
  10. package/core/http/HttpServer.ts +33 -2
  11. package/core/http/Routing/RouterBuilder.ts +21 -0
  12. package/core/infra/Infra.ts +8 -2
  13. package/core/infra/InfraConnector.ts +13 -0
  14. package/core/infra/InfraLifecycle.ts +30 -0
  15. package/core/infra/connectorConfig.ts +10 -0
  16. package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
  17. package/core/infra/connectors/llm.ts +23 -2
  18. package/core/infra/connectors/openaiCompatible.ts +26 -1
  19. package/core/infra/connectors/opensearch.ts +41 -10
  20. package/core/infra/connectors/postgres.ts +35 -6
  21. package/core/infra/connectors/redis.ts +64 -8
  22. package/core/infra/index.ts +5 -5
  23. package/core/kernel/Bazis.ts +21 -1
  24. package/core/kernel/Kernel.ts +55 -1
  25. package/core/kernel/KernelBuilder.ts +39 -1
  26. package/core/kernel/LifecycleCoordinator.ts +105 -4
  27. package/core/kernel/SupervisedHostedService.ts +5 -0
  28. package/core/kernel/config/ConfigRegistry.ts +5 -0
  29. package/core/kernel/config/ConfigValidationError.ts +37 -0
  30. package/core/kernel/config/callerFile.ts +23 -0
  31. package/core/kernel/config/defineConfig.ts +21 -17
  32. package/core/kernel/errors/StartupFailuresError.ts +17 -0
  33. package/core/kernel/errors/StartupTimeoutError.ts +8 -5
  34. package/core/kernel/errors/index.ts +1 -0
  35. package/core/kernel/index.ts +12 -0
  36. package/core/kernel/report/StartupReport.ts +134 -0
  37. package/core/kernel/report/StartupScreen.ts +459 -0
  38. package/core/kernel/report/moduleTree.ts +97 -0
  39. package/core/kernel/report/startupProblems.ts +186 -0
  40. package/core/orm/OrmLifecycle.ts +29 -7
  41. package/core/orm/buildOrmProviders.ts +7 -3
  42. package/core/orm/databaseConnector.ts +17 -1
  43. package/core/orm/index.ts +11 -0
  44. package/core/scripts/di-generate.ts +58 -9
  45. package/library/terminal/index.ts +4 -0
  46. package/library/terminal/language.ts +60 -0
  47. package/library/terminal/messages.ts +400 -0
  48. package/library/terminal/screen.ts +358 -0
  49. package/library/terminal/style.ts +143 -0
  50. package/package.json +2 -1
@@ -0,0 +1,400 @@
1
+ import type { Language } from "./language";
2
+
3
+ /**
4
+ * Words of the terminal screens. Framing, statuses and hints are translated;
5
+ * names from the code, file paths, environment variables and the raw text of
6
+ * a driver error stay as they are.
7
+ */
8
+
9
+ type Forms = readonly [one: string, few: string, many: string];
10
+
11
+ const NOUNS_EN = {
12
+ file: ["file", "files", "files"],
13
+ dep: ["dep", "deps", "deps"],
14
+ controller: ["controller", "controllers", "controllers"],
15
+ requestModel: ["request model", "request models", "request models"],
16
+ list: ["list", "lists", "lists"],
17
+ tool: ["tool", "tools", "tools"],
18
+ section: ["section", "sections", "sections"],
19
+ module: ["module", "modules", "modules"],
20
+ route: ["route", "routes", "routes"],
21
+ service: ["service", "services", "services"],
22
+ job: ["background job", "background jobs", "background jobs"],
23
+ operation: ["operation", "operations", "operations"],
24
+ table: ["table", "tables", "tables"],
25
+ schema: ["schema", "schemas", "schemas"],
26
+ migration: ["migration", "migrations", "migrations"],
27
+ problem: ["problem", "problems", "problems"],
28
+ error: ["error", "errors", "errors"],
29
+ warning: ["warning", "warnings", "warnings"],
30
+ connection: ["connection", "connections", "connections"],
31
+ test: ["test", "tests", "tests"],
32
+ change: ["change", "changes", "changes"],
33
+ part: ["part", "parts", "parts"],
34
+ } as const satisfies Record<string, Forms>;
35
+
36
+ export type Noun = keyof typeof NOUNS_EN;
37
+
38
+ const NOUNS_RU: Record<Noun, Forms> = {
39
+ file: ["файл", "файла", "файлов"],
40
+ dep: ["зависимость", "зависимости", "зависимостей"],
41
+ controller: ["контроллер", "контроллера", "контроллеров"],
42
+ requestModel: ["модель запроса", "модели запроса", "моделей запроса"],
43
+ list: ["список", "списка", "списков"],
44
+ tool: ["инструмент", "инструмента", "инструментов"],
45
+ section: ["раздел", "раздела", "разделов"],
46
+ module: ["модуль", "модуля", "модулей"],
47
+ route: ["маршрут", "маршрута", "маршрутов"],
48
+ service: ["сервис", "сервиса", "сервисов"],
49
+ job: ["фоновая задача", "фоновые задачи", "фоновых задач"],
50
+ operation: ["операция", "операции", "операций"],
51
+ table: ["таблица", "таблицы", "таблиц"],
52
+ schema: ["схема", "схемы", "схем"],
53
+ migration: ["миграция", "миграции", "миграций"],
54
+ problem: ["проблема", "проблемы", "проблем"],
55
+ error: ["ошибка", "ошибки", "ошибок"],
56
+ warning: ["предупреждение", "предупреждения", "предупреждений"],
57
+ connection: ["подключение", "подключения", "подключений"],
58
+ test: ["тест", "теста", "тестов"],
59
+ change: ["изменение", "изменения", "изменений"],
60
+ part: ["часть", "части", "частей"],
61
+ };
62
+
63
+ function formEn(count: number, forms: Forms): string {
64
+ return count === 1 ? forms[0] : forms[1];
65
+ }
66
+
67
+ function formRu(count: number, forms: Forms): string {
68
+ const mod10 = count % 10, mod100 = count % 100;
69
+ if (!Number.isInteger(count)) return forms[1];
70
+ if (mod10 === 1 && mod100 !== 11) return forms[0];
71
+ if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return forms[1];
72
+ return forms[2];
73
+ }
74
+
75
+ const en = {
76
+ language: "en" as Language,
77
+ noun: (noun: Noun, count: number) => formEn(count, NOUNS_EN[noun]),
78
+ badge: { ok: "OK", fail: "FAIL", skip: "SKIP", warn: "WARN", up: "UP", down: "DOWN", wait: "WAIT", info: "INFO" },
79
+ banner: { success: "SUCCESS", failed: "FAILED" },
80
+ section: {
81
+ build: "BUILD", configuration: "CONFIGURATION", infrastructure: "INFRASTRUCTURE", schema: "DATABASE SCHEMA",
82
+ modules: "MODULES", routes: "ROUTES", warnings: "WARNINGS", startup: "STARTUP", project: "PROJECT",
83
+ files: "FILES", next: "NEXT STEPS", tests: "TESTS", binary: "EXECUTABLE", codegen: "CODEGEN", problems: "PROBLEMS",
84
+ },
85
+ step: {
86
+ codegen: "codegen", typecheck: "typecheck", config: "config", connections: "connections", schema: "schema",
87
+ modules: "modules", services: "services", tests: "bun test", compile: "compile", files: "files", register: "register",
88
+ },
89
+ meta: { env: "env", watch: "watch", on: "on", off: "off", src: "src", pid: "pid", target: "target", out: "output", path: "path" },
90
+ common: {
91
+ of: "of", in: "in", and: "and", none: "none", ok: "ok", empty: "empty", plus: "plus", memory: "memory",
92
+ declaredIn: "declared in", code: "code", where: "where", try: "try", logs: "logs", inProcess: "in process",
93
+ dryRun: "dry run — nothing was written",
94
+ },
95
+ config: {
96
+ environment: (name: string) => `environment: ${name}`,
97
+ devDefault: (count: number) => `${count === 1 ? "secret" : "secrets"} from dev default`,
98
+ devDefaultNote: (env: string) => `← development default (set ${env} in production)`,
99
+ invalid: (total: number) => `of ${total} sections invalid`,
100
+ issue: {
101
+ missingSecret: "required secret is not set",
102
+ devSecret: "production would use the development default secret from the code",
103
+ number: "expected a finite number",
104
+ boolean: "expected a boolean (true, false, 1, 0)",
105
+ string: "expected a string",
106
+ enumValue: (values: string) => `value is not allowed, use one of: ${values}`,
107
+ conflict: "conflicting environment aliases",
108
+ validatorFailed: "validator failed",
109
+ got: (value: string, source: string) => `got ${value} from ${source}`,
110
+ },
111
+ fix: {
112
+ setVariable: (names: string) => `set ${names}`,
113
+ orSecretFile: "or a file in a secretFilesSource directory",
114
+ orProduction: (key: string) => `or declare ${key}: secret() with a value source in the production section`,
115
+ change: (names: string) => `change ${names}`,
116
+ },
117
+ },
118
+ infra: {
119
+ up: (total: number) => `of ${total} up`,
120
+ failed: (total: number) => `of ${total} connections failed`,
121
+ parallel: "checked in parallel",
122
+ waiting: "waiting for",
123
+ timeoutStep: (count: number) => `${count} connection${count === 1 ? "" : "s"} did not answer`,
124
+ connectTimeout: (seconds: string) => `connect() did not finish in ${seconds}`,
125
+ hint: {
126
+ password: (where: string) => `check the user and password: ${where}`,
127
+ refused: (kind: string) => `is ${kind} running at this address?`,
128
+ timeout: "no answer in time — the server is down or the address is wrong",
129
+ address: (names: string) => `address: ${names}`,
130
+ dns: "the host name does not resolve — check the address",
131
+ database: "the database does not exist — create it or fix the name",
132
+ deadline: "give connect() its own deadline and fail with a clear message",
133
+ raiseLimit: "or raise the limit: runApp(…, { kernel: { startupTimeoutMs } })",
134
+ },
135
+ },
136
+ schema: { add: "+", none: "schema is up to date", warnings: "schema warnings" },
137
+ modules: {
138
+ controller: "controller", service: "service", table: "table", background: "background", tool: "tool", exports: "exports",
139
+ emptyHint: "empty — add providers, controllers or tools",
140
+ every: (interval: string) => `every ${interval}`,
141
+ },
142
+ routes: { open: "open", auth: "auth", protected: "protected", roles: (roles: string) => roles },
143
+ warnings: {
144
+ openRoutes: (count: number) => `${count} route${count === 1 ? " is" : "s are"} open — no @Authorize. Protect ${count === 1 ? "it" : "them"} before production.`,
145
+ devSecret: (key: string) => `${key} uses the development default from the code.`,
146
+ },
147
+ startup: {
148
+ configuration: "configuration", container: "container", infrastructure: "infrastructure", schema: "schema", services: "services",
149
+ },
150
+ ready: {
151
+ running: (app: string, ms: string) => `${app} is running · ready in ${ms}`,
152
+ local: "Local", network: "Network", health: "Health", docs: "OpenAPI",
153
+ notExposed: (host: string) => `not exposed — listening on ${host} (set HOST=0.0.0.0)`,
154
+ watching: (src: string) => `watching ${src} · restarts on change · logs below · Ctrl+C to stop`,
155
+ noWatch: "logs below · Ctrl+C to stop",
156
+ worker: (app: string, ms: string) => `${app} started · ready in ${ms}`,
157
+ },
158
+ failure: {
159
+ config: (count: number) => `configuration is invalid · ${count} problem${count === 1 ? "" : "s"}`,
160
+ infra: (down: number, total: number) => `infrastructure is down · ${down} of ${total} connection${total === 1 ? "" : "s"}`,
161
+ timeout: (seconds: string) => `startup timed out after ${seconds}`,
162
+ di: "dependency injection is not wired",
163
+ codegen: (count: number) => `codegen failed · ${count} error${count === 1 ? "" : "s"}`,
164
+ app: "application failed to start",
165
+ typecheck: (count: number) => `typecheck failed · ${count} error${count === 1 ? "" : "s"}`,
166
+ tests: "tests failed",
167
+ command: "command failed",
168
+ skipConfig: "not checked — configuration is invalid",
169
+ skipInfra: "not started — infrastructure is down",
170
+ skipCodegen: "not started — codegen failed",
171
+ skipEarlier: "not started — an earlier stage failed",
172
+ exitNothing: (code: number) => `exit code ${code} · nothing was started`,
173
+ exitWatch: (code: number, src: string) => `exit code ${code} · watching ${src} — fix and save to restart`,
174
+ exit: (code: number) => `exit code ${code}`,
175
+ timeoutCause: (name: string, seconds: string) => `${name} did not finish starting in ${seconds} and gave no error`,
176
+ dbMissing: (users: string) => `no database is connected, but ${users} use${users.includes(",") ? "" : "s"} one`,
177
+ dbFix: "connect a database: @Infra({ db: ormBazisConnect(dbConfig) })",
178
+ },
179
+ build: {
180
+ typecheckOk: "no type errors",
181
+ done: (ms: string) => `build is ready · ${ms}`,
182
+ binary: (path: string, size: string) => `${path} · ${size}`,
183
+ binaryDone: (path: string) => `executable is ready · ${path}`,
184
+ run: "run",
185
+ production: "BAZIS_ENV defaults to production in the executable",
186
+ dotenv: "reads .env files from its working directory",
187
+ },
188
+ codegen: {
189
+ done: (ms: string) => `codegen is ready · ${ms}`,
190
+ generatedInto: "into",
191
+ unreachable: (file: string) => `${file} is not imported from the entrypoint — skipped`,
192
+ unreachableFix: "import its module in the host module, or remove the file",
193
+ outputs: "generated",
194
+ sqlFix: "inject IPostgres from bazis/core/infra instead of Bun's SQL",
195
+ redisFix: "inject IRedis from bazis/core/infra instead of Bun's RedisClient",
196
+ unknownDepFix: "register the class in a module, declare a token with createToken, or bind it with an explicit factory",
197
+ run: "run",
198
+ },
199
+ dev: {
200
+ restarting: "changed — restarting",
201
+ },
202
+ test: {
203
+ done: "all tests passed",
204
+ failed: (code: number) => `bun test exited with code ${code}`,
205
+ },
206
+ project: {
207
+ created: (name: string) => `project ${name} is created`,
208
+ planned: (name: string) => `project ${name} is planned`,
209
+ dependency: (version: string) => `bazis ${version} from npm`,
210
+ vendor: (count: number) => `vendor/bazis · ${count} package files — keep it in version control`,
211
+ linked: "bazis is linked to a local checkout",
212
+ install: "install dependencies",
213
+ dev: "start in development mode",
214
+ open: "open in the browser",
215
+ },
216
+ module: {
217
+ kind: { module: "module", pack: "pack" } as Record<"module" | "pack", string>,
218
+ created: (kind: string, name: string) => `${kind} ${name} is generated`,
219
+ planned: (kind: string, name: string) => `${kind} ${name} is planned`,
220
+ registered: (host: string) => `connected in ${host}`,
221
+ notRegistered: "not connected — codegen skips it until a module imports it",
222
+ codegenSkipped: "codegen skipped (--no-codegen) — run bazis codegen",
223
+ needsDb: "this module uses a database: connect one with @Infra({ db: ormBazisConnect(dbConfig) })",
224
+ needsCache: "this module caches: pass runApp(AppModule, { cache: memory() })",
225
+ keptOnFailure: "scaffold files were kept — fix the error and run bazis codegen",
226
+ },
227
+ errors: {
228
+ unknown: "Use bazis --help to list commands and options.",
229
+ },
230
+ };
231
+
232
+ export type Messages = typeof en;
233
+
234
+ const ru: Messages = {
235
+ language: "ru",
236
+ noun: (noun, count) => formRu(count, NOUNS_RU[noun]),
237
+ badge: { ok: "ГОТОВО", fail: "ОШИБКА", skip: "ПРОПУСК", warn: "ВНИМАНИЕ", up: "ОК", down: "СБОЙ", wait: "ЖДЁМ", info: "ИНФО" },
238
+ banner: { success: "УСПЕХ", failed: "ПРОВАЛ" },
239
+ section: {
240
+ build: "СБОРКА", configuration: "КОНФИГУРАЦИЯ", infrastructure: "ИНФРАСТРУКТУРА", schema: "СХЕМА БАЗЫ ДАННЫХ",
241
+ modules: "МОДУЛИ", routes: "МАРШРУТЫ", warnings: "ПРЕДУПРЕЖДЕНИЯ", startup: "ЗАПУСК", project: "ПРОЕКТ",
242
+ files: "ФАЙЛЫ", next: "ДАЛЬШЕ", tests: "ТЕСТЫ", binary: "ИСПОЛНЯЕМЫЙ ФАЙЛ", codegen: "КОДОГЕНЕРАЦИЯ", problems: "ПРОБЛЕМЫ",
243
+ },
244
+ step: {
245
+ codegen: "кодогенерация", typecheck: "типы", config: "конфигурация", connections: "подключения", schema: "схема",
246
+ modules: "модули", services: "сервисы", tests: "bun test", compile: "компиляция", files: "файлы", register: "подключение",
247
+ },
248
+ meta: { env: "среда", watch: "слежение", on: "вкл", off: "выкл", src: "вход", pid: "pid", target: "цель", out: "файл", path: "путь" },
249
+ common: {
250
+ of: "из", in: "в", and: "и", none: "нет", ok: "в порядке", empty: "пусто", plus: "ещё", memory: "память",
251
+ declaredIn: "объявлено в", code: "код", where: "где", try: "попробуйте", logs: "журнал", inProcess: "в процессе",
252
+ dryRun: "пробный запуск — ничего не записано",
253
+ },
254
+ config: {
255
+ environment: (name) => `среда: ${name}`,
256
+ devDefault: (count) => `${formRu(count, ["секрет", "секрета", "секретов"])} из значения для разработки`,
257
+ devDefaultNote: (env) => `← значение для разработки (в production задайте ${env})`,
258
+ invalid: (total) => `из ${total} с ошибками`,
259
+ issue: {
260
+ missingSecret: "обязательный секрет не задан",
261
+ devSecret: "в production попадёт секрет для разработки из кода",
262
+ number: "нужно конечное число",
263
+ boolean: "нужно логическое значение (true, false, 1, 0)",
264
+ string: "нужна строка",
265
+ enumValue: (values) => `значение не разрешено, используйте одно из: ${values}`,
266
+ conflict: "переменные-синонимы заданы по-разному",
267
+ validatorFailed: "проверка значения упала",
268
+ got: (value, source) => `получено ${value} из ${source}`,
269
+ },
270
+ fix: {
271
+ setVariable: (names) => `задайте ${names}`,
272
+ orSecretFile: "или файл в каталоге secretFilesSource",
273
+ orProduction: (key) => `или объявите ${key}: secret() с источником значения в разделе production`,
274
+ change: (names) => `исправьте ${names}`,
275
+ },
276
+ },
277
+ infra: {
278
+ up: (total) => `из ${total} работают`,
279
+ failed: (total) => `из ${total} не подключились`,
280
+ parallel: "проверены параллельно",
281
+ waiting: "ждём",
282
+ timeoutStep: (count) => `${count} ${formRu(count, NOUNS_RU.connection)} не ответило`,
283
+ connectTimeout: (seconds) => `connect() не завершился за ${seconds}`,
284
+ hint: {
285
+ password: (where) => `проверьте пользователя и пароль: ${where}`,
286
+ refused: (kind) => `запущен ли ${kind} по этому адресу?`,
287
+ timeout: "нет ответа вовремя — сервер не запущен или адрес неверный",
288
+ address: (names) => `адрес: ${names}`,
289
+ dns: "имя хоста не разрешается — проверьте адрес",
290
+ database: "базы данных нет — создайте её или исправьте имя",
291
+ deadline: "дайте connect() свой срок и понятную ошибку",
292
+ raiseLimit: "или увеличьте предел: runApp(…, { kernel: { startupTimeoutMs } })",
293
+ },
294
+ },
295
+ schema: { add: "+", none: "схема актуальна", warnings: "предупреждения схемы" },
296
+ modules: {
297
+ controller: "контроллер", service: "сервис", table: "таблица", background: "фон", tool: "инструмент", exports: "экспорт",
298
+ emptyHint: "пусто — добавьте провайдеры, контроллеры или инструменты",
299
+ every: (interval) => `каждые ${interval}`,
300
+ },
301
+ routes: { open: "открыт", auth: "доступ", protected: "защищён", roles: (roles) => roles },
302
+ warnings: {
303
+ openRoutes: (count) => `${count} ${formRu(count, NOUNS_RU.route)} без @Authorize. Защитите их до production.`,
304
+ devSecret: (key) => `${key} берёт значение для разработки из кода.`,
305
+ },
306
+ startup: {
307
+ configuration: "конфигурация", container: "контейнер", infrastructure: "инфраструктура", schema: "схема", services: "сервисы",
308
+ },
309
+ ready: {
310
+ running: (app, ms) => `${app} работает · готово за ${ms}`,
311
+ local: "Локально", network: "Сеть", health: "Здоровье", docs: "OpenAPI",
312
+ notExposed: (host) => `недоступно снаружи — слушает ${host} (задайте HOST=0.0.0.0)`,
313
+ watching: (src) => `слежу за ${src} · перезапуск при изменениях · журнал ниже · Ctrl+C — остановить`,
314
+ noWatch: "журнал ниже · Ctrl+C — остановить",
315
+ worker: (app, ms) => `${app} запущено · готово за ${ms}`,
316
+ },
317
+ failure: {
318
+ config: (count) => `конфигурация с ошибками · ${count} ${formRu(count, NOUNS_RU.problem)}`,
319
+ infra: (down, total) => `инфраструктура недоступна · ${down} из ${total} ${formRu(total, NOUNS_RU.connection)}`,
320
+ timeout: (seconds) => `запуск не уложился в ${seconds}`,
321
+ di: "внедрение зависимостей не собрано",
322
+ codegen: (count) => `кодогенерация не удалась · ${count} ${formRu(count, NOUNS_RU.error)}`,
323
+ app: "приложение не запустилось",
324
+ typecheck: (count) => `проверка типов не прошла · ${count} ${formRu(count, NOUNS_RU.error)}`,
325
+ tests: "тесты не прошли",
326
+ command: "команда не выполнена",
327
+ skipConfig: "не проверялось — конфигурация с ошибками",
328
+ skipInfra: "не запускалось — инфраструктура недоступна",
329
+ skipCodegen: "не запускалось — кодогенерация не удалась",
330
+ skipEarlier: "не запускалось — предыдущий этап не удался",
331
+ exitNothing: (code) => `код выхода ${code} · ничего не запущено`,
332
+ exitWatch: (code, src) => `код выхода ${code} · слежу за ${src} — исправьте и сохраните для перезапуска`,
333
+ exit: (code) => `код выхода ${code}`,
334
+ timeoutCause: (name, seconds) => `${name} не завершил запуск за ${seconds} и не сообщил об ошибке`,
335
+ dbMissing: (users) => `база данных не подключена, а её использует ${users}`,
336
+ dbFix: "подключите базу: @Infra({ db: ormBazisConnect(dbConfig) })",
337
+ },
338
+ build: {
339
+ typecheckOk: "ошибок типов нет",
340
+ done: (ms) => `сборка готова · ${ms}`,
341
+ binary: (path, size) => `${path} · ${size}`,
342
+ binaryDone: (path) => `исполняемый файл готов · ${path}`,
343
+ run: "запуск",
344
+ production: "в исполняемом файле BAZIS_ENV по умолчанию production",
345
+ dotenv: "читает .env из рабочего каталога",
346
+ },
347
+ codegen: {
348
+ done: (ms) => `кодогенерация готова · ${ms}`,
349
+ generatedInto: "в",
350
+ unreachable: (file) => `${file} не импортируется из точки входа — пропущен`,
351
+ unreachableFix: "импортируйте его модуль в модуль-хозяин или удалите файл",
352
+ outputs: "сгенерировано",
353
+ sqlFix: "внедряйте IPostgres из bazis/core/infra вместо SQL из Bun",
354
+ redisFix: "внедряйте IRedis из bazis/core/infra вместо RedisClient из Bun",
355
+ unknownDepFix: "зарегистрируйте класс в модуле, объявите токен через createToken или привяжите явной фабрикой",
356
+ run: "запуск",
357
+ },
358
+ dev: {
359
+ restarting: "изменён — перезапуск",
360
+ },
361
+ test: {
362
+ done: "все тесты прошли",
363
+ failed: (code) => `bun test завершился с кодом ${code}`,
364
+ },
365
+ project: {
366
+ created: (name) => `проект ${name} создан`,
367
+ planned: (name) => `проект ${name} запланирован`,
368
+ dependency: (version) => `bazis ${version} из npm`,
369
+ vendor: (count) => `vendor/bazis · ${count} файлов пакета — храните в системе контроля версий`,
370
+ linked: "bazis подключён из локальной копии",
371
+ install: "установите зависимости",
372
+ dev: "запустите в режиме разработки",
373
+ open: "откройте в браузере",
374
+ },
375
+ module: {
376
+ kind: { module: "модуль", pack: "составной модуль" },
377
+ created: (kind, name) => `${kind} ${name} создан`,
378
+ planned: (kind, name) => `${kind} ${name} запланирован`,
379
+ registered: (host) => `подключён в ${host}`,
380
+ notRegistered: "не подключён — кодогенерация пропускает его, пока модуль не импортирован",
381
+ codegenSkipped: "кодогенерация пропущена (--no-codegen) — запустите bazis codegen",
382
+ needsDb: "модулю нужна база данных: подключите её через @Infra({ db: ormBazisConnect(dbConfig) })",
383
+ needsCache: "модуль кэширует: передайте runApp(AppModule, { cache: memory() })",
384
+ keptOnFailure: "файлы модуля оставлены — исправьте ошибку и запустите bazis codegen",
385
+ },
386
+ errors: {
387
+ unknown: "Список команд и опций: bazis --help.",
388
+ },
389
+ };
390
+
391
+ const DICTIONARIES: Readonly<Record<Language, Messages>> = Object.freeze({ en, ru });
392
+
393
+ export function messages(language: Language): Messages {
394
+ return DICTIONARIES[language];
395
+ }
396
+
397
+ /** `3 files`, `3 файла`. */
398
+ export function count(t: Messages, value: number, noun: Noun): string {
399
+ return `${value} ${t.noun(noun, value)}`;
400
+ }