diff --git a/AGENTS.md b/AGENTS.md index 07b9b70..3d6e459 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,7 +46,8 @@ BASE APP STRUCTURE: ## Repository Structure Guidance When scaffolding implementation, use: -- `betterbase/apps/cli` → `bb` CLI project +- `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` @@ -60,6 +61,7 @@ When scaffolding implementation, use: 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). diff --git a/betterbase/.gitignore b/betterbase/.gitignore index 61b3d42..8583250 100644 --- a/betterbase/.gitignore +++ b/betterbase/.gitignore @@ -5,4 +5,5 @@ dist .next .env .env.* +!.env.example .DS_Store diff --git a/betterbase/README.md b/betterbase/README.md index 5f0da13..46bf456 100644 --- a/betterbase/README.md +++ b/betterbase/README.md @@ -4,8 +4,9 @@ Initial BetterBase monorepo scaffold with a concrete base template. ## Structure -- `apps/cli` — `bb` CLI +- `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 @@ -20,7 +21,7 @@ Initial BetterBase monorepo scaffold with a concrete base template. ## Base Template Commands -From `betterbase/templates/base`: +From `templates/base`: - `bun run dev` - `bun run db:generate` 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 index ad34245..3432b23 100644 --- a/betterbase/apps/cli/package.json +++ b/betterbase/apps/cli/package.json @@ -1,13 +1,13 @@ { - "name": "@betterbase/cli", + "name": "@betterbase/cli-legacy", "version": "0.0.0", "private": true, "type": "module", "bin": { - "bb": "./dist/index.js" + "bb-legacy": "./dist/index.js" }, "scripts": { - "build": "tsc -p tsconfig.json", + "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 index f97b591..d74eb82 100644 --- a/betterbase/apps/cli/src/index.ts +++ b/betterbase/apps/cli/src/index.ts @@ -1,3 +1,15 @@ #!/usr/bin/env node -console.log('BetterBase CLI scaffold: bb'); +/** + * 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/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..bdcbc68 --- /dev/null +++ b/betterbase/packages/cli/src/commands/init.ts @@ -0,0 +1,410 @@ +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'); + +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: 'ok', timestamp: new Date().toISOString() }); +}); +`, + ); + + await writeFile( + path.join(projectPath, 'src/routes/index.ts'), + `import { Hono } from 'hono'; +import { logger } from 'hono/logger'; +import { healthRoute } from './health'; + +const app = new Hono(); + +app.use('*', logger()); +app.route('/health', healthRoute); + +export default { + port: Number(process.env.PORT || 3000), + fetch: app.fetch, +}; +`, + ); + + 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/shared/README.md b/betterbase/packages/shared/README.md index f9eb310..559303c 100644 --- a/betterbase/packages/shared/README.md +++ b/betterbase/packages/shared/README.md @@ -1,3 +1,34 @@ -# @betterbase/shared (Scaffold) +# @betterbase/shared -Shared utilities/types placeholder. +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/base/betterbase.config.ts b/betterbase/templates/base/betterbase.config.ts index 5eca416..31181e6 100644 --- a/betterbase/templates/base/betterbase.config.ts +++ b/betterbase/templates/base/betterbase.config.ts @@ -1,19 +1,25 @@ -export interface BetterBaseConfig { - aiContext: { - enabled: boolean; - outputFile: string; - }; - database: { - provider: 'sqlite' | 'postgres'; - }; -} +import { z } from 'zod'; -export const betterbaseConfig: BetterBaseConfig = { - aiContext: { - enabled: true, - outputFile: '.betterbase-context.json', - }, +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: { - provider: 'sqlite', + local: 'sqlite://local.db', + production: null, + }, + auth: { + enabled: true, }, -}; +}); diff --git a/betterbase/templates/base/src/db/index.ts b/betterbase/templates/base/src/db/index.ts index 71ceee9..8de0521 100644 --- a/betterbase/templates/base/src/db/index.ts +++ b/betterbase/templates/base/src/db/index.ts @@ -2,6 +2,7 @@ import { Database } from 'bun:sqlite'; import { drizzle } from 'drizzle-orm/bun-sqlite'; import * as schema from './schema'; -const sqlite = new Database('local.db'); +const dbPath = process.env.DB_PATH ?? Bun.env.DB_PATH ?? 'local.db'; +const sqlite = new Database(dbPath); export const db = drizzle(sqlite, { schema }); diff --git a/betterbase/templates/base/src/index.ts b/betterbase/templates/base/src/index.ts index 62d14fc..b024a27 100644 --- a/betterbase/templates/base/src/index.ts +++ b/betterbase/templates/base/src/index.ts @@ -18,15 +18,12 @@ app.get('/', (c) => { app.onError((error, c) => { if (error instanceof HTTPException) { - return new Response( - JSON.stringify({ + return c.json( + { error: error.message, details: (error as { cause?: unknown }).cause ?? null, - }), - { - status: error.status, - headers: { 'content-type': 'application/json; charset=UTF-8' }, }, + error.status, ); } diff --git a/betterbase/templates/base/src/routes/users.ts b/betterbase/templates/base/src/routes/users.ts index 6c15267..8bf3acf 100644 --- a/betterbase/templates/base/src/routes/users.ts +++ b/betterbase/templates/base/src/routes/users.ts @@ -1,19 +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) => { - const body = await c.req.json(); - const parsed = parseBody(createUserSchema, body); + try { + const body = await c.req.json(); + const parsed = parseBody(createUserSchema, body); - return c.json( - { - message: 'User payload validated', - user: parsed, - }, - 201, - ); + 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 };