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
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Project configuration management
3
+ */
4
+
5
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
6
+ import { homedir } from 'node:os';
7
+ import { basename, join, resolve } from 'node:path';
8
+
9
+ /**
10
+ * Sanitize a project name to be a valid hostname
11
+ * - Extracts base name from paths (../my-app -> my-app)
12
+ * - Removes invalid characters
13
+ * - Converts to lowercase
14
+ * - Ensures doesn't start/end with hyphen
15
+ *
16
+ * @param {string} name - Project name to sanitize
17
+ * @returns {string} - Valid hostname
18
+ */
19
+ export function sanitizeHostname(name) {
20
+ if (!name) return 'project';
21
+
22
+ // Extract base name from path (handles ../my-app, ./my-app, /path/to/my-app)
23
+ let sanitized = basename(name);
24
+
25
+ // Remove any characters that aren't alphanumeric or hyphens
26
+ sanitized = sanitized.toLowerCase().replace(/[^a-z0-9-]/g, '-');
27
+
28
+ // Remove consecutive hyphens
29
+ sanitized = sanitized.replace(/-+/g, '-');
30
+
31
+ // Remove leading/trailing hyphens
32
+ sanitized = sanitized.replace(/^-+|-+$/g, '');
33
+
34
+ // Ensure it's not empty
35
+ if (!sanitized) return 'project';
36
+
37
+ // Ensure it doesn't exceed hostname limits (63 chars for labels)
38
+ if (sanitized.length > 63) {
39
+ sanitized = sanitized.substring(0, 63).replace(/-+$/, '');
40
+ }
41
+
42
+ return sanitized;
43
+ }
44
+
45
+ /**
46
+ * Load project configuration from multiple sources
47
+ * Priority: .vibecarbon.json > .env.local > package.json
48
+ *
49
+ * @param {string} [cwd] - Working directory (defaults to process.cwd())
50
+ * @returns {object|null} - Project config or null if not found
51
+ */
52
+ export function loadProjectConfig(cwd = process.cwd()) {
53
+ // Try to load .vibecarbon.json
54
+ const configPath = join(cwd, '.vibecarbon.json');
55
+ if (existsSync(configPath)) {
56
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
57
+
58
+ // If projectName is missing or empty, try to get it from .env.local or package.json
59
+ if (!config.projectName) {
60
+ const envPath = join(cwd, '.env.local');
61
+ if (existsSync(envPath)) {
62
+ const envContent = readFileSync(envPath, 'utf-8');
63
+ for (const line of envContent.split('\n')) {
64
+ if (line.startsWith('PROJECT_NAME=')) {
65
+ const rawValue = line.split('=')[1].replace(/^["']|["']$/g, '');
66
+ config.projectName = sanitizeHostname(rawValue);
67
+ break;
68
+ }
69
+ }
70
+ }
71
+
72
+ // Fall back to package.json name
73
+ if (!config.projectName) {
74
+ const pkgPath = join(cwd, 'package.json');
75
+ if (existsSync(pkgPath)) {
76
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
77
+ config.projectName = sanitizeHostname(pkg.name);
78
+ }
79
+ }
80
+ }
81
+
82
+ // Final fallback - ensure projectName is always set
83
+ if (!config.projectName) {
84
+ config.projectName = 'project';
85
+ }
86
+
87
+ return config;
88
+ }
89
+
90
+ // Try to load from .env.local
91
+ const envPath = join(cwd, '.env.local');
92
+ if (existsSync(envPath)) {
93
+ const envContent = readFileSync(envPath, 'utf-8');
94
+ const config = {
95
+ projectName: null,
96
+ secrets: {},
97
+ };
98
+
99
+ for (const line of envContent.split('\n')) {
100
+ if (line.startsWith('#') || !line.includes('=')) continue;
101
+ const [key, ...valueParts] = line.split('=');
102
+ const value = valueParts.join('=').replace(/^["']|["']$/g, '');
103
+
104
+ if (key === 'PROJECT_NAME') {
105
+ config.projectName = sanitizeHostname(value);
106
+ }
107
+ config.secrets[key.trim()] = value;
108
+ }
109
+
110
+ // Try to get project name from package.json
111
+ const pkgPath = join(cwd, 'package.json');
112
+ if (!config.projectName && existsSync(pkgPath)) {
113
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
114
+ config.projectName = sanitizeHostname(pkg.name);
115
+ }
116
+
117
+ // Final fallback - ensure projectName is always set
118
+ if (!config.projectName) {
119
+ config.projectName = 'project';
120
+ }
121
+
122
+ return config;
123
+ }
124
+
125
+ // Try package.json as fallback
126
+ const pkgPath = join(cwd, 'package.json');
127
+ if (existsSync(pkgPath)) {
128
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
129
+ return { projectName: sanitizeHostname(pkg.name), secrets: {} };
130
+ }
131
+
132
+ return null;
133
+ }
134
+
135
+ /**
136
+ * Save project configuration to .vibecarbon.json
137
+ *
138
+ * @param {object} config - Configuration to save
139
+ * @param {string} [cwd] - Working directory (defaults to process.cwd())
140
+ */
141
+ export function saveProjectConfig(config, cwd = process.cwd()) {
142
+ const configPath = join(cwd, '.vibecarbon.json');
143
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
144
+ }
145
+
146
+ /**
147
+ * Load S3 configuration for a specific environment
148
+ *
149
+ * @param {string} envName - Environment name (e.g., 'prod', 'staging')
150
+ * @param {string} [cwd] - Working directory (defaults to process.cwd())
151
+ * @returns {object|null} - S3 config or null if not configured
152
+ */
153
+ export function loadS3Config(envName, cwd = process.cwd()) {
154
+ const config = loadProjectConfig(cwd);
155
+ return config?.environments?.[envName]?.s3 || null;
156
+ }
157
+
158
+ /**
159
+ * Load backup configuration for a specific environment
160
+ *
161
+ * @param {string} envName - Environment name (e.g., 'prod', 'staging')
162
+ * @param {string} [cwd] - Working directory (defaults to process.cwd())
163
+ * @returns {object|null} - Backup config or null if not configured
164
+ */
165
+ export function loadBackupConfig(envName, cwd = process.cwd()) {
166
+ const config = loadProjectConfig(cwd);
167
+ return config?.environments?.[envName]?.backup || null;
168
+ }
169
+
170
+ /**
171
+ * Load backup S3 configuration for a specific environment.
172
+ * This is the dedicated backup bucket (separate from storage bucket).
173
+ *
174
+ * @param {string} envName - Environment name (e.g., 'prod', 'staging')
175
+ * @param {string} [cwd] - Working directory (defaults to process.cwd())
176
+ * @returns {object|null} - Backup S3 config or null if not configured
177
+ */
178
+ export function loadBackupS3Config(envName, cwd = process.cwd()) {
179
+ const config = loadProjectConfig(cwd);
180
+ return config?.environments?.[envName]?.backupS3 || null;
181
+ }
182
+
183
+ // ============================================================================
184
+ // GLOBAL PROJECT REGISTRY (~/.vibecarbon/projects.json)
185
+ // ============================================================================
186
+
187
+ const DEFAULT_CONFIG_DIR = join(homedir(), '.vibecarbon');
188
+
189
+ /**
190
+ * Ensure the global config directory exists with secure permissions
191
+ *
192
+ * @param {string} [configDir] - Config directory (default: ~/.vibecarbon/)
193
+ */
194
+ function ensureConfigDir(configDir = DEFAULT_CONFIG_DIR) {
195
+ if (!existsSync(configDir)) {
196
+ mkdirSync(configDir, { recursive: true, mode: 0o700 });
197
+ }
198
+ }
199
+
200
+ // ============================================================================
201
+ // GLOBAL CREDENTIAL STORAGE (~/.vibecarbon/credentials.json)
202
+ // ============================================================================
203
+
204
+ /**
205
+ * Load saved credentials from ~/.vibecarbon/credentials.json
206
+ *
207
+ * @param {string} [configDir] - Config directory for testability
208
+ * @returns {{ hetzner?: { apiToken: string }, s3?: { accessKey: string, secretKey: string }, cloudflare?: { apiToken: string } }}
209
+ */
210
+ export function loadCredentials(configDir = DEFAULT_CONFIG_DIR) {
211
+ const credPath = join(configDir, 'credentials.json');
212
+ if (!existsSync(credPath)) {
213
+ return {};
214
+ }
215
+
216
+ try {
217
+ return JSON.parse(readFileSync(credPath, 'utf-8'));
218
+ } catch {
219
+ return {};
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Save credentials to ~/.vibecarbon/credentials.json (merges with existing)
225
+ * File is written with mode 0o600 (owner read/write only).
226
+ *
227
+ * @param {object} credentials - Credential object to merge (e.g. { hetzner: { apiToken } })
228
+ * @param {string} [configDir] - Config directory for testability
229
+ */
230
+ export function saveCredentials(credentials, configDir = DEFAULT_CONFIG_DIR) {
231
+ ensureConfigDir(configDir);
232
+ const credPath = join(configDir, 'credentials.json');
233
+ const existing = loadCredentials(configDir);
234
+ const merged = { ...existing, ...credentials };
235
+ writeFileSync(credPath, `${JSON.stringify(merged, null, 2)}\n`, { mode: 0o600 });
236
+ }
237
+
238
+ /**
239
+ * Delete the credentials file
240
+ *
241
+ * @param {string} [configDir] - Config directory for testability
242
+ */
243
+ export function clearCredentials(configDir = DEFAULT_CONFIG_DIR) {
244
+ const credPath = join(configDir, 'credentials.json');
245
+ if (existsSync(credPath)) {
246
+ unlinkSync(credPath);
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Load the global project registry
252
+ *
253
+ * @param {string} [configDir] - Config directory for testability
254
+ * @returns {{ projects: Array<{ name: string, path: string, createdAt: string, updatedAt: string }> }}
255
+ */
256
+ export function loadGlobalRegistry(configDir = DEFAULT_CONFIG_DIR) {
257
+ const registryPath = join(configDir, 'projects.json');
258
+ if (!existsSync(registryPath)) {
259
+ return { projects: [] };
260
+ }
261
+
262
+ try {
263
+ const content = readFileSync(registryPath, 'utf-8');
264
+ const data = JSON.parse(content);
265
+ if (!Array.isArray(data.projects)) {
266
+ return { projects: [] };
267
+ }
268
+ return data;
269
+ } catch {
270
+ return { projects: [] };
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Register (or update) a project in the global registry.
276
+ * Upserts by resolved absolute path.
277
+ *
278
+ * @param {string} name - Project name
279
+ * @param {string} projectPath - Project directory path
280
+ * @param {string} [configDir] - Config directory for testability
281
+ */
282
+ export function registerProject(name, projectPath, configDir = DEFAULT_CONFIG_DIR) {
283
+ ensureConfigDir(configDir);
284
+ const absPath = resolve(projectPath);
285
+ const registry = loadGlobalRegistry(configDir);
286
+
287
+ const existing = registry.projects.find((p) => p.path === absPath);
288
+ const now = new Date().toISOString();
289
+
290
+ if (existing) {
291
+ existing.name = name;
292
+ existing.updatedAt = now;
293
+ } else {
294
+ registry.projects.push({ name, path: absPath, createdAt: now, updatedAt: now });
295
+ }
296
+
297
+ const registryPath = join(configDir, 'projects.json');
298
+ writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`, { mode: 0o600 });
299
+ }
300
+
301
+ /**
302
+ * Remove a project from the global registry by path.
303
+ *
304
+ * @param {string} projectPath - Project directory path
305
+ * @param {string} [configDir] - Config directory for testability
306
+ */
307
+ export function unregisterProject(projectPath, configDir = DEFAULT_CONFIG_DIR) {
308
+ const absPath = resolve(projectPath);
309
+ const registry = loadGlobalRegistry(configDir);
310
+ registry.projects = registry.projects.filter((p) => p.path !== absPath);
311
+
312
+ const registryPath = join(configDir, 'projects.json');
313
+ if (existsSync(join(configDir))) {
314
+ writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`, { mode: 0o600 });
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Remove registry entries where the directory no longer exists.
320
+ *
321
+ * @param {string} [configDir] - Config directory for testability
322
+ */
323
+ export function cleanStaleProjects(configDir = DEFAULT_CONFIG_DIR) {
324
+ const registry = loadGlobalRegistry(configDir);
325
+ const before = registry.projects.length;
326
+ registry.projects = registry.projects.filter((p) => existsSync(p.path));
327
+
328
+ if (registry.projects.length !== before) {
329
+ const registryPath = join(configDir, 'projects.json');
330
+ writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`, { mode: 0o600 });
331
+ }
332
+ }
333
+
334
+ // ============================================================================
335
+ // PROJECT DETECTION UTILITIES
336
+ // ============================================================================
337
+
338
+ /**
339
+ * Check if docker-compose.yml exists in the given directory
340
+ *
341
+ * @param {string} [cwd] - Working directory (defaults to process.cwd())
342
+ * @returns {boolean}
343
+ */
344
+ export function hasDockerCompose(cwd = process.cwd()) {
345
+ return existsSync(join(cwd, 'docker-compose.yml'));
346
+ }
347
+
348
+ /**
349
+ * Check if K8s HA overlay directories exist
350
+ *
351
+ * @param {string} [cwd] - Working directory (defaults to process.cwd())
352
+ * @returns {boolean}
353
+ */
354
+ export function isHAConfigured(cwd = process.cwd()) {
355
+ return (
356
+ existsSync(join(cwd, 'k8s/overlays/production-hel1')) ||
357
+ existsSync(join(cwd, 'k8s/overlays/production-nbg1'))
358
+ );
359
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Shared cost breakdown utilities for deploy and scale commands.
3
+ */
4
+
5
+ import { c } from './colors.js';
6
+ import { HetznerProvider } from './providers/hetzner.js';
7
+ import { parsePrice } from './server-types.js';
8
+
9
+ /**
10
+ * Build an itemized cost breakdown for the deployment.
11
+ * Returns { lines: [{label, cost}], total }.
12
+ */
13
+ export function buildCostBreakdown({
14
+ isComposeDeploy,
15
+ ha,
16
+ serverType,
17
+ masterServerType,
18
+ supabaseServerType,
19
+ workerServerType,
20
+ region,
21
+ services,
22
+ }) {
23
+ const { INFRA_COSTS, VOLUME_SIZES } = HetznerProvider;
24
+ const lines = [];
25
+ let total = 0;
26
+
27
+ if (isComposeDeploy) {
28
+ const price = parsePrice(HetznerProvider.getPrice(serverType, region));
29
+ if (ha) {
30
+ const cost = price * 2;
31
+ lines.push({ label: 'Server:', value: `${serverType} ×2`, cost });
32
+ total += cost;
33
+ } else {
34
+ lines.push({ label: 'Server:', value: serverType, cost: price });
35
+ total += price;
36
+ }
37
+ } else {
38
+ const regionMultiplier = ha ? 2 : 1;
39
+ const suffix = ha ? ' ×2' : '';
40
+
41
+ const masterPrice = parsePrice(HetznerProvider.getPrice(masterServerType, region));
42
+ const supabasePrice = parsePrice(HetznerProvider.getPrice(supabaseServerType, region));
43
+ const workerPrice = parsePrice(HetznerProvider.getPrice(workerServerType, region));
44
+
45
+ const masterCost = masterPrice * regionMultiplier;
46
+ const supabaseCost = supabasePrice * regionMultiplier;
47
+ const workerCost = workerPrice * regionMultiplier;
48
+ lines.push({ label: 'Master:', value: `${masterServerType}${suffix}`, cost: masterCost });
49
+ lines.push({ label: 'Supabase:', value: `${supabaseServerType}${suffix}`, cost: supabaseCost });
50
+ lines.push({ label: 'Worker:', value: `${workerServerType}${suffix}`, cost: workerCost });
51
+ total += masterCost + supabaseCost + workerCost;
52
+
53
+ // Floating IP
54
+ const fipCost = INFRA_COSTS.floatingIp * regionMultiplier;
55
+ lines.push({ label: `Floating IP${suffix}`, cost: fipCost });
56
+ total += fipCost;
57
+
58
+ // CSI Volumes
59
+ let volumeGb = VOLUME_SIZES.base;
60
+ if (services.observability) volumeGb += VOLUME_SIZES.observability;
61
+ if (services.n8n) volumeGb += VOLUME_SIZES.n8n;
62
+ if (services.metabase) volumeGb += VOLUME_SIZES.metabase;
63
+ if (services.redis) volumeGb += VOLUME_SIZES.redis;
64
+ const volumeCost = volumeGb * INFRA_COSTS.volumePerGb * regionMultiplier;
65
+ lines.push({ label: 'Volumes:', value: `${volumeGb}GB${suffix}`, cost: volumeCost });
66
+ total += volumeCost;
67
+ }
68
+
69
+ // S3 Object Storage (shared, not multiplied by region)
70
+ lines.push({ label: 'S3 Backups', cost: INFRA_COSTS.s3MinBucket });
71
+ total += INFRA_COSTS.s3MinBucket;
72
+
73
+ return { lines, total };
74
+ }
75
+
76
+ /**
77
+ * Format a cost breakdown into a colored, grid-aligned display string.
78
+ */
79
+ export function formatCostLines({ lines, total }) {
80
+ const labelCol = 14;
81
+ const colWidth = 30;
82
+ const header = `\n${c.bold('Estimated Monthly Cost')}`;
83
+
84
+ const priceWidth = 10;
85
+
86
+ const rows = lines.map((item) => {
87
+ const priceStr = `€${item.cost.toFixed(2)}`.padStart(priceWidth);
88
+ if (item.value) {
89
+ const paddedLabel = item.label.padEnd(labelCol);
90
+ const fullLabel = `${paddedLabel}${c.bold(item.value)}`;
91
+ // Pad the visible width (without ANSI codes) to colWidth
92
+ const visibleLen = paddedLabel.length + item.value.length;
93
+ const gap = Math.max(0, colWidth - visibleLen);
94
+ return `${fullLabel}${' '.repeat(gap)}${c.bold(priceStr)}`;
95
+ }
96
+ return `${item.label.padEnd(colWidth)}${c.bold(priceStr)}`;
97
+ });
98
+
99
+ const separator = c.dim('─'.repeat(colWidth + priceWidth));
100
+ const totalLine = `${'Est. Total'.padEnd(colWidth)}${c.boldYellow(`€${total.toFixed(2)}/mo`.padStart(priceWidth))}`;
101
+
102
+ return [header, ...rows, separator, totalLine].join('\n');
103
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Local-First Bundle Rendering
3
+ * Stages all deployment artifacts (Compose files, .env, scripts) into a local temp directory
4
+ * before any infrastructure is provisioned or network calls are made.
5
+ */
6
+
7
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import { escapeDotenv } from '../shell.js';
11
+
12
+ /**
13
+ * Render the reconcile.sh script
14
+ */
15
+ function renderReconcileScript(projectName, composeFlags, isFast = false) {
16
+ const pullStep = isFast
17
+ ? ''
18
+ : `
19
+ # 1. Pull latest images if needed
20
+ echo "Pulling images..."
21
+ docker compose ${composeFlags} pull --quiet --policy missing --ignore-buildable || true
22
+ `;
23
+
24
+ return `#!/bin/bash
25
+ set -e
26
+
27
+ # Declarative Compose Reconciler
28
+ # Generated by Vibecarbon
29
+
30
+ PROJECT_DIR="/opt/${projectName}"
31
+ cd "$PROJECT_DIR"
32
+
33
+ echo "Reconciling project: ${projectName}..."
34
+
35
+ echo "File layout in $(pwd):"
36
+ ls -R | grep ":$" | sed 's/:$//' | xargs -I {} sh -c "echo '{}:' && ls {} | head -n 5"
37
+
38
+ if [ -f ".env" ]; then
39
+ echo "Found .env file"
40
+ # Don't print secrets, but check for existence of keys
41
+ grep -E "^(S3_BUCKET|PROJECT_NAME|APP_IMAGE)=" .env | sed 's/=.*$/=***/'
42
+ else
43
+ echo "WARNING: .env file NOT FOUND in $(pwd)"
44
+ fi
45
+
46
+ ${pullStep}
47
+ # 2. Re-create containers if state differs
48
+ echo "Updating containers..."
49
+ docker compose ${composeFlags} up -d --remove-orphans
50
+
51
+ # 3. Ensure systemd service is active (if applicable)
52
+ if [ -f "/etc/systemd/system/${projectName}.service" ]; then
53
+ echo "Ensuring systemd service is active..."
54
+ systemctl daemon-reload
55
+ systemctl enable "${projectName}.service"
56
+ fi
57
+
58
+ echo "Reconciliation complete."
59
+ `;
60
+ }
61
+
62
+ /**
63
+ * Render the systemd service unit
64
+ */
65
+ function renderSystemdUnit(projectName, composeFlags, _isFast = false) {
66
+ return `[Unit]
67
+ Description=Vibecarbon Project: ${projectName}
68
+ After=docker.service
69
+ Requires=docker.service
70
+
71
+ [Service]
72
+ Type=oneshot
73
+ RemainAfterExit=yes
74
+ WorkingDirectory=/opt/${projectName}
75
+ ExecStart=/usr/bin/docker compose ${composeFlags} up -d --remove-orphans
76
+ ExecStop=/usr/bin/docker compose ${composeFlags} down
77
+ Restart=on-failure
78
+
79
+ [Install]
80
+ WantedBy=multi-user.target
81
+ `;
82
+ }
83
+
84
+ /**
85
+ * Build the compose files flags string
86
+ */
87
+ function composeFileFlags(options = {}) {
88
+ const files = ['-f', 'docker-compose.yml', '-f', 'docker-compose.prod.yml'];
89
+ if (options.observability) {
90
+ files.push('-f', 'docker-compose.observability.yml');
91
+ files.push('-f', 'docker-compose.observability.prod.yml');
92
+ }
93
+ if (options.n8n) {
94
+ files.push('-f', 'docker-compose.n8n.yml', '-f', 'docker-compose.n8n.prod.yml');
95
+ }
96
+ if (options.metabase) {
97
+ files.push('-f', 'docker-compose.metabase.yml', '-f', 'docker-compose.metabase.prod.yml');
98
+ }
99
+ if (options.redis) files.push('-f', 'docker-compose.redis.yml');
100
+ return files.join(' ');
101
+ }
102
+
103
+ /**
104
+ * Render the deployment bundle to a local temp directory
105
+ * @param {string} projectName
106
+ * @param {object} options
107
+ * @returns {string} The path to the staged bundle directory
108
+ */
109
+ export function renderBundle(projectName, options = {}) {
110
+ const cwd = process.cwd();
111
+ const stageDir = mkdtempSync(join(tmpdir(), 'vc-deploy-bundle-'));
112
+
113
+ // Core Docker files
114
+ const rootFiles = [
115
+ 'docker-compose.yml',
116
+ 'docker-compose.prod.yml',
117
+ 'Dockerfile',
118
+ '.dockerignore',
119
+ ];
120
+ if (options.observability) {
121
+ rootFiles.push('docker-compose.observability.yml', 'docker-compose.observability.prod.yml');
122
+ }
123
+ if (options.n8n) {
124
+ rootFiles.push('docker-compose.n8n.yml', 'docker-compose.n8n.prod.yml');
125
+ }
126
+ if (options.metabase) {
127
+ rootFiles.push('docker-compose.metabase.yml', 'docker-compose.metabase.prod.yml');
128
+ }
129
+ if (options.redis) rootFiles.push('docker-compose.redis.yml');
130
+
131
+ for (const file of rootFiles) {
132
+ const src = join(cwd, file);
133
+ if (existsSync(src)) {
134
+ if (options.verbose) console.log(`[bundle] Copying ${file}`);
135
+ cpSync(src, join(stageDir, file));
136
+ }
137
+ }
138
+
139
+ // Render .env locally with overrides
140
+ const localEnvPath = join(cwd, '.env');
141
+ let envContent = '';
142
+ if (existsSync(localEnvPath)) {
143
+ envContent = readFileSync(localEnvPath, 'utf-8');
144
+ }
145
+
146
+ const envOverrides = { ...(options.envOverrides || {}) };
147
+ if (options.domain) {
148
+ envOverrides.DOMAIN = options.domain;
149
+ envOverrides.ACME_EMAIL = `admin@${options.domain}`;
150
+ envOverrides.SITE_URL = `https://api.${options.domain}`;
151
+ envOverrides.SUPABASE_URL = 'http://kong:8000';
152
+ }
153
+ if (options.image) {
154
+ envOverrides.APP_IMAGE = options.image;
155
+ }
156
+ if (options.s3) {
157
+ envOverrides.S3_ACCESS_KEY = options.s3.accessKey;
158
+ envOverrides.S3_SECRET_KEY = options.s3.secretKey;
159
+ envOverrides.S3_REGION = options.s3.region;
160
+ envOverrides.S3_ENDPOINT = options.s3.endpoint;
161
+ envOverrides.S3_BUCKET = options.s3.bucket;
162
+ }
163
+ if (options.backupS3) {
164
+ envOverrides.S3_BACKUP_BUCKET = options.backupS3.bucket;
165
+ }
166
+ if (projectName) {
167
+ envOverrides.PROJECT_NAME = projectName;
168
+ }
169
+
170
+ // Merge overrides into local copy
171
+ const lines = envContent.split('\n');
172
+ const seen = new Set();
173
+ const merged = lines.map((line) => {
174
+ const m = line.match(/^([A-Z_][A-Z0-9_]*)=/);
175
+ if (m && envOverrides[m[1]] !== undefined) {
176
+ seen.add(m[1]);
177
+ return `${m[1]}=${escapeDotenv(envOverrides[m[1]])}`;
178
+ }
179
+ return line;
180
+ });
181
+ for (const [k, v] of Object.entries(envOverrides)) {
182
+ if (!seen.has(k)) merged.push(`${k}=${escapeDotenv(v)}`);
183
+ }
184
+ writeFileSync(join(stageDir, '.env'), merged.join('\n'));
185
+
186
+ const isFast = Boolean(options.image?.endsWith(':local'));
187
+ const flags = composeFileFlags(options);
188
+ writeFileSync(join(stageDir, 'reconcile.sh'), renderReconcileScript(projectName, flags, isFast), {
189
+ mode: 0o755,
190
+ });
191
+ writeFileSync(
192
+ join(stageDir, `${projectName}.service`),
193
+ renderSystemdUnit(projectName, flags, isFast),
194
+ );
195
+
196
+ const copyDirIfExist = (srcDir, destDir) => {
197
+ const src = join(cwd, srcDir);
198
+ if (existsSync(src)) {
199
+ if (options.verbose) console.log(`[bundle] Copying ${srcDir} to ${destDir}`);
200
+ cpSync(src, join(stageDir, destDir), { recursive: true });
201
+ } else {
202
+ if (options.verbose) console.log(`[bundle] Skipping ${srcDir} (not found)`);
203
+ }
204
+ };
205
+
206
+ copyDirIfExist('db', 'db');
207
+ copyDirIfExist('volumes/db', 'volumes/db');
208
+ copyDirIfExist('supabase/migrations', 'supabase/migrations');
209
+ copyDirIfExist('volumes/kong', 'volumes/kong');
210
+
211
+ // volumes/traefik/middlewares.yml
212
+ const traefikMiddlewares = join(cwd, 'volumes', 'traefik', 'middlewares.yml');
213
+ if (existsSync(traefikMiddlewares)) {
214
+ mkdirSync(join(stageDir, 'volumes', 'traefik'), { recursive: true });
215
+ cpSync(traefikMiddlewares, join(stageDir, 'volumes', 'traefik', 'middlewares.yml'));
216
+ }
217
+
218
+ copyDirIfExist('backup', 'backup');
219
+ if (options.n8n) copyDirIfExist('volumes/n8n', 'volumes/n8n');
220
+ if (options.metabase) copyDirIfExist('volumes/metabase', 'volumes/metabase');
221
+
222
+ if (options.observability) {
223
+ copyDirIfExist('prometheus', 'prometheus');
224
+ copyDirIfExist('grafana', 'grafana');
225
+ for (const subdir of ['prometheus', 'grafana', 'loki', 'promtail']) {
226
+ copyDirIfExist(`volumes/${subdir}`, `volumes/${subdir}`);
227
+ }
228
+ }
229
+
230
+ return stageDir;
231
+ }