vibecarbon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (495) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +188 -0
  3. package/carbon/.claude/hooks/task-completed-gate.sh +19 -0
  4. package/carbon/.claude/hooks/teammate-idle-gate.sh +29 -0
  5. package/carbon/.claude/settings.json +29 -0
  6. package/carbon/.cursor/rules/vibecarbon.mdc +6 -0
  7. package/carbon/.dockerignore +57 -0
  8. package/carbon/.env.example +219 -0
  9. package/carbon/.github/copilot-instructions.md +3 -0
  10. package/carbon/.github/workflows/deploy.yml +346 -0
  11. package/carbon/.github/workflows/vibecarbon-build.yml +81 -0
  12. package/carbon/.windsurfrules +74 -0
  13. package/carbon/AGENTS.md +422 -0
  14. package/carbon/CLAUDE.md +3 -0
  15. package/carbon/DEVELOPMENT.md +187 -0
  16. package/carbon/Dockerfile +98 -0
  17. package/carbon/LICENSE +21 -0
  18. package/carbon/PRODUCTION.md +364 -0
  19. package/carbon/README.md +193 -0
  20. package/carbon/TESTING.md +138 -0
  21. package/carbon/backup/Dockerfile +75 -0
  22. package/carbon/backup/backup.sh +95 -0
  23. package/carbon/backup/compose-backup.sh +140 -0
  24. package/carbon/biome.json +83 -0
  25. package/carbon/cloud-init/docker-ce-setup.yaml +55 -0
  26. package/carbon/cloud-init/k3s/master-init.sh +215 -0
  27. package/carbon/cloud-init/k3s/supabase-init.sh +167 -0
  28. package/carbon/cloud-init/k3s/worker-init.sh +147 -0
  29. package/carbon/components.json +24 -0
  30. package/carbon/content/blog/authentication-guide.mdx +34 -0
  31. package/carbon/content/blog/getting-started.mdx +41 -0
  32. package/carbon/content/changelog/v0-1-0.mdx +40 -0
  33. package/carbon/content/docs/analytics.mdx +90 -0
  34. package/carbon/content/docs/authentication.mdx +231 -0
  35. package/carbon/content/docs/background-jobs.mdx +116 -0
  36. package/carbon/content/docs/cli.mdx +630 -0
  37. package/carbon/content/docs/database.mdx +236 -0
  38. package/carbon/content/docs/deployment.mdx +227 -0
  39. package/carbon/content/docs/development.mdx +238 -0
  40. package/carbon/content/docs/environments.mdx +84 -0
  41. package/carbon/content/docs/getting-started.mdx +112 -0
  42. package/carbon/content/docs/legal/privacy-policy.mdx +89 -0
  43. package/carbon/content/docs/legal/terms-of-service.mdx +99 -0
  44. package/carbon/content/docs/optional-services.mdx +160 -0
  45. package/carbon/db/Dockerfile +23 -0
  46. package/carbon/docker-compose.dev-init.yml +5 -0
  47. package/carbon/docker-compose.metabase.override.yml +14 -0
  48. package/carbon/docker-compose.metabase.prod.yml +28 -0
  49. package/carbon/docker-compose.metabase.yml +84 -0
  50. package/carbon/docker-compose.n8n.override.yml +14 -0
  51. package/carbon/docker-compose.n8n.prod.yml +31 -0
  52. package/carbon/docker-compose.n8n.yml +97 -0
  53. package/carbon/docker-compose.observability.override.yml +18 -0
  54. package/carbon/docker-compose.observability.prod.yml +28 -0
  55. package/carbon/docker-compose.observability.yml +125 -0
  56. package/carbon/docker-compose.override.yml +28 -0
  57. package/carbon/docker-compose.prod.yml +294 -0
  58. package/carbon/docker-compose.yml +508 -0
  59. package/carbon/docker-entrypoint.sh +12 -0
  60. package/carbon/ha/activate-standby.sh +52 -0
  61. package/carbon/ha/primary-init.sql +36 -0
  62. package/carbon/ha/standby-init.sh +74 -0
  63. package/carbon/k8s/LICENSE +99 -0
  64. package/carbon/k8s/README.md +272 -0
  65. package/carbon/k8s/base/app/deployment.yaml +130 -0
  66. package/carbon/k8s/base/app/hpa.yaml +44 -0
  67. package/carbon/k8s/base/app/kustomization.yaml +10 -0
  68. package/carbon/k8s/base/app/network-policy.yaml +124 -0
  69. package/carbon/k8s/base/app/pdb.yaml +14 -0
  70. package/carbon/k8s/base/app/rbac.yaml +36 -0
  71. package/carbon/k8s/base/app/service.yaml +16 -0
  72. package/carbon/k8s/base/backup/cronjob.yaml +120 -0
  73. package/carbon/k8s/base/backup/kustomization.yaml +7 -0
  74. package/carbon/k8s/base/backup/network-policy.yaml +31 -0
  75. package/carbon/k8s/base/backup/rbac.yaml +7 -0
  76. package/carbon/k8s/base/cluster-autoscaler/deployment.yaml +103 -0
  77. package/carbon/k8s/base/cluster-autoscaler/kustomization.yaml +17 -0
  78. package/carbon/k8s/base/cluster-autoscaler/rbac.yaml +109 -0
  79. package/carbon/k8s/base/config/configmap.yaml +48 -0
  80. package/carbon/k8s/base/config/kustomization.yaml +8 -0
  81. package/carbon/k8s/base/hetzner-ccm/kustomization.yaml +9 -0
  82. package/carbon/k8s/base/hetzner-csi/kustomization.yaml +9 -0
  83. package/carbon/k8s/base/kustomization.yaml +43 -0
  84. package/carbon/k8s/base/namespace.yaml +7 -0
  85. package/carbon/k8s/base/network-policies.yaml +95 -0
  86. package/carbon/k8s/base/registry/kustomization.yaml +5 -0
  87. package/carbon/k8s/base/registry/local-registry.yaml +193 -0
  88. package/carbon/k8s/base/traefik/certificate.yaml +21 -0
  89. package/carbon/k8s/base/traefik/configmap.yaml +13 -0
  90. package/carbon/k8s/base/traefik/deployment.yaml +165 -0
  91. package/carbon/k8s/base/traefik/ingressroute.yaml +141 -0
  92. package/carbon/k8s/base/traefik/kustomization.yaml +11 -0
  93. package/carbon/k8s/base/traefik/middleware.yaml +109 -0
  94. package/carbon/k8s/base/traefik/network-policy.yaml +92 -0
  95. package/carbon/k8s/base/traefik/service.yaml +38 -0
  96. package/carbon/k8s/flux/README.md +49 -0
  97. package/carbon/k8s/flux/clusters/primary/vibecarbon.yaml +128 -0
  98. package/carbon/k8s/flux/clusters/standby/vibecarbon.yaml +83 -0
  99. package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-release.yaml +38 -0
  100. package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-repository.yaml +16 -0
  101. package/carbon/k8s/gitops/cert-manager-webhook-hetzner/kustomization.yaml +10 -0
  102. package/carbon/k8s/gitops/supabase/helm-release.yaml +66 -0
  103. package/carbon/k8s/gitops/supabase/helm-repository.yaml +18 -0
  104. package/carbon/k8s/gitops/supabase/kustomization.yaml +33 -0
  105. package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-cloudflare.yaml +51 -0
  106. package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-hetzner.yaml +59 -0
  107. package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-manual.yaml +43 -0
  108. package/carbon/k8s/infra/cert-manager-resources/kustomization.yaml +15 -0
  109. package/carbon/k8s/infra/kustomization.yaml +21 -0
  110. package/carbon/k8s/infra/traefik-crds/kustomization.yaml +10 -0
  111. package/carbon/k8s/overlays/local/configmap.yaml +23 -0
  112. package/carbon/k8s/overlays/local/ingressroute-studio.yaml +104 -0
  113. package/carbon/k8s/overlays/local/kustomization.yaml +215 -0
  114. package/carbon/k8s/overlays/local/namespace.yaml +8 -0
  115. package/carbon/k8s/overlays/local/secrets.yaml +32 -0
  116. package/carbon/k8s/overlays/production/kustomization.yaml +34 -0
  117. package/carbon/k8s/test-local.sh +318 -0
  118. package/carbon/k8s/values/supabase.values.yaml +133 -0
  119. package/carbon/package.json +154 -0
  120. package/carbon/runtime.Dockerfile +17 -0
  121. package/carbon/scripts/_dev-jwt.mjs +45 -0
  122. package/carbon/scripts/check-rls.ts +53 -0
  123. package/carbon/scripts/dev-init.js +191 -0
  124. package/carbon/scripts/dev.js +191 -0
  125. package/carbon/scripts/docker-down.js +63 -0
  126. package/carbon/scripts/docker-logs.js +59 -0
  127. package/carbon/scripts/docker-up.js +222 -0
  128. package/carbon/scripts/generate-dev-configs.sh +131 -0
  129. package/carbon/scripts/generate-rss.ts +116 -0
  130. package/carbon/scripts/generate-sitemap.ts +102 -0
  131. package/carbon/scripts/k8s-apply.js +71 -0
  132. package/carbon/scripts/k8s-delete.js +75 -0
  133. package/carbon/scripts/lib/manifest.js +176 -0
  134. package/carbon/scripts/secret-scan.mjs +278 -0
  135. package/carbon/scripts/validate-dev-configs.sh +101 -0
  136. package/carbon/src/client/App.tsx +202 -0
  137. package/carbon/src/client/assets/hyperformant-dark.svg +29 -0
  138. package/carbon/src/client/assets/hyperformant-light.svg +29 -0
  139. package/carbon/src/client/assets/logos/aider.svg +1 -0
  140. package/carbon/src/client/assets/logos/antigravity.svg +1 -0
  141. package/carbon/src/client/assets/logos/bolt.svg +1 -0
  142. package/carbon/src/client/assets/logos/claude-code.svg +1 -0
  143. package/carbon/src/client/assets/logos/copilot.svg +1 -0
  144. package/carbon/src/client/assets/logos/cursor.svg +1 -0
  145. package/carbon/src/client/assets/logos/gemini-cli.svg +1 -0
  146. package/carbon/src/client/assets/logos/lovable.svg +1 -0
  147. package/carbon/src/client/assets/logos/openai-codex.svg +1 -0
  148. package/carbon/src/client/assets/logos/v0.svg +1 -0
  149. package/carbon/src/client/assets/logos/windsurf.svg +1 -0
  150. package/carbon/src/client/assets/vibecarbon-icon.svg +19 -0
  151. package/carbon/src/client/assets/vibecarbon-logo-dark.svg +29 -0
  152. package/carbon/src/client/assets/vibecarbon-logo-light.svg +29 -0
  153. package/carbon/src/client/components/AIIntegrationSection.tsx +120 -0
  154. package/carbon/src/client/components/AppSidebar.tsx +760 -0
  155. package/carbon/src/client/components/ArchitectureSection.tsx +46 -0
  156. package/carbon/src/client/components/CTAFooter.tsx +59 -0
  157. package/carbon/src/client/components/ComparisonSection.tsx +132 -0
  158. package/carbon/src/client/components/ContentPanel.tsx +66 -0
  159. package/carbon/src/client/components/ContentSkeleton.tsx +21 -0
  160. package/carbon/src/client/components/ErrorBoundary.tsx +46 -0
  161. package/carbon/src/client/components/FAQSection.tsx +76 -0
  162. package/carbon/src/client/components/FileUpload.tsx +210 -0
  163. package/carbon/src/client/components/HeaderActions.tsx +17 -0
  164. package/carbon/src/client/components/Hero.tsx +608 -0
  165. package/carbon/src/client/components/ImpersonationBanner.tsx +31 -0
  166. package/carbon/src/client/components/LanguageSwitcher.tsx +67 -0
  167. package/carbon/src/client/components/Logo.tsx +87 -0
  168. package/carbon/src/client/components/LogoStrip.tsx +76 -0
  169. package/carbon/src/client/components/MetricsStrip.tsx +48 -0
  170. package/carbon/src/client/components/Nav.tsx +195 -0
  171. package/carbon/src/client/components/NewsletterSignup.tsx +147 -0
  172. package/carbon/src/client/components/NotificationDrawer.tsx +171 -0
  173. package/carbon/src/client/components/PageHeader.tsx +24 -0
  174. package/carbon/src/client/components/PlanGate.tsx +36 -0
  175. package/carbon/src/client/components/PricingSection.tsx +371 -0
  176. package/carbon/src/client/components/SEO.tsx +34 -0
  177. package/carbon/src/client/components/SmoothScroll.tsx +45 -0
  178. package/carbon/src/client/components/TechStackSection.tsx +165 -0
  179. package/carbon/src/client/components/WorkflowSection.tsx +604 -0
  180. package/carbon/src/client/components/admin/DockerLogs.tsx +276 -0
  181. package/carbon/src/client/components/admin/PerformanceMetrics.tsx +230 -0
  182. package/carbon/src/client/components/admin/ServiceCard.tsx +45 -0
  183. package/carbon/src/client/components/admin/ServicesStatus.tsx +296 -0
  184. package/carbon/src/client/components/architecture/ArchitectureDiagram.tsx +413 -0
  185. package/carbon/src/client/components/auth/AuthProvider.tsx +466 -0
  186. package/carbon/src/client/components/effects/FilmGrainOverlay.tsx +35 -0
  187. package/carbon/src/client/components/effects/FresnelEdge.tsx +49 -0
  188. package/carbon/src/client/components/effects/GlowTracker.tsx +46 -0
  189. package/carbon/src/client/components/effects/SparkBurst.tsx +145 -0
  190. package/carbon/src/client/components/effects/VGlowEffect.tsx +83 -0
  191. package/carbon/src/client/components/effects/index.ts +2 -0
  192. package/carbon/src/client/components/layouts/SidebarLayout.tsx +20 -0
  193. package/carbon/src/client/components/scroll/ScrollSection.tsx +76 -0
  194. package/carbon/src/client/components/scroll/ScrollytellingProvider.tsx +81 -0
  195. package/carbon/src/client/components/scroll/index.ts +2 -0
  196. package/carbon/src/client/components/ui/accordion.tsx +71 -0
  197. package/carbon/src/client/components/ui/alert-dialog.tsx +162 -0
  198. package/carbon/src/client/components/ui/alert.tsx +73 -0
  199. package/carbon/src/client/components/ui/aspect-ratio.tsx +22 -0
  200. package/carbon/src/client/components/ui/avatar.tsx +91 -0
  201. package/carbon/src/client/components/ui/badge.tsx +50 -0
  202. package/carbon/src/client/components/ui/breadcrumb.tsx +100 -0
  203. package/carbon/src/client/components/ui/button-group.tsx +78 -0
  204. package/carbon/src/client/components/ui/button.tsx +120 -0
  205. package/carbon/src/client/components/ui/calendar.tsx +182 -0
  206. package/carbon/src/client/components/ui/card.tsx +85 -0
  207. package/carbon/src/client/components/ui/carousel.tsx +227 -0
  208. package/carbon/src/client/components/ui/chart.tsx +357 -0
  209. package/carbon/src/client/components/ui/checkbox.tsx +27 -0
  210. package/carbon/src/client/components/ui/collapsible.tsx +15 -0
  211. package/carbon/src/client/components/ui/command.tsx +178 -0
  212. package/carbon/src/client/components/ui/context-menu.tsx +233 -0
  213. package/carbon/src/client/components/ui/dialog.tsx +132 -0
  214. package/carbon/src/client/components/ui/drawer.tsx +118 -0
  215. package/carbon/src/client/components/ui/dropdown-menu.tsx +242 -0
  216. package/carbon/src/client/components/ui/empty.tsx +94 -0
  217. package/carbon/src/client/components/ui/field.tsx +226 -0
  218. package/carbon/src/client/components/ui/hover-card.tsx +44 -0
  219. package/carbon/src/client/components/ui/input-group.tsx +146 -0
  220. package/carbon/src/client/components/ui/input-otp.tsx +83 -0
  221. package/carbon/src/client/components/ui/input.tsx +20 -0
  222. package/carbon/src/client/components/ui/item.tsx +188 -0
  223. package/carbon/src/client/components/ui/kbd.tsx +26 -0
  224. package/carbon/src/client/components/ui/label.tsx +21 -0
  225. package/carbon/src/client/components/ui/menubar.tsx +250 -0
  226. package/carbon/src/client/components/ui/navigation-menu.tsx +155 -0
  227. package/carbon/src/client/components/ui/pagination.tsx +102 -0
  228. package/carbon/src/client/components/ui/popover.tsx +75 -0
  229. package/carbon/src/client/components/ui/progress.tsx +66 -0
  230. package/carbon/src/client/components/ui/radio-group.tsx +36 -0
  231. package/carbon/src/client/components/ui/resizable.tsx +46 -0
  232. package/carbon/src/client/components/ui/scroll-area.tsx +48 -0
  233. package/carbon/src/client/components/ui/select.tsx +186 -0
  234. package/carbon/src/client/components/ui/separator.tsx +21 -0
  235. package/carbon/src/client/components/ui/sheet.tsx +124 -0
  236. package/carbon/src/client/components/ui/sidebar.tsx +702 -0
  237. package/carbon/src/client/components/ui/skeleton.tsx +13 -0
  238. package/carbon/src/client/components/ui/slider.tsx +57 -0
  239. package/carbon/src/client/components/ui/sonner.tsx +45 -0
  240. package/carbon/src/client/components/ui/spinner.tsx +15 -0
  241. package/carbon/src/client/components/ui/switch.tsx +30 -0
  242. package/carbon/src/client/components/ui/table.tsx +89 -0
  243. package/carbon/src/client/components/ui/tabs.tsx +73 -0
  244. package/carbon/src/client/components/ui/textarea.tsx +18 -0
  245. package/carbon/src/client/components/ui/toggle-group.tsx +87 -0
  246. package/carbon/src/client/components/ui/toggle.tsx +44 -0
  247. package/carbon/src/client/components/ui/tooltip.tsx +56 -0
  248. package/carbon/src/client/hooks/api/index.ts +14 -0
  249. package/carbon/src/client/hooks/api/useAuthSettings.ts +80 -0
  250. package/carbon/src/client/hooks/api/useSubscription.ts +87 -0
  251. package/carbon/src/client/hooks/use-mobile.ts +21 -0
  252. package/carbon/src/client/hooks/useMousePosition.ts +91 -0
  253. package/carbon/src/client/hooks/useNotifications.tsx +124 -0
  254. package/carbon/src/client/hooks/useOrganizationMembers.ts +127 -0
  255. package/carbon/src/client/hooks/useOrganizations.tsx +230 -0
  256. package/carbon/src/client/hooks/usePackageManager.ts +69 -0
  257. package/carbon/src/client/hooks/useReducedMotion.ts +20 -0
  258. package/carbon/src/client/hooks/useRunningServices.ts +114 -0
  259. package/carbon/src/client/hooks/useScrollProgress.ts +56 -0
  260. package/carbon/src/client/index.css +467 -0
  261. package/carbon/src/client/index.html +56 -0
  262. package/carbon/src/client/lib/admin-services.ts +151 -0
  263. package/carbon/src/client/lib/api.ts +32 -0
  264. package/carbon/src/client/lib/blog.ts +35 -0
  265. package/carbon/src/client/lib/changelog.ts +33 -0
  266. package/carbon/src/client/lib/docs-search.ts +101 -0
  267. package/carbon/src/client/lib/docs.ts +37 -0
  268. package/carbon/src/client/lib/i18n.ts +32 -0
  269. package/carbon/src/client/lib/supabase.ts +72 -0
  270. package/carbon/src/client/lib/tailwind-colors.ts +357 -0
  271. package/carbon/src/client/lib/theme.ts +117 -0
  272. package/carbon/src/client/lib/utils.ts +22 -0
  273. package/carbon/src/client/locales/de.json +529 -0
  274. package/carbon/src/client/locales/en.json +461 -0
  275. package/carbon/src/client/locales/es.json +529 -0
  276. package/carbon/src/client/locales/fr.json +529 -0
  277. package/carbon/src/client/locales/pt.json +529 -0
  278. package/carbon/src/client/main.tsx +56 -0
  279. package/carbon/src/client/mdx.d.ts +13 -0
  280. package/carbon/src/client/pages/ApiDocs.tsx +76 -0
  281. package/carbon/src/client/pages/AuthCallback.tsx +34 -0
  282. package/carbon/src/client/pages/Blog.tsx +167 -0
  283. package/carbon/src/client/pages/Changelog.tsx +171 -0
  284. package/carbon/src/client/pages/Charts.tsx +388 -0
  285. package/carbon/src/client/pages/Checkout.tsx +227 -0
  286. package/carbon/src/client/pages/Contact.tsx +174 -0
  287. package/carbon/src/client/pages/Dashboard.tsx +368 -0
  288. package/carbon/src/client/pages/Docs.tsx +372 -0
  289. package/carbon/src/client/pages/ForgotPassword.tsx +111 -0
  290. package/carbon/src/client/pages/Home.tsx +187 -0
  291. package/carbon/src/client/pages/Legal.tsx +100 -0
  292. package/carbon/src/client/pages/Login.tsx +408 -0
  293. package/carbon/src/client/pages/MFAVerify.tsx +156 -0
  294. package/carbon/src/client/pages/NotFound.tsx +21 -0
  295. package/carbon/src/client/pages/Onboarding.tsx +246 -0
  296. package/carbon/src/client/pages/ResetPassword.tsx +200 -0
  297. package/carbon/src/client/pages/UIComponents.tsx +390 -0
  298. package/carbon/src/client/pages/admin/ContactSubmissions.tsx +220 -0
  299. package/carbon/src/client/pages/admin/Dashboard.tsx +24 -0
  300. package/carbon/src/client/pages/admin/Infrastructure.tsx +257 -0
  301. package/carbon/src/client/pages/admin/Jobs.tsx +225 -0
  302. package/carbon/src/client/pages/admin/Logs.tsx +18 -0
  303. package/carbon/src/client/pages/admin/Newsletter.tsx +300 -0
  304. package/carbon/src/client/pages/admin/Notifications.tsx +603 -0
  305. package/carbon/src/client/pages/admin/Organizations.tsx +306 -0
  306. package/carbon/src/client/pages/admin/Settings.tsx +314 -0
  307. package/carbon/src/client/pages/admin/Theme.tsx +465 -0
  308. package/carbon/src/client/pages/admin/Users.tsx +365 -0
  309. package/carbon/src/client/pages/api-docs.css +156 -0
  310. package/carbon/src/client/pages/organizations/Details.tsx +200 -0
  311. package/carbon/src/client/pages/organizations/Members.tsx +402 -0
  312. package/carbon/src/client/pages/settings/Billing.tsx +473 -0
  313. package/carbon/src/client/pages/settings/Profile.tsx +160 -0
  314. package/carbon/src/client/pages/settings/Security.tsx +341 -0
  315. package/carbon/src/client/public/favicon.svg +19 -0
  316. package/carbon/src/client/public/robots.txt +8 -0
  317. package/carbon/src/server/billing/index.ts +104 -0
  318. package/carbon/src/server/billing/provider.ts +81 -0
  319. package/carbon/src/server/billing/providers/paddle.ts +314 -0
  320. package/carbon/src/server/billing/providers/polar.ts +325 -0
  321. package/carbon/src/server/billing/providers/stripe.ts +233 -0
  322. package/carbon/src/server/emails/templates.ts +116 -0
  323. package/carbon/src/server/index.ts +554 -0
  324. package/carbon/src/server/lib/auth.ts +6 -0
  325. package/carbon/src/server/lib/email.ts +64 -0
  326. package/carbon/src/server/lib/env.ts +112 -0
  327. package/carbon/src/server/lib/errors.ts +21 -0
  328. package/carbon/src/server/lib/logger.ts +17 -0
  329. package/carbon/src/server/lib/rate-limiter.ts +288 -0
  330. package/carbon/src/server/lib/request.ts +34 -0
  331. package/carbon/src/server/lib/stripe.ts +42 -0
  332. package/carbon/src/server/lib/supabase.ts +65 -0
  333. package/carbon/src/server/middleware/requirePlan.ts +80 -0
  334. package/carbon/src/server/routes/_internal/services-status.ts +958 -0
  335. package/carbon/src/server/routes/_internal/verify-role.ts +185 -0
  336. package/carbon/src/server/routes/health.ts +48 -0
  337. package/carbon/src/server/routes/v1/admin/contact.ts +128 -0
  338. package/carbon/src/server/routes/v1/admin/jobs.ts +171 -0
  339. package/carbon/src/server/routes/v1/admin/newsletter.ts +237 -0
  340. package/carbon/src/server/routes/v1/auth.ts +390 -0
  341. package/carbon/src/server/routes/v1/billing.ts +718 -0
  342. package/carbon/src/server/routes/v1/contact.ts +93 -0
  343. package/carbon/src/server/routes/v1/index.ts +1333 -0
  344. package/carbon/src/server/routes/v1/newsletter.ts +181 -0
  345. package/carbon/src/server/routes/v1/performance.ts +157 -0
  346. package/carbon/src/server/routes/v1/stats.ts +170 -0
  347. package/carbon/src/server/routes/v1/theme.ts +106 -0
  348. package/carbon/src/server/routes/webhooks/billing.ts +376 -0
  349. package/carbon/src/server/routes/webhooks/stripe.ts +276 -0
  350. package/carbon/src/server/types.ts +11 -0
  351. package/carbon/src/shared/pricing.ts +155 -0
  352. package/carbon/src/shared/types.ts +338 -0
  353. package/carbon/supabase/migrations/00001_init.sql +717 -0
  354. package/carbon/supabase/migrations/00002_theme_settings.sql +13 -0
  355. package/carbon/supabase/migrations/00003_pg_cron.sql +121 -0
  356. package/carbon/supabase/migrations/00004_contact_newsletter.sql +81 -0
  357. package/carbon/supabase/migrations/00005_localization_languages.sql +22 -0
  358. package/carbon/supabase/seed.sql +16 -0
  359. package/carbon/tests/_helpers/app.ts +45 -0
  360. package/carbon/tests/_helpers/env.ts +37 -0
  361. package/carbon/tests/_helpers/factories.ts +69 -0
  362. package/carbon/tests/_helpers/jwt.ts +23 -0
  363. package/carbon/tests/_helpers/setup-integration.ts +20 -0
  364. package/carbon/tests/_helpers/setup-rtl.ts +12 -0
  365. package/carbon/tests/component/ErrorBoundary.test.tsx +53 -0
  366. package/carbon/tests/component/use-auth-settings.test.tsx +119 -0
  367. package/carbon/tests/integration/server/routes/contact.test.ts +162 -0
  368. package/carbon/tests/integration/server/routes/health.test.ts +61 -0
  369. package/carbon/tests/structural/i18n-parity.test.ts +42 -0
  370. package/carbon/tests/unit/client/utils.test.ts +49 -0
  371. package/carbon/tests/unit/shared/pricing.test.ts +93 -0
  372. package/carbon/tsconfig.json +27 -0
  373. package/carbon/tsconfig.server.json +27 -0
  374. package/carbon/tsconfig.test.json +9 -0
  375. package/carbon/vite.config.ts +110 -0
  376. package/carbon/vitest.config.ts +74 -0
  377. package/carbon/volumes/db/jwt.sql +57 -0
  378. package/carbon/volumes/db/metabase-init.sh +29 -0
  379. package/carbon/volumes/db/n8n-init.sh +25 -0
  380. package/carbon/volumes/db/realtime.sql +33 -0
  381. package/carbon/volumes/db/roles.sql +93 -0
  382. package/carbon/volumes/db/set-passwords.sh +12 -0
  383. package/carbon/volumes/db/super-admin.dev.sql +113 -0
  384. package/carbon/volumes/db/super-admin.generated.sql +113 -0
  385. package/carbon/volumes/db/super-admin.sql +114 -0
  386. package/carbon/volumes/grafana/dashboards/logs.json +179 -0
  387. package/carbon/volumes/grafana/dashboards/overview.json +523 -0
  388. package/carbon/volumes/grafana/dashboards/postgresql.json +337 -0
  389. package/carbon/volumes/grafana/dashboards.dev/logs.json +179 -0
  390. package/carbon/volumes/grafana/dashboards.dev/overview.json +156 -0
  391. package/carbon/volumes/grafana/dashboards.dev/postgresql.json +337 -0
  392. package/carbon/volumes/grafana/provisioning/dashboards/dashboards.dev.yml +16 -0
  393. package/carbon/volumes/grafana/provisioning/dashboards/dashboards.yml +16 -0
  394. package/carbon/volumes/grafana/provisioning/datasources/datasources.yml +43 -0
  395. package/carbon/volumes/kong/docker-entrypoint.sh +9 -0
  396. package/carbon/volumes/kong/kong.yml +208 -0
  397. package/carbon/volumes/loki/loki-config.yml +58 -0
  398. package/carbon/volumes/n8n/hooks.js +131 -0
  399. package/carbon/volumes/n8n/scripts/setup.sh +66 -0
  400. package/carbon/volumes/prometheus/prometheus.yml +43 -0
  401. package/carbon/volumes/promtail/promtail-config.yml +64 -0
  402. package/carbon/volumes/traefik/middlewares.dev.yml +83 -0
  403. package/carbon/volumes/traefik/middlewares.yml +95 -0
  404. package/carbon/volumes/traefik/vite-dev.yml +20 -0
  405. package/package.json +95 -0
  406. package/src/access.js +354 -0
  407. package/src/activate.js +187 -0
  408. package/src/add.js +718 -0
  409. package/src/backup.js +786 -0
  410. package/src/cli.js +350 -0
  411. package/src/configure.js +967 -0
  412. package/src/console.js +155 -0
  413. package/src/create.js +1499 -0
  414. package/src/deploy.js +311 -0
  415. package/src/destroy.js +2033 -0
  416. package/src/diagnose.js +735 -0
  417. package/src/down.js +80 -0
  418. package/src/failover.js +1032 -0
  419. package/src/lib/backup-s3.js +179 -0
  420. package/src/lib/build.js +33 -0
  421. package/src/lib/checksum.js +28 -0
  422. package/src/lib/ci-setup.js +666 -0
  423. package/src/lib/cli/help.js +129 -0
  424. package/src/lib/cli/parse-flags.js +160 -0
  425. package/src/lib/cli/select-action.js +65 -0
  426. package/src/lib/cli/select-environment.js +108 -0
  427. package/src/lib/cli/tty-guard.js +75 -0
  428. package/src/lib/cloudflare.js +447 -0
  429. package/src/lib/colors.js +52 -0
  430. package/src/lib/command.js +361 -0
  431. package/src/lib/config.js +359 -0
  432. package/src/lib/cost.js +103 -0
  433. package/src/lib/deploy/bundle.js +231 -0
  434. package/src/lib/deploy/compose/acme-verify.js +121 -0
  435. package/src/lib/deploy/compose/build-args.js +78 -0
  436. package/src/lib/deploy/compose/ha.js +1874 -0
  437. package/src/lib/deploy/compose/index.js +1294 -0
  438. package/src/lib/deploy/compose/reconcile.js +74 -0
  439. package/src/lib/deploy/github.js +382 -0
  440. package/src/lib/deploy/image.js +191 -0
  441. package/src/lib/deploy/index.js +119 -0
  442. package/src/lib/deploy/k8s/LICENSE +99 -0
  443. package/src/lib/deploy/k8s/gitops-deploy.js +298 -0
  444. package/src/lib/deploy/k8s/ha/index.js +1000 -0
  445. package/src/lib/deploy/k8s/index.js +62 -0
  446. package/src/lib/deploy/k8s/k3s.js +2515 -0
  447. package/src/lib/deploy/orchestrator.js +1401 -0
  448. package/src/lib/deploy/prompts.js +545 -0
  449. package/src/lib/deploy/remote-build.js +130 -0
  450. package/src/lib/deploy/state.js +120 -0
  451. package/src/lib/deploy/utils.js +328 -0
  452. package/src/lib/deploy-logger.js +93 -0
  453. package/src/lib/dns-propagation.js +48 -0
  454. package/src/lib/environment.js +58 -0
  455. package/src/lib/fetch-retry.js +103 -0
  456. package/src/lib/github-environments.js +335 -0
  457. package/src/lib/hetzner-dns.js +377 -0
  458. package/src/lib/hetzner-guided-setup.js +275 -0
  459. package/src/lib/host-keys.js +37 -0
  460. package/src/lib/iac/cloud-init.js +52 -0
  461. package/src/lib/iac/index.js +325 -0
  462. package/src/lib/iac/programs/hetzner-compose.js +130 -0
  463. package/src/lib/iac/programs/hetzner-k8s.js +320 -0
  464. package/src/lib/kubectl.js +81 -0
  465. package/src/lib/licensing/index.js +259 -0
  466. package/src/lib/licensing/tiers.js +181 -0
  467. package/src/lib/licensing/validator.js +171 -0
  468. package/src/lib/merge-package-json.js +90 -0
  469. package/src/lib/operator-ip.js +381 -0
  470. package/src/lib/package-manager.js +111 -0
  471. package/src/lib/perf.js +71 -0
  472. package/src/lib/project-guard.js +39 -0
  473. package/src/lib/project.js +334 -0
  474. package/src/lib/providers/base.js +276 -0
  475. package/src/lib/providers/hetzner-s3.js +656 -0
  476. package/src/lib/providers/hetzner.js +755 -0
  477. package/src/lib/providers/index.js +164 -0
  478. package/src/lib/s3.js +510 -0
  479. package/src/lib/secret-scan.js +583 -0
  480. package/src/lib/secrets.js +63 -0
  481. package/src/lib/server-types.js +195 -0
  482. package/src/lib/shell.js +91 -0
  483. package/src/lib/ssh.js +241 -0
  484. package/src/lib/tracker.js +242 -0
  485. package/src/lib/upgrade-policy.js +170 -0
  486. package/src/lib/validators.js +105 -0
  487. package/src/lib/version.js +5 -0
  488. package/src/remove.js +292 -0
  489. package/src/reset.js +97 -0
  490. package/src/restore.js +871 -0
  491. package/src/scale.js +1734 -0
  492. package/src/shell.js +222 -0
  493. package/src/status.js +981 -0
  494. package/src/up.js +264 -0
  495. package/src/upgrade.js +721 -0
package/src/create.js ADDED
@@ -0,0 +1,1499 @@
1
+ /**
2
+ * Vibecarbon Create Command
3
+ * Scaffolds a complete Hono + Vite + React 19 + Supabase self-hosted stack
4
+ *
5
+ * Usage:
6
+ * vibecarbon create my-app # Create new project
7
+ * vibecarbon create my-app -y # Skip prompts, use defaults
8
+ * vibecarbon create my-app --use-npm # Use npm instead of pnpm
9
+ * vibecarbon create --help # Show help
10
+ */
11
+
12
+ import { execFileSync } from 'node:child_process';
13
+ import {
14
+ chmodSync,
15
+ copyFileSync,
16
+ existsSync,
17
+ mkdirSync,
18
+ readdirSync,
19
+ readFileSync,
20
+ writeFileSync,
21
+ } from 'node:fs';
22
+ import { dirname, join } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+ import * as p from '@clack/prompts';
25
+ import { hashContent } from './lib/checksum.js';
26
+ import { renderHelp } from './lib/cli/help.js';
27
+ import { parseFlags } from './lib/cli/parse-flags.js';
28
+ import { c, printBanner } from './lib/colors.js';
29
+ import { runCommand } from './lib/command.js';
30
+ import { registerProject } from './lib/config.js';
31
+ import {
32
+ adaptDockerfileForPackageManager,
33
+ getPackageManagerVersion,
34
+ } from './lib/package-manager.js';
35
+ import { saveManifest, validateGitignore } from './lib/project.js';
36
+ import {
37
+ generateJWT,
38
+ generatePassword,
39
+ generateReplPassword,
40
+ hashPassword,
41
+ } from './lib/secrets.js';
42
+ import { escapeDotenv } from './lib/shell.js';
43
+ import { createTracker } from './lib/tracker.js';
44
+ import { getUpgradeableFiles } from './lib/upgrade-policy.js';
45
+ import {
46
+ validateAdminEmail,
47
+ validateAdminPassword,
48
+ validateProjectName,
49
+ } from './lib/validators.js';
50
+ import { VERSION } from './lib/version.js';
51
+
52
+ const __filename = fileURLToPath(import.meta.url);
53
+ const __dirname = dirname(__filename);
54
+ const TEMPLATE_DIR = join(__dirname, '..', 'carbon');
55
+
56
+ // Launch decision (2026-05-06): hide the interactive package-manager
57
+ // prompt and default to pnpm. The bun + npm code paths work — adapter
58
+ // rewrites the Dockerfile correctly, lockfile gets generated at create
59
+ // time — but pnpm has by far the most e2e-matrix coverage. Power
60
+ // users can still pick npm or bun via `--use-npm` / `--use-bun`. Flip
61
+ // this to `true` to bring the prompt back once bun/npm have matrix
62
+ // coverage parity.
63
+ const SHOW_PACKAGE_MANAGER_PROMPT = false;
64
+
65
+ // Template variable placeholders
66
+ const PLACEHOLDERS = {
67
+ PROJECT_NAME: '{{PROJECT_NAME}}',
68
+ GITHUB_OWNER: '{{GITHUB_OWNER}}',
69
+ DB_PASSWORD: '{{DB_PASSWORD}}',
70
+ JWT_SECRET: '{{JWT_SECRET}}',
71
+ ANON_KEY: '{{ANON_KEY}}',
72
+ SERVICE_ROLE_KEY: '{{SERVICE_ROLE_KEY}}',
73
+ REALTIME_SECRET: '{{REALTIME_SECRET}}',
74
+ LOGFLARE_API_KEY: '{{LOGFLARE_API_KEY}}',
75
+ VAULT_ENC_KEY: '{{VAULT_ENC_KEY}}',
76
+ PG_META_CRYPTO_KEY: '{{PG_META_CRYPTO_KEY}}',
77
+ DB_ENC_KEY: '{{DB_ENC_KEY}}',
78
+ REPL_PASSWORD: '{{REPL_PASSWORD}}',
79
+ SITE_URL: '{{SITE_URL}}',
80
+ ADMIN_EMAIL: '{{ADMIN_EMAIL}}',
81
+ ADMIN_PASSWORD: '{{ADMIN_PASSWORD}}',
82
+ ADMIN_PASSWORD_HASH: '{{ADMIN_PASSWORD_HASH}}',
83
+ };
84
+
85
+ // Placeholders that are NEVER substituted into checked-in template files at
86
+ // create time. Their values land in `.env.local` (gitignored) via direct
87
+ // dotenv writes; the deploy code (src/lib/deploy/**) patches them into k8s
88
+ // manifests at deploy time. Substituting them here would bake a high-entropy
89
+ // secret into a file that lands in `git ls-files` and trips the preflight
90
+ // secret-scan during the next `vibecarbon deploy` — the exact regression
91
+ // fixed by this set (k8s/values/supabase.values.yaml ADMIN_PASSWORD).
92
+ //
93
+ // ADMIN_PASSWORD_HASH is intentionally NOT in this set: volumes/db/super-admin.sql
94
+ // is a Postgres init script with no deploy-time substitution layer, so its
95
+ // bcrypt hash must be substituted at create time for first-boot admin user
96
+ // creation to work. The scanner does not match `password_hash :=` (its regex
97
+ // requires `password\s*[:=]`, not `password_hash\s*:=`), so a hash leak there
98
+ // is benign for the scanner. If a future template adds a hash to another
99
+ // file, treat it as a separate fix.
100
+ const SECRET_PLACEHOLDERS = new Set([
101
+ 'DB_PASSWORD',
102
+ 'JWT_SECRET',
103
+ 'ANON_KEY',
104
+ 'SERVICE_ROLE_KEY',
105
+ 'REALTIME_SECRET',
106
+ 'LOGFLARE_API_KEY',
107
+ 'VAULT_ENC_KEY',
108
+ 'PG_META_CRYPTO_KEY',
109
+ 'DB_ENC_KEY',
110
+ 'REPL_PASSWORD',
111
+ 'ADMIN_PASSWORD',
112
+ ]);
113
+
114
+ // ============================================================================
115
+ // CLI ARGUMENT PARSING
116
+ // ============================================================================
117
+
118
+ // ============================================================================
119
+ // COMMAND SPEC — single source of truth for argv parsing AND help output.
120
+ // ============================================================================
121
+
122
+ /** @type {import('./lib/cli/parse-flags.js').CommandSpec & { summary?: string, description?: string, examples?: Array<{ command: string, description?: string }> }} */
123
+ const SPEC = {
124
+ name: 'create',
125
+ summary: 'Scaffold a new Vibecarbon project',
126
+ description: [
127
+ "What's included:",
128
+ ' • Hono API server with TypeScript',
129
+ ' • Vite + React 19 SPA',
130
+ ' • Supabase self-hosted (Auth, Database, Storage, Realtime)',
131
+ ' • PostgreSQL with Row Level Security',
132
+ ' • Kong API Gateway + Supabase Studio',
133
+ ' • Docker Compose for local development',
134
+ ' • Kubernetes for production with autoscaling',
135
+ ' • Traefik reverse proxy with automatic HTTPS',
136
+ ' • Shadcn UI component library',
137
+ ' • GitHub Actions CI/CD',
138
+ ' • Automated backups',
139
+ ].join('\n'),
140
+ positional: [
141
+ {
142
+ name: 'projectName',
143
+ optional: true,
144
+ description: 'Project directory name (skips the name prompt)',
145
+ },
146
+ ],
147
+ flags: [
148
+ { name: 'h', boolean: true, description: 'Show this help' },
149
+ { name: 'v', boolean: true, description: 'Show version' },
150
+ {
151
+ name: 'y',
152
+ boolean: true,
153
+ description: 'Skip prompts (requires -admin-email and -admin-password)',
154
+ },
155
+ {
156
+ name: 'git',
157
+ boolean: true,
158
+ description: 'Initialize a git repository (default: off; opt-in)',
159
+ },
160
+ {
161
+ name: 'install',
162
+ boolean: true,
163
+ description: "Run the package manager's install during create (default: off)",
164
+ },
165
+ {
166
+ name: 'skip-lockfile',
167
+ boolean: true,
168
+ description:
169
+ 'Skip lockfile generation (faster; deploy will need `vibecarbon up` first to install)',
170
+ },
171
+ {
172
+ name: 'pm',
173
+ value: '<name>',
174
+ enum: ['npm', 'pnpm', 'bun'],
175
+ description: 'Package manager to use (default: pnpm)',
176
+ },
177
+ {
178
+ name: 'admin-email',
179
+ value: '<email>',
180
+ description: 'Admin email for dashboard access (required with -y)',
181
+ },
182
+ {
183
+ name: 'admin-password',
184
+ value: '<pw>',
185
+ description: 'Admin password for dashboard access (required with -y)',
186
+ },
187
+ ],
188
+ examples: [
189
+ { command: 'vibecarbon create my-saas', description: 'create a project interactively' },
190
+ {
191
+ command:
192
+ 'vibecarbon create my-saas -y -admin-email admin@example.com -admin-password secret123',
193
+ description: 'non-interactive (CI/CD)',
194
+ },
195
+ {
196
+ command: 'vibecarbon create my-saas -git -install',
197
+ description: 'create + initialize git + run pm install',
198
+ },
199
+ ],
200
+ };
201
+
202
+ // ============================================================================
203
+ // UTILITY FUNCTIONS
204
+ // ============================================================================
205
+
206
+ function isCI() {
207
+ return (
208
+ process.env.CI === 'true' ||
209
+ process.env.CONTINUOUS_INTEGRATION === 'true' ||
210
+ process.env.GITHUB_ACTIONS === 'true' ||
211
+ process.env.GITLAB_CI === 'true' ||
212
+ process.env.CIRCLECI === 'true' ||
213
+ process.env.JENKINS_URL !== undefined
214
+ );
215
+ }
216
+
217
+ function detectPackageManager() {
218
+ // Check user agent first (set by npm/pnpm/bun when running npx/pnpx/etc)
219
+ // Only detect pnpm/bun from user agent - if someone runs via npx, we still
220
+ // want to default to pnpm rather than assuming they want npm for their project
221
+ const ua = process.env.npm_config_user_agent || '';
222
+ if (ua.startsWith('pnpm')) return 'pnpm';
223
+ if (ua.startsWith('bun')) return 'bun';
224
+
225
+ // Check for lockfiles in cwd
226
+ if (existsSync('pnpm-lock.yaml')) return 'pnpm';
227
+ if (existsSync('bun.lock') || existsSync('bun.lockb')) return 'bun';
228
+ if (existsSync('package-lock.json')) return 'npm';
229
+
230
+ return 'pnpm'; // default
231
+ }
232
+
233
+ function getInstallCommand(pm) {
234
+ switch (pm) {
235
+ case 'npm':
236
+ return 'npm install';
237
+ case 'bun':
238
+ return 'bun install';
239
+ default:
240
+ // Use --no-frozen-lockfile for new projects (no lockfile exists yet)
241
+ // This is needed in CI environments where pnpm defaults to frozen-lockfile
242
+ return 'pnpm install --no-frozen-lockfile';
243
+ }
244
+ }
245
+
246
+ // Recommended minimum versions for best experience
247
+ const RECOMMENDED_VERSIONS = {
248
+ npm: { min: 10, message: 'npm 10+ recommended for better performance and security' },
249
+ pnpm: { min: 9, message: 'pnpm 9+ recommended for better performance' },
250
+ bun: { min: 1.2, message: 'bun 1.2+ recommended (uses text-based bun.lock)' },
251
+ };
252
+
253
+ function checkPackageManagerVersion(pm) {
254
+ try {
255
+ const version = runCommand([pm, '--version'], {
256
+ encoding: 'utf-8',
257
+ stdio: ['pipe', 'pipe', 'pipe'],
258
+ }).trim();
259
+ const parts = version.split('.');
260
+ const major = parseInt(parts[0], 10);
261
+ const minor = parseInt(parts[1] || '0', 10);
262
+ const recommended = RECOMMENDED_VERSIONS[pm];
263
+
264
+ if (recommended) {
265
+ const reqMajor = Math.floor(recommended.min);
266
+ const reqMinor = Math.round((recommended.min - reqMajor) * 10);
267
+ if (major < reqMajor || (major === reqMajor && minor < reqMinor)) {
268
+ return {
269
+ current: version,
270
+ isOutdated: true,
271
+ message: recommended.message,
272
+ };
273
+ }
274
+ }
275
+ return { current: version, isOutdated: false };
276
+ } catch {
277
+ return { current: null, isOutdated: false };
278
+ }
279
+ }
280
+
281
+ const INSTALL_INSTRUCTIONS = {
282
+ pnpm: 'npm install -g pnpm',
283
+ bun: 'curl -fsSL https://bun.sh/install | bash',
284
+ };
285
+
286
+ function isPackageManagerInstalled(pm) {
287
+ try {
288
+ execFileSync(pm, ['--version'], { stdio: ['pipe', 'pipe', 'pipe'] });
289
+ return true;
290
+ } catch {
291
+ return false;
292
+ }
293
+ }
294
+
295
+ async function ensurePackageManagerInstalled(pm, skipPrompts = false) {
296
+ if (isPackageManagerInstalled(pm)) return pm;
297
+
298
+ const installCmd = INSTALL_INSTRUCTIONS[pm];
299
+ if (installCmd) {
300
+ p.log.warn(`${c.bold(pm)} is not installed. To install it, run:\n\n ${c.info(installCmd)}\n`);
301
+ } else {
302
+ p.log.warn(`${c.bold(pm)} is not installed.`);
303
+ }
304
+
305
+ if (skipPrompts) {
306
+ p.log.info(`Falling back to ${c.bold('npm')}.`);
307
+ return 'npm';
308
+ }
309
+
310
+ const fallback = await p.select({
311
+ message: 'Choose a package manager',
312
+ options: [
313
+ { value: 'npm', label: 'npm', hint: 'already installed with Node.js' },
314
+ { value: 'pnpm', label: 'pnpm' },
315
+ { value: 'bun', label: 'bun' },
316
+ ].filter((opt) => opt.value !== pm),
317
+ });
318
+
319
+ if (p.isCancel(fallback)) {
320
+ p.cancel('Operation cancelled.');
321
+ process.exit(0);
322
+ }
323
+
324
+ // Recurse in case the fallback isn't installed either
325
+ return ensurePackageManagerInstalled(fallback);
326
+ }
327
+
328
+ function copyTemplate(templatePath, destPath, variables) {
329
+ const fullTemplatePath = join(TEMPLATE_DIR, templatePath);
330
+
331
+ if (!existsSync(fullTemplatePath)) {
332
+ return false;
333
+ }
334
+
335
+ let content = readFileSync(fullTemplatePath, 'utf-8');
336
+
337
+ for (const [key, placeholder] of Object.entries(PLACEHOLDERS)) {
338
+ if (SECRET_PLACEHOLDERS.has(key)) continue;
339
+ if (variables[key] !== undefined) {
340
+ content = content.replaceAll(placeholder, variables[key]);
341
+ }
342
+ }
343
+
344
+ mkdirSync(dirname(destPath), { recursive: true });
345
+ writeFileSync(destPath, content);
346
+ return true;
347
+ }
348
+
349
+ function copyTemplateDir(templateSubdir, destDir, variables, exclude = [], rawCopy = []) {
350
+ const fullTemplatePath = join(TEMPLATE_DIR, templateSubdir);
351
+
352
+ if (!existsSync(fullTemplatePath)) {
353
+ return;
354
+ }
355
+
356
+ const entries = readdirSync(fullTemplatePath, { withFileTypes: true });
357
+
358
+ for (const entry of entries) {
359
+ const destPath = join(destDir, entry.name);
360
+
361
+ if (exclude.includes(entry.name)) continue;
362
+
363
+ if (entry.isDirectory()) {
364
+ mkdirSync(destPath, { recursive: true });
365
+ copyTemplateDir(join(templateSubdir, entry.name), destPath, variables, exclude, rawCopy);
366
+ } else if (rawCopy.includes(entry.name)) {
367
+ mkdirSync(dirname(destPath), { recursive: true });
368
+ copyFileSync(join(fullTemplatePath, entry.name), destPath);
369
+ } else {
370
+ copyTemplate(join(templateSubdir, entry.name), destPath, variables);
371
+ }
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Validate that no unreplaced {{PLACEHOLDER}} patterns remain in generated files.
377
+ * Only checks for placeholders that should have been replaced during creation
378
+ * (those in PLACEHOLDERS minus SECRET_PLACEHOLDERS — the latter are intentionally
379
+ * left as placeholders for deploy-time substitution). Deploy-only placeholders
380
+ * like {{DOMAIN}}, {{ACME_EMAIL}} etc. are also expected to remain.
381
+ *
382
+ * Returns an array of { file, placeholders } objects for files with issues.
383
+ */
384
+ function validatePlaceholders(projectDir, exclude = []) {
385
+ const issues = [];
386
+
387
+ // Build pattern to match only creation-time placeholders (secrets are
388
+ // deploy-time substituted, so they're expected to remain unreplaced).
389
+ const creationPlaceholders = Object.keys(PLACEHOLDERS).filter(
390
+ (key) => !SECRET_PLACEHOLDERS.has(key),
391
+ );
392
+ const placeholderPattern = new RegExp(`\\{\\{(${creationPlaceholders.join('|')})\\}\\}`, 'g');
393
+
394
+ // Files/directories to skip (binary files, lockfiles, etc.)
395
+ const skipPatterns = [
396
+ 'node_modules',
397
+ '.git',
398
+ 'pnpm-lock.yaml',
399
+ 'package-lock.json',
400
+
401
+ 'bun.lock',
402
+ 'bun.lockb',
403
+ '.png',
404
+ '.jpg',
405
+ '.jpeg',
406
+ '.gif',
407
+ '.ico',
408
+ '.woff',
409
+ '.woff2',
410
+ '.ttf',
411
+ '.eot',
412
+ ];
413
+
414
+ function scanDir(dir) {
415
+ if (!existsSync(dir)) return;
416
+
417
+ const entries = readdirSync(dir, { withFileTypes: true });
418
+
419
+ for (const entry of entries) {
420
+ const fullPath = join(dir, entry.name);
421
+ const relativePath = fullPath.replace(`${projectDir}/`, '');
422
+
423
+ // Skip excluded paths
424
+ if (exclude.includes(entry.name)) continue;
425
+ if (skipPatterns.some((p) => entry.name.includes(p) || entry.name.endsWith(p))) continue;
426
+
427
+ if (entry.isDirectory()) {
428
+ scanDir(fullPath);
429
+ } else {
430
+ try {
431
+ const content = readFileSync(fullPath, 'utf-8');
432
+ const matches = content.match(placeholderPattern);
433
+ if (matches) {
434
+ // Get unique placeholders
435
+ const uniquePlaceholders = [...new Set(matches)];
436
+ issues.push({ file: relativePath, placeholders: uniquePlaceholders });
437
+ }
438
+ } catch {
439
+ // Skip files that can't be read as text (binary files)
440
+ }
441
+ }
442
+ }
443
+ }
444
+
445
+ scanDir(projectDir);
446
+ return issues;
447
+ }
448
+
449
+ // Helper to allow UI updates between operations
450
+ function tick() {
451
+ return new Promise((resolve) => setTimeout(resolve, 400));
452
+ }
453
+
454
+ // ============================================================================
455
+ // MAIN BOOTSTRAP FUNCTION
456
+ // ============================================================================
457
+
458
+ async function bootstrap(cliArgs) {
459
+ const { values, positional, errors } = parseFlags(cliArgs, SPEC);
460
+
461
+ if (errors.length > 0) {
462
+ for (const e of errors) {
463
+ process.stderr.write(`${c.error('✗')} ${e}\n`);
464
+ }
465
+ process.stderr.write(`Run ${c.info('vibecarbon create -h')} for usage.\n`);
466
+ process.exit(1);
467
+ }
468
+
469
+ if (values.h) {
470
+ process.stdout.write(renderHelp(SPEC));
471
+ process.exit(0);
472
+ }
473
+ if (values.v) {
474
+ console.log(`create-vibecarbon v${VERSION}`);
475
+ process.exit(0);
476
+ }
477
+
478
+ // Build the legacy `args` shape that the orchestration code reads.
479
+ // -git is opt-in (default off), so noGit (the legacy field) is the
480
+ // INVERSE of values.git.
481
+ const args = {
482
+ projectName: positional.projectName || null,
483
+ yes: !!values.y,
484
+ install: !!values.install,
485
+ skipLockfile: !!values['skip-lockfile'],
486
+ noGit: !values.git,
487
+ packageManager: /** @type {string|null} */ (values.pm),
488
+ adminEmail: /** @type {string|null} */ (values['admin-email']),
489
+ adminPassword: /** @type {string|null} */ (values['admin-password']),
490
+ };
491
+
492
+ const skipPrompts = args.yes || isCI();
493
+
494
+ // Start interactive UI
495
+ printBanner();
496
+ p.intro(`${c.bold('vibecarbon create')} ${c.dim(`v${VERSION}`)}`);
497
+
498
+ // Gather configuration.
499
+ // projectName is the basename; it's also the directory name created in cwd.
500
+ // Legacy behavior allowed paths like '../my-app'; we now require a bare
501
+ // basename so the generated project always lands in a predictable place
502
+ // and can't escape cwd (CVE-class path-traversal).
503
+ let projectName = args.projectName;
504
+ let packageManager = args.packageManager || detectPackageManager();
505
+ let initGit = !args.noGit;
506
+ let adminEmail = args.adminEmail;
507
+ let adminPassword = args.adminPassword;
508
+
509
+ if (!skipPrompts) {
510
+ // Interactive prompts
511
+ const project = await p.group(
512
+ {
513
+ name: () => {
514
+ if (projectName) return Promise.resolve(projectName);
515
+ return p.text({
516
+ message: 'What is your project name?',
517
+ placeholder: 'my-saas',
518
+ validate: (value) => {
519
+ const err = validateProjectName(value);
520
+ if (err) return err;
521
+ if (existsSync(join(process.cwd(), value))) {
522
+ return `Directory "${value}" already exists`;
523
+ }
524
+ },
525
+ });
526
+ },
527
+ adminEmail: () => {
528
+ if (adminEmail) return Promise.resolve(adminEmail);
529
+ return p.text({
530
+ message: 'Admin email (for all dashboard access):',
531
+ placeholder: 'admin@example.com',
532
+ validate: (value) => {
533
+ const err = validateAdminEmail(value);
534
+ if (err) return err;
535
+ },
536
+ });
537
+ },
538
+ adminPassword: () => {
539
+ if (adminPassword) return Promise.resolve(adminPassword);
540
+ return p.password({
541
+ message: 'Admin password (min 8 characters):',
542
+ validate: (value) => {
543
+ const err = validateAdminPassword(value);
544
+ if (err) return err;
545
+ },
546
+ });
547
+ },
548
+ packageManager: () => {
549
+ if (args.packageManager) return Promise.resolve(args.packageManager);
550
+ // Launch decision (2026-05-06): hide the interactive package-
551
+ // manager prompt and default to pnpm. The bun + npm code paths
552
+ // work (vibecarbon create --use-bun and --use-npm both produce
553
+ // green builds end-to-end), but pnpm has the most e2e-
554
+ // matrix coverage by far. Power users can still pick npm/bun
555
+ // via the CLI flags. Re-enable the prompt by flipping
556
+ // SHOW_PACKAGE_MANAGER_PROMPT back to true once bun/npm have
557
+ // matrix coverage parity.
558
+ if (!SHOW_PACKAGE_MANAGER_PROMPT) return Promise.resolve('pnpm');
559
+ return p.select({
560
+ message: 'Which package manager?',
561
+ options: [
562
+ { value: 'pnpm', label: 'pnpm', hint: 'recommended' },
563
+ { value: 'npm', label: 'npm' },
564
+ { value: 'bun', label: 'bun' },
565
+ ],
566
+ initialValue: packageManager,
567
+ });
568
+ },
569
+ git: () => {
570
+ if (args.noGit) return Promise.resolve(false);
571
+ return p.confirm({
572
+ message: 'Initialize a git repository for version control?',
573
+ initialValue: true,
574
+ });
575
+ },
576
+ },
577
+ {
578
+ onCancel: () => {
579
+ p.cancel('Operation cancelled.');
580
+ process.exit(0);
581
+ },
582
+ },
583
+ );
584
+
585
+ projectName = project.name;
586
+ adminEmail = project.adminEmail;
587
+ adminPassword = project.adminPassword;
588
+ packageManager = project.packageManager;
589
+ initGit = project.git;
590
+ } else {
591
+ // Non-interactive validation
592
+ if (!projectName) {
593
+ p.log.error('Project name required. Usage: npx create-vibecarbon <project-name> [options]');
594
+ process.exit(1);
595
+ }
596
+ const nameErr = validateProjectName(projectName);
597
+ if (nameErr) {
598
+ p.log.error(`Invalid project name "${projectName}": ${nameErr}`);
599
+ process.exit(1);
600
+ }
601
+ if (existsSync(join(process.cwd(), projectName))) {
602
+ p.log.error(`Directory "${projectName}" already exists.`);
603
+ process.exit(1);
604
+ }
605
+ // Require admin credentials in non-interactive mode
606
+ if (!adminEmail) {
607
+ p.log.error('Admin email required. Use --admin-email=you@example.com');
608
+ process.exit(1);
609
+ }
610
+ if (!adminPassword) {
611
+ p.log.error('Admin password required. Use --admin-password=yourpassword');
612
+ process.exit(1);
613
+ }
614
+ const emailErr = validateAdminEmail(adminEmail);
615
+ if (emailErr) {
616
+ p.log.error(`Invalid --admin-email "${adminEmail}": ${emailErr}`);
617
+ process.exit(1);
618
+ }
619
+ const pwErr = validateAdminPassword(adminPassword);
620
+ if (pwErr) {
621
+ p.log.error(`Invalid --admin-password: ${pwErr}`);
622
+ process.exit(1);
623
+ }
624
+ }
625
+
626
+ // projectDir is the full path where files are created.
627
+ // projectName is also the directory name since we require basenames only.
628
+ const projectDir = join(process.cwd(), projectName);
629
+
630
+ // Show configuration
631
+ p.note(
632
+ [
633
+ `Project: ${projectName}`,
634
+ `Admin: ${adminEmail}`,
635
+ `Package manager: ${packageManager}`,
636
+ `Git: ${initGit ? 'yes' : 'no'}`,
637
+ ].join('\n'),
638
+ 'Configuration',
639
+ );
640
+
641
+ // Verify package manager is installed
642
+ packageManager = await ensurePackageManagerInstalled(packageManager, skipPrompts);
643
+
644
+ // Check for outdated package manager and warn
645
+ const versionCheck = checkPackageManagerVersion(packageManager);
646
+ if (versionCheck.isOutdated) {
647
+ p.log.warn(
648
+ `${c.dim(`${packageManager} ${versionCheck.current} detected.`)} ${versionCheck.message}`,
649
+ );
650
+ }
651
+
652
+ // Generate secure secrets
653
+ const dbPassword = generatePassword();
654
+ const jwtSecret = generatePassword(64);
655
+ const realtimeSecret = generatePassword(64); // SECRET_KEY_BASE (min 64 chars)
656
+ const logflareApiKey = generatePassword(32);
657
+ const vaultEncKey = generatePassword(16); // VAULT_ENC_KEY (exactly 32 hex chars = 16 bytes)
658
+ const pgMetaCryptoKey = generatePassword(32); // PG_META_CRYPTO_KEY (min 32 chars)
659
+ const dbEncKey = generatePassword(16); // DB_ENC_KEY for Realtime aes_128_ecb (must be exactly 16 bytes)
660
+ const replPassword = generateReplPassword();
661
+ const anonKey = generateJWT(jwtSecret, {
662
+ role: 'anon',
663
+ iss: 'supabase',
664
+ ref: projectName,
665
+ });
666
+
667
+ const serviceRoleKey = generateJWT(jwtSecret, {
668
+ role: 'service_role',
669
+ iss: 'supabase',
670
+ ref: projectName,
671
+ });
672
+
673
+ const variables = {
674
+ PROJECT_NAME: projectName,
675
+ GITHUB_OWNER: projectName, // Default to project name; rewritten by `vibecarbon configure` → CI/CD (or by deploy's rewriteOwnerAndRepo fallback)
676
+ DB_PASSWORD: dbPassword,
677
+ JWT_SECRET: jwtSecret,
678
+ ANON_KEY: anonKey,
679
+ SERVICE_ROLE_KEY: serviceRoleKey,
680
+ REALTIME_SECRET: realtimeSecret,
681
+ LOGFLARE_API_KEY: logflareApiKey,
682
+ VAULT_ENC_KEY: vaultEncKey,
683
+ PG_META_CRYPTO_KEY: pgMetaCryptoKey,
684
+ DB_ENC_KEY: dbEncKey,
685
+ REPL_PASSWORD: replPassword,
686
+ SITE_URL: 'http://localhost:5173',
687
+ ADMIN_EMAIL: adminEmail,
688
+ ADMIN_PASSWORD: adminPassword || '',
689
+ ADMIN_PASSWORD_HASH: adminPassword ? hashPassword(adminPassword) : '',
690
+ };
691
+
692
+ // Create project
693
+ const tracker = createTracker('create', { project: projectName });
694
+ const s = tracker.spinner();
695
+
696
+ s.start('Creating project structure');
697
+ mkdirSync(projectDir, { recursive: true });
698
+
699
+ const dirs = [
700
+ 'src/server/routes/api',
701
+ 'src/server/routes/_internal',
702
+ 'src/server/routes/v1',
703
+ 'src/server/lib',
704
+ 'src/client/components/auth',
705
+ 'src/client/components/ui',
706
+ 'src/client/pages',
707
+ 'src/client/lib',
708
+ 'src/shared',
709
+ 'supabase/migrations',
710
+ 'db',
711
+ 'public',
712
+ 'backup',
713
+ 'volumes/kong',
714
+ 'volumes/db',
715
+ 'volumes/traefik',
716
+ 'volumes/prometheus',
717
+ 'volumes/loki',
718
+ 'volumes/promtail',
719
+ 'volumes/grafana/provisioning/dashboards',
720
+ 'volumes/grafana/provisioning/datasources',
721
+ 'volumes/grafana/dashboards',
722
+ 'volumes/grafana/dashboards.dev',
723
+ '.github/workflows',
724
+ 'k8s/base/app',
725
+ 'k8s/base/backup',
726
+ 'k8s/base/config',
727
+ 'k8s/base/hetzner-ccm',
728
+ 'k8s/base/hetzner-csi',
729
+ 'k8s/base/traefik',
730
+ 'k8s/infra/cert-manager-resources',
731
+ 'k8s/infra/traefik-crds',
732
+ 'k8s/overlays/production',
733
+ 'k8s/overlays/local',
734
+ 'k8s/flux/clusters/primary',
735
+ 'k8s/flux/clusters/standby',
736
+ 'k8s/gitops/supabase',
737
+ 'k8s/gitops/cert-manager-webhook-hetzner',
738
+ 'k8s/values',
739
+ ];
740
+ for (const dir of dirs) {
741
+ mkdirSync(join(projectDir, dir), { recursive: true });
742
+ }
743
+ await tick();
744
+
745
+ s.message('Generating configuration files');
746
+ copyTemplate('package.json', join(projectDir, 'package.json'), variables);
747
+ // Lock files are generated by the package manager during install — don't copy stale ones
748
+ copyTemplate('tsconfig.json', join(projectDir, 'tsconfig.json'), variables);
749
+ copyTemplate('tsconfig.server.json', join(projectDir, 'tsconfig.server.json'), variables);
750
+ copyTemplate('vite.config.ts', join(projectDir, 'vite.config.ts'), variables);
751
+ copyTemplate('biome.json', join(projectDir, 'biome.json'), variables);
752
+ copyTemplate('components.json', join(projectDir, 'components.json'), variables);
753
+
754
+ // Test scaffolding — three-tier vitest setup + helpers + golden seed tests.
755
+ // See carbon/TESTING.md for the tier conventions; the test-maintainer
756
+ // subagent extends from this seed when writing new tests.
757
+ copyTemplate('vitest.config.ts', join(projectDir, 'vitest.config.ts'), variables);
758
+ copyTemplate('tsconfig.test.json', join(projectDir, 'tsconfig.test.json'), variables);
759
+ copyTemplate('TESTING.md', join(projectDir, 'TESTING.md'), variables);
760
+ copyTemplateDir('tests', join(projectDir, 'tests'), variables);
761
+ // Copy .env.example WITHOUT replacing placeholders (it's committed to git as documentation)
762
+ const envExampleContent = readFileSync(join(TEMPLATE_DIR, '.env.example'), 'utf-8');
763
+ writeFileSync(join(projectDir, '.env.example'), envExampleContent);
764
+ const envLocal = generateEnvLocal(projectName, variables);
765
+ writeFileSync(join(projectDir, '.env.local'), envLocal, { mode: 0o600 });
766
+ // Also create .env for docker-compose (reads .env by default)
767
+ writeFileSync(join(projectDir, '.env'), envLocal, { mode: 0o600 });
768
+ await tick();
769
+
770
+ s.message('Generating Docker configuration');
771
+ copyTemplate('Dockerfile', join(projectDir, 'Dockerfile'), variables);
772
+ adaptDockerfileForPackageManager(projectDir, packageManager);
773
+ copyTemplate('docker-entrypoint.sh', join(projectDir, 'docker-entrypoint.sh'), variables);
774
+ copyTemplate('.dockerignore', join(projectDir, '.dockerignore'), variables);
775
+ copyTemplate('docker-compose.yml', join(projectDir, 'docker-compose.yml'), variables);
776
+ copyTemplate('docker-compose.prod.yml', join(projectDir, 'docker-compose.prod.yml'), variables);
777
+ copyTemplate(
778
+ 'docker-compose.observability.yml',
779
+ join(projectDir, 'docker-compose.observability.yml'),
780
+ variables,
781
+ );
782
+ copyTemplate(
783
+ 'docker-compose.override.yml',
784
+ join(projectDir, 'docker-compose.override.yml'),
785
+ variables,
786
+ );
787
+ copyTemplate(
788
+ 'docker-compose.observability.override.yml',
789
+ join(projectDir, 'docker-compose.observability.override.yml'),
790
+ variables,
791
+ );
792
+ copyTemplate('PRODUCTION.md', join(projectDir, 'PRODUCTION.md'), variables);
793
+ copyTemplate('DEVELOPMENT.md', join(projectDir, 'DEVELOPMENT.md'), variables);
794
+ copyTemplate(
795
+ 'docker-compose.dev-init.yml',
796
+ join(projectDir, 'docker-compose.dev-init.yml'),
797
+ variables,
798
+ );
799
+ copyTemplate(
800
+ 'docker-compose.observability.prod.yml',
801
+ join(projectDir, 'docker-compose.observability.prod.yml'),
802
+ variables,
803
+ );
804
+ chmodSync(join(projectDir, 'docker-entrypoint.sh'), 0o755);
805
+
806
+ await tick();
807
+
808
+ s.message('Generating backend files');
809
+ copyTemplate('volumes/kong/kong.yml', join(projectDir, 'volumes/kong/kong.yml'), variables);
810
+ copyTemplate(
811
+ 'volumes/kong/docker-entrypoint.sh',
812
+ join(projectDir, 'volumes/kong/docker-entrypoint.sh'),
813
+ variables,
814
+ );
815
+ copyTemplate('volumes/db/roles.sql', join(projectDir, 'volumes/db/roles.sql'), variables);
816
+ copyTemplate(
817
+ 'volumes/db/set-passwords.sh',
818
+ join(projectDir, 'volumes/db/set-passwords.sh'),
819
+ variables,
820
+ );
821
+ copyTemplate('volumes/db/jwt.sql', join(projectDir, 'volumes/db/jwt.sql'), variables);
822
+ copyTemplate('volumes/db/realtime.sql', join(projectDir, 'volumes/db/realtime.sql'), variables);
823
+ copyTemplate(
824
+ 'volumes/db/super-admin.sql',
825
+ join(projectDir, 'volumes/db/super-admin.sql'),
826
+ variables,
827
+ );
828
+ copyTemplate(
829
+ 'volumes/db/super-admin.dev.sql',
830
+ join(projectDir, 'volumes/db/super-admin.dev.sql'),
831
+ variables,
832
+ );
833
+ copyTemplate(
834
+ 'volumes/traefik/middlewares.yml',
835
+ join(projectDir, 'volumes/traefik/middlewares.yml'),
836
+ variables,
837
+ );
838
+ copyTemplate(
839
+ 'volumes/traefik/middlewares.dev.yml',
840
+ join(projectDir, 'volumes/traefik/middlewares.dev.yml'),
841
+ variables,
842
+ );
843
+ copyTemplate(
844
+ 'volumes/traefik/vite-dev.yml',
845
+ join(projectDir, 'volumes/traefik/vite-dev.yml'),
846
+ variables,
847
+ );
848
+
849
+ // Observability config files (used by deploy --observability)
850
+ copyTemplate(
851
+ 'volumes/prometheus/prometheus.yml',
852
+ join(projectDir, 'volumes/prometheus/prometheus.yml'),
853
+ variables,
854
+ );
855
+ copyTemplate(
856
+ 'volumes/loki/loki-config.yml',
857
+ join(projectDir, 'volumes/loki/loki-config.yml'),
858
+ variables,
859
+ );
860
+ copyTemplate(
861
+ 'volumes/promtail/promtail-config.yml',
862
+ join(projectDir, 'volumes/promtail/promtail-config.yml'),
863
+ variables,
864
+ );
865
+ copyTemplate(
866
+ 'volumes/grafana/provisioning/dashboards/dashboards.yml',
867
+ join(projectDir, 'volumes/grafana/provisioning/dashboards/dashboards.yml'),
868
+ variables,
869
+ );
870
+ copyTemplate(
871
+ 'volumes/grafana/provisioning/dashboards/dashboards.dev.yml',
872
+ join(projectDir, 'volumes/grafana/provisioning/dashboards/dashboards.dev.yml'),
873
+ variables,
874
+ );
875
+ copyTemplate(
876
+ 'volumes/grafana/provisioning/datasources/datasources.yml',
877
+ join(projectDir, 'volumes/grafana/provisioning/datasources/datasources.yml'),
878
+ variables,
879
+ );
880
+ copyTemplate(
881
+ 'volumes/grafana/dashboards/logs.json',
882
+ join(projectDir, 'volumes/grafana/dashboards/logs.json'),
883
+ variables,
884
+ );
885
+ copyTemplate(
886
+ 'volumes/grafana/dashboards/overview.json',
887
+ join(projectDir, 'volumes/grafana/dashboards/overview.json'),
888
+ variables,
889
+ );
890
+ copyTemplate(
891
+ 'volumes/grafana/dashboards/postgresql.json',
892
+ join(projectDir, 'volumes/grafana/dashboards/postgresql.json'),
893
+ variables,
894
+ );
895
+ copyTemplate(
896
+ 'volumes/grafana/dashboards.dev/logs.json',
897
+ join(projectDir, 'volumes/grafana/dashboards.dev/logs.json'),
898
+ variables,
899
+ );
900
+ copyTemplate(
901
+ 'volumes/grafana/dashboards.dev/overview.json',
902
+ join(projectDir, 'volumes/grafana/dashboards.dev/overview.json'),
903
+ variables,
904
+ );
905
+ copyTemplate(
906
+ 'volumes/grafana/dashboards.dev/postgresql.json',
907
+ join(projectDir, 'volumes/grafana/dashboards.dev/postgresql.json'),
908
+ variables,
909
+ );
910
+
911
+ copyTemplate('backup/backup.sh', join(projectDir, 'backup/backup.sh'), variables);
912
+ copyTemplate('backup/compose-backup.sh', join(projectDir, 'backup/compose-backup.sh'), variables);
913
+ copyTemplate('backup/Dockerfile', join(projectDir, 'backup/Dockerfile'), variables);
914
+ chmodSync(join(projectDir, 'backup/backup.sh'), 0o755);
915
+ chmodSync(join(projectDir, 'backup/compose-backup.sh'), 0o755);
916
+ // CI/CD workflow files are NOT shipped at create time — `vibecarbon
917
+ // configure` → CI/CD installs them from src/lib/ci-setup.js when the user
918
+ // explicitly opts in. Direct deploy is the default; shipping the workflow
919
+ // files in fresh projects would silently flip ciAvailable() and force
920
+ // every deploy through GitHub Actions.
921
+ copyTemplate(
922
+ 'supabase/migrations/00001_init.sql',
923
+ join(projectDir, 'supabase/migrations/00001_init.sql'),
924
+ variables,
925
+ );
926
+ copyTemplate('supabase/seed.sql', join(projectDir, 'supabase/seed.sql'), variables);
927
+ // Copy entire src directory (server, client, shared)
928
+ copyTemplateDir('src', join(projectDir, 'src'), variables);
929
+
930
+ // Blog content
931
+ copyTemplateDir('content', join(projectDir, 'content'), variables);
932
+
933
+ // Custom database files (WAL-G Dockerfile)
934
+ copyTemplateDir('db', join(projectDir, 'db'), variables);
935
+
936
+ // Kubernetes manifests (exclude optional features like n8n - they're added via `vibecarbon add`)
937
+ copyTemplateDir('k8s', join(projectDir, 'k8s'), variables, ['n8n']);
938
+
939
+ // Scripts (manifest-based docker/k8s commands)
940
+ // generate-dev-configs.sh contains {{PLACEHOLDER}} tokens as its own substitution keys,
941
+ // so it must be copied verbatim to avoid leaking real secrets into the script.
942
+ copyTemplateDir(
943
+ 'scripts',
944
+ join(projectDir, 'scripts'),
945
+ variables,
946
+ [],
947
+ ['generate-dev-configs.sh'],
948
+ );
949
+
950
+ // Make shell scripts executable
951
+ const shellScripts = [
952
+ 'k8s/test-local.sh',
953
+ 'backup/backup.sh',
954
+ 'docker-entrypoint.sh',
955
+ 'volumes/kong/docker-entrypoint.sh',
956
+ 'volumes/db/set-passwords.sh',
957
+ ];
958
+ for (const script of shellScripts) {
959
+ const scriptPath = join(projectDir, script);
960
+ if (existsSync(scriptPath)) {
961
+ chmodSync(scriptPath, 0o755);
962
+ }
963
+ }
964
+ await tick();
965
+
966
+ // Validate no unreplaced placeholders remain
967
+ s.message('Validating generated files');
968
+ const placeholderIssues = validatePlaceholders(projectDir, ['generate-dev-configs.sh', 'agents']);
969
+ if (placeholderIssues.length > 0) {
970
+ s.stop('Validation warning');
971
+ p.log.warn('Some files contain unreplaced placeholders:');
972
+ for (const issue of placeholderIssues.slice(0, 5)) {
973
+ p.log.warn(` ${c.dim(issue.file)}: ${issue.placeholders.join(', ')}`);
974
+ }
975
+ if (placeholderIssues.length > 5) {
976
+ p.log.warn(` ... and ${placeholderIssues.length - 5} more files`);
977
+ }
978
+ p.log.warn('This may indicate a bug in the template. Please report this issue.');
979
+ }
980
+ await tick();
981
+
982
+ s.message('Generating documentation and AI rules');
983
+ const readme = generateReadme(projectName, packageManager);
984
+ writeFileSync(join(projectDir, 'README.md'), readme);
985
+
986
+ // AI development experience files
987
+ copyTemplate('AGENTS.md', join(projectDir, 'AGENTS.md'), variables);
988
+ copyTemplate('CLAUDE.md', join(projectDir, 'CLAUDE.md'), variables);
989
+ copyTemplate('.windsurfrules', join(projectDir, '.windsurfrules'), variables);
990
+ copyTemplate(
991
+ '.github/copilot-instructions.md',
992
+ join(projectDir, '.github/copilot-instructions.md'),
993
+ variables,
994
+ );
995
+ mkdirSync(join(projectDir, '.cursor', 'rules'), { recursive: true });
996
+ copyTemplate(
997
+ '.cursor/rules/vibecarbon.mdc',
998
+ join(projectDir, '.cursor/rules/vibecarbon.mdc'),
999
+ variables,
1000
+ );
1001
+
1002
+ // Claude Code subagents — copy from repo root, stripping auto-generated memory footer
1003
+ const agentsSourceDir = join(__dirname, '..', '.claude', 'agents');
1004
+ if (existsSync(agentsSourceDir)) {
1005
+ const agentsDestDir = join(projectDir, '.claude', 'agents');
1006
+ mkdirSync(agentsDestDir, { recursive: true });
1007
+ for (const file of readdirSync(agentsSourceDir)) {
1008
+ if (!file.endsWith('.md')) continue;
1009
+ let content = readFileSync(join(agentsSourceDir, file), 'utf-8');
1010
+ // Strip the auto-generated "Persistent Agent Memory" section — Claude Code
1011
+ // re-injects it at runtime based on the `memory:` frontmatter field
1012
+ content = content.replace(/\n# Persistent Agent Memory[\s\S]*$/, '\n');
1013
+ writeFileSync(join(agentsDestDir, file), content);
1014
+ }
1015
+ }
1016
+
1017
+ // Copy .claude/settings.json (agent team config + hooks)
1018
+ const settingsSource = join(TEMPLATE_DIR, '.claude', 'settings.json');
1019
+ if (existsSync(settingsSource)) {
1020
+ const settingsDest = join(projectDir, '.claude', 'settings.json');
1021
+ mkdirSync(dirname(settingsDest), { recursive: true });
1022
+ copyTemplate(join('.claude', 'settings.json'), settingsDest, variables);
1023
+ }
1024
+
1025
+ // Copy .claude/hooks/
1026
+ const hooksSourceDir = join(TEMPLATE_DIR, '.claude', 'hooks');
1027
+ if (existsSync(hooksSourceDir)) {
1028
+ const hooksDestDir = join(projectDir, '.claude', 'hooks');
1029
+ mkdirSync(hooksDestDir, { recursive: true });
1030
+ for (const file of readdirSync(hooksSourceDir)) {
1031
+ copyTemplate(join('.claude', 'hooks', file), join(hooksDestDir, file), variables);
1032
+ }
1033
+ for (const file of readdirSync(hooksDestDir)) {
1034
+ if (file.endsWith('.sh')) chmodSync(join(hooksDestDir, file), 0o755);
1035
+ }
1036
+ }
1037
+
1038
+ // Create agent memory directories
1039
+ for (const agent of [
1040
+ 'backend-engineer',
1041
+ 'frontend-engineer',
1042
+ 'security-reviewer',
1043
+ 'test-maintainer',
1044
+ 'lead-coordinator',
1045
+ ]) {
1046
+ mkdirSync(join(projectDir, '.claude', 'agent-memory', agent), { recursive: true });
1047
+ }
1048
+
1049
+ // Update package.json with project name and package manager
1050
+ const packageJson = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf-8'));
1051
+ packageJson.packageManager = getPackageManagerVersion(packageManager);
1052
+ packageJson.name = projectName;
1053
+
1054
+ // Replace pnpm references in scripts with the selected package manager
1055
+ if (packageManager !== 'pnpm' && packageJson.scripts) {
1056
+ const run = packageManager === 'npm' ? 'npm run' : packageManager;
1057
+ for (const [key, value] of Object.entries(packageJson.scripts)) {
1058
+ if (typeof value === 'string' && value.includes('pnpm ')) {
1059
+ packageJson.scripts[key] = value.replaceAll('pnpm ', `${run} `);
1060
+ }
1061
+ }
1062
+ }
1063
+
1064
+ writeFileSync(join(projectDir, 'package.json'), `${JSON.stringify(packageJson, null, 2)}\n`);
1065
+
1066
+ // Deps are deferred to `vibecarbon up` — it auto-installs on first run if
1067
+ // node_modules is missing. Keeps `create` fast (e2e saw ~35s shaved
1068
+ // when running many `create` calls in parallel) and avoids running the
1069
+ // wrong PM against an empty tree. Opt-in via --install if you want the old
1070
+ // behavior.
1071
+ if (args.install) {
1072
+ s.message(`Installing dependencies with ${packageManager}`);
1073
+ await tick();
1074
+ const installCmd = getInstallCommand(packageManager);
1075
+ if (!runCommand(installCmd, { cwd: projectDir, cleanEnv: true })) {
1076
+ s.stop('Failed to install dependencies');
1077
+ p.log.error('Installation failed. You can try running the install command manually.');
1078
+ process.exit(1);
1079
+ }
1080
+ await tick();
1081
+ } else if (args.skipLockfile) {
1082
+ // Caller opted out of lockfile gen — they'll run `vibecarbon up` (or
1083
+ // the install manually) before `deploy`. Used by tests that only
1084
+ // verify scaffolding, and by scripted CI flows that install later.
1085
+ } else {
1086
+ // Generate the lockfile so the Dockerfile's `COPY package.json
1087
+ // <lockfile> ./` step has something to copy. Without this, going from
1088
+ // `vibecarbon create` straight to `vibecarbon deploy` (skipping `up`)
1089
+ // fails the docker build at the COPY step. `vibecarbon up` would
1090
+ // generate the lockfile too, but we shouldn't make `up` a hard
1091
+ // prerequisite for `deploy`.
1092
+ //
1093
+ // pnpm/npm have lockfile-only modes that skip node_modules (fast,
1094
+ // ~2s). Bun has no equivalent — `bun install` always touches
1095
+ // node_modules — but bun installs fast enough that the cost is
1096
+ // acceptable for create. Either way, `vibecarbon up` later sees
1097
+ // node_modules present and skips re-install.
1098
+ let lockfileCmd = null;
1099
+ let lockfileMsg = null;
1100
+ if (packageManager === 'pnpm') {
1101
+ lockfileCmd = ['pnpm', 'install', '--lockfile-only'];
1102
+ lockfileMsg = 'Generating pnpm-lock.yaml (lockfile only)';
1103
+ } else if (packageManager === 'npm') {
1104
+ lockfileCmd = ['npm', 'install', '--package-lock-only'];
1105
+ lockfileMsg = 'Generating package-lock.json (lockfile only)';
1106
+ } else if (packageManager === 'bun') {
1107
+ lockfileCmd = ['bun', 'install'];
1108
+ lockfileMsg = 'Generating bun.lock (full install — bun has no lockfile-only mode)';
1109
+ }
1110
+ if (lockfileCmd) {
1111
+ s.message(lockfileMsg);
1112
+ await tick();
1113
+ // Non-fatal if this fails — CI will regenerate or the user can install.
1114
+ runCommand(lockfileCmd, {
1115
+ cwd: projectDir,
1116
+ cleanEnv: true,
1117
+ ignoreError: true,
1118
+ });
1119
+ await tick();
1120
+ }
1121
+ }
1122
+
1123
+ // Initialize git
1124
+ if (initGit) {
1125
+ s.message('Initializing git repository');
1126
+ copyTemplate('.gitignore', join(projectDir, '.gitignore'), variables);
1127
+
1128
+ const missingPatterns = validateGitignore(join(projectDir, '.gitignore'));
1129
+ if (missingPatterns.length > 0) {
1130
+ p.log.error(
1131
+ `Generated .gitignore is missing required patterns: ${missingPatterns.join(', ')}`,
1132
+ );
1133
+ process.exit(1);
1134
+ }
1135
+
1136
+ runCommand('git init', { cwd: projectDir, cleanEnv: true });
1137
+
1138
+ // Install pre-commit hook. Two responsibilities:
1139
+ // 1. Lint staged code (catches biome/format issues early).
1140
+ // 2. Run vibecarbon's secret scanner against staged files. The
1141
+ // scanner script lives at scripts/secret-scan.mjs (copied from
1142
+ // the template) so this hook works without the vibecarbon CLI
1143
+ // installed globally — every clone of the project gets the
1144
+ // same protection on a fresh `git init`.
1145
+ //
1146
+ // We don't install lefthook here because most projects already have
1147
+ // a pre-commit setup of their own; a plain shell hook is simpler
1148
+ // and harder to break. If a contributor wants a richer hook
1149
+ // manager, lefthook.yml in the template gives them a starting
1150
+ // point — `npx lefthook install` will replace this with that
1151
+ // config.
1152
+ const hooksDir = join(projectDir, '.git', 'hooks');
1153
+ const runCmd =
1154
+ packageManager === 'npm' ? 'npm run' : packageManager === 'bun' ? 'bun run' : 'pnpm';
1155
+ const preCommitHook = `#!/bin/sh
1156
+ # Pre-commit hook installed by vibecarbon create.
1157
+ # Edit freely — but please keep the secret-scan step; it's the first
1158
+ # layer of defense against committing API keys, JWTs, private keys,
1159
+ # etc. \`vibecarbon deploy\` will refuse to push if it finds them
1160
+ # anyway, so removing this just delays the failure.
1161
+
1162
+ set -e
1163
+
1164
+ # 1. Lint
1165
+ ${runCmd} lint
1166
+
1167
+ # 2. Secret scan over staged files. Skip if the script is missing
1168
+ # (e.g. the user wiped scripts/ during a refactor); the deploy gate
1169
+ # will still catch it.
1170
+ if [ -f scripts/secret-scan.mjs ]; then
1171
+ staged=$(git diff --cached --name-only --diff-filter=ACMR)
1172
+ if [ -n "$staged" ]; then
1173
+ echo "$staged" | node scripts/secret-scan.mjs --stdin
1174
+ fi
1175
+ fi
1176
+ `;
1177
+ writeFileSync(join(hooksDir, 'pre-commit'), preCommitHook);
1178
+ chmodSync(join(hooksDir, 'pre-commit'), 0o755);
1179
+
1180
+ // Install pre-push hook. Runs the project's `test:prepush` script —
1181
+ // lint + unit + component + integration. Integration mocks externals,
1182
+ // so this works without Docker and is fast enough for every push.
1183
+ // Bypass via `git push --no-verify` if you understand why your tests
1184
+ // are red. The test-maintainer subagent shipped with the project
1185
+ // keeps this suite green as code changes.
1186
+ const prePushHook = `#!/bin/sh
1187
+ # Pre-push hook installed by vibecarbon create.
1188
+ # Runs lint + unit + component + integration tests.
1189
+ #
1190
+ # No-ops gracefully if node_modules isn't present yet — a fresh
1191
+ # \`vibecarbon create -git\` lands you on a commit that's pushable
1192
+ # before you've run \`pnpm install\`. Once deps land the hook gates
1193
+ # every subsequent push. Bypass at will with \`git push --no-verify\`.
1194
+
1195
+ set -e
1196
+
1197
+ if [ ! -d node_modules ]; then
1198
+ echo "pre-push: node_modules not present — skipping tests."
1199
+ echo " Run '${runCmd} install' to enable the test gate."
1200
+ exit 0
1201
+ fi
1202
+
1203
+ ${runCmd} test:prepush
1204
+ `;
1205
+ writeFileSync(join(hooksDir, 'pre-push'), prePushHook);
1206
+ chmodSync(join(hooksDir, 'pre-push'), 0o755);
1207
+ await tick();
1208
+ }
1209
+
1210
+ // Store template version and file checksums for `vibecarbon upgrade`
1211
+ s.message('Recording file checksums');
1212
+ const upgradeableFiles = getUpgradeableFiles(TEMPLATE_DIR);
1213
+ const fileChecksums = {};
1214
+ for (const relPath of upgradeableFiles) {
1215
+ const filePath = join(projectDir, relPath);
1216
+ if (existsSync(filePath)) {
1217
+ try {
1218
+ const content = readFileSync(filePath, 'utf-8');
1219
+ fileChecksums[relPath] = hashContent(content);
1220
+ } catch {
1221
+ // Skip files that can't be read as text (binary)
1222
+ }
1223
+ }
1224
+ }
1225
+ saveManifest(
1226
+ {
1227
+ version: '1',
1228
+ templateVersion: VERSION,
1229
+ fileChecksums,
1230
+ services: {},
1231
+ },
1232
+ projectDir,
1233
+ );
1234
+ await tick();
1235
+
1236
+ s.stop('Project created successfully');
1237
+ registerProject(projectName, projectDir);
1238
+ tracker.finish();
1239
+ await tick();
1240
+
1241
+ // Next steps
1242
+ const nextSteps = [
1243
+ '# Change directory:',
1244
+ `cd ${projectName}`,
1245
+ '',
1246
+ '# Run locally:',
1247
+ 'vibecarbon up',
1248
+ '',
1249
+ '# Deploy to cloud:',
1250
+ 'vibecarbon deploy',
1251
+ ];
1252
+
1253
+ p.note(nextSteps.join('\n'), 'Next steps');
1254
+ await tick();
1255
+
1256
+ p.outro(`Happy building! Admin: ${adminEmail}`);
1257
+ }
1258
+
1259
+ // ============================================================================
1260
+ // GENERATOR FUNCTIONS
1261
+ // ============================================================================
1262
+
1263
+ function generateEnvLocal(projectName, variables) {
1264
+ // Quote-style policy:
1265
+ // - Machine-generated secrets (JWT_SECRET, ANON_KEY, etc) use double-quoted
1266
+ // form because they are drawn from the base64/base64url alphabet and can
1267
+ // never contain a shell metacharacter.
1268
+ // - User-supplied secrets (ADMIN_PASSWORD, REPL_PASSWORD) use escapeDotenv
1269
+ // (single-quoted, POSIX) so any hostile character round-trips safely.
1270
+ return `# =============================================================================
1271
+ # SUPABASE CONFIGURATION
1272
+ # Generated by create-vibecarbon - DO NOT COMMIT THIS FILE
1273
+ # =============================================================================
1274
+
1275
+ # Project name (used by docker-compose for container naming and Vite HTML title)
1276
+ PROJECT_NAME="${projectName}"
1277
+ VITE_PROJECT_NAME="${projectName}"
1278
+
1279
+ # Supabase URLs
1280
+ SUPABASE_URL="http://localhost:8000"
1281
+ VITE_SUPABASE_URL="http://localhost:8000"
1282
+
1283
+ # Supabase Keys
1284
+ SUPABASE_ANON_KEY="${variables.ANON_KEY}"
1285
+ VITE_SUPABASE_ANON_KEY="${variables.ANON_KEY}"
1286
+ SUPABASE_SERVICE_ROLE_KEY="${variables.SERVICE_ROLE_KEY}"
1287
+
1288
+ # JWT Secret (for token generation)
1289
+ JWT_SECRET="${variables.JWT_SECRET}"
1290
+
1291
+ # Database
1292
+ DB_PASSWORD="${variables.DB_PASSWORD}"
1293
+ POSTGRES_PASSWORD="${variables.DB_PASSWORD}"
1294
+
1295
+ # Encryption Keys
1296
+ REALTIME_SECRET="${variables.REALTIME_SECRET}"
1297
+ VAULT_ENC_KEY="${variables.VAULT_ENC_KEY}"
1298
+ PG_META_CRYPTO_KEY="${variables.PG_META_CRYPTO_KEY}"
1299
+ DB_ENC_KEY="${variables.DB_ENC_KEY}"
1300
+ LOGFLARE_API_KEY="${variables.LOGFLARE_API_KEY}"
1301
+
1302
+ # HA PostgreSQL replication (used by vibecarbon deploy --ha)
1303
+ REPL_PASSWORD=${escapeDotenv(variables.REPL_PASSWORD)}
1304
+
1305
+ # =============================================================================
1306
+ # ADMIN CREDENTIALS
1307
+ # =============================================================================
1308
+
1309
+ # Admin user with dashboard access (created during project setup)
1310
+ # Note: Password stored here for your reference only. Database stores bcrypt hash.
1311
+ ADMIN_EMAIL="${variables.ADMIN_EMAIL}"
1312
+ ADMIN_PASSWORD=${escapeDotenv(variables.ADMIN_PASSWORD)}
1313
+
1314
+ # Site URL
1315
+ SITE_URL="http://localhost:5173"
1316
+
1317
+ # =============================================================================
1318
+ # OAUTH PROVIDERS (optional)
1319
+ # =============================================================================
1320
+
1321
+ GOOGLE_ENABLED="false"
1322
+ GOOGLE_CLIENT_ID=""
1323
+ GOOGLE_CLIENT_SECRET=""
1324
+
1325
+ MICROSOFT_ENABLED="false"
1326
+ MICROSOFT_CLIENT_ID=""
1327
+ MICROSOFT_CLIENT_SECRET=""
1328
+ MICROSOFT_TENANT_ID=""
1329
+
1330
+ # =============================================================================
1331
+ # EXTERNAL SERVICES (optional)
1332
+ # =============================================================================
1333
+
1334
+ # Stripe
1335
+ STRIPE_SECRET_KEY=""
1336
+ STRIPE_WEBHOOK_SECRET=""
1337
+
1338
+ # SMTP Email (shared by Supabase Auth and app transactional emails)
1339
+ SMTP_HOST=""
1340
+ SMTP_PORT=465
1341
+ SMTP_USER=""
1342
+ SMTP_PASS=""
1343
+ SMTP_ADMIN_EMAIL="noreply@example.com"
1344
+ SMTP_SENDER_NAME="${variables.PROJECT_NAME}"
1345
+
1346
+ # =============================================================================
1347
+ # OPTIONAL SERVICES (for admin dashboard visibility)
1348
+ # =============================================================================
1349
+ # These control which services appear in the admin dashboard
1350
+ # Set automatically when using 'vibecarbon add' or manual docker-compose files
1351
+
1352
+ VITE_N8N_ENABLED="false"
1353
+ VITE_OBSERVABILITY_ENABLED="false"
1354
+
1355
+ # =============================================================================
1356
+ # PORT CONFIGURATION (for running multiple projects)
1357
+ # =============================================================================
1358
+ # Use DEV_PORT_OFFSET to shift all ports when running multiple projects.
1359
+ # Example: DEV_PORT_OFFSET=100 shifts Vite to 5273, API to 3100, etc.
1360
+
1361
+ DEV_PORT_OFFSET="0"
1362
+
1363
+ # Override individual ports (takes precedence over offset):
1364
+ # DEV_VITE_PORT="5173"
1365
+ # DEV_API_PORT="3000"
1366
+ # DEV_KONG_PORT="8000"
1367
+ # DEV_KONG_SSL_PORT="8443"
1368
+ # DEV_TRAEFIK_PORT="80"
1369
+
1370
+ # =============================================================================
1371
+ # ENVIRONMENT
1372
+ # =============================================================================
1373
+
1374
+ NODE_ENV="development"
1375
+ PORT="3000"
1376
+ `.trim();
1377
+ }
1378
+
1379
+ function generateReadme(projectName, packageManager = 'pnpm') {
1380
+ const run = packageManager === 'npm' ? 'npm run' : packageManager;
1381
+ let readme = `# ${projectName}
1382
+
1383
+ A modern web app built with Vibecarbon.
1384
+
1385
+ ## Stack
1386
+
1387
+ - **Hono** - Fast, lightweight web framework
1388
+ - **Vite** - Next-generation frontend tooling
1389
+ - **React 19** - UI library (SPA mode)
1390
+ - **Supabase** - Self-hosted backend (Auth, Database, Storage, Realtime)
1391
+ - **PostgreSQL** - Database (via Supabase)
1392
+ - **Tailwind CSS** - Utility-first styling
1393
+ - **Shadcn UI** - Component library
1394
+ - **Kubernetes** - Container orchestration & autoscaling
1395
+ - **Traefik** - Reverse proxy with automatic HTTPS
1396
+
1397
+ ## Getting Started
1398
+
1399
+ Start everything with a single command:
1400
+
1401
+ \`\`\`bash
1402
+ vibecarbon up
1403
+ \`\`\`
1404
+
1405
+ This starts Docker containers, runs database migrations, and launches the dev servers.
1406
+
1407
+ ## Services
1408
+
1409
+ | Service | URL | Description |
1410
+ |---------|-----|-------------|
1411
+ | Vite Dev | http://localhost:5173 | Frontend development server |
1412
+ | Hono API | http://localhost:3000 | Application API |
1413
+ | Supabase API | http://localhost:8000 | Supabase API Gateway (Kong) |
1414
+ | Supabase Studio | http://studio.localhost | Database management UI (requires admin login) |
1415
+ | Traefik Dashboard | http://traefik.localhost | Reverse proxy dashboard (requires admin login) |
1416
+
1417
+ ## Available Scripts
1418
+
1419
+ - \`${run} dev:start\` - Full cold start (Docker + migrations + dev servers)
1420
+ - \`${run} dev\` - Start both API and frontend dev servers (fast restart)
1421
+ - \`${run} dev:server\` - Start Hono API server only
1422
+ - \`${run} dev:client\` - Start Vite dev server only
1423
+ - \`${run} build\` - Build for production
1424
+ - \`${run} start\` - Run production build
1425
+ - \`${run} docker:up\` - Start all services
1426
+ - \`${run} dev:stop\` - Stop all services
1427
+ - \`${run} docker:logs\` - View service logs
1428
+ - \`${run} db:migrate\` - Run database migrations
1429
+ `;
1430
+
1431
+ readme += `
1432
+ ## Troubleshooting
1433
+
1434
+ ### Database connection errors
1435
+
1436
+ If services fail with "password authentication failed", the database volume was created with different credentials. Reset it:
1437
+
1438
+ \`\`\`bash
1439
+ ${run} docker:reset
1440
+ ${run} db:migrate
1441
+ \`\`\`
1442
+
1443
+ > **Note:** This deletes all data. For production, back up first.
1444
+
1445
+ ### Services not starting
1446
+
1447
+ Check logs for specific errors:
1448
+
1449
+ \`\`\`bash
1450
+ ${run} docker:logs
1451
+ \`\`\`
1452
+
1453
+ ### Port conflicts
1454
+
1455
+ If ports 5173, 3000, 3001, or 8000 are in use, stop conflicting services or modify \`docker-compose.yml\`.
1456
+
1457
+ ## Learn More
1458
+
1459
+ - [Supabase Documentation](https://supabase.com/docs)
1460
+ - [Hono Documentation](https://hono.dev)
1461
+ - [Vite Documentation](https://vitejs.dev)
1462
+ `;
1463
+
1464
+ return readme.trim();
1465
+ }
1466
+
1467
+ // ============================================================================
1468
+ // RUN FUNCTION (called by CLI entry point)
1469
+ // ============================================================================
1470
+
1471
+ export async function run(args) {
1472
+ await bootstrap(args);
1473
+ }
1474
+
1475
+ // ============================================================================
1476
+ // EXPORTS FOR TESTING
1477
+ // ============================================================================
1478
+
1479
+ export {
1480
+ bootstrap,
1481
+ checkPackageManagerVersion,
1482
+ copyTemplate,
1483
+ copyTemplateDir,
1484
+ detectPackageManager,
1485
+ generateEnvLocal,
1486
+ generateJWT,
1487
+ generatePassword,
1488
+ generateReadme,
1489
+ getInstallCommand,
1490
+ getPackageManagerVersion,
1491
+ isCI,
1492
+ PLACEHOLDERS,
1493
+ RECOMMENDED_VERSIONS,
1494
+ runCommand,
1495
+ SPEC,
1496
+ TEMPLATE_DIR,
1497
+ VERSION,
1498
+ validatePlaceholders,
1499
+ };