diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ba5b073 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,72 @@ +# AGENTS.md — BetterBase Repository Operating Guide + +## Mission Context +This repository contains planning artifacts and the early implementation scaffold for **BetterBase**: +- An AI-native backend platform inspired by Supabase. +- Built with a **TypeScript-first** developer experience. +- Runtime and tooling emphasis: **Bun**, Turborepo, Drizzle, BetterAuth, Hono. + +## Assistant Identity / Model Context +- If asked which model is running, reply with the runtime-configured model identifier when available; otherwise provide a neutral capability-focused response. + +## Current Strategic Inputs +Primary planning docs to align with before implementation: +- `betterbase_blueprint.md` +- `betterbase_reuse_strategy.md` + +## Persistent Project Prompt (Apply to Every New Task) +Use and preserve the following project prompt context for future BetterBase implementation tasks: + +PROJECT: BetterBase - AI-Native Backend Framework +STACK: Bun + TypeScript + Hono + Drizzle ORM + SQLite (local) / Postgres (production) + +PHILOSOPHY: +- AI-first: Generate `.betterbase-context.json` for AI agents to read +- Docker-less: Use `bun:sqlite` for <100ms startup +- Zero lock-in: Users own their schemas +- Type-safe: Strict TypeScript, Zod validation everywhere + +BASE APP STRUCTURE: +- `/src/db` (`schema.ts`, `index.ts`) +- `/src/routes` (API endpoints) +- `/src/middleware` (auth, validation) +- `/src/lib` (utilities) +- `betterbase.config.ts` +- `drizzle.config.ts` + +## Engineering Defaults +1. **Runtime:** Bun (prefer Bun commands and Bun workspaces). +2. **Language:** TypeScript in strict mode. +3. **Monorepo:** Turborepo with `apps/*` and `packages/*`. +4. **Core stack direction:** + - API/server templates: Hono + - ORM/migrations: Drizzle + - Auth direction: BetterAuth +5. **Approach:** Reuse Better-T-Stack patterns where strategic, build BetterBase-differentiating features from scratch. + +## Repository Structure Guidance +When scaffolding implementation, use: +- `betterbase/packages/cli` → canonical `bb` CLI implementation +- `betterbase/apps/cli` → legacy wrapper/stub (delegates to package CLI) +- `betterbase/apps/dashboard` → dashboard app +- `betterbase/packages/core` → backend/core engine +- `betterbase/packages/client` → `@betterbase/client` +- `betterbase/packages/shared` → shared utilities/types +- `betterbase/templates/base` → base starter template +- `betterbase/templates/auth` → auth starter template + +## Workflow Rules for Agents +1. Read this file and planning docs before major code generation. +2. Keep changes incremental and commit in logical units. +3. Prefer small, composable files and clear package boundaries. +4. Avoid introducing lock-in assumptions that conflict with BetterBase goals. +5. When uncertain, bias toward the blueprint and reuse strategy docs. +6. Ensure new templates follow the persistent project prompt above. + +## Quality & Validation +- Run lightweight checks whenever possible (format/lint/typecheck when available). +- Keep generated scaffolding runnable with Bun commands. + +## Documentation Expectations +- Update docs when structure or commands change. +- Keep command examples Bun-first. diff --git a/betterbase/.gitignore b/betterbase/.gitignore new file mode 100644 index 0000000..51c8bb1 --- /dev/null +++ b/betterbase/.gitignore @@ -0,0 +1,25 @@ +node_modules +.bun +.turbo +dist +.next + +.vscode/ +.idea/ + +.env +.env.* +.env.local +.env.test +!.env.example + +*.log +npm-debug.log +yarn-error.log +pnpm-debug.log + +coverage/ +.cache/ +.parcel-cache/ + +.DS_Store diff --git a/betterbase/README.md b/betterbase/README.md new file mode 100644 index 0000000..46bf456 --- /dev/null +++ b/betterbase/README.md @@ -0,0 +1,29 @@ +# BetterBase Monorepo + +Initial BetterBase monorepo scaffold with a concrete base template. + +## Structure + +- `apps/cli` — legacy CLI wrapper/stub +- `apps/dashboard` — dashboard/studio app +- `packages/cli` — canonical `@betterbase/cli` implementation +- `packages/core` — core backend engine +- `packages/client` — SDK (`@betterbase/client`) +- `packages/shared` — shared utilities/types +- `templates/base` — Bun + TypeScript + Hono + Drizzle starter template +- `templates/auth` — auth template placeholder + +## Tooling Direction + +- Runtime/package manager: **Bun** +- Workspace orchestration: **Turborepo** +- Language: **TypeScript** + +## Base Template Commands + +From `templates/base`: + +- `bun run dev` +- `bun run db:generate` +- `bun run db:push` +- `bun run typecheck` diff --git a/betterbase/apps/cli/README.md b/betterbase/apps/cli/README.md new file mode 100644 index 0000000..0d18f7a --- /dev/null +++ b/betterbase/apps/cli/README.md @@ -0,0 +1,6 @@ +# @betterbase/cli-legacy + +This package is a legacy wrapper placeholder. + +- Canonical CLI implementation: `betterbase/packages/cli` (`@betterbase/cli`) +- This package exists only for backwards compatibility and forwards execution. diff --git a/betterbase/apps/cli/package.json b/betterbase/apps/cli/package.json new file mode 100644 index 0000000..191344d --- /dev/null +++ b/betterbase/apps/cli/package.json @@ -0,0 +1,20 @@ +{ + "name": "@betterbase/cli-legacy", + "version": "0.0.0", + "private": true, + "type": "module", + "bin": { + "bb-legacy": "./dist/index.js" + }, + "scripts": { + "build": "bun build ./src/index.ts --outfile ./dist/index.js --target bun", + "dev": "bun run src/index.ts", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@betterbase/cli": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.9.3" + } +} diff --git a/betterbase/apps/cli/src/index.ts b/betterbase/apps/cli/src/index.ts new file mode 100644 index 0000000..5a4ec6e --- /dev/null +++ b/betterbase/apps/cli/src/index.ts @@ -0,0 +1,21 @@ +#!/usr/bin/env bun + +/** + * Legacy bb wrapper entrypoint. + * + * Forwards execution to the canonical CLI implementation in packages/cli. + */ +export async function runLegacyCli(): Promise { + const { runCli } = await import('@betterbase/cli'); + await runCli(process.argv); +} + +if (import.meta.main) { + (async () => { + await runLegacyCli(); + })().catch((error) => { + const message = error instanceof Error ? error.message : String(error); + console.error(message); + process.exitCode = 1; + }); +} diff --git a/betterbase/apps/cli/tsconfig.json b/betterbase/apps/cli/tsconfig.json new file mode 100644 index 0000000..4031161 --- /dev/null +++ b/betterbase/apps/cli/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": ".", + "types": ["bun"] + }, + "include": ["src", "test"] +} diff --git a/betterbase/apps/dashboard/README.md b/betterbase/apps/dashboard/README.md new file mode 100644 index 0000000..49b4e5c --- /dev/null +++ b/betterbase/apps/dashboard/README.md @@ -0,0 +1,16 @@ +# Dashboard App (Scaffold) + +Initial dashboard app scaffold for BetterBase. + +## Included + +- `src/index.ts` placeholder entry +- `package.json` with Bun scripts +- `tsconfig.json` strict TypeScript setup + +## Planned Features (Optional) + +- [ ] Table browser and editor +- [ ] API explorer / request runner +- [ ] Auth and session management views +- [ ] Project settings and environment controls diff --git a/betterbase/apps/dashboard/package.json b/betterbase/apps/dashboard/package.json new file mode 100644 index 0000000..735c886 --- /dev/null +++ b/betterbase/apps/dashboard/package.json @@ -0,0 +1,11 @@ +{ + "name": "@betterbase/dashboard", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "echo 'dashboard: no build step yet'", + "dev": "bun run src/index.ts", + "typecheck": "tsc -p tsconfig.json --noEmit" + } +} diff --git a/betterbase/apps/dashboard/src/index.ts b/betterbase/apps/dashboard/src/index.ts new file mode 100644 index 0000000..5e3adbc --- /dev/null +++ b/betterbase/apps/dashboard/src/index.ts @@ -0,0 +1 @@ +console.log('BetterBase Dashboard scaffold initialized.'); diff --git a/betterbase/apps/dashboard/tsconfig.json b/betterbase/apps/dashboard/tsconfig.json new file mode 100644 index 0000000..473c6ec --- /dev/null +++ b/betterbase/apps/dashboard/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["bun"], + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/betterbase/bun.lock b/betterbase/bun.lock new file mode 100644 index 0000000..33b7c9d --- /dev/null +++ b/betterbase/bun.lock @@ -0,0 +1,195 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "betterbase", + "devDependencies": { + "turbo": "^2.0.0", + "typescript": "^5.6.0", + }, + }, + "apps/cli": { + "name": "@betterbase/cli-legacy", + "version": "0.0.0", + "bin": { + "bb-legacy": "./dist/index.js", + }, + "dependencies": { + "@betterbase/cli": "workspace:*", + }, + "devDependencies": { + "typescript": "^5.9.3", + }, + }, + "apps/dashboard": { + "name": "@betterbase/dashboard", + "version": "0.1.0", + }, + "packages/cli": { + "name": "@betterbase/cli", + "version": "0.1.0", + "bin": { + "bb": "./dist/index.js", + }, + "dependencies": { + "chalk": "^5.3.0", + "commander": "^12.1.0", + "inquirer": "^10.2.2", + "zod": "^3.23.8", + }, + "devDependencies": { + "@types/bun": "^1.3.9", + "typescript": "^5.9.3", + }, + }, + "packages/client": { + "name": "@betterbase/client", + "version": "0.1.0", + "dependencies": { + "@betterbase/shared": "workspace:*", + }, + }, + "packages/core": { + "name": "@betterbase/core", + "version": "0.1.0", + "dependencies": { + "@betterbase/shared": "workspace:*", + }, + }, + "packages/shared": { + "name": "@betterbase/shared", + "version": "0.1.0", + }, + }, + "packages": { + "@betterbase/cli": ["@betterbase/cli@workspace:packages/cli"], + + "@betterbase/cli-legacy": ["@betterbase/cli-legacy@workspace:apps/cli"], + + "@betterbase/client": ["@betterbase/client@workspace:packages/client"], + + "@betterbase/core": ["@betterbase/core@workspace:packages/core"], + + "@betterbase/dashboard": ["@betterbase/dashboard@workspace:apps/dashboard"], + + "@betterbase/shared": ["@betterbase/shared@workspace:packages/shared"], + + "@inquirer/checkbox": ["@inquirer/checkbox@2.5.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/figures": "^1.0.5", "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" } }, "sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA=="], + + "@inquirer/confirm": ["@inquirer/confirm@3.2.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/type": "^1.5.3" } }, "sha512-oOIwPs0Dvq5220Z8lGL/6LHRTEr9TgLHmiI99Rj1PJ1p1czTys+olrgBqZk4E2qC0YTzeHprxSQmoHioVdJ7Lw=="], + + "@inquirer/core": ["@inquirer/core@9.2.1", "", { "dependencies": { "@inquirer/figures": "^1.0.6", "@inquirer/type": "^2.0.0", "@types/mute-stream": "^0.0.4", "@types/node": "^22.5.5", "@types/wrap-ansi": "^3.0.0", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^1.0.0", "signal-exit": "^4.1.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" } }, "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg=="], + + "@inquirer/editor": ["@inquirer/editor@2.2.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/type": "^1.5.3", "external-editor": "^3.1.0" } }, "sha512-9KHOpJ+dIL5SZli8lJ6xdaYLPPzB8xB9GZItg39MBybzhxA16vxmszmQFrRwbOA918WA2rvu8xhDEg/p6LXKbw=="], + + "@inquirer/expand": ["@inquirer/expand@2.3.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/type": "^1.5.3", "yoctocolors-cjs": "^2.1.2" } }, "sha512-qnJsUcOGCSG1e5DTOErmv2BPQqrtT6uzqn1vI/aYGiPKq+FgslGZmtdnXbhuI7IlT7OByDoEEqdnhUnVR2hhLw=="], + + "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], + + "@inquirer/input": ["@inquirer/input@2.3.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/type": "^1.5.3" } }, "sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw=="], + + "@inquirer/number": ["@inquirer/number@1.1.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/type": "^1.5.3" } }, "sha512-ilUnia/GZUtfSZy3YEErXLJ2Sljo/mf9fiKc08n18DdwdmDbOzRcTv65H1jjDvlsAuvdFXf4Sa/aL7iw/NanVA=="], + + "@inquirer/password": ["@inquirer/password@2.2.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2" } }, "sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg=="], + + "@inquirer/prompts": ["@inquirer/prompts@5.5.0", "", { "dependencies": { "@inquirer/checkbox": "^2.5.0", "@inquirer/confirm": "^3.2.0", "@inquirer/editor": "^2.2.0", "@inquirer/expand": "^2.3.0", "@inquirer/input": "^2.3.0", "@inquirer/number": "^1.1.0", "@inquirer/password": "^2.2.0", "@inquirer/rawlist": "^2.3.0", "@inquirer/search": "^1.1.0", "@inquirer/select": "^2.5.0" } }, "sha512-BHDeL0catgHdcHbSFFUddNzvx/imzJMft+tWDPwTm3hfu8/tApk1HrooNngB2Mb4qY+KaRWF+iZqoVUPeslEog=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@2.3.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/type": "^1.5.3", "yoctocolors-cjs": "^2.1.2" } }, "sha512-zzfNuINhFF7OLAtGHfhwOW2TlYJyli7lOUoJUXw/uyklcwalV6WRXBXtFIicN8rTRK1XTiPWB4UY+YuW8dsnLQ=="], + + "@inquirer/search": ["@inquirer/search@1.1.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/figures": "^1.0.5", "@inquirer/type": "^1.5.3", "yoctocolors-cjs": "^2.1.2" } }, "sha512-h+/5LSj51dx7hp5xOn4QFnUaKeARwUCLs6mIhtkJ0JYPBLmEYjdHSYh7I6GrLg9LwpJ3xeX0FZgAG1q0QdCpVQ=="], + + "@inquirer/select": ["@inquirer/select@2.5.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/figures": "^1.0.5", "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" } }, "sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA=="], + + "@inquirer/type": ["@inquirer/type@1.5.5", "", { "dependencies": { "mute-stream": "^1.0.0" } }, "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA=="], + + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + + "@types/mute-stream": ["@types/mute-stream@0.0.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow=="], + + "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], + + "@types/wrap-ansi": ["@types/wrap-ansi@3.0.0", "", {}, "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g=="], + + "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], + + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "inquirer": ["inquirer@10.2.2", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/prompts": "^5.5.0", "@inquirer/type": "^1.5.3", "@types/mute-stream": "^0.0.4", "ansi-escapes": "^4.3.2", "mute-stream": "^1.0.0", "run-async": "^3.0.0", "rxjs": "^7.8.1" } }, "sha512-tyao/4Vo36XnUItZ7DnUXX4f1jVao2mSrleV/5IPtW/XAEA26hRVsbc68nuTEKWcr5vMP/1mVoT2O7u8H4v1Vg=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="], + + "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], + + "run-async": ["run-async@3.0.0", "", {}, "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q=="], + + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "turbo": ["turbo@2.8.9", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.9", "turbo-darwin-arm64": "2.8.9", "turbo-linux-64": "2.8.9", "turbo-linux-arm64": "2.8.9", "turbo-windows-64": "2.8.9", "turbo-windows-arm64": "2.8.9" }, "bin": { "turbo": "bin/turbo" } }, "sha512-G+Mq8VVQAlpz/0HTsxiNNk/xywaHGl+dk1oiBREgOEVCCDjXInDlONWUn5srRnC9s5tdHTFD1bx1N19eR4hI+g=="], + + "turbo-darwin-64": ["turbo-darwin-64@2.8.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-KnCw1ZI9KTnEAhdI9avZrnZ/z4wsM++flMA1w8s8PKOqi5daGpFV36qoPafg4S8TmYMe52JPWEoFr0L+lQ5JIw=="], + + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CbD5Y2NKJKBXTOZ7z7Cc7vGlFPZkYjApA7ri9lH4iFwKV1X7MoZswh9gyRLetXYWImVX1BqIvP8KftulJg/wIA=="], + + "turbo-linux-64": ["turbo-linux-64@2.8.9", "", { "os": "linux", "cpu": "x64" }, "sha512-OXC9HdCtsHvyH+5KUoH8ds+p5WU13vdif0OPbsFzZca4cUXMwKA3HWwUuCgQetk0iAE4cscXpi/t8A263n3VTg=="], + + "turbo-linux-arm64": ["turbo-linux-arm64@2.8.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-yI5n8jNXiFA6+CxnXG0gO7h5ZF1+19K8uO3/kXPQmyl37AdiA7ehKJQOvf9OPAnmkGDHcF2HSCPltabERNRmug=="], + + "turbo-windows-64": ["turbo-windows-64@2.8.9", "", { "os": "win32", "cpu": "x64" }, "sha512-/OztzeGftJAg258M/9vK2ZCkUKUzqrWXJIikiD2pm8TlqHcIYUmepDbyZSDfOiUjMy6NzrLFahpNLnY7b5vNgg=="], + + "turbo-windows-arm64": ["turbo-windows-arm64@2.8.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-xZ2VTwVTjIqpFZKN4UBxDHCPM3oJ2J5cpRzCBSmRpJ/Pn33wpiYjs+9FB2E03svKaD04/lSSLlEUej0UYsugfg=="], + + "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@inquirer/core/@inquirer/type": ["@inquirer/type@2.0.0", "", { "dependencies": { "mute-stream": "^1.0.0" } }, "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag=="], + + "@inquirer/core/@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="], + + "@inquirer/core/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + } +} diff --git a/betterbase/package.json b/betterbase/package.json new file mode 100644 index 0000000..eaa5a17 --- /dev/null +++ b/betterbase/package.json @@ -0,0 +1,19 @@ +{ + "name": "betterbase", + "private": true, + "packageManager": "bun@1.1.38", + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "build": "turbo run build", + "dev": "turbo run dev --parallel", + "lint": "turbo run lint", + "typecheck": "turbo run typecheck" + }, + "devDependencies": { + "turbo": "^2.0.0", + "typescript": "^5.6.0" + } +} diff --git a/betterbase/packages/cli/bun.lockb b/betterbase/packages/cli/bun.lockb new file mode 100644 index 0000000..34db38c --- /dev/null +++ b/betterbase/packages/cli/bun.lockb @@ -0,0 +1,155 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "betterbase", + "devDependencies": { + "turbo": "^2.0.0", + "typescript": "^5.6.0", + }, + }, + "apps/cli": { + "name": "@betterbase/cli-legacy", + "version": "0.0.0", + "bin": { + "bb-legacy": "./dist/index.js", + }, + }, + "packages/cli": { + "name": "@betterbase/cli", + "version": "0.1.0", + "bin": { + "bb": "./dist/index.js", + }, + "dependencies": { + "chalk": "^5.3.0", + "commander": "^12.1.0", + "inquirer": "^10.2.2", + "zod": "^3.23.8", + }, + "devDependencies": { + "@types/bun": "^1.3.9", + "typescript": "^5.6.0", + }, + }, + }, + "packages": { + "@betterbase/cli": ["@betterbase/cli@workspace:packages/cli"], + + "@betterbase/cli-legacy": ["@betterbase/cli-legacy@workspace:apps/cli"], + + "@inquirer/checkbox": ["@inquirer/checkbox@2.5.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/figures": "^1.0.5", "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" } }, "sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA=="], + + "@inquirer/confirm": ["@inquirer/confirm@3.2.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/type": "^1.5.3" } }, "sha512-oOIwPs0Dvq5220Z8lGL/6LHRTEr9TgLHmiI99Rj1PJ1p1czTys+olrgBqZk4E2qC0YTzeHprxSQmoHioVdJ7Lw=="], + + "@inquirer/core": ["@inquirer/core@9.2.1", "", { "dependencies": { "@inquirer/figures": "^1.0.6", "@inquirer/type": "^2.0.0", "@types/mute-stream": "^0.0.4", "@types/node": "^22.5.5", "@types/wrap-ansi": "^3.0.0", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^1.0.0", "signal-exit": "^4.1.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" } }, "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg=="], + + "@inquirer/editor": ["@inquirer/editor@2.2.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/type": "^1.5.3", "external-editor": "^3.1.0" } }, "sha512-9KHOpJ+dIL5SZli8lJ6xdaYLPPzB8xB9GZItg39MBybzhxA16vxmszmQFrRwbOA918WA2rvu8xhDEg/p6LXKbw=="], + + "@inquirer/expand": ["@inquirer/expand@2.3.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/type": "^1.5.3", "yoctocolors-cjs": "^2.1.2" } }, "sha512-qnJsUcOGCSG1e5DTOErmv2BPQqrtT6uzqn1vI/aYGiPKq+FgslGZmtdnXbhuI7IlT7OByDoEEqdnhUnVR2hhLw=="], + + "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], + + "@inquirer/input": ["@inquirer/input@2.3.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/type": "^1.5.3" } }, "sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw=="], + + "@inquirer/number": ["@inquirer/number@1.1.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/type": "^1.5.3" } }, "sha512-ilUnia/GZUtfSZy3YEErXLJ2Sljo/mf9fiKc08n18DdwdmDbOzRcTv65H1jjDvlsAuvdFXf4Sa/aL7iw/NanVA=="], + + "@inquirer/password": ["@inquirer/password@2.2.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2" } }, "sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg=="], + + "@inquirer/prompts": ["@inquirer/prompts@5.5.0", "", { "dependencies": { "@inquirer/checkbox": "^2.5.0", "@inquirer/confirm": "^3.2.0", "@inquirer/editor": "^2.2.0", "@inquirer/expand": "^2.3.0", "@inquirer/input": "^2.3.0", "@inquirer/number": "^1.1.0", "@inquirer/password": "^2.2.0", "@inquirer/rawlist": "^2.3.0", "@inquirer/search": "^1.1.0", "@inquirer/select": "^2.5.0" } }, "sha512-BHDeL0catgHdcHbSFFUddNzvx/imzJMft+tWDPwTm3hfu8/tApk1HrooNngB2Mb4qY+KaRWF+iZqoVUPeslEog=="], + + "@inquirer/rawlist": ["@inquirer/rawlist@2.3.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/type": "^1.5.3", "yoctocolors-cjs": "^2.1.2" } }, "sha512-zzfNuINhFF7OLAtGHfhwOW2TlYJyli7lOUoJUXw/uyklcwalV6WRXBXtFIicN8rTRK1XTiPWB4UY+YuW8dsnLQ=="], + + "@inquirer/search": ["@inquirer/search@1.1.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/figures": "^1.0.5", "@inquirer/type": "^1.5.3", "yoctocolors-cjs": "^2.1.2" } }, "sha512-h+/5LSj51dx7hp5xOn4QFnUaKeARwUCLs6mIhtkJ0JYPBLmEYjdHSYh7I6GrLg9LwpJ3xeX0FZgAG1q0QdCpVQ=="], + + "@inquirer/select": ["@inquirer/select@2.5.0", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/figures": "^1.0.5", "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" } }, "sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA=="], + + "@inquirer/type": ["@inquirer/type@1.5.5", "", { "dependencies": { "mute-stream": "^1.0.0" } }, "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA=="], + + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + + "@types/mute-stream": ["@types/mute-stream@0.0.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow=="], + + "@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="], + + "@types/wrap-ansi": ["@types/wrap-ansi@3.0.0", "", {}, "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g=="], + + "ansi-escapes": ["ansi-escapes@4.3.2", "", { "dependencies": { "type-fest": "^0.21.3" } }, "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "chardet": ["chardet@0.7.0", "", {}, "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="], + + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "external-editor": ["external-editor@3.1.0", "", { "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" } }, "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "inquirer": ["inquirer@10.2.2", "", { "dependencies": { "@inquirer/core": "^9.1.0", "@inquirer/prompts": "^5.5.0", "@inquirer/type": "^1.5.3", "@types/mute-stream": "^0.0.4", "ansi-escapes": "^4.3.2", "mute-stream": "^1.0.0", "run-async": "^3.0.0", "rxjs": "^7.8.1" } }, "sha512-tyao/4Vo36XnUItZ7DnUXX4f1jVao2mSrleV/5IPtW/XAEA26hRVsbc68nuTEKWcr5vMP/1mVoT2O7u8H4v1Vg=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "mute-stream": ["mute-stream@1.0.0", "", {}, "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA=="], + + "os-tmpdir": ["os-tmpdir@1.0.2", "", {}, "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g=="], + + "run-async": ["run-async@3.0.0", "", {}, "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q=="], + + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "turbo": ["turbo@2.8.9", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.9", "turbo-darwin-arm64": "2.8.9", "turbo-linux-64": "2.8.9", "turbo-linux-arm64": "2.8.9", "turbo-windows-64": "2.8.9", "turbo-windows-arm64": "2.8.9" }, "bin": { "turbo": "bin/turbo" } }, "sha512-G+Mq8VVQAlpz/0HTsxiNNk/xywaHGl+dk1oiBREgOEVCCDjXInDlONWUn5srRnC9s5tdHTFD1bx1N19eR4hI+g=="], + + "turbo-darwin-64": ["turbo-darwin-64@2.8.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-KnCw1ZI9KTnEAhdI9avZrnZ/z4wsM++flMA1w8s8PKOqi5daGpFV36qoPafg4S8TmYMe52JPWEoFr0L+lQ5JIw=="], + + "turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-CbD5Y2NKJKBXTOZ7z7Cc7vGlFPZkYjApA7ri9lH4iFwKV1X7MoZswh9gyRLetXYWImVX1BqIvP8KftulJg/wIA=="], + + "turbo-linux-64": ["turbo-linux-64@2.8.9", "", { "os": "linux", "cpu": "x64" }, "sha512-OXC9HdCtsHvyH+5KUoH8ds+p5WU13vdif0OPbsFzZca4cUXMwKA3HWwUuCgQetk0iAE4cscXpi/t8A263n3VTg=="], + + "turbo-linux-arm64": ["turbo-linux-arm64@2.8.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-yI5n8jNXiFA6+CxnXG0gO7h5ZF1+19K8uO3/kXPQmyl37AdiA7ehKJQOvf9OPAnmkGDHcF2HSCPltabERNRmug=="], + + "turbo-windows-64": ["turbo-windows-64@2.8.9", "", { "os": "win32", "cpu": "x64" }, "sha512-/OztzeGftJAg258M/9vK2ZCkUKUzqrWXJIikiD2pm8TlqHcIYUmepDbyZSDfOiUjMy6NzrLFahpNLnY7b5vNgg=="], + + "turbo-windows-arm64": ["turbo-windows-arm64@2.8.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-xZ2VTwVTjIqpFZKN4UBxDHCPM3oJ2J5cpRzCBSmRpJ/Pn33wpiYjs+9FB2E03svKaD04/lSSLlEUej0UYsugfg=="], + + "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@inquirer/core/@inquirer/type": ["@inquirer/type@2.0.0", "", { "dependencies": { "mute-stream": "^1.0.0" } }, "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag=="], + } +} diff --git a/betterbase/packages/cli/package.json b/betterbase/packages/cli/package.json new file mode 100644 index 0000000..5b8543d --- /dev/null +++ b/betterbase/packages/cli/package.json @@ -0,0 +1,29 @@ +{ + "name": "@betterbase/cli", + "version": "0.1.0", + "private": true, + "type": "module", + "bin": { + "bb": "./dist/index.js" + }, + "scripts": { + "build": "bun run src/build.ts", + "dev": "bun run src/index.ts", + "test": "bun test", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "chalk": "^5.3.0", + "commander": "^12.1.0", + "inquirer": "^10.2.2", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/bun": "^1.3.9", + "typescript": "^5.9.3" + }, + "exports": { + ".": "./src/index.ts" + }, + "main": "./src/index.ts" +} diff --git a/betterbase/packages/cli/src/build.ts b/betterbase/packages/cli/src/build.ts new file mode 100644 index 0000000..198205e --- /dev/null +++ b/betterbase/packages/cli/src/build.ts @@ -0,0 +1,38 @@ +/** + * Build the CLI as a standalone bundled executable output. + */ +export async function buildStandaloneCli(): Promise { + const result = await Bun.build({ + entrypoints: ['./src/index.ts'], + outdir: './dist', + target: 'bun', + format: 'esm', + minify: false, + sourcemap: 'external', + naming: 'index.js', + }); + + if (!result.success) { + const diagnostics = result.logs.map((log) => (typeof log === 'string' ? log : JSON.stringify(log))).join('\n'); + throw new Error(`Build failed with ${result.logs.length} error(s).\n${diagnostics}`); + } + + const outputPath = './dist/index.js'; + const compiled = await Bun.file(outputPath).text(); + await Bun.write(outputPath, `#!/usr/bin/env bun\n${compiled}`); +} + +async function main(): Promise { + await buildStandaloneCli(); +} + +const isEsmMain = typeof import.meta !== 'undefined' && import.meta.main; +const cjs = globalThis as unknown as { require?: { main?: unknown }; module?: unknown }; +const isCjsMain = cjs.require?.main !== undefined && cjs.require.main === cjs.module; + +if (isEsmMain || isCjsMain) { + main().catch((error) => { + console.error('Build failed:', error); + process.exit(1); + }); +} diff --git a/betterbase/packages/cli/src/commands/init.ts b/betterbase/packages/cli/src/commands/init.ts new file mode 100644 index 0000000..5859b89 --- /dev/null +++ b/betterbase/packages/cli/src/commands/init.ts @@ -0,0 +1,677 @@ +import { mkdir, rm, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { z } from 'zod'; +import * as logger from '../utils/logger'; +import * as prompts from '../utils/prompts'; + +const projectNameSchema = z + .string() + .trim() + .min(1) + .regex(/^[a-zA-Z0-9-_]+$/, 'Project name can only contain letters, numbers, hyphens, and underscores.'); + +const initOptionsSchema = z.object({ + projectName: projectNameSchema.optional(), +}); + +const databaseModeSchema = z.enum(['local', 'neon', 'turso']); + +type DatabaseMode = z.infer; + +export type InitCommandOptions = z.infer; + +function getDatabaseLabel(databaseMode: DatabaseMode): string { + if (databaseMode === 'neon') { + return 'Neon (serverless Postgres)'; + } + + if (databaseMode === 'turso') { + return 'Turso (edge SQLite)'; + } + + return 'SQLite (local.db)'; +} + +async function installDependencies(projectPath: string): Promise { + const installProcess = Bun.spawn(['bun', 'install'], { + cwd: projectPath, + stdout: 'inherit', + stderr: 'inherit', + }); + + const exitCode = await installProcess.exited; + + if (exitCode !== 0) { + throw new Error('Dependency installation failed. Please run `bun install` manually.'); + } +} + +async function initializeGitRepository(projectPath: string): Promise { + const gitProcess = Bun.spawn(['git', 'init'], { + cwd: projectPath, + stdout: 'ignore', + stderr: 'ignore', + }); + + const exitCode = await gitProcess.exited; + + if (exitCode !== 0) { + logger.warn('Git initialization failed. You can run `git init` manually.'); + } +} + +function buildPackageJson(projectName: string, databaseMode: DatabaseMode, useAuth: boolean): string { + const dependencies: Record = { + hono: '^4.11.9', + 'drizzle-orm': '^0.44.5', + zod: '^3.25.76', + }; + + if (databaseMode === 'turso') { + dependencies['@libsql/client'] = '^0.14.0'; + } + + if (databaseMode === 'neon') { + dependencies.pg = '^8.13.1'; + } + + if (useAuth) { + dependencies['better-auth'] = '^1.1.15'; + } + + const json = { + name: projectName, + private: true, + type: 'module', + scripts: { + dev: 'bun run src/index.ts', + 'db:generate': 'drizzle-kit generate', + 'db:push': 'bun run src/db/migrate.ts', + }, + dependencies, + devDependencies: { + '@types/bun': '^1.3.9', + 'drizzle-kit': '^0.31.4', + typescript: '^5.9.3', + }, + }; + + return `${JSON.stringify(json, null, 2)}\n`; +} + +function buildDrizzleConfig(databaseMode: DatabaseMode): string { + const dialect: Record = { + local: 'sqlite', + neon: 'postgresql', + turso: 'turso', + }; + + const databaseUrl: Record = { + local: "process.env.DATABASE_URL || 'file:local.db'", + neon: "process.env.DATABASE_URL || 'postgres://localhost'", + turso: "process.env.DATABASE_URL || 'libsql://localhost'", + }; + + const tursoAuthTokenLine = databaseMode === 'turso' ? "\n authToken: process.env.TURSO_AUTH_TOKEN || ''," : ''; + + return `import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/db/schema.ts', + out: './drizzle', + dialect: '${dialect[databaseMode]}', + dbCredentials: { + url: ${databaseUrl[databaseMode]},${tursoAuthTokenLine} + }, +}); +`; +} + +async function buildSchema(databaseMode: DatabaseMode): Promise { + if (databaseMode === 'neon') { + return `import { integer, pgTable, timestamp, varchar } from 'drizzle-orm/pg-core'; + +export const users = pgTable('users', { + id: integer('id').generatedAlwaysAsIdentity().primaryKey(), + email: varchar('email', { length: 255 }).notNull().unique(), + name: varchar('name', { length: 255 }), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), +}); +`; + } + + return `import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; + +/** + * Adds created_at and updated_at timestamp columns. + * Note: .$onUpdate(() => new Date()) runs when updates go through Drizzle. + * For raw SQL writes, add a DB trigger if you need automatic updated_at changes. + */ +export const timestamps = { + createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), + updatedAt: integer('updated_at', { mode: 'timestamp' }) + .$defaultFn(() => new Date()) + .$onUpdate(() => new Date()), +}; + +/** + * UUID primary-key helper. + */ +export const uuid = (name = 'id') => + text(name) + .primaryKey() + .$defaultFn(() => crypto.randomUUID()); + +/** + * Soft-delete helper. + */ +export const softDelete = { + deletedAt: integer('deleted_at', { mode: 'timestamp' }), +}; + +/** + * Shared status enum helper. + */ +export const statusEnum = (name = 'status') => + text(name, { enum: ['active', 'inactive', 'pending'] }).default('active'); + +/** + * Currency helper stored as integer cents. + */ +export const moneyColumn = (name: string) => integer(name).notNull().default(0); + +/** + * JSON text helper with type support. + */ +export const jsonColumn = (name: string) => text(name, { mode: 'json' }).$type(); + +export const users = sqliteTable('users', { + id: uuid(), + email: text('email').notNull().unique(), + name: text('name'), + status: statusEnum(), + ...timestamps, + ...softDelete, +}); + +export const posts = sqliteTable('posts', { + id: uuid(), + title: text('title').notNull(), + content: text('content'), + userId: text('user_id').references(() => users.id), + ...timestamps, +}); +`; +} + +function buildMigrateScript(databaseMode: DatabaseMode): string { + if (databaseMode === 'neon') { + return `import { migrate } from 'drizzle-orm/node-postgres/migrator'; +import { drizzle } from 'drizzle-orm/node-postgres'; +import { Pool } from 'pg'; + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}); + +const db = drizzle(pool); + +try { + await migrate(db, { migrationsFolder: './drizzle' }); + console.log('Migrations applied successfully.'); +} catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error('Failed to apply migrations:', message); + process.exit(1); +} finally { + await pool.end(); +} +`; + } + + if (databaseMode === 'turso') { + return `import { createClient } from '@libsql/client'; +import { drizzle } from 'drizzle-orm/libsql'; +import { migrate } from 'drizzle-orm/libsql/migrator'; + +const client = createClient({ + url: process.env.DATABASE_URL || 'file:local.db', + authToken: process.env.TURSO_AUTH_TOKEN, +}); + +const db = drizzle(client); + +try { + await migrate(db, { migrationsFolder: './drizzle' }); + console.log('Migrations applied successfully.'); +} catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error('Failed to apply migrations:', message); + process.exit(1); +} +`; + } + + return `import { Database } from 'bun:sqlite'; +import { drizzle } from 'drizzle-orm/bun-sqlite'; +import { migrate } from 'drizzle-orm/bun-sqlite/migrator'; + +try { + const sqlite = new Database(process.env.DB_PATH ?? 'local.db', { create: true }); + const db = drizzle(sqlite); + + migrate(db, { migrationsFolder: './drizzle' }); + console.log('Migrations applied successfully.'); +} catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error('Failed to apply migrations:', message); + process.exit(1); +} +`; +} + +function buildDbIndex(databaseMode: DatabaseMode): string { + if (databaseMode === 'neon') { + return `import { drizzle } from 'drizzle-orm/node-postgres'; +import { Pool } from 'pg'; +import * as schema from './schema'; + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}); + +export const db = drizzle(pool, { schema }); +`; + } + + if (databaseMode === 'turso') { + return `import { createClient } from '@libsql/client'; +import { drizzle } from 'drizzle-orm/libsql'; +import * as schema from './schema'; + +const client = createClient({ + url: process.env.DATABASE_URL || 'file:local.db', + authToken: process.env.TURSO_AUTH_TOKEN, +}); + +export const db = drizzle(client, { schema }); +`; + } + + return `import { Database } from 'bun:sqlite'; +import { drizzle } from 'drizzle-orm/bun-sqlite'; +import * as schema from './schema'; + +const client = new Database(process.env.DB_PATH ?? 'local.db', { create: true }); + +export const db = drizzle(client, { schema }); +`; +} + +function buildAuthMiddleware(): string { + return `import { createMiddleware } from 'hono/factory'; + +export const authMiddleware = createMiddleware(async (_c, next) => { + // TODO: wire BetterAuth session validation. + await next(); +}); +`; +} + +function buildReadme(projectName: string): string { + return `# ${projectName} + +Generated with BetterBase CLI. + +## Scripts + +- \`bun run dev\` +- \`bun run db:generate\` +- \`bun run db:push\` +`; +} + +function buildRoutesIndex(): string { + return `import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { logger } from 'hono/logger'; +import { HTTPException } from 'hono/http-exception'; +import { healthRoute } from './health'; +import { usersRoute } from './users'; + +export default function registerRoutes(app: Hono): void { + app.use('*', cors()); + app.use('*', logger()); + + app.onError((err, c) => { + const isHttpError = err instanceof HTTPException; + const showDetailedError = process.env.NODE_ENV === 'development' || isHttpError; + + return c.json( + { + error: showDetailedError ? err.message : 'Internal Server Error', + stack: process.env.NODE_ENV === 'development' ? err.stack : undefined, + details: isHttpError ? (err as { cause?: unknown }).cause ?? null : null, + }, + isHttpError ? err.status : 500, + ); + }); + + app.route('/health', healthRoute); + app.route('/api/users', usersRoute); +} +`; +} + +async function writeProjectFiles( + projectPath: string, + projectName: string, + databaseMode: DatabaseMode, + useAuth: boolean, +): Promise { + await mkdir(path.join(projectPath, 'src/db'), { recursive: true }); + await mkdir(path.join(projectPath, 'src/routes'), { recursive: true }); + await mkdir(path.join(projectPath, 'src/middleware'), { recursive: true }); + await mkdir(path.join(projectPath, 'src/lib'), { recursive: true }); + + await writeFile( + path.join(projectPath, 'betterbase.config.ts'), + `export default { + mode: '${databaseMode}', + database: { + local: 'local.db', + production: process.env.DATABASE_URL, + }, + auth: { + enabled: ${useAuth}, + }, +}; +`, + ); + + await writeFile(path.join(projectPath, 'drizzle.config.ts'), buildDrizzleConfig(databaseMode)); + await writeFile(path.join(projectPath, 'package.json'), buildPackageJson(projectName, databaseMode, useAuth)); + + await writeFile( + path.join(projectPath, 'tsconfig.json'), + `{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "esModuleInterop": true, + "types": ["bun"], + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "drizzle.config.ts", "betterbase.config.ts"] +} +`, + ); + + await writeFile( + path.join(projectPath, '.env.example'), + `DATABASE_URL= +DB_PATH=local.db +TURSO_AUTH_TOKEN= +NODE_ENV=development +PORT=3000 +`, + ); + + await writeFile( + path.join(projectPath, '.gitignore'), + `node_modules +bun.lockb +.env +.env.* +!.env.example +local.db +.drizzle +`, + ); + + await writeFile(path.join(projectPath, 'README.md'), buildReadme(projectName)); + await writeFile(path.join(projectPath, 'src/db/schema.ts'), await buildSchema(databaseMode)); + await writeFile(path.join(projectPath, 'src/db/index.ts'), buildDbIndex(databaseMode)); + + await writeFile(path.join(projectPath, 'src/db/migrate.ts'), buildMigrateScript(databaseMode)); + + await writeFile( + path.join(projectPath, 'src/routes/health.ts'), + `import { sql } from 'drizzle-orm'; +import { Hono } from 'hono'; +import { db } from '../db'; + +export const healthRoute = new Hono(); + +healthRoute.get('/', async (c) => { + try { + await db.run(sql\`select 1\`); + + return c.json({ + status: 'healthy', + database: 'connected', + timestamp: new Date().toISOString(), + }); + } catch { + return c.json( + { + status: 'unhealthy', + database: 'disconnected', + timestamp: new Date().toISOString(), + }, + 503, + ); + } +}); +`, + ); + + await writeFile( + path.join(projectPath, 'src/middleware/validation.ts'), + `import { HTTPException } from 'hono/http-exception'; +import type { ZodType } from 'zod'; + +export function parseBody(schema: ZodType, body: unknown): T { + const result = schema.safeParse(body); + + if (!result.success) { + throw new HTTPException(400, { + message: 'Validation failed', + cause: { + errors: result.error.issues.map((issue) => ({ + path: issue.path.join('.'), + message: issue.message, + code: issue.code, + })), + }, + }); + } + + return result.data; +} +`, + ); + + await writeFile( + path.join(projectPath, 'src/routes/users.ts'), + `import { Hono } from 'hono'; +import { HTTPException } from 'hono/http-exception'; +import { z } from 'zod'; +import { db } from '../db'; +import { users } from '../db/schema'; +import { parseBody } from '../middleware/validation'; + +const createUserSchema = z.object({ + email: z.string().email(), + name: z.string().min(1), +}); + +export const usersRoute = new Hono(); + +usersRoute.get('/', async (c) => { + const allUsers = await db.select().from(users); + return c.json({ users: allUsers }); +}); + +usersRoute.post('/', async (c) => { + try { + const body = await c.req.json(); + const parsed = parseBody(createUserSchema, body); + + // TODO: persist parsed user via db.insert(users) or a dedicated UsersService. + return c.json({ + message: 'User payload validated (not persisted)', + user: parsed, + }); + } catch (error) { + if (error instanceof HTTPException) { + throw error; + } + + if (error instanceof SyntaxError) { + throw new HTTPException(400, { message: 'Malformed JSON body' }); + } + + throw error; + } +}); +`, + ); + + await writeFile(path.join(projectPath, 'src/routes/index.ts'), buildRoutesIndex()); + + await writeFile( + path.join(projectPath, 'src/index.ts'), + `import { Hono } from 'hono'; +import registerRoutes from './routes'; + +const app = new Hono(); +registerRoutes(app); + +const server = Bun.serve({ + fetch: app.fetch, + port: Number(process.env.PORT ?? 3000), + development: process.env.NODE_ENV === 'development', +}); + +console.log(\`🚀 Server running at http://localhost:\${server.port}\`); +for (const route of app.routes) { + console.log(\` \${route.method} \${route.path}\`); +} + +process.on('SIGTERM', () => { + server.stop(); +}); + +process.on('SIGINT', () => { + server.stop(); +}); + +export default server; +`, + ); + + await writeFile( + path.join(projectPath, 'src/lib/utils.ts'), + `export function notImplemented(feature: string): never { + throw new Error(\`\${feature} is not implemented yet.\`); +} +`, + ); + + if (useAuth) { + await writeFile(path.join(projectPath, 'src/middleware/auth.ts'), buildAuthMiddleware()); + } +} + +/** + * Run the `bb init` command. + */ +export async function runInitCommand(rawOptions: InitCommandOptions): Promise { + const options = initOptionsSchema.parse(rawOptions); + + const projectNameInput = + options.projectName ?? + (await prompts.text({ + message: 'What is your project name?', + initial: 'my-betterbase-app', + })); + + const projectName = projectNameSchema.parse(projectNameInput); + const projectPath = path.resolve(process.cwd(), projectName); + + const databaseMode = databaseModeSchema.parse( + await prompts.select({ + message: 'Choose your database setup:', + initial: 'local', + choices: [ + { name: 'Local SQLite (development only)', value: 'local' }, + { name: 'Connect to Neon (serverless Postgres)', value: 'neon' }, + { name: 'Connect to Turso (edge SQLite)', value: 'turso' }, + ], + }), + ); + + const useAuth = await prompts.confirm({ + message: 'Add authentication? (yes/no)', + initial: true, + }); + + const useGit = await prompts.confirm({ + message: 'Initialize git repository? (yes/no)', + initial: true, + }); + + let createdProjectDir = false; + + try { + await mkdir(projectPath); + createdProjectDir = true; + } catch (error) { + const code = (error as NodeJS.ErrnoException | undefined)?.code; + if (code === 'EEXIST') { + throw new Error(`Directory \`${projectName}\` already exists. Choose another project name.`); + } + + const message = error instanceof Error ? error.message : 'Unknown directory creation error'; + throw new Error(`Failed to create project directory: ${message}`); + } + + try { + logger.info('Creating project files...'); + await writeProjectFiles(projectPath, projectName, databaseMode, useAuth); + + logger.info('Installing dependencies with bun...'); + await installDependencies(projectPath); + + if (useGit) { + logger.info('Initializing git repository...'); + await initializeGitRepository(projectPath); + } + + logger.success('BetterBase project created successfully!'); + console.log(''); + console.log(`📁 Project: ${projectName}`); + console.log(`🗄️ Database: ${getDatabaseLabel(databaseMode)}`); + console.log(`🔐 Auth: ${useAuth ? 'Enabled' : 'Disabled'}`); + console.log(''); + console.log('Next steps:'); + console.log(` cd ${projectName}`); + console.log(' bun run dev'); + console.log(''); + console.log('Your backend is running at http://localhost:3000'); + } catch (error) { + if (createdProjectDir) { + try { + await rm(projectPath, { recursive: true, force: true }); + } catch (cleanupError) { + const cleanupMessage = cleanupError instanceof Error ? cleanupError.message : String(cleanupError); + logger.warn(`Failed to cleanup \`${projectName}\`: ${cleanupMessage}`); + } + } + + throw error; + } +} diff --git a/betterbase/packages/cli/src/commands/migrate.ts b/betterbase/packages/cli/src/commands/migrate.ts new file mode 100644 index 0000000..602915b --- /dev/null +++ b/betterbase/packages/cli/src/commands/migrate.ts @@ -0,0 +1,32 @@ +import { z } from 'zod'; +import * as logger from '../utils/logger'; +import * as prompts from '../utils/prompts'; + +const migrateOptionsSchema = z.object({ + destructive: z.boolean().optional(), +}); + +export type MigrateCommandOptions = z.infer; + +/** + * Run the `bb migrate` command. + */ +export async function runMigrateCommand(rawOptions: MigrateCommandOptions): Promise { + const options = migrateOptionsSchema.parse(rawOptions); + + const shouldContinue = + options.destructive === true + ? true + : await prompts.confirm({ + message: 'This migration may include destructive changes. Continue?', + initial: false, + }); + + if (!shouldContinue) { + logger.warn('Migration cancelled by user.'); + return; + } + + logger.info('Analyzing migration plan...'); + logger.success('Migration scaffold complete. (Placeholder implementation)'); +} diff --git a/betterbase/packages/cli/src/index.ts b/betterbase/packages/cli/src/index.ts new file mode 100644 index 0000000..e0136da --- /dev/null +++ b/betterbase/packages/cli/src/index.ts @@ -0,0 +1,63 @@ +import { Command, CommanderError } from 'commander'; +import { runInitCommand } from './commands/init'; +import { runMigrateCommand } from './commands/migrate'; +import * as logger from './utils/logger'; +import packageJson from '../package.json'; + +/** + * Create and configure the BetterBase CLI program. + */ +export function createProgram(): Command { + const program = new Command(); + + program + .name('bb') + .description('BetterBase CLI') + .version(packageJson.version, '-v, --version', 'display the CLI version') + .exitOverride(); + + program + .command('init') + .description('Initialize a BetterBase project') + .argument('[project-name]', 'project name') + .action(async (projectName?: string) => { + await runInitCommand({ projectName }); + }); + + program + .command('migrate') + .description('Run BetterBase database migrations') + .option('--destructive', 'allow destructive migration flow') + .action(async (options: { destructive?: boolean }) => { + await runMigrateCommand({ destructive: options.destructive }); + }); + + return program; +} + +/** + * Execute the CLI with process arguments. + */ +export async function runCli(argv: string[] = process.argv): Promise { + const program = createProgram(); + + try { + await program.parseAsync(argv); + } catch (err) { + if (err instanceof CommanderError && (err.code === 'commander.helpDisplayed' || err.code === 'commander.version')) { + return; + } + + throw err; + } +} + +if (import.meta.main) { + try { + await runCli(); + } catch (err) { + const message = err instanceof Error ? err.message : 'Unknown CLI error'; + logger.error(message); + process.exitCode = 1; + } +} diff --git a/betterbase/packages/cli/src/utils/logger.ts b/betterbase/packages/cli/src/utils/logger.ts new file mode 100644 index 0000000..0bbc131 --- /dev/null +++ b/betterbase/packages/cli/src/utils/logger.ts @@ -0,0 +1,29 @@ +import chalk from 'chalk'; + +/** + * Print an informational message to stderr. + */ +export function info(message: string): void { + console.error(chalk.blue(`ℹ ${message}`)); +} + +/** + * Print a warning message to stderr. + */ +export function warn(message: string): void { + console.warn(chalk.yellow(`⚠ ${message}`)); +} + +/** + * Print an error message to stderr. + */ +export function error(message: string): void { + console.error(chalk.red(`✖ ${message}`)); +} + +/** + * Print a success message to stderr. + */ +export function success(message: string): void { + console.error(chalk.green(`✔ ${message}`)); +} diff --git a/betterbase/packages/cli/src/utils/prompts.ts b/betterbase/packages/cli/src/utils/prompts.ts new file mode 100644 index 0000000..a1f2d0e --- /dev/null +++ b/betterbase/packages/cli/src/utils/prompts.ts @@ -0,0 +1,88 @@ +import inquirer from 'inquirer'; +import { z } from 'zod'; + +const textOptionsSchema = z.object({ + message: z.string().min(1), + initial: z.string().optional(), +}); + +const confirmOptionsSchema = z.object({ + message: z.string().min(1), + initial: z.boolean().optional(), +}); + +const selectOptionSchema = z.object({ + name: z.string().min(1), + value: z.string().min(1), +}); + +const selectOptionsSchema = z + .object({ + message: z.string().min(1), + choices: z.array(selectOptionSchema).min(1), + initial: z.string().optional(), + }) + .refine( + ({ choices, initial }) => initial === undefined || choices.some((choice) => choice.value === initial), + { + message: 'Select initial value must match one of the choice values.', + path: ['initial'], + }, + ); + +/** + * Prompt for text input. + */ +export async function text(options: { message: string; initial?: string }): Promise { + const parsed = textOptionsSchema.parse(options); + + const response = await inquirer.prompt<{ value: string }>([ + { + type: 'input', + name: 'value', + message: parsed.message, + default: parsed.initial, + }, + ]); + + return response.value; +} + +/** + * Prompt for yes/no confirmation. + */ +export async function confirm(options: { message: string; initial?: boolean }): Promise { + const parsed = confirmOptionsSchema.parse(options); + + const response = await inquirer.prompt<{ value: boolean }>([ + { + type: 'confirm', + name: 'value', + message: parsed.message, + default: parsed.initial, + }, + ]); + + return response.value; +} + +/** + * Prompt for selecting one option. + */ +export async function select( + options: { message: string; choices: Array<{ name: string; value: string }>; initial?: string }, +): Promise { + const parsed = selectOptionsSchema.parse(options); + + const response = await inquirer.prompt<{ value: string }>([ + { + type: 'list', + name: 'value', + message: parsed.message, + choices: parsed.choices, + default: parsed.initial, + }, + ]); + + return response.value; +} diff --git a/betterbase/packages/cli/test/smoke.test.ts b/betterbase/packages/cli/test/smoke.test.ts new file mode 100644 index 0000000..f082470 --- /dev/null +++ b/betterbase/packages/cli/test/smoke.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, test } from 'bun:test'; +import { createProgram } from '../src/index'; + +describe('cli', () => { + test('has expected program name', () => { + const program = createProgram(); + expect(program.name()).toBe('bb'); + }); + + test('supports init positional argument', () => { + const program = createProgram(); + const init = program.commands.find((command) => command.name() === 'init'); + expect(init).toBeDefined(); + expect(init?.registeredArguments[0]?.name()).toBe('project-name'); + }); + + test('registers migrate command', () => { + const program = createProgram(); + const migrate = program.commands.find((command) => command.name() === 'migrate'); + expect(migrate).toBeDefined(); + }); +}); diff --git a/betterbase/packages/cli/tsconfig.json b/betterbase/packages/cli/tsconfig.json new file mode 100644 index 0000000..ad7b36f --- /dev/null +++ b/betterbase/packages/cli/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["bun"], + "outDir": "dist" + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/betterbase/packages/client/README.md b/betterbase/packages/client/README.md new file mode 100644 index 0000000..088b424 --- /dev/null +++ b/betterbase/packages/client/README.md @@ -0,0 +1,9 @@ +# @betterbase/client + +Client SDK scaffold for BetterBase. + +## Included + +- `src/index.ts` with a minimal `BetterBaseClient` class +- `package.json` workspace wiring +- `tsconfig.json` strict TypeScript setup diff --git a/betterbase/packages/client/package.json b/betterbase/packages/client/package.json new file mode 100644 index 0000000..e2ea772 --- /dev/null +++ b/betterbase/packages/client/package.json @@ -0,0 +1,17 @@ +{ + "name": "@betterbase/client", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@betterbase/shared": "workspace:*" + }, + "scripts": { + "build": "echo 'client: no build step yet'", + "dev": "echo 'client: no dev step yet'", + "typecheck": "tsc -p tsconfig.json --noEmit" + } +} diff --git a/betterbase/packages/client/src/index.ts b/betterbase/packages/client/src/index.ts new file mode 100644 index 0000000..42debc1 --- /dev/null +++ b/betterbase/packages/client/src/index.ts @@ -0,0 +1,17 @@ +import type { BetterBaseResponse } from '@betterbase/shared'; + +export interface BetterBaseClientOptions { + projectUrl: string; + apiKey?: string; +} + +export class BetterBaseClient { + constructor(private readonly options: BetterBaseClientOptions) {} + + getProjectUrl(): BetterBaseResponse { + return { + data: this.options.projectUrl, + error: null, + }; + } +} diff --git a/betterbase/packages/client/tsconfig.json b/betterbase/packages/client/tsconfig.json new file mode 100644 index 0000000..473c6ec --- /dev/null +++ b/betterbase/packages/client/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["bun"], + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/betterbase/packages/core/README.md b/betterbase/packages/core/README.md new file mode 100644 index 0000000..e74f0cd --- /dev/null +++ b/betterbase/packages/core/README.md @@ -0,0 +1,9 @@ +# @betterbase/core + +Core engine scaffold for BetterBase. + +## Included + +- `src/index.ts` with a typed `createCoreProject` helper +- `package.json` workspace wiring +- `tsconfig.json` strict TypeScript setup diff --git a/betterbase/packages/core/package.json b/betterbase/packages/core/package.json new file mode 100644 index 0000000..b2e8122 --- /dev/null +++ b/betterbase/packages/core/package.json @@ -0,0 +1,17 @@ +{ + "name": "@betterbase/core", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@betterbase/shared": "workspace:*" + }, + "scripts": { + "build": "echo 'core: no build step yet'", + "dev": "echo 'core: no dev step yet'", + "typecheck": "tsc -p tsconfig.json --noEmit" + } +} diff --git a/betterbase/packages/core/src/index.ts b/betterbase/packages/core/src/index.ts new file mode 100644 index 0000000..fc8ee49 --- /dev/null +++ b/betterbase/packages/core/src/index.ts @@ -0,0 +1,13 @@ +import type { BetterBaseResponse } from '@betterbase/shared'; + +export interface BetterBaseProjectConfig { + name: string; + mode: 'local' | 'neon' | 'turso'; +} + +export function createCoreProject(config: BetterBaseProjectConfig): BetterBaseResponse { + return { + data: config, + error: null, + }; +} diff --git a/betterbase/packages/core/tsconfig.json b/betterbase/packages/core/tsconfig.json new file mode 100644 index 0000000..473c6ec --- /dev/null +++ b/betterbase/packages/core/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["bun"], + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/betterbase/packages/shared/README.md b/betterbase/packages/shared/README.md new file mode 100644 index 0000000..559303c --- /dev/null +++ b/betterbase/packages/shared/README.md @@ -0,0 +1,34 @@ +# @betterbase/shared + +Shared types, utilities, constants, and schemas used across BetterBase packages. + +## Installation + +From the monorepo root: + +```bash +bun add @betterbase/shared --filter +``` + +Or add a workspace dependency in your package `package.json`. + +## Usage + +```ts +import type { YourType } from '@betterbase/shared'; +import { yourUtility } from '@betterbase/shared'; +``` + +## What to add here + +- [ ] Common TypeScript types and interfaces +- [ ] Shared utilities/helpers +- [ ] Shared constants and enums +- [ ] Shared validation schemas (e.g. Zod) +- [ ] Shared error/result primitives + +## Notes + +- Keep exports stable and documented. +- If publishing externally later, add changelog/versioning guidance. +- Include runnable usage examples as the package grows. diff --git a/betterbase/packages/shared/package.json b/betterbase/packages/shared/package.json new file mode 100644 index 0000000..d422872 --- /dev/null +++ b/betterbase/packages/shared/package.json @@ -0,0 +1,14 @@ +{ + "name": "@betterbase/shared", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "build": "echo 'shared: no build step yet'", + "dev": "echo 'shared: no dev step yet'", + "typecheck": "tsc -p tsconfig.json --noEmit" + } +} diff --git a/betterbase/packages/shared/src/index.ts b/betterbase/packages/shared/src/index.ts new file mode 100644 index 0000000..6bcfd3f --- /dev/null +++ b/betterbase/packages/shared/src/index.ts @@ -0,0 +1,17 @@ +export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue }; + +export interface BetterBaseResult { + data: T; + error: null; +} + +export interface BetterBaseError { + data: null; + error: { message: string; code: string }; +} + +export type BetterBaseResponse = BetterBaseResult | BetterBaseError; + +export function noop(): void { + // Shared placeholder utility for future cross-package helpers. +} diff --git a/betterbase/packages/shared/tsconfig.json b/betterbase/packages/shared/tsconfig.json new file mode 100644 index 0000000..473c6ec --- /dev/null +++ b/betterbase/packages/shared/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["bun"], + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/betterbase/templates/auth/README.md b/betterbase/templates/auth/README.md new file mode 100644 index 0000000..29652b6 --- /dev/null +++ b/betterbase/templates/auth/README.md @@ -0,0 +1,11 @@ +# Auth Template + +Starter auth template scaffold for BetterBase. + +## Included + +- `src/index.ts` Hono app entry +- `src/routes/auth.ts` auth health route +- `src/middleware/auth.ts` auth middleware placeholder +- `src/db/schema.ts` sessions schema scaffold +- `package.json` + `tsconfig.json` diff --git a/betterbase/templates/auth/bun.lock b/betterbase/templates/auth/bun.lock new file mode 100644 index 0000000..dace2a8 --- /dev/null +++ b/betterbase/templates/auth/bun.lock @@ -0,0 +1,69 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "betterbase-auth-template", + "dependencies": { + "better-auth": "^1.1.15", + "drizzle-orm": "^0.44.5", + "hono": "^4.6.10", + "zod": "^3.23.8", + }, + "devDependencies": { + "@types/bun": "^1.3.9", + "typescript": "^5.9.3", + }, + }, + }, + "packages": { + "@better-auth/core": ["@better-auth/core@1.4.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-q+awYgC7nkLEBdx2sW0iJjkzgSHlIxGnOpsN1r/O1+a4m7osJNHtfK2mKJSL1I+GfNyIlxJF8WvD/NLuYMpmcg=="], + + "@better-auth/telemetry": ["@better-auth/telemetry@1.4.18", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.18" } }, "sha512-e5rDF8S4j3Um/0LIVATL2in9dL4lfO2fr2v1Wio4qTMRbfxqnUDTa+6SZtwdeJrbc4O+a3c+IyIpjG9Q/6GpfQ=="], + + "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], + + "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], + + "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], + + "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + + "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], + + "better-auth": ["better-auth@1.4.18", "", { "dependencies": { "@better-auth/core": "1.4.18", "@better-auth/telemetry": "1.4.18", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.8", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.3.5" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-bnyifLWBPcYVltH3RhS7CM62MoelEqC6Q+GnZwfiDWNfepXoQZBjEvn4urcERC7NTKgKq5zNBM8rvPvRBa6xcg=="], + + "better-call": ["better-call@1.1.8", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw=="], + + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + + "drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="], + + "hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="], + + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + + "kysely": ["kysely@0.28.11", "", {}, "sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg=="], + + "nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="], + + "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "better-auth/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + } +} diff --git a/betterbase/templates/auth/package.json b/betterbase/templates/auth/package.json new file mode 100644 index 0000000..8336498 --- /dev/null +++ b/betterbase/templates/auth/package.json @@ -0,0 +1,19 @@ +{ + "name": "betterbase-auth-template", + "private": true, + "type": "module", + "scripts": { + "dev": "bun run src/index.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "hono": "^4.6.10", + "zod": "^3.23.8", + "drizzle-orm": "^0.44.5", + "better-auth": "^1.1.15" + }, + "devDependencies": { + "@types/bun": "^1.3.9", + "typescript": "^5.9.3" + } +} diff --git a/betterbase/templates/auth/src/db/schema.ts b/betterbase/templates/auth/src/db/schema.ts new file mode 100644 index 0000000..5b95e5b --- /dev/null +++ b/betterbase/templates/auth/src/db/schema.ts @@ -0,0 +1,7 @@ +import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; + +export const sessions = sqliteTable('sessions', { + id: integer('id').primaryKey({ autoIncrement: true }), + userId: text('user_id').notNull(), + token: text('token').notNull(), +}); diff --git a/betterbase/templates/auth/src/index.ts b/betterbase/templates/auth/src/index.ts new file mode 100644 index 0000000..1d03e73 --- /dev/null +++ b/betterbase/templates/auth/src/index.ts @@ -0,0 +1,10 @@ +import { Hono } from 'hono'; +import { authRoute } from './routes/auth'; + +const app = new Hono(); +app.route('/auth', authRoute); + +export default { + port: Number(process.env.PORT ?? 3000), + fetch: app.fetch, +}; diff --git a/betterbase/templates/auth/src/middleware/auth.ts b/betterbase/templates/auth/src/middleware/auth.ts new file mode 100644 index 0000000..1d3dcaa --- /dev/null +++ b/betterbase/templates/auth/src/middleware/auth.ts @@ -0,0 +1,6 @@ +import { createMiddleware } from 'hono/factory'; + +export const authMiddleware = createMiddleware(async (_c, next) => { + // TODO: Wire BetterAuth session checks into the auth template. + await next(); +}); diff --git a/betterbase/templates/auth/src/routes/auth.ts b/betterbase/templates/auth/src/routes/auth.ts new file mode 100644 index 0000000..e075f7a --- /dev/null +++ b/betterbase/templates/auth/src/routes/auth.ts @@ -0,0 +1,7 @@ +import { Hono } from 'hono'; + +export const authRoute = new Hono(); + +authRoute.get('/health', (c) => { + return c.json({ status: 'ok', feature: 'auth-template' }); +}); diff --git a/betterbase/templates/auth/tsconfig.json b/betterbase/templates/auth/tsconfig.json new file mode 100644 index 0000000..473c6ec --- /dev/null +++ b/betterbase/templates/auth/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["bun"], + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/betterbase/templates/base/README.md b/betterbase/templates/base/README.md new file mode 100644 index 0000000..e6d4cf1 --- /dev/null +++ b/betterbase/templates/base/README.md @@ -0,0 +1,28 @@ +# Base Template (Bun + TypeScript + Hono + Drizzle) + +Starter template aligned to BetterBase defaults: +- Bun runtime +- TypeScript strict mode +- Hono API server +- Drizzle ORM with SQLite local default +- Zod available for request validation + +## Structure + +```txt +src/ + db/ + index.ts + schema.ts + routes/ + index.ts + health.ts + users.ts + middleware/ + validation.ts + lib/ + env.ts + index.ts +betterbase.config.ts +drizzle.config.ts +``` diff --git a/betterbase/templates/base/betterbase.config.ts b/betterbase/templates/base/betterbase.config.ts new file mode 100644 index 0000000..361834c --- /dev/null +++ b/betterbase/templates/base/betterbase.config.ts @@ -0,0 +1,25 @@ +import { z } from 'zod'; + +export const BetterBaseConfigSchema = z.object({ + mode: z.enum(['local', 'neon', 'turso']), + database: z.object({ + local: z.string(), + production: z.string().nullable().optional(), + }), + auth: z.object({ + enabled: z.boolean(), + }), +}); + +export type BetterBaseConfig = z.infer; + +export const betterbaseConfig: BetterBaseConfig = BetterBaseConfigSchema.parse({ + mode: 'local', + database: { + local: 'local.db', + production: null, + }, + auth: { + enabled: true, + }, +}); diff --git a/betterbase/templates/base/bun.lock b/betterbase/templates/base/bun.lock new file mode 100644 index 0000000..57b6cd8 --- /dev/null +++ b/betterbase/templates/base/bun.lock @@ -0,0 +1,235 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "betterbase-base-template", + "dependencies": { + "drizzle-orm": "^0.44.5", + "hono": "^4.6.10", + "zod": "^3.23.8", + }, + "devDependencies": { + "@types/bun": "^1.3.9", + "drizzle-kit": "^0.31.4", + "typescript": "^5.9.3", + }, + }, + }, + "packages": { + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + + "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "better-sqlite3": ["better-sqlite3@11.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ=="], + + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "drizzle-kit": ["drizzle-kit@0.31.9", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg=="], + + "drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + + "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], + + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + + "hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + + "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + + "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + } +} diff --git a/betterbase/templates/base/drizzle.config.ts b/betterbase/templates/base/drizzle.config.ts new file mode 100644 index 0000000..c41cbd5 --- /dev/null +++ b/betterbase/templates/base/drizzle.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/db/schema.ts', + out: './drizzle', + dialect: 'sqlite', + dbCredentials: { + url: 'file:local.db', + }, + verbose: true, + strict: true, +}); diff --git a/betterbase/templates/base/package.json b/betterbase/templates/base/package.json new file mode 100644 index 0000000..e631611 --- /dev/null +++ b/betterbase/templates/base/package.json @@ -0,0 +1,21 @@ +{ + "name": "betterbase-base-template", + "private": true, + "type": "module", + "scripts": { + "dev": "bun --hot run src/index.ts", + "db:generate": "drizzle-kit generate", + "db:push": "bun run src/db/migrate.ts", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "hono": "^4.6.10", + "zod": "^3.23.8", + "drizzle-orm": "^0.44.5" + }, + "devDependencies": { + "@types/bun": "^1.3.9", + "drizzle-kit": "^0.31.4", + "typescript": "^5.9.3" + } +} diff --git a/betterbase/templates/base/src/db/index.ts b/betterbase/templates/base/src/db/index.ts new file mode 100644 index 0000000..ddbbc3f --- /dev/null +++ b/betterbase/templates/base/src/db/index.ts @@ -0,0 +1,8 @@ +import { Database } from 'bun:sqlite'; +import { drizzle } from 'drizzle-orm/bun-sqlite'; +import * as schema from './schema'; + +const dbPath = process.env.DB_PATH ?? 'local.db'; +const sqlite = new Database(dbPath, { create: true }); + +export const db = drizzle(sqlite, { schema }); diff --git a/betterbase/templates/base/src/db/migrate.ts b/betterbase/templates/base/src/db/migrate.ts new file mode 100644 index 0000000..c590040 --- /dev/null +++ b/betterbase/templates/base/src/db/migrate.ts @@ -0,0 +1,15 @@ +import { Database } from 'bun:sqlite'; +import { drizzle } from 'drizzle-orm/bun-sqlite'; +import { migrate } from 'drizzle-orm/bun-sqlite/migrator'; + +try { + const sqlite = new Database(process.env.DB_PATH ?? 'local.db', { create: true }); + const db = drizzle(sqlite); + + migrate(db, { migrationsFolder: './drizzle' }); + console.log('Migrations applied successfully.'); +} catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error('Failed to apply migrations:', message); + process.exit(1); +} diff --git a/betterbase/templates/base/src/db/schema.ts b/betterbase/templates/base/src/db/schema.ts new file mode 100644 index 0000000..ef5c131 --- /dev/null +++ b/betterbase/templates/base/src/db/schema.ts @@ -0,0 +1,69 @@ +import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; + +/** + * Adds created_at and updated_at timestamp columns. + * created_at is set on insert and updated_at is refreshed on updates. + * Note: .$onUpdate(() => new Date()) applies when updates go through Drizzle. + * Raw SQL writes will not auto-update this value without a DB trigger. + * + * @example + * export const users = sqliteTable('users', { + * id: uuid(), + * email: text('email'), + * ...timestamps, + * }); + */ +export const timestamps = { + createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), + updatedAt: integer('updated_at', { mode: 'timestamp' }) + .$defaultFn(() => new Date()) + .$onUpdate(() => new Date()), +}; + +/** + * UUID primary-key helper. + */ +export const uuid = (name = 'id') => + text(name) + .primaryKey() + .$defaultFn(() => crypto.randomUUID()); + +/** + * Soft-delete helper. + */ +export const softDelete = { + deletedAt: integer('deleted_at', { mode: 'timestamp' }), +}; + +/** + * Shared status enum helper. + */ +export const statusEnum = (name = 'status') => + text(name, { enum: ['active', 'inactive', 'pending'] }).default('active'); + +/** + * Currency helper stored as integer cents. + */ +export const moneyColumn = (name: string) => integer(name).notNull().default(0); + +/** + * JSON text helper with type support. + */ +export const jsonColumn = (name: string) => text(name, { mode: 'json' }).$type(); + +export const users = sqliteTable('users', { + id: uuid(), + email: text('email').notNull().unique(), + name: text('name'), + status: statusEnum(), + ...timestamps, + ...softDelete, +}); + +export const posts = sqliteTable('posts', { + id: uuid(), + title: text('title').notNull(), + content: text('content'), + userId: text('user_id').references(() => users.id), + ...timestamps, +}); diff --git a/betterbase/templates/base/src/index.ts b/betterbase/templates/base/src/index.ts new file mode 100644 index 0000000..4065ba5 --- /dev/null +++ b/betterbase/templates/base/src/index.ts @@ -0,0 +1,26 @@ +import { Hono } from 'hono'; +import { registerRoutes } from './routes'; + +const app = new Hono(); +registerRoutes(app); + +const server = Bun.serve({ + fetch: app.fetch, + port: Number(process.env.PORT ?? 3000), + development: process.env.NODE_ENV === 'development', +}); + +console.log(`🚀 Server running at http://localhost:${server.port}`); +for (const route of app.routes) { + console.log(` ${route.method} ${route.path}`); +} + +process.on('SIGTERM', () => { + server.stop(); +}); + +process.on('SIGINT', () => { + server.stop(); +}); + +export { app, server }; diff --git a/betterbase/templates/base/src/lib/env.ts b/betterbase/templates/base/src/lib/env.ts new file mode 100644 index 0000000..c4a0b38 --- /dev/null +++ b/betterbase/templates/base/src/lib/env.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +const envSchema = z.object({ + NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), + PORT: z.coerce.number().default(3000), +}); + +export const env = envSchema.parse(process.env); diff --git a/betterbase/templates/base/src/middleware/validation.ts b/betterbase/templates/base/src/middleware/validation.ts new file mode 100644 index 0000000..98950c7 --- /dev/null +++ b/betterbase/templates/base/src/middleware/validation.ts @@ -0,0 +1,21 @@ +import { HTTPException } from 'hono/http-exception'; +import type { ZodType } from 'zod'; + +export function parseBody(schema: ZodType, body: unknown): T { + const result = schema.safeParse(body); + + if (!result.success) { + throw new HTTPException(400, { + message: 'Validation failed', + cause: { + errors: result.error.issues.map((issue) => ({ + path: issue.path.join('.'), + message: issue.message, + code: issue.code, + })), + }, + }); + } + + return result.data; +} diff --git a/betterbase/templates/base/src/routes/health.ts b/betterbase/templates/base/src/routes/health.ts new file mode 100644 index 0000000..85c2add --- /dev/null +++ b/betterbase/templates/base/src/routes/health.ts @@ -0,0 +1,26 @@ +import { sql } from 'drizzle-orm'; +import { Hono } from 'hono'; +import { db } from '../db'; + +export const healthRoute = new Hono(); + +healthRoute.get('/', async (c) => { + try { + await db.run(sql`select 1`); + + return c.json({ + status: 'healthy', + database: 'connected', + timestamp: new Date().toISOString(), + }); + } catch { + return c.json( + { + status: 'unhealthy', + database: 'disconnected', + timestamp: new Date().toISOString(), + }, + 503, + ); + } +}); diff --git a/betterbase/templates/base/src/routes/index.ts b/betterbase/templates/base/src/routes/index.ts new file mode 100644 index 0000000..64a9e83 --- /dev/null +++ b/betterbase/templates/base/src/routes/index.ts @@ -0,0 +1,28 @@ +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { logger } from 'hono/logger'; +import { HTTPException } from 'hono/http-exception'; +import { healthRoute } from './health'; +import { usersRoute } from './users'; + +export function registerRoutes(app: Hono): void { + app.use('*', cors()); + app.use('*', logger()); + + app.onError((err, c) => { + const isHttpError = err instanceof HTTPException; + const showDetailedError = process.env.NODE_ENV === 'development' || isHttpError; + + return c.json( + { + error: showDetailedError ? err.message : 'Internal Server Error', + stack: process.env.NODE_ENV === 'development' ? err.stack : undefined, + details: isHttpError ? (err as { cause?: unknown }).cause ?? null : null, + }, + isHttpError ? err.status : 500, + ); + }); + + app.route('/health', healthRoute); + app.route('/api/users', usersRoute); +} diff --git a/betterbase/templates/base/src/routes/users.ts b/betterbase/templates/base/src/routes/users.ts new file mode 100644 index 0000000..29f17c6 --- /dev/null +++ b/betterbase/templates/base/src/routes/users.ts @@ -0,0 +1,41 @@ +import { Hono } from 'hono'; +import { HTTPException } from 'hono/http-exception'; +import { z } from 'zod'; +import { db } from '../db'; +import { users } from '../db/schema'; +import { parseBody } from '../middleware/validation'; + +export const createUserSchema = z.object({ + email: z.string().email(), + name: z.string().min(1), +}); + +export const usersRoute = new Hono(); + +usersRoute.get('/', async (c) => { + const allUsers = await db.select().from(users); + return c.json({ users: allUsers }); +}); + +usersRoute.post('/', async (c) => { + try { + const body = await c.req.json(); + const parsed = parseBody(createUserSchema, body); + + // TODO: persist parsed user via db.insert(users) or a dedicated UsersService. + return c.json({ + message: 'User payload validated (not persisted)', + user: parsed, + }); + } catch (error) { + if (error instanceof HTTPException) { + throw error; + } + + if (error instanceof SyntaxError) { + throw new HTTPException(400, { message: 'Malformed JSON body' }); + } + + throw error; + } +}); diff --git a/betterbase/templates/base/tsconfig.json b/betterbase/templates/base/tsconfig.json new file mode 100644 index 0000000..e0c0da0 --- /dev/null +++ b/betterbase/templates/base/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "types": [ + "bun" + ], + "outDir": "dist" + }, + "include": [ + "src/**/*.ts", + "drizzle.config.ts", + "betterbase.config.ts" + ] +} diff --git a/betterbase/tsconfig.base.json b/betterbase/tsconfig.base.json new file mode 100644 index 0000000..b1d6a90 --- /dev/null +++ b/betterbase/tsconfig.base.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "forceConsistentCasingInFileNames": true + } +} diff --git a/betterbase/turbo.json b/betterbase/turbo.json new file mode 100644 index 0000000..18d6836 --- /dev/null +++ b/betterbase/turbo.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://turbo.build/schema.json", + "tasks": { + "build": { + "dependsOn": [ + "^build" + ], + "outputs": [ + "dist/**", + ".next/**" + ] + }, + "dev": { + "cache": false, + "persistent": true + }, + "lint": {}, + "typecheck": { + "dependsOn": [ + "^typecheck" + ] + } + } +}