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
package/cli/build.ts CHANGED
@@ -1,9 +1,11 @@
1
- import { existsSync, mkdtempSync, rmSync, watch } from "node:fs";
1
+ import { existsSync, mkdtempSync, rmSync, statSync, watch } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import path from "node:path";
4
- import { resolveBun } from "./codegen";
4
+ import { PALETTE, formatBytes, formatDuration, type Problem } from "../library/terminal";
5
+ import { resolveBun, type CodegenOptions } from "./codegen";
6
+ import { commandFailed, commandHeader, createScreens, readFrameLines, type CliOutput } from "./ui";
5
7
 
6
- type Codegen = (cwd: string, target?: string) => Promise<number>;
8
+ type Codegen = (cwd: string, target?: string, options?: CodegenOptions) => Promise<number>;
7
9
 
8
10
  export interface BuildOptions {
9
11
  /** Also compile the entrypoint into a standalone executable. */
@@ -19,23 +21,46 @@ export interface CompileOptions {
19
21
  readonly dotenv?: boolean;
20
22
  }
21
23
 
22
- /**
23
- * `bazis dev`: codegen of every target, then run the default target's entrypoint from source.
24
- * With `watch`, a change under src/ (except src/generated) stops the app,
25
- * reruns codegen and starts it again; a failed codegen waits for the next change.
26
- * The app runs as `development` unless BAZIS_ENV is set in the shell.
27
- */
24
+ const defaultUi = (): CliOutput => createScreens(process.stdout, process.stderr);
25
+
28
26
  /** `bazis dev` means development; an BAZIS_ENV from the shell still wins. */
29
27
  export function devEnvironment(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
30
28
  return { ...env, BAZIS_ENV: env.BAZIS_ENV || "development" };
31
29
  }
32
30
 
33
- export async function runDev(cwd: string, codegen: Codegen, log: (message: string) => void, options: { readonly watch?: boolean } = {}): Promise<number> {
31
+ /**
32
+ * `bazis dev`: codegen of every target, then run the default target's entrypoint from source.
33
+ * With `watch`, a change under src/ (except src/generated) stops the app,
34
+ * reruns codegen and starts it again; a failed codegen waits for the next change.
35
+ * The app runs as `development` unless BAZIS_ENV is set in the shell. The CLI
36
+ * prints the header and the build; the application prints the rest of the screen.
37
+ */
38
+ export async function runDev(cwd: string, codegen: Codegen, ui: CliOutput = defaultUi(), options: { readonly watch?: boolean } = {}): Promise<number> {
39
+ const s = ui.out;
40
+ const t = s.t;
34
41
  const bun = await resolveBun(cwd);
35
42
  const entry = await projectEntry(cwd);
36
- const env = devEnvironment(process.env);
43
+ const watchDir = "src/";
44
+ const env: NodeJS.ProcessEnv = {
45
+ ...devEnvironment(process.env),
46
+ // The CLI already printed the logo; the application continues the screen.
47
+ BAZIS_UI_HEADER: "0",
48
+ BAZIS_LANG: process.env.BAZIS_LANG || s.language,
49
+ ...(options.watch ? { BAZIS_UI_WATCH: watchDir } : {}),
50
+ };
51
+ commandHeader(s, "dev", cwd, [
52
+ s.kv(t.meta.env, s.strong(env.BAZIS_ENV ?? "development", PALETTE.yellow)),
53
+ s.kv(t.meta.watch, options.watch ? s.strong(t.meta.on, PALETTE.lime) : s.muted(t.meta.off)),
54
+ s.kv(t.meta.src, s.text(path.relative(cwd, entry))),
55
+ ]);
56
+ const build = async (footer?: string): Promise<number> => {
57
+ s.section(t.section.build);
58
+ const generated = await codegen(cwd, "all", { ui, ...(footer === undefined ? {} : { footer }) });
59
+ if (generated === 0) s.line();
60
+ return generated;
61
+ };
37
62
  if (!options.watch) {
38
- const generated = await codegen(cwd, "all");
63
+ const generated = await build();
39
64
  if (generated !== 0) return generated;
40
65
  const child = Bun.spawn([bun, "run", entry], { cwd, env, stdin: "inherit", stdout: "inherit", stderr: "inherit" });
41
66
  // The app owns graceful shutdown; forward the signal instead of dying first.
@@ -62,7 +87,7 @@ export async function runDev(cwd: string, codegen: Codegen, log: (message: strin
62
87
  await running.exited;
63
88
  };
64
89
  const start = async () => {
65
- if (await codegen(cwd, "all") !== 0) { log("[bazis] codegen failed; waiting for changes..."); return; }
90
+ if (await build(t.failure.exitWatch(1, watchDir)) !== 0) return;
66
91
  child = Bun.spawn([bun, "run", entry], { cwd, env, stdin: "inherit", stdout: "inherit", stderr: "inherit" });
67
92
  };
68
93
  const watcher = watch(path.join(cwd, "src"), { recursive: true }, (_event, file) => {
@@ -70,7 +95,13 @@ export async function runDev(cwd: string, codegen: Codegen, log: (message: strin
70
95
  if (!file || /^generated([\\/]|$)/.test(String(file))) return;
71
96
  clearTimeout(timer);
72
97
  timer = setTimeout(() => {
73
- queue = queue.then(async () => { log(`[bazis] ${file} changed, restarting...`); await stop(); await start(); });
98
+ queue = queue.then(async () => {
99
+ s.line();
100
+ s.line(` ${s.strong("↻", PALETTE.link)} ${s.text(`src/${String(file).replaceAll("\\", "/")}`)} ${s.muted(t.dev.restarting)}`);
101
+ s.line();
102
+ await stop();
103
+ await start();
104
+ });
74
105
  }, 150);
75
106
  });
76
107
  const finished = Promise.withResolvers<number>();
@@ -81,7 +112,6 @@ export async function runDev(cwd: string, codegen: Codegen, log: (message: strin
81
112
  };
82
113
  process.once("SIGINT", shutdown);
83
114
  process.once("SIGTERM", shutdown);
84
- log("[bazis] watching src/ for changes (Ctrl+C to stop)");
85
115
  queue = queue.then(start);
86
116
  try {
87
117
  return await finished.promise;
@@ -92,25 +122,111 @@ export async function runDev(cwd: string, codegen: Codegen, log: (message: strin
92
122
  }
93
123
 
94
124
  /** `bazis test [args]`: codegen of every target, then `bun test` with the given arguments. */
95
- export async function runTest(cwd: string, args: readonly string[], codegen: Codegen): Promise<number> {
96
- const generated = await codegen(cwd, "all");
125
+ export async function runTest(cwd: string, args: readonly string[], codegen: Codegen, ui: CliOutput = defaultUi()): Promise<number> {
126
+ const s = ui.out;
127
+ const t = s.t;
128
+ commandHeader(s, "test", cwd, [s.kv(t.meta.env, s.strong("test", PALETTE.link)), ...(args.length ? [s.text(`bun test ${args.join(" ")}`)] : [])]);
129
+ s.section(t.section.build);
130
+ const generated = await codegen(cwd, "all", { ui });
97
131
  if (generated !== 0) return generated;
98
- return await Bun.spawn([await resolveBun(cwd), "test", ...args], { cwd, stdin: "inherit", stdout: "inherit", stderr: "inherit" }).exited;
132
+ s.line();
133
+ s.section(t.section.tests);
134
+ s.line();
135
+ const startedAt = performance.now();
136
+ const exitCode = await Bun.spawn([await resolveBun(cwd), "test", ...args], { cwd, stdin: "inherit", stdout: "inherit", stderr: "inherit" }).exited;
137
+ s.line();
138
+ if (exitCode === 0) {
139
+ s.banner("success", `${t.test.done} · ${formatDuration(performance.now() - startedAt)}`);
140
+ return 0;
141
+ }
142
+ ui.err.banner("failed", t.test.failed(exitCode));
143
+ ui.err.footer(t.failure.exit(exitCode));
144
+ return exitCode;
99
145
  }
100
146
 
101
147
  /** `bazis build [--bin]`: codegen of every target and typecheck; with `bin`, also compile. */
102
- export async function runBuild(cwd: string, options: BuildOptions, codegen: Codegen, log: (message: string) => void): Promise<number> {
103
- const generated = await codegen(cwd, "all");
148
+ export async function runBuild(cwd: string, options: BuildOptions, codegen: Codegen, ui: CliOutput = defaultUi()): Promise<number> {
149
+ const s = ui.out;
150
+ const t = s.t;
151
+ const startedAt = performance.now();
152
+ const outfile = options.bin ? path.resolve(cwd, options.outfile ?? path.join("bin", await binaryName(cwd))) : undefined;
153
+ commandHeader(s, "build", cwd, [
154
+ s.kv(t.meta.target, s.text(options.bin ? "executable" : "typecheck")),
155
+ ...(outfile === undefined ? [] : [s.kv(t.meta.out, s.text(path.relative(cwd, outfile)))]),
156
+ ]);
157
+ s.section(t.section.build);
158
+ const generated = await codegen(cwd, "all", { ui });
104
159
  if (generated !== 0) return generated;
105
160
  const bun = await resolveBun(cwd);
106
161
  const tsc = path.join(cwd, "node_modules/typescript/bin/tsc");
107
162
  if (!existsSync(tsc)) throw new Error("TypeScript is not installed in this project. Run: bun add -d typescript");
108
- log("[bazis] typecheck...");
109
- const typecheck = await Bun.spawn([bun, tsc, "--noEmit"], { cwd, stdout: "inherit", stderr: "inherit" }).exited;
110
- if (typecheck !== 0 || !options.bin) return typecheck;
111
- const outfile = path.resolve(cwd, options.outfile ?? path.join("bin", await binaryName(cwd)));
112
- log(`[bazis] compile ${path.relative(cwd, outfile)}...`);
113
- return compileBinary(bun, await projectEntry(cwd), outfile, { dotenv: options.dotenv });
163
+ const stop = s.progress(t.step.typecheck);
164
+ const typecheckStarted = performance.now();
165
+ const child = Bun.spawn([bun, tsc, "--noEmit", "--pretty", "false"], { cwd, stdout: "pipe", stderr: "pipe" });
166
+ const [stdout, stderr, typecheck] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
167
+ stop();
168
+ const typecheckMs = formatDuration(performance.now() - typecheckStarted);
169
+ if (typecheck !== 0) {
170
+ const problems = typeProblems(`${stdout}\n${stderr}`, cwd);
171
+ s.step("fail", t.step.typecheck, s.num(problems.length, t.noun("error", problems.length), PALETTE.red), typecheckMs);
172
+ s.line();
173
+ if (problems.length === 0) ui.err.line(`${stdout}${stderr}`.trimEnd());
174
+ return commandFailed(ui.err, t.failure.typecheck(problems.length), problems.slice(0, 20), typecheck);
175
+ }
176
+ s.step("ok", t.step.typecheck, s.muted(t.build.typecheckOk), typecheckMs);
177
+ s.line();
178
+ if (outfile === undefined) {
179
+ s.banner("success", t.build.done(formatDuration(performance.now() - startedAt)));
180
+ return 0;
181
+ }
182
+ s.section(t.section.binary);
183
+ const stopCompile = s.progress(t.step.compile);
184
+ const compileStarted = performance.now();
185
+ const compiled = await compileBinaryAsync(bun, await projectEntry(cwd), outfile, { dotenv: options.dotenv });
186
+ stopCompile();
187
+ const compileMs = formatDuration(performance.now() - compileStarted);
188
+ const relative = path.relative(cwd, outfile);
189
+ if (compiled.exitCode !== 0) {
190
+ s.step("fail", t.step.compile, "", compileMs);
191
+ s.line();
192
+ return commandFailed(ui.err, t.failure.command, [{ tag: "COMPILE", where: relative, cause: compiled.output.trim() || `bun build exited with code ${compiled.exitCode}` }], compiled.exitCode);
193
+ }
194
+ s.step("ok", t.step.compile, `${s.text(relative)}${s.dot}${s.strong(formatBytes(statSync(outfile).size))}`, compileMs);
195
+ s.line();
196
+ s.banner("success", t.build.binaryDone(relative));
197
+ s.panel([
198
+ s.panelRow(t.build.run, s.code(`./${relative}`)),
199
+ "",
200
+ s.muted(t.build.production),
201
+ ...(options.dotenv ? [s.muted(t.build.dotenv)] : []),
202
+ ]);
203
+ s.line();
204
+ return 0;
205
+ }
206
+
207
+ /** `src/a.ts(12,5): error TS2322: …` → a problem with the code around the line. */
208
+ export function typeProblems(output: string, cwd: string): Problem[] {
209
+ const problems: Problem[] = [];
210
+ for (const line of output.split("\n")) {
211
+ const match = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.*)$/.exec(line.trim());
212
+ if (!match) {
213
+ // tsc continues a message on indented lines.
214
+ const last = problems.at(-1);
215
+ if (last !== undefined && /^\s+\S/.test(line)) problems[problems.length - 1] = { ...last, cause: `${last.cause}\n${line.trim()}` };
216
+ continue;
217
+ }
218
+ const [, file, lineText, columnText, code, message] = match as unknown as [string, string, string, string, string, string];
219
+ const lineNumber = Number(lineText);
220
+ const lines = readFrameLines(path.resolve(cwd, file), lineNumber);
221
+ problems.push({
222
+ tag: "TYPE",
223
+ where: `${file}:${lineNumber}:${columnText}`,
224
+ cause: message,
225
+ ...(lines.length === 0 ? {} : { frame: { lines, line: lineNumber, column: Number(columnText), length: 1 } }),
226
+ code,
227
+ });
228
+ }
229
+ return problems;
114
230
  }
115
231
 
116
232
  /**
@@ -127,19 +243,38 @@ export async function runBuild(cwd: string, options: BuildOptions, codegen: Code
127
243
  */
128
244
  export function compileBinary(bun: string, entry: string, outfile: string, options: CompileOptions = {}): number {
129
245
  const workDir = mkdtempSync(path.join(tmpdir(), "bazis-compile-"));
130
- const autoload = ["--no-compile-autoload-bunfig", options.dotenv === true ? "--compile-autoload-dotenv" : "--no-compile-autoload-dotenv"];
131
246
  try {
132
- const result = Bun.spawnSync([bun, "build", "--compile", ...autoload, path.resolve(entry), "--outfile", path.resolve(outfile)],
133
- { cwd: workDir, stdout: "inherit", stderr: "inherit" });
247
+ const result = Bun.spawnSync(compileArgs(bun, entry, outfile, options), { cwd: workDir, stdout: "inherit", stderr: "inherit" });
134
248
  return result.exitCode ?? 1;
135
249
  } finally {
136
- if (existsSync("/usr/bin/chflags")) Bun.spawnSync(["/usr/bin/chflags", "-R", "nouchg", workDir]);
137
- try {
138
- rmSync(workDir, { recursive: true, force: true });
139
- } catch (error) {
140
- // The binary itself is complete; report the leftover without failing the build.
141
- console.error(`[bazis] temporary directory was not removed: ${workDir}`, error);
142
- }
250
+ removeWorkDir(workDir);
251
+ }
252
+ }
253
+
254
+ /** {@link compileBinary} without blocking, with the compiler's output captured for the screen. */
255
+ export async function compileBinaryAsync(bun: string, entry: string, outfile: string, options: CompileOptions = {}): Promise<{ exitCode: number; output: string }> {
256
+ const workDir = mkdtempSync(path.join(tmpdir(), "bazis-compile-"));
257
+ try {
258
+ const child = Bun.spawn(compileArgs(bun, entry, outfile, options), { cwd: workDir, stdout: "pipe", stderr: "pipe" });
259
+ const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
260
+ return { exitCode, output: `${stdout}${stderr}` };
261
+ } finally {
262
+ removeWorkDir(workDir);
263
+ }
264
+ }
265
+
266
+ function compileArgs(bun: string, entry: string, outfile: string, options: CompileOptions): string[] {
267
+ const autoload = ["--no-compile-autoload-bunfig", options.dotenv === true ? "--compile-autoload-dotenv" : "--no-compile-autoload-dotenv"];
268
+ return [bun, "build", "--compile", ...autoload, path.resolve(entry), "--outfile", path.resolve(outfile)];
269
+ }
270
+
271
+ function removeWorkDir(workDir: string): void {
272
+ if (existsSync("/usr/bin/chflags")) Bun.spawnSync(["/usr/bin/chflags", "-R", "nouchg", workDir]);
273
+ try {
274
+ rmSync(workDir, { recursive: true, force: true });
275
+ } catch (error) {
276
+ // The binary itself is complete; report the leftover without failing the build.
277
+ console.error(`[bazis] temporary directory was not removed: ${workDir}`, error);
143
278
  }
144
279
  }
145
280
 
package/cli/codegen.ts CHANGED
@@ -1,21 +1,172 @@
1
- import { existsSync } from "node:fs";
1
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
2
3
  import path from "node:path";
4
+ import { PALETTE, formatDuration, type Problem, type Screen } from "../library/terminal";
5
+ import { commandFailed, createScreens, readFrameLines, type CliOutput } from "./ui";
3
6
 
4
7
  const GENERATOR = "core/scripts/di-generate.ts";
5
8
 
6
- /** Run the framework generator for the project in `cwd`; returns its exit code. */
7
- export async function runCodegen(cwd: string, target?: string): Promise<number> {
9
+ /** What the generator reports through `BAZIS_CODEGEN_REPORT`. */
10
+ export interface CodegenReport {
11
+ readonly ok: boolean;
12
+ readonly ms: number;
13
+ readonly targets: readonly {
14
+ readonly name: string; readonly sources: number; readonly deps: number; readonly controllers: number;
15
+ readonly requestModels: number; readonly listModels: number; readonly tools: number; readonly ms: number;
16
+ }[];
17
+ readonly outputs: readonly string[];
18
+ readonly warnings: readonly string[];
19
+ readonly errors: readonly string[];
20
+ }
21
+
22
+ export interface CodegenOptions {
23
+ readonly ui?: CliOutput;
24
+ /** The last line of a failure: the exit code, or what `dev --watch` does next. */
25
+ readonly footer?: string;
26
+ }
27
+
28
+ /**
29
+ * Runs the framework generator for the project in `cwd` and renders its
30
+ * result as the `codegen` step: counts and generated files, warnings, and on
31
+ * failure one problem per error with the code around it. Returns its exit code.
32
+ */
33
+ export async function runCodegen(cwd: string, target?: string, options: CodegenOptions = {}): Promise<number> {
8
34
  if (target !== undefined) {
9
35
  const configFile = Bun.file(path.join(cwd, "bazis.config.json"));
10
36
  if (!await configFile.exists()) throw new Error("--target requires bazis.config.json in the current directory.");
11
37
  const config = await configFile.json();
12
38
  if (target !== "all" && !Object.hasOwn(config.targets ?? {}, target)) throw new Error(`Unknown codegen target: ${target}`);
13
39
  }
40
+ const ui = options.ui ?? createScreens(process.stdout, process.stderr);
41
+ const s = ui.out;
14
42
  const generator = resolveGenerator(cwd);
15
43
  // TypeScript 7 has no compiler API: the preload switches it to @typescript/typescript6.
16
44
  const preload = path.join(path.dirname(generator), "typescriptApiPreload.ts");
17
45
  const args = [await resolveBun(cwd), ...(existsSync(preload) ? ["--preload", preload] : []), generator, ...(target === undefined ? [] : ["--target", target])];
18
- return await Bun.spawn(args, { cwd, stdout: "inherit", stderr: "inherit" }).exited;
46
+ const workDir = mkdtempSync(path.join(tmpdir(), "bazis-codegen-"));
47
+ const reportFile = path.join(workDir, "report.json");
48
+ const stop = s.progress(s.t.step.codegen);
49
+ const startedAt = performance.now();
50
+ try {
51
+ const child = Bun.spawn(args, { cwd, env: { ...process.env, BAZIS_CODEGEN_REPORT: reportFile }, stdout: "pipe", stderr: "pipe" });
52
+ const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
53
+ stop();
54
+ const report = readReport(reportFile);
55
+ if (report === undefined) {
56
+ // The generator did not get to its report: show what it printed.
57
+ s.step("fail", s.t.step.codegen, "", formatDuration(performance.now() - startedAt));
58
+ ui.err.line([stdout, stderr].filter((text) => text.trim()).join("\n").trimEnd());
59
+ return exitCode === 0 ? 1 : exitCode;
60
+ }
61
+ renderCodegen(ui, report, cwd, options.footer);
62
+ return report.ok && exitCode === 0 ? 0 : (exitCode || 1);
63
+ } finally {
64
+ stop();
65
+ rmSync(workDir, { recursive: true, force: true });
66
+ }
67
+ }
68
+
69
+ function readReport(file: string): CodegenReport | undefined {
70
+ try {
71
+ return JSON.parse(readFileSync(file, "utf8")) as CodegenReport;
72
+ } catch {
73
+ return undefined;
74
+ }
75
+ }
76
+
77
+ export function renderCodegen(ui: CliOutput, report: CodegenReport, cwd: string, footer?: string): void {
78
+ const s = ui.out;
79
+ const t = s.t;
80
+ const time = formatDuration(report.ms);
81
+ const target = report.targets.find((item) => item.name === "production") ?? report.targets[0];
82
+ if (!report.ok) {
83
+ s.step("fail", t.step.codegen, `${s.num(report.errors.length, t.noun("error", report.errors.length), PALETTE.red)}${report.warnings.length ? `${s.dot}${s.num(report.warnings.length, t.noun("warning", report.warnings.length), PALETTE.orange)}` : ""}`, time);
84
+ printWarnings(s, report.warnings);
85
+ s.line();
86
+ commandFailed(ui.err, t.failure.codegen(report.errors.length), report.errors.map((error) => codegenProblem(error, cwd, ui.err)), 1, footer);
87
+ return;
88
+ }
89
+ const counts = target === undefined ? [] : [
90
+ s.num(target.sources, t.noun("file", target.sources)),
91
+ s.num(target.deps, t.noun("dep", target.deps)),
92
+ s.num(target.controllers, t.noun("controller", target.controllers)),
93
+ s.num(target.requestModels, t.noun("requestModel", target.requestModels)),
94
+ s.num(target.listModels, t.noun("list", target.listModels)),
95
+ s.num(target.tools, t.noun("tool", target.tools)),
96
+ ];
97
+ s.step("ok", t.step.codegen, counts.join(s.dot), time);
98
+ const directories = new Map<string, string[]>();
99
+ for (const output of report.outputs) {
100
+ const directory = path.dirname(output);
101
+ directories.set(directory, [...(directories.get(directory) ?? []), path.basename(output, ".ts")]);
102
+ }
103
+ s.tree([...directories].map(([directory, files]) => ({ text: `${s.muted(`${directory}/`)} ${s.text(files.join(" · "))}` })), s.style.color ? 9 : 4);
104
+ printWarnings(s, report.warnings);
105
+ }
106
+
107
+ function printWarnings(s: Screen, warnings: readonly string[]): void {
108
+ for (const warning of warnings) {
109
+ const unassigned = /^BAZIS_CODEGEN_SOURCE_UNASSIGNED: ([^:]+):/.exec(warning);
110
+ if (unassigned) {
111
+ s.warning(`${s.t.codegen.unreachable(unassigned[1]!)}${s.dot}${s.muted(s.t.codegen.unreachableFix)}`);
112
+ } else {
113
+ s.warning(warning);
114
+ }
115
+ }
116
+ }
117
+
118
+ /** `CODE: file:line: message` → a problem with the code around the line. */
119
+ export function codegenProblem(error: string, cwd: string, screen: Screen): Problem {
120
+ const t = screen.t;
121
+ const parsed = /^([A-Z][A-Z0-9_]+): (?:([^\s:]+\.tsx?):(\d+): )?([\s\S]*)$/.exec(error);
122
+ if (parsed === null) return { tag: "CODEGEN", where: "codegen", cause: error };
123
+ const [, code, file, lineText, message] = parsed as unknown as [string, string, string | undefined, string | undefined, string];
124
+ const tag = code.startsWith("BAZIS_DI_") ? "DI" : code.startsWith("BAZIS_HTTP_") ? "HTTP" : "CODEGEN";
125
+ const fixes: string[] = [];
126
+ let frame: Problem["frame"];
127
+ let line = lineText === undefined ? undefined : Number(lineText);
128
+ if (file !== undefined && line !== undefined) {
129
+ const type = /has type "([^"]+)"/.exec(message)?.[1];
130
+ // The location is the class; the parameter with that type is a few lines below.
131
+ if (type !== undefined) line = parameterLine(path.resolve(cwd, file), line, type) ?? line;
132
+ const lines = readFrameLines(path.resolve(cwd, file), line);
133
+ const text = lines.find(([number]) => number === line)?.[1] ?? "";
134
+ const column = type === undefined ? -1 : text.search(new RegExp(`(?<=:\\s*)${escapeRegExp(type)}\\b`));
135
+ frame = lines.length === 0 ? undefined : {
136
+ lines,
137
+ line,
138
+ ...(column >= 0 ? { column: column + 1, length: type!.length } : {}),
139
+ };
140
+ if (type === "SQL") fixes.push(t.codegen.sqlFix);
141
+ else if (type === "RedisClient") fixes.push(t.codegen.redisFix);
142
+ else if (code === "BAZIS_DI_DEPENDENCY_UNKNOWN") fixes.push(t.codegen.unknownDepFix);
143
+ }
144
+ return {
145
+ tag,
146
+ where: file === undefined ? code : `${file}:${line}`,
147
+ cause: message.trim(),
148
+ fixes,
149
+ ...(frame === undefined ? {} : { frame }),
150
+ code,
151
+ };
152
+ }
153
+
154
+ /** The first line from `from` on that declares something of type `type`. */
155
+ function parameterLine(file: string, from: number, type: string): number | undefined {
156
+ try {
157
+ const lines = readFileSync(file, "utf8").split("\n");
158
+ const pattern = new RegExp(`:\\s*${escapeRegExp(type)}\\b`);
159
+ for (let index = from - 1; index < Math.min(lines.length, from + 40); index += 1) {
160
+ if (pattern.test(lines[index] ?? "")) return index + 1;
161
+ }
162
+ } catch {
163
+ return undefined;
164
+ }
165
+ return undefined;
166
+ }
167
+
168
+ function escapeRegExp(text: string): string {
169
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19
170
  }
20
171
 
21
172
  /**
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { copyFile, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { parseModuleName } from "./naming";
4
+ import { parseLanguage, systemLanguage } from "../library/terminal";
4
5
 
5
6
  export interface GenerateProjectOptions {
6
7
  readonly name: string;
@@ -159,7 +160,9 @@ function buildProjectFiles(name: string, dependency: string, frameworkMode: Fram
159
160
  },
160
161
  include: ["src/**/*"],
161
162
  };
162
- const codegen = { version: 1, defaultTarget: "production", targets: { production: { entrypoints: ["src/index.ts"] } } };
163
+ // Output language of the CLI and the startup screen, from the system locale; BAZIS_LANG and --lang override it.
164
+ const language = parseLanguage(systemLanguage()) ?? "en";
165
+ const codegen = { version: 1, defaultTarget: "production", language, targets: { production: { entrypoints: ["src/index.ts"] } } };
163
166
  return [
164
167
  ["package.json", `${JSON.stringify(manifest, null, 2)}\n`],
165
168
  ["tsconfig.json", `${JSON.stringify(tsconfig, null, 2)}\n`],
@@ -184,7 +187,10 @@ const HEALTH_TEST = `import { expect, test } from "bun:test";
184
187
 
185
188
  // Starts the app from source exactly as \`bazis dev\` does and checks /health.
186
189
  test("app starts and answers /health", async () => {
187
- const port = String(20000 + Math.floor(Math.random() * 20000));
190
+ // A free port from the operating system, not a random guess that may be taken.
191
+ const probe = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response() });
192
+ const port = String(probe.port);
193
+ probe.stop(true);
188
194
  const app = Bun.spawn([process.execPath, "run", "src/index.ts"], { env: { ...process.env, HOST: "127.0.0.1", PORT: port }, stdout: "ignore", stderr: "inherit" });
189
195
  try {
190
196
  let status = 0;