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 +17 -17
- package/dist/cli.mjs +3 -3
- package/dist/index.d.mts +8 -7
- package/dist/index.mjs +1 -1
- package/dist/{src-VGvTc2ik.mjs → src-Bx8Ofji-.mjs} +632 -50
- package/package.json +12 -12
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-
|
|
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:
|
|
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-
|
|
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.
|
|
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.
|
|
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(
|
|
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,
|
|
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
|
|
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 [
|
|
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 = "
|
|
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
|
-
|
|
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")
|
|
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
|
|
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: ${
|
|
6356
|
-
else if (serverDeploy === "cloudflare" && webDeploy !== "cloudflare" && !isBackendSelf) instructions.push(`${pc.bold("Deploy server with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${
|
|
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.
|
|
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": "
|
|
74
|
-
"@better-t-stack/types": "
|
|
75
|
-
"@clack/core": "^1.
|
|
76
|
-
"@clack/prompts": "^1.
|
|
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.
|
|
79
|
-
"better-result": "^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.
|
|
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.
|
|
93
|
-
"zod": "^4.
|
|
92
|
+
"yaml": "^2.8.4",
|
|
93
|
+
"zod": "^4.4.2"
|
|
94
94
|
},
|
|
95
95
|
"devDependencies": {
|
|
96
|
-
"@types/bun": "^1.3.
|
|
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.
|
|
100
|
+
"tsdown": "^0.21.10",
|
|
101
101
|
"typescript": "^6.0.3"
|
|
102
102
|
}
|
|
103
103
|
}
|