From 55eb9778c646414bdec3df28b5bb7c2c8cf76548 Mon Sep 17 00:00:00 2001 From: weroperking <139503221+weroperking@users.noreply.github.com> Date: Tue, 17 Feb 2026 19:12:16 +0200 Subject: [PATCH] feat(template): add Bun.serve local dev server with Hono middleware --- AGENTS.md | 72 +++ betterbase/.gitignore | 9 + betterbase/README.md | 29 ++ betterbase/apps/cli/README.md | 6 + betterbase/apps/cli/package.json | 14 + betterbase/apps/cli/src/index.ts | 15 + betterbase/apps/cli/tsconfig.json | 10 + betterbase/apps/dashboard/README.md | 28 ++ betterbase/package.json | 19 + betterbase/packages/cli/bun.lockb | 155 ++++++ betterbase/packages/cli/package.json | 25 + betterbase/packages/cli/src/build.ts | 24 + betterbase/packages/cli/src/commands/init.ts | 460 ++++++++++++++++++ .../packages/cli/src/commands/migrate.ts | 33 ++ betterbase/packages/cli/src/index.ts | 53 ++ betterbase/packages/cli/src/utils/logger.ts | 29 ++ betterbase/packages/cli/src/utils/prompts.ts | 80 +++ betterbase/packages/cli/test/smoke.test.ts | 16 + betterbase/packages/cli/tsconfig.json | 8 + betterbase/packages/client/README.md | 3 + betterbase/packages/core/README.md | 3 + betterbase/packages/shared/README.md | 34 ++ betterbase/templates/auth/README.md | 3 + betterbase/templates/base/README.md | 28 ++ .../templates/base/betterbase.config.ts | 25 + betterbase/templates/base/bun.lock | 153 ++++++ betterbase/templates/base/drizzle.config.ts | 12 + betterbase/templates/base/package.json | 21 + betterbase/templates/base/src/db/index.ts | 8 + betterbase/templates/base/src/db/schema.ts | 14 + betterbase/templates/base/src/index.ts | 1 + betterbase/templates/base/src/lib/env.ts | 8 + .../base/src/middleware/validation.ts | 28 ++ .../templates/base/src/routes/health.ts | 11 + betterbase/templates/base/src/routes/index.ts | 62 +++ betterbase/templates/base/src/routes/users.ts | 32 ++ betterbase/templates/base/tsconfig.json | 17 + betterbase/tsconfig.base.json | 13 + betterbase/turbo.json | 24 + 39 files changed, 1585 insertions(+) create mode 100644 AGENTS.md create mode 100644 betterbase/.gitignore create mode 100644 betterbase/README.md create mode 100644 betterbase/apps/cli/README.md create mode 100644 betterbase/apps/cli/package.json create mode 100644 betterbase/apps/cli/src/index.ts create mode 100644 betterbase/apps/cli/tsconfig.json create mode 100644 betterbase/apps/dashboard/README.md create mode 100644 betterbase/package.json create mode 100644 betterbase/packages/cli/bun.lockb create mode 100644 betterbase/packages/cli/package.json create mode 100644 betterbase/packages/cli/src/build.ts create mode 100644 betterbase/packages/cli/src/commands/init.ts create mode 100644 betterbase/packages/cli/src/commands/migrate.ts create mode 100644 betterbase/packages/cli/src/index.ts create mode 100644 betterbase/packages/cli/src/utils/logger.ts create mode 100644 betterbase/packages/cli/src/utils/prompts.ts create mode 100644 betterbase/packages/cli/test/smoke.test.ts create mode 100644 betterbase/packages/cli/tsconfig.json create mode 100644 betterbase/packages/client/README.md create mode 100644 betterbase/packages/core/README.md create mode 100644 betterbase/packages/shared/README.md create mode 100644 betterbase/templates/auth/README.md create mode 100644 betterbase/templates/base/README.md create mode 100644 betterbase/templates/base/betterbase.config.ts create mode 100644 betterbase/templates/base/bun.lock create mode 100644 betterbase/templates/base/drizzle.config.ts create mode 100644 betterbase/templates/base/package.json create mode 100644 betterbase/templates/base/src/db/index.ts create mode 100644 betterbase/templates/base/src/db/schema.ts create mode 100644 betterbase/templates/base/src/index.ts create mode 100644 betterbase/templates/base/src/lib/env.ts create mode 100644 betterbase/templates/base/src/middleware/validation.ts create mode 100644 betterbase/templates/base/src/routes/health.ts create mode 100644 betterbase/templates/base/src/routes/index.ts create mode 100644 betterbase/templates/base/src/routes/users.ts create mode 100644 betterbase/templates/base/tsconfig.json create mode 100644 betterbase/tsconfig.base.json create mode 100644 betterbase/turbo.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3d6e459 --- /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, respond with: **GPT-5.2-Codex, created by OpenAI**. + +## 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..8583250 --- /dev/null +++ b/betterbase/.gitignore @@ -0,0 +1,9 @@ +node_modules +.bun +.turbo +dist +.next +.env +.env.* +!.env.example +.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..3432b23 --- /dev/null +++ b/betterbase/apps/cli/package.json @@ -0,0 +1,14 @@ +{ + "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" + } +} diff --git a/betterbase/apps/cli/src/index.ts b/betterbase/apps/cli/src/index.ts new file mode 100644 index 0000000..d74eb82 --- /dev/null +++ b/betterbase/apps/cli/src/index.ts @@ -0,0 +1,15 @@ +#!/usr/bin/env node + +/** + * Legacy bb wrapper entrypoint. + * + * Forwards execution to the canonical CLI implementation in packages/cli. + */ +export async function runLegacyCli(): Promise { + const cliModule = await import('../../../packages/cli/src/index'); + await cliModule.runCli(process.argv); +} + +if (import.meta.main) { + await runLegacyCli(); +} diff --git a/betterbase/apps/cli/tsconfig.json b/betterbase/apps/cli/tsconfig.json new file mode 100644 index 0000000..a47cede --- /dev/null +++ b/betterbase/apps/cli/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src" + ] +} diff --git a/betterbase/apps/dashboard/README.md b/betterbase/apps/dashboard/README.md new file mode 100644 index 0000000..08f5b35 --- /dev/null +++ b/betterbase/apps/dashboard/README.md @@ -0,0 +1,28 @@ +# Dashboard App (Scaffold) + +Future BetterBase dashboard/studio app. + +## Planned Features (Optional) + +- [ ] Table browser and editor +- [ ] API explorer / request runner +- [ ] Auth and session management views +- [ ] Project settings and environment controls + +## Tech Stack (Optional) + +- Framework: _TBD_ (e.g., Next.js / Astro / React) +- UI: _TBD_ (e.g., Tailwind CSS / shadcn/ui) +- State/Data: _TBD_ (e.g., TanStack Query) + +## Setup (Placeholder) + +- `cd betterbase/apps/dashboard` +- `bun install` +- `bun run dev` + +## Related Docs / Links (Optional) + +- Design doc: _TBD_ +- Roadmap ticket(s): _TBD_ +- UX references: _TBD_ 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..5ed2153 --- /dev/null +++ b/betterbase/packages/cli/package.json @@ -0,0 +1,25 @@ +{ + "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.6.0" + } +} diff --git a/betterbase/packages/cli/src/build.ts b/betterbase/packages/cli/src/build.ts new file mode 100644 index 0000000..4e34db1 --- /dev/null +++ b/betterbase/packages/cli/src/build.ts @@ -0,0 +1,24 @@ +/** + * 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) { + throw new Error(`Build failed with ${result.logs.length} error(s).`); + } + + const outputPath = './dist/index.js'; + const compiled = await Bun.file(outputPath).text(); + await Bun.write(outputPath, `#!/usr/bin/env bun\n${compiled}`); +} + +await buildStandaloneCli(); diff --git a/betterbase/packages/cli/src/commands/init.ts b/betterbase/packages/cli/src/commands/init.ts new file mode 100644 index 0000000..6a856c8 --- /dev/null +++ b/betterbase/packages/cli/src/commands/init.ts @@ -0,0 +1,460 @@ +import { mkdir, 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.36.4', + zod: '^3.25.76', + }; + + if (databaseMode === 'local') { + dependencies['better-sqlite3'] = '^11.7.0'; + } + + 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': 'drizzle-kit push', + }, + dependencies, + devDependencies: { + '@types/bun': '^1.3.9', + 'drizzle-kit': '^0.27.2', + 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', + }; + + return `import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/db/schema.ts', + out: './drizzle', + dialect: '${dialect[databaseMode]}', + dbCredentials: { + url: process.env.DATABASE_URL || 'file:local.db', + }, +}); +`; +} + +function buildSchema(databaseMode: DatabaseMode): string { + 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 { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; + +export const users = sqliteTable('users', { + id: integer('id').primaryKey({ autoIncrement: true }), + email: text('email').notNull().unique(), + name: text('name'), + createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()), +}); +`; +} + +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', +}); + +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('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\` +`; +} + +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: 'sqlite://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, + "noImplicitAny": 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= +NODE_ENV=development +PORT=3000 +`, + ); + + await writeFile( + path.join(projectPath, '.gitignore'), + `node_modules +bun.lockb +.env +local.db +.drizzle +`, + ); + + await writeFile(path.join(projectPath, 'README.md'), buildReadme(projectName)); + + await writeFile(path.join(projectPath, 'src/db/schema.ts'), buildSchema(databaseMode)); + + await writeFile(path.join(projectPath, 'src/db/index.ts'), buildDbIndex(databaseMode)); + + await writeFile( + path.join(projectPath, 'src/routes/health.ts'), + `import { Hono } from 'hono'; + +export const healthRoute = new Hono(); + +healthRoute.get('/', (c) => { + return c.json({ + status: 'healthy', + database: 'connected', + timestamp: new Date().toISOString(), + }); +}); +`, + ); + + await writeFile( + path.join(projectPath, 'src/routes/index.ts'), + `import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { logger } from 'hono/logger'; +import { HTTPException } from 'hono/http-exception'; +import { db } from '../db'; +import { users } from '../db/schema'; +import { healthRoute } from './health'; + +const app = new Hono(); + +app.use('*', cors()); +app.use('*', logger()); +app.use('*', async (c, next) => { + const start = performance.now(); + await next(); + const duration = (performance.now() - start).toFixed(2); + console.log(\`⏱ \${c.req.method} \${c.req.path} - \${duration}ms\`); +}); + +app.onError((err, c) => { + console.error('Error:', err); + return c.json( + { + error: err.message, + stack: process.env.NODE_ENV === 'development' ? err.stack : undefined, + details: err instanceof HTTPException ? (err as { cause?: unknown }).cause ?? null : null, + }, + err instanceof HTTPException ? err.status : 500, + ); +}); + +app.route('/health', healthRoute); + +app.get('/api/users', async (c) => { + const allUsers = await db.select().from(users); + return c.json({ users: allUsers }); +}); + +const server = Bun.serve({ + fetch: app.fetch, + port: Number(process.env.PORT ?? 3000), + development: process.env.NODE_ENV === 'development', +}); + +console.log('\x1b[32m🚀 BetterBase dev server started\x1b[0m'); +console.log(\`\x1b[36m→ URL:\x1b[0m http://localhost:\${server.port}\`); +console.log('\x1b[35m→ Routes:\x1b[0m'); +console.log(' GET /health'); +console.log(' GET /api/users'); + +process.on('SIGTERM', () => { + console.log('SIGTERM received, closing server...'); + server.stop(); +}); + +process.on('SIGINT', () => { + console.log('SIGINT received, closing server...'); + server.stop(); +}); +`, + ); + + await writeFile( + path.join(projectPath, 'src/index.ts'), + `import server from './routes/index'; + +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, + }); + + try { + await mkdir(projectPath); + } 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) { + const message = error instanceof Error ? error.message : 'Unknown init error'; + throw new Error(`Failed to initialize project: ${message}`); + } +} diff --git a/betterbase/packages/cli/src/commands/migrate.ts b/betterbase/packages/cli/src/commands/migrate.ts new file mode 100644 index 0000000..0364885 --- /dev/null +++ b/betterbase/packages/cli/src/commands/migrate.ts @@ -0,0 +1,33 @@ +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); + + logger.info('Analyzing schema changes...'); + + const shouldContinue = + options.destructive === true + ? await prompts.confirm({ + message: 'Destructive changes detected. Continue?', + initial: false, + }) + : true; + + if (!shouldContinue) { + logger.warn('Migration cancelled by user.'); + return; + } + + logger.success('Migration flow completed (placeholder).'); +} diff --git a/betterbase/packages/cli/src/index.ts b/betterbase/packages/cli/src/index.ts new file mode 100644 index 0000000..e2da69b --- /dev/null +++ b/betterbase/packages/cli/src/index.ts @@ -0,0 +1,53 @@ +import { Command } 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'); + + 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(); + await program.parseAsync(argv); +} + +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..fe71756 --- /dev/null +++ b/betterbase/packages/cli/src/utils/logger.ts @@ -0,0 +1,29 @@ +import chalk from 'chalk'; + +/** + * Print an informational message to stdout. + */ +export function info(message: string): void { + console.log(chalk.blue(`ℹ ${message}`)); +} + +/** + * Print a warning message to stdout. + */ +export function warn(message: string): void { + console.log(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 stdout. + */ +export function success(message: string): void { + console.log(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..591236f --- /dev/null +++ b/betterbase/packages/cli/src/utils/prompts.ts @@ -0,0 +1,80 @@ +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(), +}); + +/** + * 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..aef00ac --- /dev/null +++ b/betterbase/packages/cli/test/smoke.test.ts @@ -0,0 +1,16 @@ +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'); + }); +}); 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..cce3839 --- /dev/null +++ b/betterbase/packages/client/README.md @@ -0,0 +1,3 @@ +# @betterbase/client (Scaffold) + +Client SDK package placeholder. diff --git a/betterbase/packages/core/README.md b/betterbase/packages/core/README.md new file mode 100644 index 0000000..a0d42e1 --- /dev/null +++ b/betterbase/packages/core/README.md @@ -0,0 +1,3 @@ +# @betterbase/core (Scaffold) + +Core engine package placeholder. 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/templates/auth/README.md b/betterbase/templates/auth/README.md new file mode 100644 index 0000000..b744b15 --- /dev/null +++ b/betterbase/templates/auth/README.md @@ -0,0 +1,3 @@ +# Auth Template (Scaffold) + +Placeholder for BetterAuth template files. 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..31181e6 --- /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: 'sqlite://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..d096091 --- /dev/null +++ b/betterbase/templates/base/bun.lock @@ -0,0 +1,153 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "betterbase-base-template", + "dependencies": { + "drizzle-orm": "^0.36.4", + "hono": "^4.6.10", + "zod": "^3.23.8", + }, + "devDependencies": { + "@types/bun": "^1.3.9", + "drizzle-kit": "^0.27.2", + "typescript": "^5.6.0", + }, + }, + }, + "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.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], + + "@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=="], + + "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=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "drizzle-kit": ["drizzle-kit@0.27.2", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-F6cFZ1wxa9XzFyeeQsp/0/lIzUbDuQjS8/njpYBDWa+wdWmXuY+Z/X2hHFK/9PGHZkv3c9mER+mVWfKlp/B6Vw=="], + + "drizzle-orm": ["drizzle-orm@0.36.4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=3", "@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", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/react": ">=18", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "react": ">=18", "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/react", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "knex", "kysely", "mysql2", "pg", "postgres", "react", "sql.js", "sqlite3"] }, "sha512-1OZY3PXD7BR00Gl61UUOFihslDldfH4NFRH2MbP54Yxi0G/PKn4HfO65JYZ7c16DeP3SpM3Aw+VXVG9j6CRSXA=="], + + "esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], + + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + + "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], + + "hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "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=="], + + "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=="], + + "@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..3144f6d --- /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/routes/index.ts", + "db:generate": "drizzle-kit generate", + "db:push": "drizzle-kit push", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "hono": "^4.6.10", + "zod": "^3.23.8", + "drizzle-orm": "^0.36.4" + }, + "devDependencies": { + "@types/bun": "^1.3.9", + "drizzle-kit": "^0.27.2", + "typescript": "^5.6.0" + } +} diff --git a/betterbase/templates/base/src/db/index.ts b/betterbase/templates/base/src/db/index.ts new file mode 100644 index 0000000..e2a72b6 --- /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 ?? Bun.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/schema.ts b/betterbase/templates/base/src/db/schema.ts new file mode 100644 index 0000000..3a7f526 --- /dev/null +++ b/betterbase/templates/base/src/db/schema.ts @@ -0,0 +1,14 @@ +import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; + +export const users = sqliteTable('users', { + id: integer('id').primaryKey({ autoIncrement: true }), + email: text('email').notNull().unique(), + name: text('name').notNull(), + createdAt: integer('created_at', { mode: 'timestamp_ms' }) + .$defaultFn(() => new Date()) + .notNull(), + updatedAt: integer('updated_at', { mode: 'timestamp_ms' }) + .$defaultFn(() => new Date()) + .$onUpdate(() => new Date()) + .notNull(), +}); diff --git a/betterbase/templates/base/src/index.ts b/betterbase/templates/base/src/index.ts new file mode 100644 index 0000000..80be963 --- /dev/null +++ b/betterbase/templates/base/src/index.ts @@ -0,0 +1 @@ +import './routes/index'; 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..2330e7c --- /dev/null +++ b/betterbase/templates/base/src/middleware/validation.ts @@ -0,0 +1,28 @@ +import { HTTPException } from 'hono/http-exception'; +import type { ZodType } from 'zod'; +import { z } 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; +} + +// TODO: Placeholder schema for scaffolded user-creation routes. +export const createUserSchema = z.object({ + email: z.string().email(), + name: z.string().min(1), +}); diff --git a/betterbase/templates/base/src/routes/health.ts b/betterbase/templates/base/src/routes/health.ts new file mode 100644 index 0000000..c8995ca --- /dev/null +++ b/betterbase/templates/base/src/routes/health.ts @@ -0,0 +1,11 @@ +import { Hono } from 'hono'; + +export const healthRoute = new Hono(); + +healthRoute.get('/', (c) => { + return c.json({ + status: 'healthy', + database: 'connected', + timestamp: new Date().toISOString(), + }); +}); diff --git a/betterbase/templates/base/src/routes/index.ts b/betterbase/templates/base/src/routes/index.ts new file mode 100644 index 0000000..d5a0174 --- /dev/null +++ b/betterbase/templates/base/src/routes/index.ts @@ -0,0 +1,62 @@ +import { Hono } from 'hono'; +import { cors } from 'hono/cors'; +import { logger } from 'hono/logger'; +import { HTTPException } from 'hono/http-exception'; +import { db } from '../db'; +import { users } from '../db/schema'; +import { healthRoute } from './health'; +import { usersRoute } from './users'; + +const app = new Hono(); + +app.use('*', cors()); +app.use('*', logger()); +app.use('*', async (c, next) => { + const start = performance.now(); + await next(); + const duration = (performance.now() - start).toFixed(2); + console.log(`⏱ ${c.req.method} ${c.req.path} - ${duration}ms`); +}); + +app.onError((err, c) => { + console.error('Error:', err); + return c.json( + { + error: err.message, + stack: process.env.NODE_ENV === 'development' ? err.stack : undefined, + details: err instanceof HTTPException ? (err as { cause?: unknown }).cause ?? null : null, + }, + err instanceof HTTPException ? err.status : 500, + ); +}); + +app.route('/health', healthRoute); +app.route('/users', usersRoute); + +app.get('/api/users', async (c) => { + const allUsers = await db.select().from(users); + return c.json({ users: allUsers }); +}); + +const server = Bun.serve({ + fetch: app.fetch, + port: Number(process.env.PORT ?? 3000), + development: process.env.NODE_ENV === 'development', +}); + +console.log('\x1b[32m🚀 BetterBase dev server started\x1b[0m'); +console.log(`\x1b[36m→ URL:\x1b[0m http://localhost:${server.port}`); +console.log('\x1b[35m→ Routes:\x1b[0m'); +console.log(' GET /health'); +console.log(' GET /api/users'); +console.log(' POST /users'); + +process.on('SIGTERM', () => { + console.log('SIGTERM received, closing server...'); + server.stop(); +}); + +process.on('SIGINT', () => { + console.log('SIGINT received, closing server...'); + server.stop(); +}); diff --git a/betterbase/templates/base/src/routes/users.ts b/betterbase/templates/base/src/routes/users.ts new file mode 100644 index 0000000..8bf3acf --- /dev/null +++ b/betterbase/templates/base/src/routes/users.ts @@ -0,0 +1,32 @@ +import { Hono } from 'hono'; +import { HTTPException } from 'hono/http-exception'; +import { createUserSchema, parseBody } from '../middleware/validation'; + +const usersRoute = new Hono(); + +usersRoute.post('/', async (c) => { + try { + const body = await c.req.json(); + const parsed = parseBody(createUserSchema, body); + + return c.json( + { + message: 'User payload validated', + user: parsed, + }, + 201, + ); + } catch (error) { + if (error instanceof HTTPException) { + throw error; + } + + if (error instanceof SyntaxError) { + throw new HTTPException(400, { message: 'Malformed JSON body' }); + } + + throw new HTTPException(400, { message: 'Invalid request body' }); + } +}); + +export { usersRoute }; 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" + ] + } + } +}