create-better-t-stack 3.27.5 → 3.28.1-pr1036.ff20c00

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/README.md CHANGED
@@ -29,21 +29,21 @@ Follow the prompts to configure your project or use the `--yes` flag for default
29
29
 
30
30
  ## Features
31
31
 
32
- | Category | Options |
33
- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
34
- | **TypeScript** | End-to-end type safety across all parts of your application |
35
- | **Frontend** | • React with TanStack Router<br>• React with React Router<br>• React with TanStack Start (SSR)<br>• Next.js<br>• SvelteKit<br>• Nuxt (Vue)<br>• SolidJS<br>• Astro<br>• React Native bare Expo<br>• React Native with NativeWind (via Expo)<br>• React Native with Unistyles (via Expo)<br>• None |
36
- | **Backend** | • Hono<br>• Express<br>• Elysia<br>• Fastify<br>• Self (fullstack inside the web app)<br>• Convex<br>• None |
37
- | **API Layer** | • tRPC (type-safe APIs)<br>• oRPC (OpenAPI-compatible type-safe APIs)<br>• None |
38
- | **Runtime** | • Bun<br>• Node.js<br>• Cloudflare Workers<br>• None |
39
- | **Database** | • SQLite<br>• PostgreSQL<br>• MySQL<br>• MongoDB<br>• None |
40
- | **ORM** | • Drizzle (TypeScript-first)<br>• Prisma (feature-rich)<br>• Mongoose (for MongoDB)<br>• None |
41
- | **Database Setup** | • Turso (SQLite)<br>• Cloudflare D1 (SQLite)<br>• Neon (PostgreSQL)<br>• Supabase (PostgreSQL)<br>• Prisma Postgres<br>• MongoDB Atlas<br>• None (manual setup) |
42
- | **Authentication** | • Better Auth<br>• Clerk |
43
- | **Styling** | Tailwind CSS with a shared shadcn/ui package for React web apps |
44
- | **Addons** | • PWA support<br>• Tauri (desktop applications)<br>• Electrobun (lightweight desktop shell)<br>• Starlight and Fumadocs (documentation sites)<br>• Biome, Oxlint, Ultracite (linting and formatting)<br>• Lefthook, Husky (Git hooks)<br>• MCP, Skills (agent tooling)<br>• OpenTUI, WXT (platform extensions)<br>• Turborepo or Nx (monorepo orchestration) |
45
- | **Examples** | • Todo app<br>• AI Chat interface (using Vercel AI SDK) |
46
- | **Developer Experience** | • Automatic Git initialization<br>• Package manager choice (npm, pnpm, bun)<br>• Automatic dependency installation |
32
+ | Category | Options |
33
+ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34
+ | **TypeScript** | End-to-end type safety across all parts of your application |
35
+ | **Frontend** | • React with TanStack Router<br>• React with React Router<br>• React with TanStack Start (SSR)<br>• Next.js<br>• SvelteKit<br>• Nuxt (Vue)<br>• SolidJS<br>• Astro<br>• React Native bare Expo<br>• React Native with NativeWind (via Expo)<br>• React Native with Unistyles (via Expo)<br>• None |
36
+ | **Backend** | • Hono<br>• Express<br>• Elysia<br>• Fastify<br>• Self (fullstack inside the web app)<br>• Convex<br>• None |
37
+ | **API Layer** | • tRPC (type-safe APIs)<br>• oRPC (OpenAPI-compatible type-safe APIs)<br>• None |
38
+ | **Runtime** | • Bun<br>• Node.js<br>• Cloudflare Workers<br>• None |
39
+ | **Database** | • SQLite<br>• PostgreSQL<br>• MySQL<br>• MongoDB<br>• None |
40
+ | **ORM** | • Drizzle (TypeScript-first)<br>• Prisma (feature-rich)<br>• Mongoose (for MongoDB)<br>• None |
41
+ | **Database Setup** | • Turso (SQLite)<br>• Cloudflare D1 (SQLite)<br>• Neon (PostgreSQL)<br>• Supabase (PostgreSQL)<br>• Prisma Postgres<br>• MongoDB Atlas<br>• None (manual setup) |
42
+ | **Authentication** | • Better Auth<br>• Clerk |
43
+ | **Styling** | Tailwind CSS with a shared shadcn/ui package for React web apps |
44
+ | **Addons** | • PWA support<br>• Tauri (desktop applications)<br>• Electrobun (lightweight desktop shell)<br>• Starlight and Fumadocs (documentation sites)<br>• Biome, Oxlint, Ultracite (linting and formatting)<br>• Lefthook, Husky (Git hooks)<br>• evlog (request logging for server/fullstack backends)<br>• MCP, Skills (agent tooling)<br>• OpenTUI, WXT (platform extensions)<br>• Turborepo or Nx (monorepo orchestration) |
45
+ | **Examples** | • Todo app<br>• AI Chat interface (using Vercel AI SDK) |
46
+ | **Developer Experience** | • Automatic Git initialization<br>• Package manager choice (npm, pnpm, bun)<br>• Automatic dependency installation |
47
47
 
48
48
  ## Usage
49
49
 
@@ -60,7 +60,7 @@ Options:
60
60
  --auth <provider> Authentication (better-auth, clerk, none)
61
61
  --payments <provider> Payments provider (polar, none)
62
62
  --frontend <types...> Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, astro, native-bare, native-uniwind, native-unistyles, none)
63
- --addons <types...> Additional addons (pwa, tauri, electrobun, starlight, biome, lefthook, husky, mcp, turborepo, nx, fumadocs, ultracite, oxlint, opentui, wxt, skills, none)
63
+ --addons <types...> Additional addons (pwa, tauri, electrobun, starlight, biome, lefthook, husky, mcp, turborepo, nx, fumadocs, ultracite, oxlint, opentui, wxt, skills, evlog, none)
64
64
  --examples <types...> Examples to include (todo, ai, none)
65
65
  --git Initialize git repository
66
66
  --no-git Skip git initialization
@@ -239,7 +239,7 @@ npx create-better-t-stack --frontend none --backend hono --api trpc --database n
239
239
  - **ORM 'none'**: Can be used when you want to handle database operations manually or use a different ORM.
240
240
  - **Runtime 'none'**: Only available with Convex backend, backend `none`, or backend `self`.
241
241
  - **Cloudflare Workers runtime**: Only compatible with Hono backend. If a database is used, MongoDB is not supported.
242
- - **Cloudflare D1 setup**: Requires `sqlite` and either `--runtime workers --server-deploy cloudflare` or `--backend self --web-deploy cloudflare`. For `backend self`, D1 is supported on `next`, `tanstack-start`, `nuxt`, and `astro`.
242
+ - **Cloudflare D1 setup**: Requires `sqlite` and either `--runtime workers --server-deploy cloudflare` or `--backend self --web-deploy cloudflare`. For `backend self`, D1 is supported on `next`, `tanstack-start`, `nuxt`, `svelte`, and `astro`.
243
243
  - **Addons 'none'**: Skips all addons.
244
244
  - **Examples 'none'**: Skips all example implementations (todo, AI chat).
245
245
  - **Nuxt, Svelte, SolidJS, and Astro** frontends are only compatible with oRPC API layer
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { _ as types_exports, i as SchemaNameSchema, l as create, m as getSchemaResult, s as add, u as createBtsCli, v as getLatestCLIVersion } from "./src-VGvTc2ik.mjs";
2
+ import { _ as types_exports, i as SchemaNameSchema, l as create, m as getSchemaResult, s as add, u as createBtsCli, v as getLatestCLIVersion } from "./src-Bx8Ofji-.mjs";
3
3
  import z from "zod";
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -9,6 +9,7 @@ const ToolResponseSchema = z.object({
9
9
  data: z.any().optional(),
10
10
  error: z.string().optional()
11
11
  });
12
+ const SchemaToolInputSchema = z.object({ name: SchemaNameSchema.optional().describe("Schema name to inspect. Defaults to all.") });
12
13
  const McpCreateProjectInputSchema = types_exports.CreateInputSchema.safeExtend({
13
14
  projectName: z.string().describe("Project name or relative path"),
14
15
  frontend: z.array(types_exports.FrontendSchema).describe("Explicit frontend app surfaces. Do not use native frontends as styling options."),
@@ -134,7 +135,6 @@ function createBtsMcpServer() {
134
135
  server.registerTool("bts_get_stack_guidance", {
135
136
  title: "Get Better T Stack MCP Guidance",
136
137
  description: "Read MCP-specific guidance for choosing valid Better T Stack configurations. Use this before planning when user intent is ambiguous. This explains the full explicit config required by MCP project creation, plus important field semantics and ambiguity rules.",
137
- inputSchema: z.object({}),
138
138
  outputSchema: ToolResponseSchema,
139
139
  annotations: {
140
140
  title: "Get Better T Stack MCP Guidance",
@@ -153,7 +153,7 @@ function createBtsMcpServer() {
153
153
  server.registerTool("bts_get_schema", {
154
154
  title: "Get Better T Stack Schemas",
155
155
  description: "Inspect Better T Stack CLI and input schemas so agents can plan valid create/add requests. Use this together with bts_get_stack_guidance before creating a project if any part of the request is ambiguous.",
156
- inputSchema: z.object({ name: SchemaNameSchema.optional().describe("Schema name to inspect. Defaults to all.") }),
156
+ inputSchema: SchemaToolInputSchema,
157
157
  outputSchema: ToolResponseSchema,
158
158
  annotations: {
159
159
  title: "Get Better T Stack Schemas",
package/dist/index.d.mts CHANGED
@@ -152,6 +152,7 @@ declare const SchemaNameSchema: z.ZodDefault<z.ZodEnum<{
152
152
  addInput: "addInput";
153
153
  projectConfig: "projectConfig";
154
154
  betterTStackConfig: "betterTStackConfig";
155
+ betterTStackConfigFile: "betterTStackConfigFile";
155
156
  initResult: "initResult";
156
157
  }>>;
157
158
  type SchemaName = z.infer<typeof SchemaNameSchema>;
@@ -174,7 +175,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
174
175
  auth?: "none" | "better-auth" | "clerk" | undefined;
175
176
  payments?: "none" | "polar" | undefined;
176
177
  frontend?: ("none" | "tanstack-router" | "react-router" | "tanstack-start" | "next" | "nuxt" | "native-bare" | "native-uniwind" | "native-unistyles" | "svelte" | "solid" | "astro")[] | undefined;
177
- addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills")[] | undefined;
178
+ addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
178
179
  examples?: ("none" | "todo" | "ai")[] | undefined;
179
180
  git?: boolean | undefined;
180
181
  packageManager?: "bun" | "npm" | "pnpm" | undefined;
@@ -241,7 +242,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
241
242
  scope?: "project" | "global" | undefined;
242
243
  agents?: ("antigravity" | "cline" | "cursor" | "claude-code" | "codex" | "opencode" | "gemini-cli" | "goose" | "github-copilot" | "windsurf" | "roo" | "kilo" | "openhands" | "trae" | "amp" | "pi" | "qoder" | "qwen-code" | "kiro-cli" | "droid" | "command-code" | "clawdbot" | "zencoder" | "neovate" | "mcpjam")[] | undefined;
243
244
  selections?: {
244
- source: "vercel-labs/agent-skills" | "vercel/ai" | "vercel/turborepo" | "yusukebe/hono-skill" | "vercel-labs/next-skills" | "nuxt/ui" | "heroui-inc/heroui" | "shadcn/ui" | "better-auth/skills" | "clerk/skills" | "neondatabase/agent-skills" | "supabase/agent-skills" | "planetscale/database-skills" | "expo/skills" | "prisma/skills" | "elysiajs/skills" | "waynesutton/convexskills" | "msmps/opentui-skill" | "haydenbleasel/ultracite";
245
+ source: "vercel-labs/agent-skills" | "vercel/ai" | "vercel/turborepo" | "yusukebe/hono-skill" | "vercel-labs/next-skills" | "nuxt/ui" | "heroui-inc/heroui" | "shadcn/ui" | "better-auth/skills" | "clerk/skills" | "neondatabase/agent-skills" | "supabase/agent-skills" | "planetscale/database-skills" | "expo/skills" | "prisma/skills" | "elysiajs/skills" | "waynesutton/convexskills" | "msmps/opentui-skill" | "haydenbleasel/ultracite" | "https://www.evlog.dev";
245
246
  skills: string[];
246
247
  }[] | undefined;
247
248
  } | undefined;
@@ -273,7 +274,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
273
274
  auth?: "none" | "better-auth" | "clerk" | undefined;
274
275
  payments?: "none" | "polar" | undefined;
275
276
  frontend?: ("none" | "tanstack-router" | "react-router" | "tanstack-start" | "next" | "nuxt" | "native-bare" | "native-uniwind" | "native-unistyles" | "svelte" | "solid" | "astro")[] | undefined;
276
- addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills")[] | undefined;
277
+ addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
277
278
  examples?: ("none" | "todo" | "ai")[] | undefined;
278
279
  git?: boolean | undefined;
279
280
  packageManager?: "bun" | "npm" | "pnpm" | undefined;
@@ -294,7 +295,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
294
295
  }>;
295
296
  schema: _$_trpc_server0.TRPCQueryProcedure<{
296
297
  input: {
297
- name?: "all" | "cli" | "database" | "orm" | "backend" | "runtime" | "frontend" | "addons" | "examples" | "packageManager" | "databaseSetup" | "api" | "auth" | "payments" | "webDeploy" | "serverDeploy" | "directoryConflict" | "template" | "addonOptions" | "dbSetupOptions" | "createInput" | "addInput" | "projectConfig" | "betterTStackConfig" | "initResult" | undefined;
298
+ name?: "all" | "cli" | "database" | "orm" | "backend" | "runtime" | "frontend" | "addons" | "examples" | "packageManager" | "databaseSetup" | "api" | "auth" | "payments" | "webDeploy" | "serverDeploy" | "directoryConflict" | "template" | "addonOptions" | "dbSetupOptions" | "createInput" | "addInput" | "projectConfig" | "betterTStackConfig" | "betterTStackConfigFile" | "initResult" | undefined;
298
299
  };
299
300
  output: unknown;
300
301
  meta: TrpcCliMeta;
@@ -316,7 +317,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
316
317
  }>;
317
318
  add: _$_trpc_server0.TRPCMutationProcedure<{
318
319
  input: {
319
- addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills")[] | undefined;
320
+ addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
320
321
  install?: boolean | undefined;
321
322
  packageManager?: "bun" | "npm" | "pnpm" | undefined;
322
323
  projectDir?: string | undefined;
@@ -326,7 +327,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
326
327
  }>;
327
328
  addJson: _$_trpc_server0.TRPCMutationProcedure<{
328
329
  input: {
329
- addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills")[] | undefined;
330
+ addons?: ("none" | "pwa" | "tauri" | "electrobun" | "starlight" | "biome" | "lefthook" | "husky" | "mcp" | "turborepo" | "nx" | "fumadocs" | "ultracite" | "oxlint" | "opentui" | "wxt" | "skills" | "evlog")[] | undefined;
330
331
  addonOptions?: {
331
332
  wxt?: {
332
333
  template: "svelte" | "solid" | "vanilla" | "vue" | "react";
@@ -351,7 +352,7 @@ declare const router: _$_trpc_server0.TRPCBuiltRouter<{
351
352
  scope?: "project" | "global" | undefined;
352
353
  agents?: ("antigravity" | "cline" | "cursor" | "claude-code" | "codex" | "opencode" | "gemini-cli" | "goose" | "github-copilot" | "windsurf" | "roo" | "kilo" | "openhands" | "trae" | "amp" | "pi" | "qoder" | "qwen-code" | "kiro-cli" | "droid" | "command-code" | "clawdbot" | "zencoder" | "neovate" | "mcpjam")[] | undefined;
353
354
  selections?: {
354
- source: "vercel-labs/agent-skills" | "vercel/ai" | "vercel/turborepo" | "yusukebe/hono-skill" | "vercel-labs/next-skills" | "nuxt/ui" | "heroui-inc/heroui" | "shadcn/ui" | "better-auth/skills" | "clerk/skills" | "neondatabase/agent-skills" | "supabase/agent-skills" | "planetscale/database-skills" | "expo/skills" | "prisma/skills" | "elysiajs/skills" | "waynesutton/convexskills" | "msmps/opentui-skill" | "haydenbleasel/ultracite";
355
+ source: "vercel-labs/agent-skills" | "vercel/ai" | "vercel/turborepo" | "yusukebe/hono-skill" | "vercel-labs/next-skills" | "nuxt/ui" | "heroui-inc/heroui" | "shadcn/ui" | "better-auth/skills" | "clerk/skills" | "neondatabase/agent-skills" | "supabase/agent-skills" | "planetscale/database-skills" | "expo/skills" | "prisma/skills" | "elysiajs/skills" | "waynesutton/convexskills" | "msmps/opentui-skill" | "haydenbleasel/ultracite" | "https://www.evlog.dev";
355
356
  skills: string[];
356
357
  }[] | undefined;
357
358
  } | undefined;
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import { C as ProjectCreationError, S as DirectoryConflictError, T as ValidationError, a as TEMPLATE_COUNT, b as CompatibilityError, c as builder, d as createVirtual, f as docs, g as sponsors, h as router, i as SchemaNameSchema, l as create, m as getSchemaResult, n as GeneratorError, o as VirtualFileSystem, p as generate, r as Result, s as add, t as EMBEDDED_TEMPLATES, u as createBtsCli, w as UserCancelledError, x as DatabaseSetupError, y as CLIError } from "./src-VGvTc2ik.mjs";
2
+ import { C as ProjectCreationError, S as DirectoryConflictError, T as ValidationError, a as TEMPLATE_COUNT, b as CompatibilityError, c as builder, d as createVirtual, f as docs, g as sponsors, h as router, i as SchemaNameSchema, l as create, m as getSchemaResult, n as GeneratorError, o as VirtualFileSystem, p as generate, r as Result, s as add, t as EMBEDDED_TEMPLATES, u as createBtsCli, w as UserCancelledError, x as DatabaseSetupError, y as CLIError } from "./src-Bx8Ofji-.mjs";
3
3
  export { CLIError, CompatibilityError, DatabaseSetupError, DirectoryConflictError, EMBEDDED_TEMPLATES, GeneratorError, ProjectCreationError, Result, SchemaNameSchema, TEMPLATE_COUNT, UserCancelledError, ValidationError, VirtualFileSystem, add, builder, create, createBtsCli, createVirtual, docs, generate, getSchemaResult, router, sponsors };
@@ -86,6 +86,7 @@ const ADDON_COMPATIBILITY = {
86
86
  opentui: [],
87
87
  wxt: [],
88
88
  skills: [],
89
+ evlog: [],
89
90
  none: []
90
91
  };
91
92
  //#endregion
@@ -337,7 +338,7 @@ async function readHistory() {
337
338
  cause: e
338
339
  })
339
340
  });
340
- if (existsResult.isErr()) return existsResult;
341
+ if (existsResult.isErr()) return Result.err(existsResult.error);
341
342
  if (!existsResult.value) return Result.ok(emptyHistory());
342
343
  const readResult = await Result.tryPromise({
343
344
  try: async () => await fs.readJson(historyPath),
@@ -351,7 +352,7 @@ async function readHistory() {
351
352
  }
352
353
  async function writeHistory(history) {
353
354
  const ensureDirResult = await ensureHistoryDir();
354
- if (ensureDirResult.isErr()) return ensureDirResult;
355
+ if (ensureDirResult.isErr()) return Result.err(ensureDirResult.error);
355
356
  return Result.tryPromise({
356
357
  try: async () => {
357
358
  await fs.writeJson(getHistoryPath(), history, { spaces: 2 });
@@ -364,7 +365,7 @@ async function writeHistory(history) {
364
365
  }
365
366
  async function addToHistory(config, reproducibleCommand) {
366
367
  const historyResult = await readHistory();
367
- if (historyResult.isErr()) return historyResult;
368
+ if (historyResult.isErr()) return Result.err(historyResult.error);
368
369
  const history = historyResult.value;
369
370
  const entry = {
370
371
  id: generateId(),
@@ -394,7 +395,7 @@ async function addToHistory(config, reproducibleCommand) {
394
395
  }
395
396
  async function getHistory(limit = 10) {
396
397
  const historyResult = await readHistory();
397
- if (historyResult.isErr()) return historyResult;
398
+ if (historyResult.isErr()) return Result.err(historyResult.error);
398
399
  return Result.ok(historyResult.value.entries.slice(0, limit));
399
400
  }
400
401
  async function clearHistory() {
@@ -757,7 +758,7 @@ function splitFrontends(values = []) {
757
758
  }
758
759
  function ensureSingleWebAndNative(frontends) {
759
760
  const { web, native } = splitFrontends(frontends);
760
- if (web.length > 1) return validationErr$1("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid");
761
+ if (web.length > 1) return validationErr$1("Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid, astro");
761
762
  if (native.length > 1) return validationErr$1("Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles");
762
763
  return Result.ok(void 0);
763
764
  }
@@ -765,18 +766,36 @@ const FULLSTACK_FRONTENDS$1 = [
765
766
  "next",
766
767
  "tanstack-start",
767
768
  "nuxt",
769
+ "svelte",
768
770
  "astro"
769
771
  ];
772
+ const EVLOG_SERVER_BACKENDS = [
773
+ "hono",
774
+ "express",
775
+ "fastify",
776
+ "elysia"
777
+ ];
778
+ const EVLOG_FULLSTACK_FRONTENDS = FULLSTACK_FRONTENDS$1;
779
+ const evlogCompatibilityMessage = "evlog addon supports Hono, Express, Fastify, Elysia, or backend self with Next.js, TanStack Start, Nuxt, SvelteKit, or Astro. Convex and backend none are not supported yet.";
780
+ function supportsEvlogAddon(frontend = [], backend, _runtime) {
781
+ if (!backend) return true;
782
+ if (EVLOG_SERVER_BACKENDS.includes(backend)) return true;
783
+ if (backend === "self") {
784
+ if (frontend.length === 0) return true;
785
+ return frontend.some((f) => EVLOG_FULLSTACK_FRONTENDS.includes(f));
786
+ }
787
+ return false;
788
+ }
770
789
  function validateSelfBackendCompatibility(providedFlags, options, config) {
771
790
  const backend = config.backend || options.backend;
772
791
  const frontends = config.frontend || options.frontend || [];
773
792
  if (backend === "self") {
774
793
  const { web, native } = splitFrontends(frontends);
775
- if (!(web.length === 1 && FULLSTACK_FRONTENDS$1.includes(web[0]))) return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, or --frontend astro. Support for SvelteKit will be added in a future update.");
794
+ if (!(web.length === 1 && FULLSTACK_FRONTENDS$1.includes(web[0]))) return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, SvelteKit, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend svelte, or --frontend astro.");
776
795
  if (native.length > 1) return validationErr$1("Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles");
777
796
  }
778
797
  const hasFullstackFrontend = frontends.some((f) => FULLSTACK_FRONTENDS$1.includes(f));
779
- if (providedFlags.has("backend") && !hasFullstackFrontend && backend === "self") return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend astro, or choose a different backend. Support for SvelteKit will be added in a future update.");
798
+ if (providedFlags.has("backend") && !hasFullstackFrontend && backend === "self") return validationErr$1("Backend 'self' (fullstack) currently only supports Next.js, TanStack Start, Nuxt, SvelteKit, and Astro frontends. Please use --frontend next, --frontend tanstack-start, --frontend nuxt, --frontend svelte, --frontend astro, or choose a different backend.");
780
799
  return Result.ok(void 0);
781
800
  }
782
801
  function validateWorkersCompatibility(providedFlags, options, config) {
@@ -849,7 +868,11 @@ function validateServerDeployRequiresBackend(serverDeploy, backend) {
849
868
  if (serverDeploy && serverDeploy !== "none" && (!backend || backend === "none")) return validationErr$1("'--server-deploy' requires a backend. Please select a backend or set '--server-deploy none'.");
850
869
  return Result.ok(void 0);
851
870
  }
852
- function validateAddonCompatibility(addon, frontend, _auth) {
871
+ function validateAddonCompatibility(addon, frontend, _auth, backend, runtime) {
872
+ if (addon === "evlog" && !supportsEvlogAddon(frontend, backend, runtime)) return {
873
+ isCompatible: false,
874
+ reason: evlogCompatibilityMessage
875
+ };
853
876
  const compatibleFrontends = ADDON_COMPATIBILITY[addon];
854
877
  if (compatibleFrontends.length > 0) {
855
878
  if (!frontend.some((f) => compatibleFrontends.includes(f))) return {
@@ -859,23 +882,26 @@ function validateAddonCompatibility(addon, frontend, _auth) {
859
882
  }
860
883
  return { isCompatible: true };
861
884
  }
862
- function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth) {
885
+ function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth, backend, runtime) {
863
886
  return allAddons.filter((addon) => {
864
887
  if (existingAddons.includes(addon)) return false;
865
888
  if (addon === "none") return false;
866
- const { isCompatible } = validateAddonCompatibility(addon, frontend, auth);
889
+ const { isCompatible } = validateAddonCompatibility(addon, frontend, auth, backend, runtime);
867
890
  return isCompatible;
868
891
  });
869
892
  }
870
- function validateAddonsAgainstFrontends(addons = [], frontends = [], auth) {
893
+ function validateAddonsAgainstFrontends(addons = [], frontends = [], auth, backend, runtime) {
871
894
  if (addons.includes("turborepo") && addons.includes("nx")) return validationErr$1("Cannot combine 'turborepo' and 'nx' addons. Choose one monorepo tool.");
872
895
  for (const addon of addons) {
873
896
  if (addon === "none") continue;
874
- const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth);
897
+ const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth, backend, runtime);
875
898
  if (!isCompatible) return validationErr$1(`Incompatible addon/frontend combination: ${reason}`);
876
899
  }
877
900
  return Result.ok(void 0);
878
901
  }
902
+ function validateAddonsAgainstConfig(addons = [], config) {
903
+ return validateAddonsAgainstFrontends(addons, config.frontend ?? [], config.auth, config.backend, config.runtime);
904
+ }
879
905
  function validatePaymentsCompatibility(payments, auth, _backend, frontends = []) {
880
906
  if (!payments || payments === "none") return Result.ok(void 0);
881
907
  if (payments === "polar") {
@@ -1207,6 +1233,10 @@ function getAddonDisplay(addon) {
1207
1233
  label = "MCP";
1208
1234
  hint = "Install MCP servers, including Better T Stack, via add-mcp";
1209
1235
  break;
1236
+ case "evlog":
1237
+ label = "evlog";
1238
+ hint = "Request logging with Better Auth context and AI SDK telemetry";
1239
+ break;
1210
1240
  default:
1211
1241
  label = addon;
1212
1242
  hint = `Add ${addon}`;
@@ -1233,6 +1263,7 @@ const ADDON_GROUPS = {
1233
1263
  "opentui",
1234
1264
  "wxt"
1235
1265
  ],
1266
+ Observability: ["evlog"],
1236
1267
  "AI & Agent Tools": ["skills", "mcp"]
1237
1268
  };
1238
1269
  function createGroupedOptions() {
@@ -1259,13 +1290,13 @@ function sortAndPruneGroupedOptions(groupedOptions) {
1259
1290
  function validateAddonSelection(selected) {
1260
1291
  if (selected?.includes("turborepo") && selected.includes("nx")) return "Choose either Turborepo or Nx as your monorepo tool, not both.";
1261
1292
  }
1262
- async function getAddonsChoice(addons, frontends, auth) {
1293
+ async function getAddonsChoice(addons, frontends, auth, backend, runtime) {
1263
1294
  if (addons !== void 0) return addons;
1264
1295
  const allAddons = types_exports.AddonsSchema.options.filter((addon) => addon !== "none");
1265
1296
  const groupedOptions = createGroupedOptions();
1266
1297
  const frontendsArray = frontends || [];
1267
1298
  for (const addon of allAddons) {
1268
- const { isCompatible } = validateAddonCompatibility(addon, frontendsArray, auth);
1299
+ const { isCompatible } = validateAddonCompatibility(addon, frontendsArray, auth, backend, runtime);
1269
1300
  if (!isCompatible) continue;
1270
1301
  const { label, hint } = getAddonDisplay(addon);
1271
1302
  addOptionToGroup(groupedOptions, {
@@ -1285,10 +1316,10 @@ async function getAddonsChoice(addons, frontends, auth) {
1285
1316
  if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1286
1317
  return response;
1287
1318
  }
1288
- async function getAddonsToAdd(frontend, existingAddons = [], auth) {
1319
+ async function getAddonsToAdd(config) {
1289
1320
  const groupedOptions = createGroupedOptions();
1290
- const frontendArray = frontend || [];
1291
- const compatibleAddons = getCompatibleAddons(types_exports.AddonsSchema.options.filter((addon) => addon !== "none"), frontendArray, existingAddons, auth);
1321
+ const frontendArray = config.frontend || [];
1322
+ const compatibleAddons = getCompatibleAddons(types_exports.AddonsSchema.options.filter((addon) => addon !== "none"), frontendArray, config.addons, config.auth, config.backend, config.runtime);
1292
1323
  for (const addon of compatibleAddons) {
1293
1324
  const { label, hint } = getAddonDisplay(addon);
1294
1325
  addOptionToGroup(groupedOptions, {
@@ -1381,6 +1412,541 @@ const addPackageDependency = async (opts) => {
1381
1412
  await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
1382
1413
  };
1383
1414
  //#endregion
1415
+ //#region src/helpers/addons/evlog-setup.ts
1416
+ const evlogBackends = [
1417
+ "hono",
1418
+ "express",
1419
+ "fastify",
1420
+ "elysia"
1421
+ ];
1422
+ const evlogWebFrontends = [
1423
+ "next",
1424
+ "nuxt",
1425
+ "svelte",
1426
+ "tanstack-start",
1427
+ "astro"
1428
+ ];
1429
+ function isEvlogBackend(backend) {
1430
+ return evlogBackends.includes(backend);
1431
+ }
1432
+ function getEvlogWebFrontend(frontends) {
1433
+ return frontends.find((frontend) => evlogWebFrontends.includes(frontend));
1434
+ }
1435
+ function shouldIdentifyWebAuth(config) {
1436
+ return config.auth === "better-auth" && config.backend === "self";
1437
+ }
1438
+ function prependMissingImports(content, imports) {
1439
+ const missingImports = imports.filter((line) => !content.includes(line));
1440
+ if (missingImports.length === 0) return content;
1441
+ const importBlock = `${missingImports.join("\n")}\n`;
1442
+ const referenceMatch = content.match(/^(?:\/\/\/ <reference[^\n]*>\n)+/);
1443
+ if (referenceMatch) return `${referenceMatch[0]}${importBlock}${content.slice(referenceMatch[0].length)}`;
1444
+ return `${importBlock}${content}`;
1445
+ }
1446
+ function addNamedImport(content, moduleName, names) {
1447
+ const importRegex = new RegExp(`import \\{([^}]+)\\} from "${moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}";`);
1448
+ const match = content.match(importRegex);
1449
+ if (!match) return prependMissingImports(content, [`import { ${names.join(", ")} } from "${moduleName}";`]);
1450
+ const nextNames = [...match[1].split(",").map((name) => name.trim()).filter(Boolean)];
1451
+ for (const name of names) if (!nextNames.includes(name)) nextNames.push(name);
1452
+ return content.replace(match[0], `import { ${nextNames.join(", ")} } from "${moduleName}";`);
1453
+ }
1454
+ function insertBeforeOnce(content, marker, snippet, alreadyPresent) {
1455
+ if (content.includes(alreadyPresent)) return content;
1456
+ if (!content.includes(marker)) return content;
1457
+ return content.replace(marker, `${snippet}${marker}`);
1458
+ }
1459
+ function insertAfterOnce(content, marker, snippet, alreadyPresent) {
1460
+ if (content.includes(alreadyPresent)) return content;
1461
+ if (!content.includes(marker)) return content;
1462
+ return content.replace(marker, `${marker}${snippet}`);
1463
+ }
1464
+ async function writeFileIfChanged(filePath, content) {
1465
+ if ((await fs.pathExists(filePath) ? await fs.readFile(filePath, "utf-8") : void 0) === content) return;
1466
+ await fs.ensureDir(path.dirname(filePath));
1467
+ await fs.writeFile(filePath, content);
1468
+ }
1469
+ async function updateFileIfExists(filePath, update) {
1470
+ if (!await fs.pathExists(filePath)) return;
1471
+ const content = await fs.readFile(filePath, "utf-8");
1472
+ const nextContent = update(content);
1473
+ if (nextContent !== content) await fs.writeFile(filePath, nextContent);
1474
+ }
1475
+ function usesCreateAuthFactory(config) {
1476
+ return config.runtime === "workers" || config.serverDeploy === "cloudflare" || config.backend === "self" && config.webDeploy === "cloudflare";
1477
+ }
1478
+ function getAuthImportLine(config) {
1479
+ return usesCreateAuthFactory(config) ? `import { createAuth } from "@${config.projectName}/auth";` : `import { auth } from "@${config.projectName}/auth";`;
1480
+ }
1481
+ function getAuthExpression(config) {
1482
+ return usesCreateAuthFactory(config) ? "createAuth()" : "auth";
1483
+ }
1484
+ function addAiSdkEvlogTelemetry(content, loggerExpression) {
1485
+ let nextContent = addNamedImport(content, "evlog/ai", ["createAILogger", "createEvlogIntegration"]);
1486
+ if (!nextContent.includes("const ai = createAILogger(")) nextContent = nextContent.replace(/^(\s*)const model = wrapLanguageModel\({/m, (_match, indent) => `${indent}const ai = createAILogger(${loggerExpression});\n${indent}const model = wrapLanguageModel({`);
1487
+ if (!nextContent.includes("model: ai.wrap(model)")) nextContent = nextContent.replace(/(const result = streamText\({\n\s*)model,/, "$1model: ai.wrap(model),");
1488
+ if (!nextContent.includes("createEvlogIntegration(ai)")) nextContent = nextContent.replace(/(messages:\s*await convertToModelMessages\([^)]+\),?)/, (match) => `${match.endsWith(",") ? match : `${match},`}\n\t\texperimental_telemetry: {\n\t\t\tisEnabled: true,\n\t\t\tintegrations: [createEvlogIntegration(ai)],\n\t\t},`);
1489
+ return nextContent;
1490
+ }
1491
+ function addEvlogBetterAuthServerSetup(content, backend, authExpression) {
1492
+ let nextContent = addNamedImport(content, "evlog/better-auth", ["createAuthMiddleware", "type BetterAuthInstance"]);
1493
+ const usesAuthFactory = authExpression.endsWith("()");
1494
+ const evlogAuthExpression = `${authExpression} as BetterAuthInstance`;
1495
+ const authOptions = "{ exclude: [\"/api/auth/**\"], maskEmail: true }";
1496
+ const identifySnippet = usesAuthFactory ? "" : `const identifyUser = createAuthMiddleware(${evlogAuthExpression}, ${authOptions});\n\n`;
1497
+ const identifyUserSetup = usesAuthFactory ? `\n\tconst identifyUser = createAuthMiddleware(${evlogAuthExpression}, ${authOptions});` : "";
1498
+ if (backend === "hono") {
1499
+ nextContent = insertBeforeOnce(nextContent, "const app = new Hono", identifySnippet, "createAuthMiddleware(");
1500
+ return insertAfterOnce(nextContent, "app.use(evlog());", `\napp.use("*", async (c, next) => {${identifyUserSetup}\n\tawait identifyUser(c.get("log"), c.req.raw.headers, c.req.path);\n\tawait next();\n});`, "identifyUser(c.get(\"log\")");
1501
+ }
1502
+ if (backend === "express") {
1503
+ nextContent = insertBeforeOnce(nextContent, "const app = express();", identifySnippet, "createAuthMiddleware(");
1504
+ return insertAfterOnce(nextContent, "app.use(evlog());", `\napp.use(async (req, _res, next) => {${identifyUserSetup}\n\tawait identifyUser(req.log, req.headers, req.path);\n\tnext();\n});`, "identifyUser(req.log");
1505
+ }
1506
+ if (backend === "fastify") {
1507
+ nextContent = addNamedImport(nextContent, "evlog/fastify", ["useLogger"]);
1508
+ nextContent = insertBeforeOnce(nextContent, "const fastify = Fastify", identifySnippet, "createAuthMiddleware(");
1509
+ return insertAfterOnce(nextContent, "fastify.register(evlog);", `\nfastify.addHook("preHandler", async (request) => {${identifyUserSetup}\n\tawait identifyUser(useLogger(), request.headers, request.url);\n});`, "identifyUser(useLogger()");
1510
+ }
1511
+ nextContent = insertBeforeOnce(nextContent, "new Elysia", identifySnippet, "createAuthMiddleware(");
1512
+ return insertAfterOnce(nextContent, ".use(evlog())", `\n\t.derive(async ({ request, log }) => {${identifyUserSetup.replace(/\n\t/g, "\n ")}\n\t\tawait identifyUser(log, request.headers, new URL(request.url).pathname);\n\t\treturn {};\n\t})`, "identifyUser(log");
1513
+ }
1514
+ function addEvlogServerSetup(content, backend, serviceName) {
1515
+ const initSnippet = `initLogger({\n\tenv: { service: "${serviceName}" },\n});\n\n`;
1516
+ if (backend === "hono") {
1517
+ let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog, type EvlogVariables } from \"evlog/hono\";"]);
1518
+ nextContent = insertBeforeOnce(nextContent, "const app = new Hono", initSnippet, "initLogger({");
1519
+ nextContent = nextContent.replace("const app = new Hono();", "const app = new Hono<EvlogVariables>();");
1520
+ nextContent = nextContent.replace("import { logger } from \"hono/logger\";\n", "").replace(/\napp\.use\(logger\(\)\);/, "");
1521
+ return insertAfterOnce(nextContent, "const app = new Hono<EvlogVariables>();", "\n\napp.use(evlog());", "app.use(evlog());");
1522
+ }
1523
+ if (backend === "express") {
1524
+ let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/express\";"]);
1525
+ nextContent = insertBeforeOnce(nextContent, "const app = express();", initSnippet, "initLogger({");
1526
+ return insertAfterOnce(nextContent, "const app = express();", "\n\napp.use(evlog());", "app.use(evlog());");
1527
+ }
1528
+ if (backend === "fastify") {
1529
+ let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/fastify\";"]);
1530
+ nextContent = insertBeforeOnce(nextContent, "const fastify = Fastify", initSnippet, "initLogger({");
1531
+ return insertBeforeOnce(nextContent, "fastify.register(fastifyCors", "fastify.register(evlog);\n", "fastify.register(evlog);");
1532
+ }
1533
+ let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/elysia\";"]);
1534
+ nextContent = insertBeforeOnce(nextContent, "new Elysia", initSnippet, "initLogger({");
1535
+ for (const marker of ["new Elysia({ adapter: node() })", "new Elysia()"]) nextContent = insertAfterOnce(nextContent, marker, "\n .use(evlog())", ".use(evlog())");
1536
+ return nextContent;
1537
+ }
1538
+ function addNuxtEvlogSetup(content, serviceName) {
1539
+ let nextContent = content;
1540
+ if (!nextContent.includes("\"evlog/nuxt\"") && !nextContent.includes("'evlog/nuxt'")) nextContent = nextContent.replace(/modules:\s*\[/, (match) => `${match}\n "evlog/nuxt",`);
1541
+ if (!nextContent.includes("evlog:")) nextContent = nextContent.replace(/\n\}\)\s*$/, (match) => {
1542
+ const contentBeforeConfigClose = nextContent.slice(0, -match.length);
1543
+ return `${!/[,{]\s*$/.test(contentBeforeConfigClose) ? "," : ""}\n evlog: {\n env: { service: "${serviceName}" },\n },\n})`;
1544
+ });
1545
+ return nextContent;
1546
+ }
1547
+ function addSvelteViteEvlogSetup(content, serviceName) {
1548
+ let nextContent = prependMissingImports(content, ["import evlog from \"evlog/vite\";"]);
1549
+ if (nextContent.includes("evlog({")) return nextContent;
1550
+ return nextContent.replace("plugins: [tailwindcss(), sveltekit()],", `plugins: [\n tailwindcss(),\n sveltekit(),\n evlog({ service: "${serviceName}" }),\n ],`);
1551
+ }
1552
+ function addSvelteHooksEvlogSetup(content) {
1553
+ let nextContent = prependMissingImports(content, ["import { createEvlogHooks } from \"evlog/sveltekit\";"]);
1554
+ if (!nextContent.includes("export const handle") && !nextContent.includes("const authHandle")) {
1555
+ if (!nextContent.includes("createEvlogHooks()")) nextContent = `${nextContent.trimEnd()}\n\nexport const { handle, handleError } = createEvlogHooks();\n`;
1556
+ return nextContent;
1557
+ }
1558
+ nextContent = prependMissingImports(nextContent, ["import { sequence } from \"@sveltejs/kit/hooks\";"]);
1559
+ if (!nextContent.includes("const { handle: evlogHandle, handleError }")) nextContent = nextContent.replace(/((?:import .+\n)+)/, `$1\nconst { handle: evlogHandle, handleError } = createEvlogHooks();\n\n`);
1560
+ nextContent = nextContent.replace(/export const handle(:\s*Handle)?\s*=\s*async/, (_match, typeAnnotation) => `const authHandle${typeAnnotation ?? ""} = async`);
1561
+ if (!nextContent.includes("sequence(evlogHandle, authHandle)")) nextContent = `${nextContent.trimEnd()}\n\nexport const handle = sequence(evlogHandle as Handle, authHandle);\nexport { handleError };\n`;
1562
+ return nextContent;
1563
+ }
1564
+ function addSvelteLocalsType(content) {
1565
+ let nextContent = prependMissingImports(content, ["import type { RequestLogger } from \"evlog\";"]);
1566
+ if (nextContent.includes("log: RequestLogger")) return nextContent;
1567
+ if (nextContent.includes("// interface Locals {}")) return nextContent.replace("// interface Locals {}", "interface Locals {\n log: RequestLogger;\n }");
1568
+ return nextContent.replace("namespace App {", "namespace App {\n interface Locals {\n log: RequestLogger;\n }\n");
1569
+ }
1570
+ function addTanstackStartRootEvlogSetup(content) {
1571
+ let nextContent = prependMissingImports(content, ["import { createMiddleware } from \"@tanstack/react-start\";", "import { evlogErrorHandler } from \"evlog/nitro/v3\";"]);
1572
+ const middlewareEntry = "createMiddleware().server(evlogErrorHandler)";
1573
+ if (nextContent.includes(`middleware: [${middlewareEntry}]`)) return nextContent;
1574
+ if (nextContent.includes("middleware: [")) return nextContent.replace("middleware: [", `middleware: [${middlewareEntry}, `);
1575
+ if (/server:\s*{/.test(nextContent)) return nextContent.replace(/server:\s*{\n/, `server: {\n middleware: [${middlewareEntry}],\n`);
1576
+ return nextContent.replace("head: () => ({", `server: {\n middleware: [${middlewareEntry}],\n },\n\n head: () => ({`);
1577
+ }
1578
+ function addAstroMiddlewareEvlogSetup(content, serviceName) {
1579
+ let nextContent = prependMissingImports(content, ["import { createRequestLogger, initLogger } from \"evlog\";"]);
1580
+ const initSnippet = `initLogger({\n env: { service: "${serviceName}" },\n});\n\n`;
1581
+ nextContent = insertBeforeOnce(nextContent, "export const onRequest", initSnippet, "initLogger({");
1582
+ if (nextContent.includes("createRequestLogger({")) return nextContent;
1583
+ const contextMarker = "export const onRequest = defineMiddleware(async (context, next) => {";
1584
+ if (nextContent.includes(contextMarker)) {
1585
+ nextContent = insertAfterOnce(nextContent, contextMarker, `\n const url = new URL(context.request.url);\n const log = createRequestLogger({\n method: context.request.method,\n path: url.pathname,\n });\n\n context.locals.log = log;\n`, "const log = createRequestLogger({");
1586
+ return nextContent.replace("return next();", "const response = await next();\n log.emit();\n return response;");
1587
+ }
1588
+ const localsMarker = "export const onRequest = defineMiddleware(async ({ request, locals }, next) => {";
1589
+ if (nextContent.includes(localsMarker)) {
1590
+ nextContent = insertAfterOnce(nextContent, localsMarker, `\n const url = new URL(request.url);\n const log = createRequestLogger({\n method: request.method,\n path: url.pathname,\n });\n\n locals.log = log;\n`, "const log = createRequestLogger({");
1591
+ return nextContent.replace("return next();", "const response = await next();\n log.emit();\n return response;");
1592
+ }
1593
+ return nextContent;
1594
+ }
1595
+ function addAstroLocalsType(content) {
1596
+ let nextContent = prependMissingImports(content, ["import type { RequestLogger } from \"evlog\";"]);
1597
+ if (nextContent.includes("log: RequestLogger")) return nextContent;
1598
+ if (nextContent.includes("interface Locals {")) return nextContent.replace("interface Locals {", "interface Locals {\n log: RequestLogger;");
1599
+ if (nextContent.includes("declare namespace App {")) return nextContent.replace("declare namespace App {", "declare namespace App {\n interface Locals {\n log: RequestLogger;\n }\n");
1600
+ return `${nextContent.trimEnd()}\n\ndeclare namespace App {\n interface Locals {\n log: RequestLogger;\n }\n}\n`;
1601
+ }
1602
+ function addNextRouteWrappers(content) {
1603
+ let nextContent = prependMissingImports(content, ["import { withEvlog } from \"@/lib/evlog\";"]);
1604
+ if (nextContent.includes("withEvlog(handler)") || nextContent.includes("withEvlog(handleRequest)")) return nextContent;
1605
+ nextContent = nextContent.replace("export { handler as GET, handler as POST };", "export const GET = withEvlog(handler);\nexport const POST = withEvlog(handler);");
1606
+ for (const method of [
1607
+ "GET",
1608
+ "POST",
1609
+ "PUT",
1610
+ "PATCH",
1611
+ "DELETE"
1612
+ ]) nextContent = nextContent.replace(`export const ${method} = handleRequest;`, `export const ${method} = withEvlog(handleRequest);`);
1613
+ return nextContent;
1614
+ }
1615
+ function addNextAiEvlogSetup(content) {
1616
+ let nextContent = addNamedImport(content, "@/lib/evlog", ["withEvlog"]);
1617
+ if (!nextContent.includes("withEvlog(async (req: Request)")) {
1618
+ nextContent = nextContent.replace("export async function POST(req: Request) {", "export const POST = withEvlog(async (req: Request) => {");
1619
+ if (nextContent.includes("export const POST = withEvlog(async (req: Request) => {")) nextContent = nextContent.replace(/\n}\s*$/, "\n});\n");
1620
+ }
1621
+ return nextContent;
1622
+ }
1623
+ function addNuxtAiEvlogSetup(content) {
1624
+ return addAiSdkEvlogTelemetry(content, "useLogger(event)");
1625
+ }
1626
+ function addSvelteAiEvlogSetup(content) {
1627
+ return addAiSdkEvlogTelemetry(content.replace("export const POST: RequestHandler = async ({ request }) => {", "export const POST: RequestHandler = async ({ request, locals }) => {"), "locals.log");
1628
+ }
1629
+ function addTanstackStartAiEvlogSetup(content) {
1630
+ return addAiSdkEvlogTelemetry(prependMissingImports(content, ["import type { RequestLogger } from \"evlog\";", "import { useRequest } from \"nitro/context\";"]), "useRequest().context.log as RequestLogger");
1631
+ }
1632
+ function addBackendAiEvlogSetup(content, backend) {
1633
+ if (backend === "hono") return addAiSdkEvlogTelemetry(content, "c.get(\"log\")");
1634
+ if (backend === "express") return addAiSdkEvlogTelemetry(content, "req.log");
1635
+ if (backend === "fastify") return addAiSdkEvlogTelemetry(addNamedImport(content, "evlog/fastify", ["useLogger"]), "useLogger()");
1636
+ return addAiSdkEvlogTelemetry(content, "context.log");
1637
+ }
1638
+ function addNextBetterAuthToRoute(content) {
1639
+ let nextContent = addNamedImport(content, "@/lib/evlog-auth", ["identifyEvlogUser"]);
1640
+ nextContent = nextContent.replace("function handler(req:", "async function handler(req:");
1641
+ for (const marker of [
1642
+ "async function handler(req: NextRequest) {",
1643
+ "async function handleRequest(req: NextRequest) {",
1644
+ "export const POST = withEvlog(async (req: Request) => {"
1645
+ ]) nextContent = insertAfterOnce(nextContent, marker, "\n await identifyEvlogUser(req);", "identifyEvlogUser(req)");
1646
+ return nextContent;
1647
+ }
1648
+ function addSvelteBetterAuthEvlogSetup(content, config) {
1649
+ if (!content.includes("authHandle") || content.includes("evlogAuthHandle")) return content;
1650
+ let nextContent = addNamedImport(content, "evlog/better-auth", ["createAuthMiddleware", "type BetterAuthInstance"]);
1651
+ if (!nextContent.includes(`@${config.projectName}/auth`)) nextContent = prependMissingImports(nextContent, [getAuthImportLine(config)]);
1652
+ if (usesCreateAuthFactory(config) && config.webDeploy === "cloudflare" && !nextContent.includes(`@${config.projectName}/env/server`)) nextContent = prependMissingImports(nextContent, [`import { env as localEnv } from "@${config.projectName}/env/server";`]);
1653
+ const authExpression = getAuthExpression(config);
1654
+ const authOptions = "{ exclude: [\"/api/auth/**\"], maskEmail: true }";
1655
+ const authHandleSnippet = usesCreateAuthFactory(config) && config.webDeploy === "cloudflare" ? `const evlogAuthHandle: Handle = async ({ event, resolve }) => {\n\tif (building) {\n\t\treturn resolve(event);\n\t}\n\n\tconst authEnv = event.platform?.env ?? localEnv;\n\tconst identifyUser = createAuthMiddleware(createAuth(authEnv) as BetterAuthInstance, ${authOptions});\n\tawait identifyUser(event.locals.log, event.request.headers, event.url.pathname);\n\treturn resolve(event);\n};\n\n` : `const identifyUser = createAuthMiddleware(${authExpression} as BetterAuthInstance, ${authOptions});\n\nconst evlogAuthHandle: Handle = async ({ event, resolve }) => {\n\tawait identifyUser(event.locals.log, event.request.headers, event.url.pathname);\n\treturn resolve(event);\n};\n\n`;
1656
+ nextContent = insertAfterOnce(nextContent, "const { handle: evlogHandle, handleError } = createEvlogHooks();\n\n", authHandleSnippet, "evlogAuthHandle");
1657
+ return nextContent.replace("sequence(evlogHandle as Handle, authHandle)", "sequence(evlogHandle as Handle, evlogAuthHandle, authHandle)").replace("sequence(evlogHandle, authHandle)", "sequence(evlogHandle as Handle, evlogAuthHandle, authHandle)");
1658
+ }
1659
+ function addAstroBetterAuthEvlogSetup(content, config) {
1660
+ if (content.includes("createAuthMiddleware(")) return content;
1661
+ let nextContent = addNamedImport(content, "evlog/better-auth", ["createAuthMiddleware", "type BetterAuthInstance"]);
1662
+ if (!nextContent.includes(`@${config.projectName}/auth`)) nextContent = prependMissingImports(nextContent, [getAuthImportLine(config)]);
1663
+ const authExpression = getAuthExpression(config);
1664
+ const authOptions = "{ exclude: [\"/api/auth/**\"], maskEmail: true }";
1665
+ const usesFactory = usesCreateAuthFactory(config);
1666
+ if (!usesFactory) nextContent = insertBeforeOnce(nextContent, "export const onRequest", `const identifyUser = createAuthMiddleware(${authExpression} as BetterAuthInstance, ${authOptions});\n\n`, "const identifyUser = createAuthMiddleware(");
1667
+ for (const marker of ["context.locals.log = log;", "locals.log = log;"]) {
1668
+ if (!nextContent.includes(marker)) continue;
1669
+ const requestExpression = marker.startsWith("context") ? "context.request" : "request";
1670
+ const identifySnippet = usesFactory ? `\n\n const identifyUser = createAuthMiddleware(${authExpression} as BetterAuthInstance, ${authOptions});\n await identifyUser(log, ${requestExpression}.headers, url.pathname);` : `\n\n await identifyUser(log, ${requestExpression}.headers, url.pathname);`;
1671
+ return insertAfterOnce(nextContent, marker, identifySnippet, "identifyUser(log");
1672
+ }
1673
+ return nextContent;
1674
+ }
1675
+ function getNextEvlogFile(serviceName) {
1676
+ return `import { createEvlog } from "evlog/next";
1677
+ import { createInstrumentation } from "evlog/next/instrumentation";
1678
+
1679
+ export const { withEvlog, useLogger, log, createError } = createEvlog({
1680
+ service: "${serviceName}",
1681
+ });
1682
+
1683
+ export const { register, onRequestError } = createInstrumentation({
1684
+ service: "${serviceName}",
1685
+ });
1686
+ `;
1687
+ }
1688
+ function getNextInstrumentationFile() {
1689
+ return `import { defineNodeInstrumentation } from "evlog/next/instrumentation";
1690
+
1691
+ export const { register, onRequestError } = defineNodeInstrumentation(() => import("./src/lib/evlog"));
1692
+ `;
1693
+ }
1694
+ function getNextProxyFile() {
1695
+ return `import { evlogMiddleware } from "evlog/next";
1696
+
1697
+ export const proxy = evlogMiddleware();
1698
+
1699
+ export const config = {
1700
+ matcher: ["/api/:path*"],
1701
+ };
1702
+ `;
1703
+ }
1704
+ function getNextEvlogAuthFile(config) {
1705
+ if (usesCreateAuthFactory(config)) return `${getAuthImportLine(config)}
1706
+ import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
1707
+ import { useLogger } from "@/lib/evlog";
1708
+
1709
+ export async function identifyEvlogUser(request: Request) {
1710
+ const identifyUser = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
1711
+ exclude: ["/api/auth/**"],
1712
+ maskEmail: true,
1713
+ });
1714
+ await identifyUser(useLogger(), request.headers, new URL(request.url).pathname);
1715
+ }
1716
+ `;
1717
+ return `${getAuthImportLine(config)}
1718
+ import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
1719
+ import { useLogger } from "@/lib/evlog";
1720
+
1721
+ const identifyUser = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
1722
+ exclude: ["/api/auth/**"],
1723
+ maskEmail: true,
1724
+ });
1725
+
1726
+ export async function identifyEvlogUser(request: Request) {
1727
+ await identifyUser(useLogger(), request.headers, new URL(request.url).pathname);
1728
+ }
1729
+ `;
1730
+ }
1731
+ function getNitroEvlogAuthPluginFile(config) {
1732
+ if (usesCreateAuthFactory(config)) return `${getAuthImportLine(config)}
1733
+ import { createAuthIdentifier, type BetterAuthInstance } from "evlog/better-auth";
1734
+
1735
+ export default defineNitroPlugin((nitroApp) => {
1736
+ nitroApp.hooks.hook("request", async (event) => {
1737
+ const identify = createAuthIdentifier(${getAuthExpression(config)} as BetterAuthInstance, {
1738
+ exclude: ["/api/auth/**"],
1739
+ maskEmail: true,
1740
+ });
1741
+ await identify(event);
1742
+ });
1743
+ });
1744
+ `;
1745
+ return `${getAuthImportLine(config)}
1746
+ import { createAuthIdentifier, type BetterAuthInstance } from "evlog/better-auth";
1747
+
1748
+ export default defineNitroPlugin((nitroApp) => {
1749
+ nitroApp.hooks.hook(
1750
+ "request",
1751
+ createAuthIdentifier(${getAuthExpression(config)} as BetterAuthInstance, {
1752
+ exclude: ["/api/auth/**"],
1753
+ maskEmail: true,
1754
+ }),
1755
+ );
1756
+ });
1757
+ `;
1758
+ }
1759
+ function getNuxtEvlogAuthMiddlewareFile(config) {
1760
+ if (usesCreateAuthFactory(config)) return `${getAuthImportLine(config)}
1761
+ import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
1762
+
1763
+ export default defineEventHandler(async (event) => {
1764
+ if (!event.context.log) return;
1765
+ const identify = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
1766
+ exclude: ["/api/auth/**"],
1767
+ maskEmail: true,
1768
+ });
1769
+ await identify(event.context.log, event.headers, event.path);
1770
+ });
1771
+ `;
1772
+ return `${getAuthImportLine(config)}
1773
+ import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
1774
+
1775
+ const identify = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
1776
+ exclude: ["/api/auth/**"],
1777
+ maskEmail: true,
1778
+ });
1779
+
1780
+ export default defineEventHandler(async (event) => {
1781
+ if (!event.context.log) return;
1782
+ await identify(event.context.log, event.headers, event.path);
1783
+ });
1784
+ `;
1785
+ }
1786
+ function getTanstackNitroConfigFile(serviceName) {
1787
+ return `import { defineConfig } from "nitro";
1788
+ import evlog from "evlog/nitro/v3";
1789
+
1790
+ export default defineConfig({
1791
+ experimental: {
1792
+ asyncContext: true,
1793
+ },
1794
+ modules: [
1795
+ evlog({
1796
+ env: { service: "${serviceName}" },
1797
+ }),
1798
+ ],
1799
+ });
1800
+ `;
1801
+ }
1802
+ function getAstroMiddlewareFile(serviceName) {
1803
+ return `import { defineMiddleware } from "astro:middleware";
1804
+ import { createRequestLogger, initLogger } from "evlog";
1805
+
1806
+ initLogger({
1807
+ env: { service: "${serviceName}" },
1808
+ });
1809
+
1810
+ export const onRequest = defineMiddleware(async ({ request, locals }, next) => {
1811
+ const url = new URL(request.url);
1812
+ const log = createRequestLogger({
1813
+ method: request.method,
1814
+ path: url.pathname,
1815
+ });
1816
+
1817
+ locals.log = log;
1818
+
1819
+ try {
1820
+ const response = await next();
1821
+ log.emit();
1822
+ return response;
1823
+ } catch (error) {
1824
+ log.error(error instanceof Error ? error : new Error(String(error)));
1825
+ log.emit();
1826
+ throw error;
1827
+ }
1828
+ });
1829
+ `;
1830
+ }
1831
+ function getAstroEnvFile() {
1832
+ return `/// <reference types="astro/client" />
1833
+
1834
+ import type { RequestLogger } from "evlog";
1835
+
1836
+ declare namespace App {
1837
+ interface Locals {
1838
+ log: RequestLogger;
1839
+ }
1840
+ }
1841
+ `;
1842
+ }
1843
+ async function setupNextEvlog(config, serviceName) {
1844
+ const webDir = path.join(config.projectDir, "apps/web");
1845
+ const evlogPath = path.join(webDir, "src/lib/evlog.ts");
1846
+ if (!await fs.pathExists(evlogPath)) await writeFileIfChanged(evlogPath, getNextEvlogFile(serviceName));
1847
+ const identifyWebAuth = shouldIdentifyWebAuth(config);
1848
+ if (identifyWebAuth) {
1849
+ const evlogAuthPath = path.join(webDir, "src/lib/evlog-auth.ts");
1850
+ if (!await fs.pathExists(evlogAuthPath)) await writeFileIfChanged(evlogAuthPath, getNextEvlogAuthFile(config));
1851
+ }
1852
+ const instrumentationPath = path.join(webDir, "instrumentation.ts");
1853
+ if (!await fs.pathExists(instrumentationPath)) await writeFileIfChanged(instrumentationPath, getNextInstrumentationFile());
1854
+ const proxyPath = path.join(webDir, "src/proxy.ts");
1855
+ const rootProxyPath = path.join(webDir, "proxy.ts");
1856
+ if (!await fs.pathExists(proxyPath) && !await fs.pathExists(rootProxyPath)) await writeFileIfChanged(proxyPath, getNextProxyFile());
1857
+ const updateNextApiRoute = (content) => {
1858
+ let nextContent = addNextRouteWrappers(content);
1859
+ if (identifyWebAuth) nextContent = addNextBetterAuthToRoute(nextContent);
1860
+ return nextContent;
1861
+ };
1862
+ await updateFileIfExists(path.join(webDir, "src/app/api/trpc/[trpc]/route.ts"), updateNextApiRoute);
1863
+ await updateFileIfExists(path.join(webDir, "src/app/api/rpc/[[...rest]]/route.ts"), updateNextApiRoute);
1864
+ if (config.examples.includes("ai")) await updateFileIfExists(path.join(webDir, "src/app/api/ai/route.ts"), (content) => {
1865
+ let nextContent = addNextAiEvlogSetup(content);
1866
+ if (identifyWebAuth) nextContent = addNextBetterAuthToRoute(nextContent);
1867
+ return nextContent;
1868
+ });
1869
+ }
1870
+ async function setupNuxtEvlog(config, serviceName) {
1871
+ const webDir = path.join(config.projectDir, "apps/web");
1872
+ await updateFileIfExists(path.join(webDir, "nuxt.config.ts"), (content) => addNuxtEvlogSetup(content, serviceName));
1873
+ if (shouldIdentifyWebAuth(config)) {
1874
+ const oldAuthPluginPath = path.join(webDir, "server/plugins/evlog-auth.ts");
1875
+ if (await fs.pathExists(oldAuthPluginPath)) {
1876
+ if ((await fs.readFile(oldAuthPluginPath, "utf-8")).includes("evlog/better-auth")) await fs.remove(oldAuthPluginPath);
1877
+ }
1878
+ const authMiddlewarePath = path.join(webDir, "server/middleware/evlog-auth.ts");
1879
+ if (!await fs.pathExists(authMiddlewarePath)) await writeFileIfChanged(authMiddlewarePath, getNuxtEvlogAuthMiddlewareFile(config));
1880
+ }
1881
+ if (config.examples.includes("ai")) await updateFileIfExists(path.join(webDir, "server/api/ai.post.ts"), addNuxtAiEvlogSetup);
1882
+ }
1883
+ async function setupSvelteEvlog(config, serviceName) {
1884
+ const webDir = path.join(config.projectDir, "apps/web");
1885
+ await updateFileIfExists(path.join(webDir, "vite.config.ts"), (content) => addSvelteViteEvlogSetup(content, serviceName));
1886
+ const hooksPath = path.join(webDir, "src/hooks.server.ts");
1887
+ if (await fs.pathExists(hooksPath)) await updateFileIfExists(hooksPath, addSvelteHooksEvlogSetup);
1888
+ else await writeFileIfChanged(hooksPath, `import { createEvlogHooks } from "evlog/sveltekit";
1889
+
1890
+ export const { handle, handleError } = createEvlogHooks();
1891
+ `);
1892
+ await updateFileIfExists(path.join(webDir, "src/app.d.ts"), addSvelteLocalsType);
1893
+ if (shouldIdentifyWebAuth(config)) await updateFileIfExists(path.join(webDir, "src/hooks.server.ts"), (content) => addSvelteBetterAuthEvlogSetup(content, config));
1894
+ if (config.examples.includes("ai")) await updateFileIfExists(path.join(webDir, "src/routes/api/ai/+server.ts"), addSvelteAiEvlogSetup);
1895
+ }
1896
+ async function setupTanstackStartEvlog(config, serviceName) {
1897
+ const webDir = path.join(config.projectDir, "apps/web");
1898
+ const nitroConfigPath = path.join(webDir, "nitro.config.ts");
1899
+ if (!await fs.pathExists(nitroConfigPath)) await writeFileIfChanged(nitroConfigPath, getTanstackNitroConfigFile(serviceName));
1900
+ await updateFileIfExists(path.join(webDir, "src/routes/__root.tsx"), addTanstackStartRootEvlogSetup);
1901
+ if (shouldIdentifyWebAuth(config)) {
1902
+ const authPluginPath = path.join(webDir, "server/plugins/evlog-auth.ts");
1903
+ if (!await fs.pathExists(authPluginPath)) await writeFileIfChanged(authPluginPath, getNitroEvlogAuthPluginFile(config));
1904
+ }
1905
+ if (config.examples.includes("ai")) await updateFileIfExists(path.join(webDir, "src/routes/api/ai/$.ts"), addTanstackStartAiEvlogSetup);
1906
+ }
1907
+ async function setupAstroEvlog(config, serviceName) {
1908
+ const webDir = path.join(config.projectDir, "apps/web");
1909
+ const middlewarePath = path.join(webDir, "src/middleware.ts");
1910
+ if (!await fs.pathExists(middlewarePath)) await writeFileIfChanged(middlewarePath, getAstroMiddlewareFile(serviceName));
1911
+ else await updateFileIfExists(middlewarePath, (content) => addAstroMiddlewareEvlogSetup(content, serviceName));
1912
+ const envPath = path.join(webDir, "src/env.d.ts");
1913
+ if (!await fs.pathExists(envPath)) await writeFileIfChanged(envPath, getAstroEnvFile());
1914
+ else await updateFileIfExists(envPath, addAstroLocalsType);
1915
+ if (shouldIdentifyWebAuth(config)) await updateFileIfExists(middlewarePath, (content) => addAstroBetterAuthEvlogSetup(content, config));
1916
+ }
1917
+ async function setupEvlogWeb(config) {
1918
+ const frontend = getEvlogWebFrontend(config.frontend);
1919
+ if (!frontend) return;
1920
+ const serviceName = `${config.projectName}-web`;
1921
+ if (frontend === "next") await setupNextEvlog(config, serviceName);
1922
+ else if (frontend === "nuxt") await setupNuxtEvlog(config, serviceName);
1923
+ else if (frontend === "svelte") await setupSvelteEvlog(config, serviceName);
1924
+ else if (frontend === "tanstack-start") await setupTanstackStartEvlog(config, serviceName);
1925
+ else if (frontend === "astro") await setupAstroEvlog(config, serviceName);
1926
+ }
1927
+ async function setupEvlog(config) {
1928
+ return Result.tryPromise({
1929
+ try: async () => {
1930
+ if (isEvlogBackend(config.backend)) {
1931
+ const serverIndexPath = path.join(config.projectDir, "apps/server/src/index.ts");
1932
+ if (await fs.pathExists(serverIndexPath)) {
1933
+ const content = await fs.readFile(serverIndexPath, "utf-8");
1934
+ let nextContent = addEvlogServerSetup(content, config.backend, `${config.projectName}-server`);
1935
+ if (config.auth === "better-auth") nextContent = addEvlogBetterAuthServerSetup(nextContent, config.backend, getAuthExpression(config));
1936
+ if (config.examples.includes("ai")) nextContent = addBackendAiEvlogSetup(nextContent, config.backend);
1937
+ if (nextContent !== content) await fs.writeFile(serverIndexPath, nextContent);
1938
+ }
1939
+ }
1940
+ await setupEvlogWeb(config);
1941
+ },
1942
+ catch: (error) => new AddonSetupError({
1943
+ addon: "evlog",
1944
+ message: `Failed to set up evlog: ${error instanceof Error ? error.message : String(error)}`,
1945
+ cause: error
1946
+ })
1947
+ });
1948
+ }
1949
+ //#endregion
1384
1950
  //#region src/prompts/navigable-group.ts
1385
1951
  /**
1386
1952
  * Navigable group - a group of prompts that allows going back
@@ -2149,7 +2715,8 @@ const SKILL_SOURCES = {
2149
2715
  "elysiajs/skills": { label: "ElysiaJS" },
2150
2716
  "waynesutton/convexskills": { label: "Convex" },
2151
2717
  "msmps/opentui-skill": { label: "OpenTUI Platform" },
2152
- "haydenbleasel/ultracite": { label: "Ultracite" }
2718
+ "haydenbleasel/ultracite": { label: "Ultracite" },
2719
+ "https://www.evlog.dev": { label: "evlog" }
2153
2720
  };
2154
2721
  const AVAILABLE_AGENTS = [
2155
2722
  {
@@ -2289,6 +2856,7 @@ function getRecommendedSourceKeys(config) {
2289
2856
  if (backend === "convex") sources.push("waynesutton/convexskills");
2290
2857
  if (addons.includes("opentui")) sources.push("msmps/opentui-skill");
2291
2858
  if (addons.includes("ultracite")) sources.push("haydenbleasel/ultracite");
2859
+ if (addons.includes("evlog")) sources.push("https://www.evlog.dev");
2292
2860
  return sources;
2293
2861
  }
2294
2862
  const CURATED_SKILLS_BY_SOURCE = {
@@ -2335,7 +2903,6 @@ const CURATED_SKILLS_BY_SOURCE = {
2335
2903
  "building-native-ui",
2336
2904
  "native-data-fetching",
2337
2905
  "expo-deployment",
2338
- "upgrading-expo",
2339
2906
  "expo-cicd-workflows"
2340
2907
  ];
2341
2908
  if (config.frontend.includes("native-uniwind")) skills.push("expo-tailwind-setup");
@@ -2360,7 +2927,8 @@ const CURATED_SKILLS_BY_SOURCE = {
2360
2927
  "convex-security-check"
2361
2928
  ],
2362
2929
  "msmps/opentui-skill": () => ["opentui"],
2363
- "haydenbleasel/ultracite": () => ["ultracite"]
2930
+ "haydenbleasel/ultracite": () => ["ultracite"],
2931
+ "https://www.evlog.dev": () => ["review-logging-patterns", "analyze-logs"]
2364
2932
  };
2365
2933
  function getCuratedSkillNamesForSourceKey(sourceKey, config) {
2366
2934
  return CURATED_SKILLS_BY_SOURCE[sourceKey](config);
@@ -3083,6 +3651,7 @@ async function setupAddons(config) {
3083
3651
  if (addons.includes("wxt")) await runSetup(() => setupWxt(config));
3084
3652
  if (addons.includes("skills")) await runSetup(() => setupSkills(config));
3085
3653
  if (addons.includes("mcp")) await runSetup(() => setupMcp(config));
3654
+ if (addons.includes("evlog")) await runSetup(() => setupEvlog(config));
3086
3655
  }
3087
3656
  async function setupBiome(projectDir) {
3088
3657
  await addPackageDependency({
@@ -3246,7 +3815,7 @@ async function addHandlerInternal(input) {
3246
3815
  } else if (isSilent()) return Result.err(new CLIError({ message: "Addons are required in silent mode. Provide them via add() or add-json." }));
3247
3816
  else {
3248
3817
  const promptResult = await Result.tryPromise({
3249
- try: () => getAddonsToAdd(existingConfig.frontend, existingConfig.addons, existingConfig.auth),
3818
+ try: () => getAddonsToAdd(existingConfig),
3250
3819
  catch: (e) => {
3251
3820
  if (UserCancelledError.is(e)) return e;
3252
3821
  return new CLIError({
@@ -3270,6 +3839,8 @@ async function addHandlerInternal(input) {
3270
3839
  }
3271
3840
  addonsToAdd = selectedAddons;
3272
3841
  }
3842
+ const addonsValidationResult = validateAddonsAgainstConfig(addonsToAdd, existingConfig);
3843
+ if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
3273
3844
  if (!isSilent()) log.info(pc.cyan(`Adding addons: ${addonsToAdd.join(", ")}`));
3274
3845
  const updatedAddons = [...existingConfig.addons, ...addonsToAdd];
3275
3846
  const mergedAddonOptions = mergeAddonOptions(existingConfig.addonOptions, input.addonOptions);
@@ -3297,7 +3868,12 @@ async function addHandlerInternal(input) {
3297
3868
  };
3298
3869
  if (!isSilent()) log.info(pc.dim("Installing addon files..."));
3299
3870
  const vfs = new VirtualFileSystem();
3300
- for (const pkgPath of ["package.json", "apps/web/package.json"]) {
3871
+ for (const pkgPath of [
3872
+ "package.json",
3873
+ "apps/web/package.json",
3874
+ "apps/server/package.json",
3875
+ "apps/native/package.json"
3876
+ ]) {
3301
3877
  const fullPath = path.join(projectDir, pkgPath);
3302
3878
  if (await fs.pathExists(fullPath)) {
3303
3879
  const content = await fs.readFile(fullPath, "utf-8");
@@ -3452,6 +4028,7 @@ const FULLSTACK_FRONTENDS = [
3452
4028
  "next",
3453
4029
  "tanstack-start",
3454
4030
  "nuxt",
4031
+ "svelte",
3455
4032
  "astro"
3456
4033
  ];
3457
4034
  async function getBackendFrameworkChoice(backendFramework, frontends) {
@@ -3988,7 +4565,7 @@ function validateFullConfig(config, providedFlags, options) {
3988
4565
  if (config.runtime === "workers" && config.serverDeploy === "none") yield* validationErr("Cloudflare Workers runtime requires a server deployment. Please choose 'cloudflare' for --server-deploy.");
3989
4566
  if (providedFlags.has("serverDeploy") && config.serverDeploy === "cloudflare" && config.runtime !== "workers") yield* validationErr(`Server deployment '${config.serverDeploy}' requires '--runtime workers'. Please use '--runtime workers' or choose a different server deployment.`);
3990
4567
  if (config.addons && config.addons.length > 0) {
3991
- yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth);
4568
+ yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
3992
4569
  config.addons = [...new Set(config.addons)];
3993
4570
  }
3994
4571
  yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
@@ -4002,7 +4579,7 @@ function validateConfigForProgrammaticUse(config) {
4002
4579
  if (config.frontend && config.frontend.length > 0) yield* ensureSingleWebAndNative(config.frontend);
4003
4580
  yield* validateApiFrontendCompatibility(config.api, config.frontend);
4004
4581
  yield* validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend);
4005
- if (config.addons && config.addons.length > 0) yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth);
4582
+ if (config.addons && config.addons.length > 0) yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
4006
4583
  yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
4007
4584
  return Result.ok(void 0);
4008
4585
  });
@@ -4197,7 +4774,7 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
4197
4774
  api: ({ results }) => getApiChoice(flags.api, results.frontend, results.backend),
4198
4775
  auth: ({ results }) => getAuthChoice(flags.auth, results.backend, results.frontend),
4199
4776
  payments: ({ results }) => getPaymentsChoice(flags.payments, results.auth, results.backend, results.frontend),
4200
- addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend, results.auth),
4777
+ addons: ({ results }) => getAddonsChoice(flags.addons, results.frontend, results.auth, results.backend, results.runtime),
4201
4778
  examples: ({ results }) => getExamplesChoice(flags.examples, results.database, results.frontend, results.backend, results.api),
4202
4779
  dbSetup: ({ results }) => getDBSetupChoice(results.database ?? "none", flags.dbSetup, results.orm, results.backend, results.runtime),
4203
4780
  webDeploy: ({ results }) => getDeploymentChoice(flags.webDeploy, results.runtime, results.backend, results.frontend, results.dbSetup),
@@ -4291,24 +4868,14 @@ async function getProjectName(initialName) {
4291
4868
  */
4292
4869
  function isTelemetryEnabled() {
4293
4870
  const BTS_TELEMETRY_DISABLED = process.env.BTS_TELEMETRY_DISABLED;
4294
- const BTS_TELEMETRY = "1";
4871
+ const BTS_TELEMETRY = "0";
4295
4872
  if (BTS_TELEMETRY_DISABLED !== void 0) return BTS_TELEMETRY_DISABLED !== "1";
4296
4873
  if (BTS_TELEMETRY !== void 0) return BTS_TELEMETRY === "1";
4297
4874
  return true;
4298
4875
  }
4299
4876
  //#endregion
4300
4877
  //#region src/utils/analytics.ts
4301
- const CONVEX_INGEST_URL = "https://striped-seahorse-863.convex.site/api/analytics/ingest";
4302
- async function sendConvexEvent(payload) {
4303
- await Result.tryPromise({
4304
- try: () => fetch(CONVEX_INGEST_URL, {
4305
- method: "POST",
4306
- headers: { "Content-Type": "application/json" },
4307
- body: JSON.stringify(payload)
4308
- }),
4309
- catch: () => void 0
4310
- });
4311
- }
4878
+ async function sendConvexEvent(payload) {}
4312
4879
  async function trackProjectCreation(config, disableAnalytics = false) {
4313
4880
  if (!isTelemetryEnabled() || disableAnalytics) return;
4314
4881
  const { projectName: _projectName, projectDir: _projectDir, relativePath: _relativePath, ...safeConfig } = config;
@@ -4873,7 +5440,7 @@ async function initMongoDBAtlas(serverDir) {
4873
5440
  cause: e
4874
5441
  })
4875
5442
  });
4876
- if (deployResult.isErr()) return deployResult;
5443
+ if (deployResult.isErr()) return Result.err(deployResult.error);
4877
5444
  const connectionString = await text({
4878
5445
  message: "Enter your MongoDB connection string:",
4879
5446
  placeholder: "mongodb+srv://username:password@cluster.mongodb.net/database",
@@ -4984,7 +5551,7 @@ async function setupMongoDBAtlas(config, cliInput) {
4984
5551
  cliLog.success(pc.green("MongoDB Atlas setup complete! Connection saved to .env file."));
4985
5552
  return Result.ok(void 0);
4986
5553
  }
4987
- if (UserCancelledError.is(mongoConfigResult.error)) return mongoConfigResult;
5554
+ if (UserCancelledError.is(mongoConfigResult.error)) return Result.err(mongoConfigResult.error);
4988
5555
  cliLog.warn(pc.yellow("Falling back to local MongoDB configuration"));
4989
5556
  const envResult = await writeEnvFile$3(projectDir, backend);
4990
5557
  if (envResult.isErr()) return envResult;
@@ -5049,7 +5616,7 @@ async function executeNeonCommand(packageManager, commandArgsString, spinnerText
5049
5616
  }
5050
5617
  async function createNeonProject(projectName, regionId, packageManager) {
5051
5618
  const execResult = await executeNeonCommand(packageManager, `neonctl@latest projects create --name ${projectName} --region-id ${regionId} --output json`, `Creating Neon project "${projectName}"...`);
5052
- if (execResult.isErr()) return execResult;
5619
+ if (execResult.isErr()) return Result.err(execResult.error);
5053
5620
  const parseResult = Result.try({
5054
5621
  try: () => JSON.parse(execResult.value.stdout),
5055
5622
  catch: (e) => new DatabaseSetupError({
@@ -5058,7 +5625,7 @@ async function createNeonProject(projectName, regionId, packageManager) {
5058
5625
  cause: e
5059
5626
  })
5060
5627
  });
5061
- if (parseResult.isErr()) return parseResult;
5628
+ if (parseResult.isErr()) return Result.err(parseResult.error);
5062
5629
  const response = parseResult.value;
5063
5630
  if (response.project && response.connection_uris && response.connection_uris.length > 0) {
5064
5631
  const projectId = response.project.id;
@@ -5140,6 +5707,12 @@ async function setupNeonPostgres(config, cliInput) {
5140
5707
  dbSetupOptions: cliInput?.dbSetupOptions ?? config.dbSetupOptions
5141
5708
  });
5142
5709
  const target = backend === "self" ? "apps/web" : "apps/server";
5710
+ if (shouldSkipExternalCommands()) {
5711
+ const envResult = await writeEnvFile$2(projectDir, backend);
5712
+ if (envResult.isErr()) return envResult;
5713
+ displayManualSetupInstructions$2(target);
5714
+ return Result.ok(void 0);
5715
+ }
5143
5716
  if (setupMode === "manual") {
5144
5717
  const envResult = await writeEnvFile$2(projectDir, backend);
5145
5718
  if (envResult.isErr()) return envResult;
@@ -5383,7 +5956,7 @@ async function setupWithCreateDb(serverDir, packageManager, regionId) {
5383
5956
  });
5384
5957
  }
5385
5958
  });
5386
- if (execResult.isErr()) return execResult;
5959
+ if (execResult.isErr()) return Result.err(execResult.error);
5387
5960
  const parseResult = Result.try({
5388
5961
  try: () => JSON.parse(execResult.value),
5389
5962
  catch: (e) => new DatabaseSetupError({
@@ -5392,7 +5965,7 @@ async function setupWithCreateDb(serverDir, packageManager, regionId) {
5392
5965
  cause: e
5393
5966
  })
5394
5967
  });
5395
- if (parseResult.isErr()) return parseResult;
5968
+ if (parseResult.isErr()) return Result.err(parseResult.error);
5396
5969
  const createDbResponse = parseResult.value;
5397
5970
  return Result.ok({
5398
5971
  databaseUrl: createDbResponse.connectionString,
@@ -5483,7 +6056,7 @@ async function setupPrismaPostgres(config, cliInput) {
5483
6056
  }
5484
6057
  const prismaConfigResult = await setupWithCreateDb(dbDir, packageManager, cliInput?.dbSetupOptions?.prismaPostgres?.regionId ?? config.dbSetupOptions?.prismaPostgres?.regionId);
5485
6058
  if (prismaConfigResult.isErr()) {
5486
- if (UserCancelledError.is(prismaConfigResult.error)) return prismaConfigResult;
6059
+ if (UserCancelledError.is(prismaConfigResult.error)) return Result.err(prismaConfigResult.error);
5487
6060
  cliLog.error(pc.red(prismaConfigResult.error.message));
5488
6061
  const envResult = await writeEnvFile$1(projectDir, backend);
5489
6062
  if (envResult.isErr()) return envResult;
@@ -5783,7 +6356,7 @@ async function createTursoDatabase(dbName, groupName) {
5783
6356
  });
5784
6357
  }
5785
6358
  });
5786
- if (createResult.isErr()) return createResult;
6359
+ if (createResult.isErr()) return Result.err(createResult.error);
5787
6360
  s.start("Retrieving database connection details...");
5788
6361
  return Result.tryPromise({
5789
6362
  try: async () => {
@@ -5935,7 +6508,7 @@ async function setupTurso(config, cliInput) {
5935
6508
  if (!selectedGroup) if (isSilent()) selectedGroup = (await getTursoGroups())[0]?.name ?? null;
5936
6509
  else {
5937
6510
  const groupResult = await selectTursoGroup();
5938
- if (groupResult.isErr()) return groupResult;
6511
+ if (groupResult.isErr()) return Result.err(groupResult.error);
5939
6512
  selectedGroup = groupResult.value;
5940
6513
  }
5941
6514
  let suggestedName = cliInput?.dbSetupOptions?.turso?.databaseName ?? config.dbSetupOptions?.turso?.databaseName ?? path.basename(projectDir);
@@ -6177,7 +6750,10 @@ async function displayPostInstallInstructions(config) {
6177
6750
  output += `${pc.cyan("•")} Backend API: http://localhost:3000\n`;
6178
6751
  if (api === "orpc") output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:3000/api-reference\n`;
6179
6752
  }
6180
- if (isBackendSelf && api === "orpc") output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:${webPort}/api/rpc/api-reference\n`;
6753
+ if (isBackendSelf && api === "orpc") {
6754
+ const rpcPath = frontend?.includes("next") || frontend?.includes("tanstack-start") ? "/api/rpc" : "/rpc";
6755
+ output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:${webPort}${rpcPath}/api-reference\n`;
6756
+ }
6181
6757
  if (addons?.includes("starlight")) output += `${pc.cyan("•")} Docs: http://localhost:4321\n`;
6182
6758
  if (addons?.includes("fumadocs")) output += `${pc.cyan("•")} Fumadocs: http://localhost:4000\n`;
6183
6759
  }
@@ -6205,7 +6781,8 @@ async function displayPostInstallInstructions(config) {
6205
6781
  }
6206
6782
  function getNativeInstructions(isConvex, isBackendSelf, frontend, runCmd) {
6207
6783
  const envVar = isConvex ? "EXPO_PUBLIC_CONVEX_URL" : "EXPO_PUBLIC_SERVER_URL";
6208
- const exampleUrl = isConvex ? "https://<YOUR_CONVEX_URL>" : isBackendSelf ? "http://<YOUR_LOCAL_IP>:3001" : "http://<YOUR_LOCAL_IP>:3000";
6784
+ const selfBackendPort = frontend.includes("svelte") ? "5173" : frontend.includes("astro") ? "4321" : "3001";
6785
+ const exampleUrl = isConvex ? "https://<YOUR_CONVEX_URL>" : isBackendSelf ? `http://<YOUR_LOCAL_IP>:${selfBackendPort}` : "http://<YOUR_LOCAL_IP>:3000";
6209
6786
  const envFileName = ".env";
6210
6787
  const ipNote = isConvex ? "your Convex deployment URL (find after running 'dev:setup')" : "your local IP address";
6211
6788
  let instructions = `${pc.yellow("NOTE:")} For Expo connectivity issues, update\n apps/native/${envFileName} with ${ipNote}:\n ${`${envVar}=${exampleUrl}`}\n`;
@@ -6352,8 +6929,8 @@ function getPolarInstructions(backend) {
6352
6929
  function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend) {
6353
6930
  const instructions = [];
6354
6931
  const isBackendSelf = backend === "self";
6355
- if (webDeploy === "cloudflare" && serverDeploy !== "cloudflare") instructions.push(`${pc.bold("Deploy web with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`cd apps/web && ${runCmd} alchemy dev`}\n${pc.cyan("•")} Deploy: ${`cd apps/web && ${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`cd apps/web && ${runCmd} destroy`}`);
6356
- else if (serverDeploy === "cloudflare" && webDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy server with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`cd apps/server && ${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`cd apps/server && ${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`cd apps/server && ${runCmd} destroy`}`);
6932
+ if (webDeploy === "cloudflare" && serverDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy web with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
6933
+ else if (serverDeploy === "cloudflare" && webDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy server with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
6357
6934
  else if (webDeploy === "cloudflare" && (serverDeploy === "cloudflare" || isBackendSelf)) instructions.push(`${pc.bold("Deploy with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
6358
6935
  return instructions.length ? `\n${instructions.join("\n")}` : "";
6359
6936
  }
@@ -6633,6 +7210,10 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
6633
7210
  ...config,
6634
7211
  dbSetupOptions: effectiveDbSetupOptions
6635
7212
  };
7213
+ if (!input.yolo) {
7214
+ const addonsValidationResult = validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
7215
+ if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
7216
+ }
6636
7217
  const reproducibleCommand = generateReproducibleCommand(config);
6637
7218
  if (input.dryRun) {
6638
7219
  const elapsedTimeMs = Date.now() - startTime;
@@ -6779,6 +7360,7 @@ const SchemaNameSchema = z.enum([
6779
7360
  "addInput",
6780
7361
  "projectConfig",
6781
7362
  "betterTStackConfig",
7363
+ "betterTStackConfigFile",
6782
7364
  "initResult"
6783
7365
  ]).default("all");
6784
7366
  const t = initTRPC.meta().create();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-better-t-stack",
3
- "version": "3.27.5",
3
+ "version": "3.28.1-pr1036.ff20c00",
4
4
  "description": "A modern CLI tool for scaffolding end-to-end type-safe TypeScript projects with best practices and customizable configurations",
5
5
  "keywords": [
6
6
  "better-auth",
@@ -70,13 +70,13 @@
70
70
  "prepublishOnly": "npm run build"
71
71
  },
72
72
  "dependencies": {
73
- "@better-t-stack/template-generator": "^3.27.5",
74
- "@better-t-stack/types": "^3.27.5",
75
- "@clack/core": "^1.2.0",
76
- "@clack/prompts": "^1.2.0",
73
+ "@better-t-stack/template-generator": "3.28.1-pr1036.ff20c00",
74
+ "@better-t-stack/types": "3.28.1-pr1036.ff20c00",
75
+ "@clack/core": "^1.3.0",
76
+ "@clack/prompts": "^1.3.0",
77
77
  "@modelcontextprotocol/sdk": "1.29.0",
78
- "@trpc/server": "^11.16.0",
79
- "better-result": "^2.8.2",
78
+ "@trpc/server": "^11.17.0",
79
+ "better-result": "^2.9.1",
80
80
  "consola": "^3.4.2",
81
81
  "env-paths": "^4.0.0",
82
82
  "execa": "^9.6.1",
@@ -84,20 +84,20 @@
84
84
  "gradient-string": "^3.0.0",
85
85
  "handlebars": "^4.7.9",
86
86
  "jsonc-parser": "^3.3.1",
87
- "oxfmt": "^0.46.0",
87
+ "oxfmt": "^0.47.0",
88
88
  "picocolors": "^1.1.1",
89
89
  "tinyglobby": "^0.2.15",
90
90
  "trpc-cli": "^0.14.0",
91
91
  "ts-morph": "^28.0.0",
92
- "yaml": "^2.8.3",
93
- "zod": "^4.3.6"
92
+ "yaml": "^2.8.4",
93
+ "zod": "^4.4.2"
94
94
  },
95
95
  "devDependencies": {
96
- "@types/bun": "^1.3.12",
96
+ "@types/bun": "^1.3.13",
97
97
  "@types/fs-extra": "^11.0.4",
98
98
  "@types/node": "^25.6.0",
99
99
  "publint": "^0.3.18",
100
- "tsdown": "^0.21.9",
100
+ "tsdown": "^0.21.10",
101
101
  "typescript": "^6.0.3"
102
102
  }
103
103
  }