@beignet/cli 0.0.10 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/mcp.ts ADDED
@@ -0,0 +1,367 @@
1
+ import path from "node:path";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { applyDoctorFixes, inspectApp } from "./inspect.js";
6
+ import { lintApp } from "./lint.js";
7
+ import {
8
+ type MakeResourceResult,
9
+ makeAdapter,
10
+ makeContract,
11
+ makeEvent,
12
+ makeFactory,
13
+ makeFeature,
14
+ makeFeatureAddonChoices,
15
+ makeJob,
16
+ makeListener,
17
+ makeNotification,
18
+ makePolicy,
19
+ makePort,
20
+ makeResource,
21
+ makeSchedule,
22
+ makeSeed,
23
+ makeTask,
24
+ makeTest,
25
+ makeUpload,
26
+ makeUseCase,
27
+ } from "./make.js";
28
+
29
+ /**
30
+ * Options for building or running the Beignet MCP server.
31
+ */
32
+ export type McpServerOptions = {
33
+ cwd: string;
34
+ version: string;
35
+ };
36
+
37
+ const makeArtifactChoices = [
38
+ "adapter",
39
+ "contract",
40
+ "event",
41
+ "factory",
42
+ "feature",
43
+ "job",
44
+ "listener",
45
+ "notification",
46
+ "policy",
47
+ "port",
48
+ "resource",
49
+ "schedule",
50
+ "seed",
51
+ "task",
52
+ "test",
53
+ "upload",
54
+ "use-case",
55
+ ] as const;
56
+
57
+ type MakeArtifact = (typeof makeArtifactChoices)[number];
58
+
59
+ const makeInputSchema = {
60
+ artifact: z
61
+ .enum(makeArtifactChoices)
62
+ .describe("Kind of artifact to generate."),
63
+ name: z
64
+ .string()
65
+ .describe(
66
+ "Artifact name. Feature-owned artifacts use feature-scoped paths such as posts/backfill.",
67
+ ),
68
+ with: z
69
+ .array(z.enum(makeFeatureAddonChoices))
70
+ .optional()
71
+ .describe(
72
+ "feature only: add feature-owned artifacts such as policy, events, jobs, notifications, tasks, ui, and uploads.",
73
+ ),
74
+ event: z
75
+ .string()
76
+ .optional()
77
+ .describe(
78
+ "listener only (required): event to listen to, for example posts/published.",
79
+ ),
80
+ cron: z
81
+ .string()
82
+ .optional()
83
+ .describe("schedule only: cron expression. Defaults to 0 9 * * *."),
84
+ timezone: z
85
+ .string()
86
+ .optional()
87
+ .describe("schedule only: IANA timezone, for example America/Chicago."),
88
+ route: z
89
+ .boolean()
90
+ .optional()
91
+ .describe("schedule only: generate a Next.js cron route for the schedule."),
92
+ auth: z
93
+ .boolean()
94
+ .optional()
95
+ .describe(
96
+ "resource only: generate policy metadata and use-case authorization checks for mutating routes.",
97
+ ),
98
+ tenant: z
99
+ .boolean()
100
+ .optional()
101
+ .describe(
102
+ "resource only: generate tenant-scoped schemas, repository methods, and use-case checks.",
103
+ ),
104
+ events: z
105
+ .boolean()
106
+ .optional()
107
+ .describe(
108
+ "resource only: generate domain event definitions and publish resource events.",
109
+ ),
110
+ softDelete: z
111
+ .boolean()
112
+ .optional()
113
+ .describe(
114
+ "resource only: generate soft-delete persistence that archives rows with deletedAt.",
115
+ ),
116
+ force: z
117
+ .boolean()
118
+ .optional()
119
+ .describe("Overwrite conflicting files instead of failing."),
120
+ dryRun: z
121
+ .boolean()
122
+ .optional()
123
+ .describe("Preview generated changes without writing files."),
124
+ };
125
+
126
+ type MakeToolInput = {
127
+ artifact: MakeArtifact;
128
+ name: string;
129
+ with?: Array<(typeof makeFeatureAddonChoices)[number]>;
130
+ event?: string;
131
+ cron?: string;
132
+ timezone?: string;
133
+ route?: boolean;
134
+ auth?: boolean;
135
+ tenant?: boolean;
136
+ events?: boolean;
137
+ softDelete?: boolean;
138
+ force?: boolean;
139
+ dryRun?: boolean;
140
+ };
141
+
142
+ type McpToolResult = {
143
+ content: Array<{ type: "text"; text: string }>;
144
+ isError?: boolean;
145
+ };
146
+
147
+ function jsonResult(value: unknown): McpToolResult {
148
+ return {
149
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
150
+ };
151
+ }
152
+
153
+ function errorResult(error: unknown): McpToolResult {
154
+ return {
155
+ isError: true,
156
+ content: [
157
+ {
158
+ type: "text",
159
+ text: error instanceof Error ? error.message : String(error),
160
+ },
161
+ ],
162
+ };
163
+ }
164
+
165
+ async function safeToolResult(
166
+ run: () => Promise<McpToolResult>,
167
+ ): Promise<McpToolResult> {
168
+ try {
169
+ return await run();
170
+ } catch (error) {
171
+ return errorResult(error);
172
+ }
173
+ }
174
+
175
+ async function runMakeTool(
176
+ cwd: string,
177
+ input: MakeToolInput,
178
+ ): Promise<MakeResourceResult> {
179
+ const base = {
180
+ name: input.name,
181
+ cwd,
182
+ force: input.force,
183
+ dryRun: input.dryRun,
184
+ };
185
+
186
+ switch (input.artifact) {
187
+ case "adapter":
188
+ return makeAdapter(base);
189
+ case "contract":
190
+ return makeContract(base);
191
+ case "event":
192
+ return makeEvent(base);
193
+ case "factory":
194
+ return makeFactory(base);
195
+ case "feature":
196
+ return makeFeature({ ...base, with: input.with });
197
+ case "job":
198
+ return makeJob(base);
199
+ case "listener":
200
+ if (!input.event) {
201
+ throw new Error(
202
+ "make listener requires an event, for example event: posts/published.",
203
+ );
204
+ }
205
+ return makeListener({ ...base, event: input.event });
206
+ case "notification":
207
+ return makeNotification(base);
208
+ case "policy":
209
+ return makePolicy(base);
210
+ case "port":
211
+ return makePort(base);
212
+ case "resource":
213
+ return makeResource({
214
+ ...base,
215
+ auth: input.auth,
216
+ tenant: input.tenant,
217
+ events: input.events,
218
+ softDelete: input.softDelete,
219
+ });
220
+ case "schedule":
221
+ return makeSchedule({
222
+ ...base,
223
+ cron: input.cron,
224
+ timezone: input.timezone,
225
+ route: input.route,
226
+ });
227
+ case "seed":
228
+ return makeSeed(base);
229
+ case "task":
230
+ return makeTask(base);
231
+ case "test":
232
+ return makeTest(base);
233
+ case "upload":
234
+ return makeUpload(base);
235
+ case "use-case":
236
+ return makeUseCase(base);
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Build the Beignet MCP server with routes, doctor, lint, and make tools.
242
+ * Every tool resolves against the app directory captured at launch.
243
+ */
244
+ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
245
+ const cwd = path.resolve(options.cwd);
246
+ const server = new McpServer({
247
+ name: "beignet",
248
+ version: options.version,
249
+ });
250
+
251
+ server.registerTool(
252
+ "routes",
253
+ {
254
+ description:
255
+ "List the app's registered Beignet HTTP routes as JSON: method, path, contract export, contract file, and route handler mapping.",
256
+ annotations: { readOnlyHint: true },
257
+ },
258
+ async () =>
259
+ safeToolResult(async () => {
260
+ const result = await inspectApp({ cwd });
261
+ return jsonResult(
262
+ result.routes.map((route) => ({
263
+ method: route.method,
264
+ path: route.path,
265
+ contract: route.exportName,
266
+ file: route.file,
267
+ handlerFile: route.handlerFile,
268
+ handlerSource: route.handlerSource,
269
+ metadata: route.metadata,
270
+ })),
271
+ );
272
+ }),
273
+ );
274
+
275
+ server.registerTool(
276
+ "doctor",
277
+ {
278
+ description:
279
+ "Check app wiring and Beignet conventions, returning diagnostics, the detected convention, and applied fixes as JSON. Strict mode (the default) is the CI bar and includes CI-oriented warnings. Read-only; use doctor_fix to repair registration drift.",
280
+ inputSchema: {
281
+ strict: z
282
+ .boolean()
283
+ .optional()
284
+ .describe("Include CI-oriented warnings. Defaults to true."),
285
+ },
286
+ annotations: { readOnlyHint: true },
287
+ },
288
+ async (input) =>
289
+ safeToolResult(async () => {
290
+ const result = await inspectApp({ cwd, strict: input.strict ?? true });
291
+ return jsonResult({
292
+ diagnostics: result.diagnostics,
293
+ convention: result.convention,
294
+ fixes: result.fixes,
295
+ });
296
+ }),
297
+ );
298
+
299
+ server.registerTool(
300
+ "doctor_fix",
301
+ {
302
+ description:
303
+ "Apply low-risk doctor fixes, then re-check the app. Repairs route-group, schedule, task, and outbox registration drift; listener registration drift is report-only. Returns applied fixes and remaining diagnostics as JSON.",
304
+ inputSchema: {
305
+ strict: z
306
+ .boolean()
307
+ .optional()
308
+ .describe("Include CI-oriented warnings. Defaults to true."),
309
+ },
310
+ },
311
+ async (input) =>
312
+ safeToolResult(async () => {
313
+ const strict = input.strict ?? true;
314
+ const fixes = await applyDoctorFixes({ cwd, strict });
315
+ const result = await inspectApp({ cwd, strict });
316
+ return jsonResult({
317
+ diagnostics: result.diagnostics,
318
+ convention: result.convention,
319
+ fixes,
320
+ });
321
+ }),
322
+ );
323
+
324
+ server.registerTool(
325
+ "lint",
326
+ {
327
+ description:
328
+ "Check Beignet dependency-direction conventions across domain, use cases, routes, components, and infra. Returns lint diagnostics as JSON.",
329
+ annotations: { readOnlyHint: true },
330
+ },
331
+ async () =>
332
+ safeToolResult(async () => {
333
+ return jsonResult(await lintApp({ cwd }));
334
+ }),
335
+ );
336
+
337
+ server.registerTool(
338
+ "make",
339
+ {
340
+ description:
341
+ "Generate Beignet app files for one artifact: adapter, contract, event, factory, feature, job, listener, notification, policy, port, resource, schedule, seed, task, test, upload, or use-case. Generators auto-register artifacts, so doctor stays clean. Options by artifact: feature accepts with (addons); resource accepts auth, tenant, events, softDelete; listener requires event; schedule accepts cron, timezone, route. All artifacts accept force and dryRun. Returns created, updated, and skipped files as JSON.",
342
+ inputSchema: makeInputSchema,
343
+ },
344
+ async (input) =>
345
+ safeToolResult(async () => {
346
+ return jsonResult(await runMakeTool(cwd, input));
347
+ }),
348
+ );
349
+
350
+ return server;
351
+ }
352
+
353
+ /**
354
+ * Run the Beignet MCP server over stdio until the client disconnects.
355
+ * Stdout belongs to the transport; diagnostics must go to stderr.
356
+ */
357
+ export async function runMcpServer(options: McpServerOptions): Promise<void> {
358
+ const server = buildBeignetMcpServer(options);
359
+ const transport = new StdioServerTransport();
360
+ const closed = new Promise<void>((resolve) => {
361
+ server.server.onclose = () => {
362
+ resolve();
363
+ };
364
+ });
365
+ await server.connect(transport);
366
+ await closed;
367
+ }
@@ -0,0 +1,115 @@
1
+ import { json, packageRunner, type TemplateContext } from "./shared.js";
2
+
3
+ /**
4
+ * AGENTS.md teaches coding agents the conventions that are not discoverable
5
+ * from the code: registration wiring, generators, and the validation loop.
6
+ * README.md owns setup and the app map; this file must not duplicate it.
7
+ */
8
+ export function agentsMd(ctx: TemplateContext): string {
9
+ const cli = packageRunner(ctx);
10
+ const test =
11
+ ctx.packageManager === "npm"
12
+ ? "npm run test"
13
+ : `${ctx.packageManager} run test`;
14
+ const lint =
15
+ ctx.packageManager === "npm"
16
+ ? "npm run lint"
17
+ : `${ctx.packageManager} run lint`;
18
+ const format =
19
+ ctx.packageManager === "npm"
20
+ ? "npm run format"
21
+ : `${ctx.packageManager} run format`;
22
+ const typecheck =
23
+ ctx.packageManager === "npm"
24
+ ? "npm run typecheck"
25
+ : `${ctx.packageManager} run typecheck`;
26
+
27
+ return `# ${ctx.name} — agent guide
28
+
29
+ This is a Beignet contract-first app. Full framework docs live at
30
+ https://beignetjs.com (agent-friendly index at https://beignetjs.com/llms.txt).
31
+ README.md covers setup and the app map; this file covers what is not
32
+ discoverable from the code.
33
+
34
+ ## Registration is not automatic
35
+
36
+ Creating an artifact file does not wire it into the app. Non-registration is
37
+ a silent failure — the file exists but never runs:
38
+
39
+ - Feature route groups in \`features/<feature>/routes.ts\` must be composed in
40
+ \`server/routes.ts\` via \`defineRoutes([...])\`.
41
+ - Schedules must be added to the \`schedules\` array in \`server/schedules.ts\`.
42
+ - Tasks must be added to \`defineTasks([...])\` in \`server/tasks.ts\`.
43
+ - Outbox events and jobs must be registered in \`defineOutboxRegistry({...})\`
44
+ in \`server/outbox.ts\`.
45
+ - Listeners must be wired through a \`registerListeners(...)\` call in infra
46
+ wiring.
47
+
48
+ The starter ships no workflow registries — generators create them on first
49
+ use, so their absence is fine. \`${cli} doctor\` detects registration drift.
50
+ \`${cli} doctor --fix\` repairs route-group, schedule, task, and outbox
51
+ registration; listener drift is report-only and must be fixed by hand.
52
+
53
+ ## Prefer generators
54
+
55
+ \`${cli} make <artifact> <name>\` creates correctly placed, pre-registered
56
+ files — prefer it over hand-writing them. See \`${cli} make --help\` for the
57
+ artifact list. After changing the Drizzle schema in \`infra/db/schema/\`, run
58
+ \`${cli} db generate\` then \`${cli} db migrate\`.
59
+
60
+ ## Validation loop
61
+
62
+ Run after every change:
63
+
64
+ \`\`\`bash
65
+ ${lint}
66
+ ${cli} lint
67
+ ${cli} doctor --strict
68
+ ${test}
69
+ ${typecheck}
70
+ \`\`\`
71
+
72
+ \`${lint}\` runs Biome's code lint. Use \`${format}\` to apply formatting.
73
+ \`${cli} lint\` is Beignet's dependency-direction lint.
74
+
75
+ ## Placement rules
76
+
77
+ - Feature artifacts live under \`features/<feature>/\`, with tests in
78
+ \`features/<feature>/tests/\` (not \`__tests__/\`).
79
+ - Domain and use-case code must not import infra, providers, or React.
80
+ - Routes must not import concrete infra.
81
+
82
+ ## Naming grammar
83
+
84
+ - \`defineX\` declares things you register: contracts, routes, jobs,
85
+ schedules, and the rest.
86
+ - \`createX\` builds runtime objects you call: servers, clients, providers,
87
+ and the \`createX<AppContext>()\` factories that return app-bound
88
+ \`defineX\` builders.
89
+
90
+ ## MCP server
91
+
92
+ \`.mcp.json\` registers the \`beignet mcp\` stdio server, which exposes
93
+ routes/doctor/lint/make as structured tools named exactly: \`routes\`,
94
+ \`doctor\`, \`doctor_fix\`, \`lint\`, \`make\`. Clients that do not read
95
+ \`.mcp.json\` can launch \`${cli} mcp\` directly.
96
+ `;
97
+ }
98
+
99
+ /**
100
+ * .mcp.json registers the `beignet mcp` stdio server for MCP-aware clients.
101
+ * The runner must not print banners on stdout, which corrupts JSON-RPC, so
102
+ * the config launches the locally installed CLI bin via bunx/npx.
103
+ */
104
+ export function mcpJson(ctx: TemplateContext): string {
105
+ const command = ctx.packageManager === "bun" ? "bunx" : "npx";
106
+
107
+ return json({
108
+ mcpServers: {
109
+ beignet: {
110
+ command,
111
+ args: ["beignet", "mcp"],
112
+ },
113
+ },
114
+ });
115
+ }
@@ -66,6 +66,7 @@ export function packageJson(ctx: TemplateContext): string {
66
66
  const devDependencies: Record<string, string> = {
67
67
  "@beignet/cli": ctx.beignetVersion,
68
68
  "@beignet/web": ctx.beignetVersion,
69
+ "@biomejs/biome": externalVersions.biome,
69
70
  "@types/bun": externalVersions.typesBun,
70
71
  "@types/node": externalVersions.typesNode,
71
72
  "@types/react": externalVersions.typesReact,
@@ -94,6 +95,8 @@ export function packageJson(ctx: TemplateContext): string {
94
95
  dev: "next dev",
95
96
  build: "next build",
96
97
  start: "next start",
98
+ lint: "biome lint .",
99
+ format: "biome format --write .",
97
100
  test: "bun test",
98
101
  typecheck: "tsc --noEmit",
99
102
  "db:generate": "drizzle-kit generate",
@@ -136,6 +139,14 @@ export function readme(ctx: TemplateContext): string {
136
139
  ctx.packageManager === "npm"
137
140
  ? "npm run test"
138
141
  : `${ctx.packageManager} run test`;
142
+ const lint =
143
+ ctx.packageManager === "npm"
144
+ ? "npm run lint"
145
+ : `${ctx.packageManager} run lint`;
146
+ const format =
147
+ ctx.packageManager === "npm"
148
+ ? "npm run format"
149
+ : `${ctx.packageManager} run format`;
139
150
  const typecheck =
140
151
  ctx.packageManager === "npm"
141
152
  ? "npm run typecheck"
@@ -209,13 +220,15 @@ ${firstOpen}
209
220
  \`\`\`bash
210
221
  # in another terminal
211
222
  ${cli} routes
223
+ ${lint}
212
224
  ${cli} lint
213
225
  ${cli} doctor
214
226
  ${test}
215
227
  ${typecheck}
216
228
  \`\`\`
217
229
 
218
- \`routes\` shows the contracts Beignet can inspect. \`lint\` checks dependency direction. \`doctor\` catches route, OpenAPI, and resource drift.
230
+ \`routes\` shows the contracts Beignet can inspect. \`${cli} lint\` checks dependency direction. \`doctor\` catches route, OpenAPI, and resource drift.
231
+ \`${lint}\` runs Biome over the starter; use \`${format}\` to apply formatting.
219
232
  ${testingNotes}
220
233
  ## Build for production
221
234
 
@@ -231,6 +244,7 @@ ${cli} make feature projects
231
244
  ${cli} db generate
232
245
  ${cli} db migrate
233
246
  ${test}
247
+ ${lint}
234
248
  ${typecheck}
235
249
  ${cli} lint
236
250
  ${cli} doctor
@@ -429,6 +443,42 @@ const nextConfig = {};
429
443
 
430
444
  export default nextConfig;
431
445
  `,
446
+ biomeConfig: json({
447
+ $schema: "https://biomejs.dev/schemas/2.4.14/schema.json",
448
+ vcs: {
449
+ enabled: true,
450
+ clientKind: "git",
451
+ useIgnoreFile: true,
452
+ },
453
+ formatter: {
454
+ enabled: true,
455
+ indentStyle: "tab",
456
+ },
457
+ linter: {
458
+ enabled: true,
459
+ rules: {
460
+ recommended: true,
461
+ suspicious: {
462
+ noExplicitAny: "warn",
463
+ useIterableCallbackReturn: "warn",
464
+ },
465
+ correctness: {
466
+ noUnusedImports: "off",
467
+ useHookAtTopLevel: "off",
468
+ },
469
+ },
470
+ },
471
+ javascript: {
472
+ formatter: {
473
+ quoteStyle: "double",
474
+ },
475
+ },
476
+ css: {
477
+ parser: {
478
+ tailwindDirectives: true,
479
+ },
480
+ },
481
+ }),
432
482
  tsconfig: json({
433
483
  compilerOptions: {
434
484
  target: "ES2017",
@@ -207,7 +207,7 @@ export function createDrizzleTodoRepository(
207
207
  dbRepositories: `import type { DrizzleMysqlDatabase } from "@beignet/provider-db-drizzle/mysql";
208
208
  import { createDrizzleTodoRepository } from "@/infra/todos/drizzle-todo-repository";
209
209
  import type { AppTransactionPorts } from "@/ports";
210
- import * as schema from "./schema";
210
+ import type * as schema from "./schema";
211
211
 
212
212
  export function createRepositories(
213
213
  db: DrizzleMysqlDatabase<typeof schema>,
@@ -195,7 +195,7 @@ export function createDrizzleTodoRepository(
195
195
  dbRepositories: `import type { DrizzlePostgresDatabase } from "@beignet/provider-db-drizzle/postgres";
196
196
  import { createDrizzleTodoRepository } from "@/infra/todos/drizzle-todo-repository";
197
197
  import type { AppTransactionPorts } from "@/ports";
198
- import * as schema from "./schema";
198
+ import type * as schema from "./schema";
199
199
 
200
200
  export function createRepositories(
201
201
  db: DrizzlePostgresDatabase<typeof schema>,
@@ -191,7 +191,7 @@ export function createDrizzleTodoRepository(
191
191
  dbRepositories: `import type { DrizzleSqliteDatabase } from "@beignet/provider-db-drizzle/sqlite";
192
192
  import { createDrizzleTodoRepository } from "@/infra/todos/drizzle-todo-repository";
193
193
  import type { AppTransactionPorts } from "@/ports";
194
- import * as schema from "./schema";
194
+ import type * as schema from "./schema";
195
195
 
196
196
  export function createRepositories(
197
197
  db: DrizzleSqliteDatabase<typeof schema>,
@@ -354,8 +354,8 @@ export type TestDatabase = {
354
354
  };
355
355
 
356
356
  export async function createTestDatabase(): Promise<TestDatabase> {
357
- const path = join(tmpdir(), "beignet-test-" + crypto.randomUUID() + ".db");
358
- const client = createClient({ url: "file:" + path });
357
+ const path = join(tmpdir(), \`beignet-test-\${crypto.randomUUID()}.db\`);
358
+ const client = createClient({ url: \`file:\${path}\` });
359
359
  const db = drizzle(client, { schema });
360
360
  await migrate(db, { migrationsFolder: "drizzle" });
361
361
 
@@ -7,6 +7,7 @@ export type {
7
7
  export { databaseChoices, integrationChoices } from "../choices.js";
8
8
  export type { TemplateContext, TemplateFile } from "./shared.js";
9
9
 
10
+ import { agentsMd, mcpJson } from "./agents.js";
10
11
  import {
11
12
  baseFiles,
12
13
  envExample,
@@ -42,10 +43,13 @@ export function getTemplateFiles(ctx: TemplateContext): TemplateFile[] {
42
43
  const templateFiles: TemplateFile[] = [
43
44
  { path: "package.json", content: packageJson(ctx) },
44
45
  { path: "README.md", content: readme(ctx) },
46
+ { path: "AGENTS.md", content: agentsMd(ctx) },
47
+ { path: ".mcp.json", content: mcpJson(ctx) },
45
48
  { path: ".gitignore", content: baseFiles.gitignore },
46
49
  { path: ".env.example", content: envExample(ctx) },
47
50
  { path: "next-env.d.ts", content: baseFiles.nextEnv },
48
51
  { path: "next.config.js", content: baseFiles.nextConfig },
52
+ { path: "biome.json", content: baseFiles.biomeConfig },
49
53
  { path: "tsconfig.json", content: baseFiles.tsconfig },
50
54
  { path: "app-context.ts", content: serverFiles.appContext },
51
55
  {
@@ -31,6 +31,7 @@ export type TemplateContext = {
31
31
  };
32
32
 
33
33
  export const externalVersions = {
34
+ biome: "^2.4.14",
34
35
  next: "^16.2.4",
35
36
  react: "^19.2.5",
36
37
  reactDom: "^19.2.5",