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,545 @@
1
+ /**
2
+ * Deployment Prompts
3
+ * Interactive CLI flow for gathering deployment configuration
4
+ */
5
+
6
+ import * as p from '@clack/prompts';
7
+ import { getZones as cloudflareGetZones } from '../cloudflare.js';
8
+ import { c, printBanner } from '../colors.js';
9
+ import { loadProjectConfig } from '../config.js';
10
+ import { getZones as hetznerDnsGetZones } from '../hetzner-dns.js';
11
+ import { getApiToken, getS3Credentials } from '../hetzner-guided-setup.js';
12
+ import { HetznerS3Provider, sanitizeBucketName } from '../providers/hetzner-s3.js';
13
+ import { HetznerProvider } from '../providers/index.js';
14
+ import { validateDomain } from '../validators.js';
15
+ import { VERSION } from '../version.js';
16
+ import {
17
+ DEFAULT_WORKER_MAX,
18
+ DEFAULT_WORKER_MIN,
19
+ getBranchName,
20
+ getProviderConfig,
21
+ normalizeEnvName,
22
+ } from './utils.js';
23
+
24
+ // Re-export constants
25
+ export { DEFAULT_WORKER_MAX, DEFAULT_WORKER_MIN };
26
+
27
+ /**
28
+ * Resolve deploy mode (compose vs kubernetes) based on args and existing config.
29
+ */
30
+ export async function resolveDeployMode(args, envConfig) {
31
+ // Respect existing environment's deploy mode + HA setting
32
+ if (envConfig.deployMode) {
33
+ return { deployMode: envConfig.deployMode, ha: envConfig.ha?.enabled || false };
34
+ }
35
+
36
+ // Diamond or Scale tier features: resolve from CLI flags
37
+ if (args.compose && args.ha) return { deployMode: 'compose-ha', ha: true };
38
+ if (args.compose) return { deployMode: 'compose', ha: false };
39
+ if (args.k8s && args.ha) return { deployMode: 'kubernetes', ha: true };
40
+ if (args.k8s) return { deployMode: 'kubernetes', ha: false };
41
+ if (args.ha) return { deployMode: 'kubernetes', ha: true };
42
+
43
+ // Non-interactive: default to compose when --yes and no mode flags
44
+ if (args.yes) {
45
+ return { deployMode: 'compose', ha: false };
46
+ }
47
+
48
+ // Interactive: Simple choices optimized for robustness
49
+ const mode = await p.select({
50
+ message: 'Deployment architecture:',
51
+ options: [
52
+ {
53
+ value: 'compose',
54
+ label: 'Docker Compose (Fast)',
55
+ hint: '1 VPS - Best for startups and internal tools',
56
+ },
57
+ {
58
+ value: 'compose-ha',
59
+ label: 'Docker Compose HA (Auto Failover)',
60
+ hint: '2 VPS - Simple failover without K8s complexity',
61
+ },
62
+ {
63
+ value: 'kubernetes',
64
+ label: 'Kubernetes (Auto Scaling)',
65
+ hint: 'k3s + Autoscaling - Best for high-traffic apps',
66
+ },
67
+ {
68
+ value: 'kubernetes-ha',
69
+ label: 'Kubernetes HA (Auto Scaling + Failover)',
70
+ hint: 'Multi-region cluster - Maximum availability',
71
+ },
72
+ ],
73
+ });
74
+
75
+ if (p.isCancel(mode)) {
76
+ p.cancel('Operation cancelled.');
77
+ process.exit(0);
78
+ }
79
+
80
+ if (mode === 'kubernetes-ha') return { deployMode: 'kubernetes', ha: true };
81
+ if (mode === 'compose-ha') return { deployMode: 'compose-ha', ha: true };
82
+ return { deployMode: mode, ha: false };
83
+ }
84
+
85
+ /**
86
+ * Helper: fetch DNS zones with retry on transient network errors
87
+ */
88
+ async function fetchZonesWithRetry(fetchFn, providerLabel) {
89
+ const s = p.spinner();
90
+ let lastError;
91
+ for (let attempt = 1; attempt <= 3; attempt++) {
92
+ s.start(
93
+ attempt === 1
94
+ ? `Fetching your ${providerLabel} zones`
95
+ : `Retrying zone fetch (attempt ${attempt}/3)`,
96
+ );
97
+ try {
98
+ const zones = await fetchFn();
99
+ s.stop('Zones retrieved');
100
+ return zones;
101
+ } catch (error) {
102
+ lastError = error;
103
+ const isTransient =
104
+ error.message?.includes('fetch failed') ||
105
+ error.message?.includes('ECONNRESET') ||
106
+ error.message?.includes('ETIMEDOUT');
107
+ if (!isTransient || attempt === 3) {
108
+ s.stop(`Failed to fetch ${providerLabel} zones: ${error.message}`);
109
+ throw error;
110
+ }
111
+ s.stop(`Zone fetch failed (${error.message}), retrying...`);
112
+ await new Promise((r) => setTimeout(r, 2000));
113
+ }
114
+ }
115
+ throw lastError;
116
+ }
117
+
118
+ /**
119
+ * Helper: select a zone and prompt for domain
120
+ */
121
+ async function selectZoneAndDomain(zones, providerLabel, existingDomain) {
122
+ let selectedZone;
123
+
124
+ if (existingDomain) {
125
+ const domainParts = existingDomain.split('.');
126
+ for (let i = 0; i < domainParts.length - 1; i++) {
127
+ const possibleZone = domainParts.slice(i).join('.');
128
+ selectedZone = zones.find((z) => z.name === possibleZone);
129
+ if (selectedZone) break;
130
+ }
131
+ if (selectedZone) {
132
+ p.log.info(`Auto-detected zone: ${c.bold(selectedZone.name)}`);
133
+ return { zone: selectedZone, domain: existingDomain };
134
+ }
135
+ }
136
+
137
+ if (zones.length === 1) {
138
+ selectedZone = zones[0];
139
+ p.log.info(`Using zone: ${c.bold(selectedZone.name)}`);
140
+ } else {
141
+ const zoneOptions = zones.map((z) => ({ value: z, label: z.name }));
142
+ selectedZone = await p.select({
143
+ message: `Select your ${providerLabel} DNS zone`,
144
+ options: zoneOptions,
145
+ });
146
+ if (p.isCancel(selectedZone)) {
147
+ p.cancel('Operation cancelled.');
148
+ process.exit(0);
149
+ }
150
+ }
151
+
152
+ const suggestedDomain = `app.${selectedZone.name}`;
153
+ const chosenDomain = await p.text({
154
+ message: `Domain name for ${selectedZone.name}`,
155
+ placeholder: suggestedDomain,
156
+ defaultValue: suggestedDomain,
157
+ initialValue: existingDomain || undefined,
158
+ validate: (v) => {
159
+ if (!v) return undefined;
160
+ const domainErr = validateDomain(v);
161
+ if (domainErr) return domainErr;
162
+ if (!v.endsWith(selectedZone.name) && v !== selectedZone.name) {
163
+ return `Domain must be under ${selectedZone.name} (e.g., app.${selectedZone.name})`;
164
+ }
165
+ return undefined;
166
+ },
167
+ });
168
+ if (p.isCancel(chosenDomain)) {
169
+ p.cancel('Operation cancelled.');
170
+ process.exit(0);
171
+ }
172
+
173
+ return { zone: selectedZone, domain: chosenDomain || null };
174
+ }
175
+
176
+ /**
177
+ * Gather full deployment configuration through interactive prompts and CLI args
178
+ */
179
+ export async function gatherDeploymentConfig(args) {
180
+ console.clear();
181
+ printBanner();
182
+ p.intro(`${c.bold('vibecarbon deploy')} ${c.dim(`v${VERSION}`)}`);
183
+
184
+ const projectConfig = loadProjectConfig();
185
+ if (!projectConfig) {
186
+ p.log.error('No Vibecarbon project found. Run this command from your project directory.');
187
+ process.exit(1);
188
+ }
189
+
190
+ const services = projectConfig.services || {};
191
+ const environment = normalizeEnvName(args.env || 'prod');
192
+ const envConfig = projectConfig.environments?.[environment] || {};
193
+ const resuming = envConfig.status === 'deploying';
194
+
195
+ if (resuming) {
196
+ p.log.info(`Resuming previous deployment to ${c.bold(environment)}...`);
197
+ } else {
198
+ p.log.info(`Deploying to environment: ${c.bold(environment)}`);
199
+ }
200
+
201
+ const { deployMode, ha } = await resolveDeployMode(args, envConfig);
202
+ const isComposeDeploy = deployMode === 'compose' || deployMode === 'compose-ha';
203
+ const config = {
204
+ environment,
205
+ provider: 'hetzner',
206
+ ha,
207
+ observability: services.observability || false,
208
+ deployMode,
209
+ };
210
+ const providerConfig = getProviderConfig(config.provider);
211
+
212
+ // Direct vs Push prompt (compose only, when CI/CD is configured).
213
+ //
214
+ // K8s is local-first via deployK3s — no prompt, no choice. Compose with
215
+ // CI/CD offers both paths: direct is faster (~30s warm) but bypasses
216
+ // the CI review workflow; push is slower (~3 min) but goes through GHA
217
+ // and deploys via Flux.
218
+ //
219
+ // Skipped when:
220
+ // - -y / --yes — non-interactive auto-detects (push if CI configured,
221
+ // direct otherwise)
222
+ // - CI/CD not configured — direct is the only option anyway
223
+ //
224
+ // Mutates args.direct / args.push so the downstream resolveBuildMode call
225
+ // in orchestrator picks up the selection. Pre-PR-5 the operator could
226
+ // also set these via CLI flags; PR 5 dropped that surface — this prompt
227
+ // is now the only path to the override.
228
+ if (isComposeDeploy && !args.yes && !resuming) {
229
+ const { ciAvailable } = await import('../ci-setup.js');
230
+ if (ciAvailable()) {
231
+ const choice = await p.select({
232
+ message: 'How would you like to deploy?',
233
+ options: [
234
+ {
235
+ value: 'direct',
236
+ label: 'Direct',
237
+ hint: 'Build here and push to the server (~30s warm, no CI)',
238
+ },
239
+ {
240
+ value: 'push',
241
+ label: 'Push',
242
+ hint: 'Commit + GitHub Actions build & deploy (~3 min)',
243
+ },
244
+ ],
245
+ initialValue: 'direct',
246
+ });
247
+ if (p.isCancel(choice)) {
248
+ p.cancel('Operation cancelled.');
249
+ process.exit(0);
250
+ }
251
+ if (choice === 'direct') args.direct = true;
252
+ else if (choice === 'push') args.push = true;
253
+ }
254
+ }
255
+
256
+ // Region Selection (Aggressive Default)
257
+ let region = args.region || envConfig.region || 'nbg1'; // Nuremberg as default robust region
258
+ if (!args.region && !envConfig.region && !args.yes) {
259
+ const regionOptions = Object.entries(providerConfig.regions).map(([id, name]) => ({
260
+ value: id,
261
+ label: `${id} - ${name}`,
262
+ }));
263
+ region = await p.select({
264
+ message: 'Region:',
265
+ options: regionOptions,
266
+ initialValue: region,
267
+ });
268
+ if (p.isCancel(region)) {
269
+ p.cancel('Operation cancelled.');
270
+ process.exit(0);
271
+ }
272
+ }
273
+
274
+ let secondaryRegion = null;
275
+ if (ha) {
276
+ secondaryRegion =
277
+ args.secondaryRegion || envConfig.secondaryRegion || (region === 'nbg1' ? 'fsn1' : 'nbg1');
278
+ }
279
+
280
+ const apiToken = await getApiToken(projectConfig.projectName);
281
+ if (!apiToken) {
282
+ p.log.error('API token required');
283
+ process.exit(1);
284
+ }
285
+
286
+ await HetznerProvider.fetchServerTypes(apiToken);
287
+ const regionDefaults = HetznerProvider.getRegionDefaults(region);
288
+
289
+ // Server Type Selection (Streamlined)
290
+ let serverType, masterServerType, workerServerType, supabaseServerType;
291
+
292
+ if (isComposeDeploy) {
293
+ // Compose = single-host Supabase stack, so use the medium-tier default
294
+ // (2+ vCPU, 4+ GB) for the whole box. regionDefaults.supabaseType is
295
+ // live-catalog-derived so it avoids retired SKUs like cpx21 in EU.
296
+ serverType = args.serverType || envConfig.serverType || regionDefaults.supabaseType;
297
+ masterServerType = workerServerType = supabaseServerType = serverType;
298
+ } else {
299
+ // `--type <x>` (args.serverType) acts as a blanket fallback for all three
300
+ // node roles when a role-specific flag isn't set. Matches the compose
301
+ // case's simpler mental model and lets e2e pin the whole cluster
302
+ // to a single known-good SKU via `--type cx23`.
303
+ const blanket = args.serverType || envConfig.serverType;
304
+ masterServerType =
305
+ args.masterServerType || envConfig.masterServerType || blanket || regionDefaults.masterType;
306
+ supabaseServerType =
307
+ args.supabaseServerType ||
308
+ envConfig.supabaseServerType ||
309
+ blanket ||
310
+ regionDefaults.supabaseType;
311
+ workerServerType =
312
+ args.workerServerType || envConfig.workerServerType || blanket || regionDefaults.workerType;
313
+ serverType = workerServerType;
314
+ }
315
+
316
+ // DNS provider selection.
317
+ //
318
+ // Default is Hetzner: the same Cloud API token (`apiToken` resolved above)
319
+ // already administers Hetzner DNS zones, so there's zero extra setup for
320
+ // operators who keep DNS in Hetzner. Cloudflare is offered as an opt-in;
321
+ // if selected without a saved token, we prompt for one (with an offer to
322
+ // save). Manual skips DNS automation entirely.
323
+ //
324
+ // (cert-manager DNS-01 for Hetzner reads the Cloud API token from Secret
325
+ // `hetzner` / key `token`; the legacy separate DNS Console token was
326
+ // retired in May 2026 when Hetzner folded zone management into the main
327
+ // Cloud Console.)
328
+ let dnsProvider = args.dnsProvider || envConfig.dnsProvider;
329
+ let domain = args.domain || envConfig.domain || null;
330
+ let cloudflareZoneId = envConfig.dns?.cloudflareZoneId || null;
331
+ let hetznerDnsZoneId = envConfig.dns?.hetznerZoneId || null;
332
+
333
+ const { loadCredentials, saveCredentials } = await import('../config.js');
334
+ const savedCreds = loadCredentials();
335
+ let cloudflareApiToken =
336
+ process.env.CLOUDFLARE_API_TOKEN || savedCreds.cloudflare?.apiToken || null;
337
+
338
+ if (!dnsProvider) {
339
+ if (args.yes) {
340
+ dnsProvider = 'hetzner';
341
+ } else {
342
+ const choice = await p.select({
343
+ message: 'DNS provider:',
344
+ options: [
345
+ {
346
+ value: 'hetzner',
347
+ label: 'Hetzner DNS',
348
+ hint: 'Uses your Hetzner Cloud API token (no extra setup)',
349
+ },
350
+ {
351
+ value: 'cloudflare',
352
+ label: 'Cloudflare',
353
+ hint: cloudflareApiToken ? 'API token saved' : 'Will prompt for API token',
354
+ },
355
+ {
356
+ value: 'manual',
357
+ label: 'Manual',
358
+ hint: 'Skip DNS automation — configure records yourself',
359
+ },
360
+ ],
361
+ initialValue: 'hetzner',
362
+ });
363
+ if (p.isCancel(choice)) {
364
+ p.cancel('Operation cancelled.');
365
+ process.exit(0);
366
+ }
367
+ dnsProvider = choice;
368
+ }
369
+ }
370
+
371
+ // Cloudflare selected without a saved/env token → prompt for one (and
372
+ // offer to persist). Under --yes we cannot prompt; fall back to manual
373
+ // with a clear warning.
374
+ if (dnsProvider === 'cloudflare' && !cloudflareApiToken) {
375
+ if (args.yes) {
376
+ p.log.warn(
377
+ 'Cloudflare selected but no API token found (env CLOUDFLARE_API_TOKEN or ~/.vibecarbon/credentials.json). Falling back to manual DNS.',
378
+ );
379
+ dnsProvider = 'manual';
380
+ } else {
381
+ const token = await p.password({
382
+ message: 'Cloudflare API token:',
383
+ validate: (v) => (v && v.length > 0 ? undefined : 'Token is required'),
384
+ });
385
+ if (p.isCancel(token)) {
386
+ p.cancel('Operation cancelled.');
387
+ process.exit(0);
388
+ }
389
+ cloudflareApiToken = token;
390
+ const persist = await p.confirm({
391
+ message: 'Save token to ~/.vibecarbon/credentials.json for future deploys?',
392
+ initialValue: true,
393
+ });
394
+ if (!p.isCancel(persist) && persist) {
395
+ saveCredentials({ cloudflare: { apiToken: cloudflareApiToken } });
396
+ p.log.success('Cloudflare API token saved');
397
+ }
398
+ }
399
+ }
400
+
401
+ // Auto-discover zone IDs when a domain is already configured (config-driven
402
+ // re-deploys land here without prompting).
403
+ if (domain && dnsProvider === 'cloudflare' && !cloudflareZoneId && cloudflareApiToken) {
404
+ try {
405
+ const zones = await cloudflareGetZones(cloudflareApiToken);
406
+ const zone = zones.find((z) => domain.endsWith(z.name));
407
+ if (zone) cloudflareZoneId = zone.id;
408
+ } catch {
409
+ /* ignore */
410
+ }
411
+ }
412
+ if (domain && dnsProvider === 'hetzner' && !hetznerDnsZoneId) {
413
+ try {
414
+ const zones = await hetznerDnsGetZones(apiToken);
415
+ const zone = zones.find((z) => domain.endsWith(z.name));
416
+ if (zone) hetznerDnsZoneId = zone.id;
417
+ } catch {
418
+ /* ignore */
419
+ }
420
+ }
421
+
422
+ // Interactive zone-and-domain selection when no domain is configured yet.
423
+ // Skipped under --yes (only the auto-discovery above runs).
424
+ if (!args.yes) {
425
+ if (dnsProvider === 'cloudflare' && !cloudflareZoneId && cloudflareApiToken) {
426
+ try {
427
+ const zones = await fetchZonesWithRetry(
428
+ () => cloudflareGetZones(cloudflareApiToken),
429
+ 'Cloudflare',
430
+ );
431
+ const result = await selectZoneAndDomain(zones, 'Cloudflare', domain);
432
+ domain = result.domain;
433
+ cloudflareZoneId = result.zone.id;
434
+ } catch (err) {
435
+ p.log.warn(`Cloudflare unavailable (${err.message}). Falling back to manual DNS.`);
436
+ dnsProvider = 'manual';
437
+ }
438
+ } else if (dnsProvider === 'hetzner' && !hetznerDnsZoneId) {
439
+ try {
440
+ const zones = await fetchZonesWithRetry(() => hetznerDnsGetZones(apiToken), 'Hetzner');
441
+ if (zones.length === 0) {
442
+ p.log.warn('No Hetzner DNS zones found on this account. Falling back to manual DNS.');
443
+ dnsProvider = 'manual';
444
+ } else {
445
+ const result = await selectZoneAndDomain(zones, 'Hetzner', domain);
446
+ domain = result.domain;
447
+ hetznerDnsZoneId = result.zone.id;
448
+ }
449
+ } catch (err) {
450
+ p.log.warn(`Hetzner DNS unavailable (${err.message}). Falling back to manual DNS.`);
451
+ dnsProvider = 'manual';
452
+ }
453
+ }
454
+ }
455
+
456
+ // S3 (Standardized)
457
+ let s3AccessKey = args.s3AccessKey || envConfig.s3?.accessKey;
458
+ let s3SecretKey = args.s3SecretKey || null;
459
+ // Env-var override (HETZNER_S3_REGION) sits between explicit CLI flag and
460
+ // project config so operators can route the Pulumi-state + backup buckets
461
+ // to a different Hetzner OS region during a regional incident without
462
+ // editing .vibecarbon.json. Mirrors HETZNER_API_TOKEN's role for the cloud
463
+ // API. Surfaced after 2026-04-29 nbg1 OS degradation killed both HA
464
+ // scenarios on Pulumi backend ops (lock delete 503, history save 504,
465
+ // checkpoint read conn-reset).
466
+ const s3Region =
467
+ args.s3Region ||
468
+ process.env.HETZNER_S3_REGION ||
469
+ envConfig.s3?.region ||
470
+ HetznerS3Provider.resolveS3Region(region);
471
+
472
+ if (!s3AccessKey) {
473
+ const credentials = await getS3Credentials(projectConfig.projectName);
474
+ s3AccessKey = credentials.accessKey;
475
+ s3SecretKey = credentials.secretKey;
476
+ }
477
+
478
+ const s3Provider = new HetznerS3Provider(s3AccessKey, s3SecretKey, s3Region);
479
+ const bucketName = sanitizeBucketName(projectConfig.projectName);
480
+ const backupBucketName = sanitizeBucketName(projectConfig.projectName, 'backups');
481
+ const backupConfig = {
482
+ schedule: args.backupSchedule || envConfig.backup?.schedule || '0 */6 * * *',
483
+ retentionDays: Number.parseInt(
484
+ args.backupRetentionDays || envConfig.backup?.retentionDays || '30',
485
+ 10,
486
+ ),
487
+ };
488
+
489
+ const branchName = getBranchName(config.environment);
490
+
491
+ if (!args.yes && !resuming) {
492
+ p.note(
493
+ `
494
+ Env: ${c.bold(config.environment)}
495
+ Region: ${c.bold(region)}${ha ? ` + ${secondaryRegion}` : ''}
496
+ Stack: ${c.bold(isComposeDeploy ? 'Docker Compose' : 'Kubernetes (k3s)')}
497
+ Domain: ${c.bold(domain || 'None')}
498
+ `,
499
+ 'Deployment Summary',
500
+ );
501
+
502
+ const confirmed = await p.confirm({ message: 'Proceed?', initialValue: true });
503
+ if (!confirmed || p.isCancel(confirmed)) {
504
+ p.cancel('Operation cancelled.');
505
+ process.exit(0);
506
+ }
507
+ }
508
+
509
+ return {
510
+ projectConfig,
511
+ envConfig,
512
+ environment,
513
+ resuming,
514
+ config,
515
+ apiToken,
516
+ region,
517
+ secondaryRegion,
518
+ serverType,
519
+ masterServerType,
520
+ supabaseServerType,
521
+ workerServerType,
522
+ minWorkers: args.minWorkers ?? DEFAULT_WORKER_MIN,
523
+ maxWorkers: args.maxWorkers ?? DEFAULT_WORKER_MAX,
524
+ domain,
525
+ dnsProvider,
526
+ cloudflareApiToken,
527
+ cloudflareZoneId,
528
+ hetznerDnsZoneId,
529
+ s3Config: {
530
+ accessKey: s3AccessKey,
531
+ secretKey: s3SecretKey,
532
+ bucket: bucketName,
533
+ region: s3Region,
534
+ endpoint: s3Provider.getEndpoint(),
535
+ },
536
+ backupS3Config: {
537
+ bucket: backupBucketName,
538
+ region: s3Region,
539
+ endpoint: s3Provider.getEndpoint(),
540
+ },
541
+ backupConfig,
542
+ services,
543
+ branchName,
544
+ };
545
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Remote Build Logic
3
+ * Uses native Docker via SSH contexts to build images directly on target architecture.
4
+ * Bypasses local QEMU and remote registries for maximum speed.
5
+ */
6
+
7
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import * as p from '@clack/prompts';
11
+ import { runCommand } from '../command.js';
12
+ import { perfTimer } from '../perf.js';
13
+
14
+ /**
15
+ * Execute a remote build using native Docker over SSH
16
+ * @param {string} ip - Remote host IP
17
+ * @param {string} sshKeyPath - Path to the SSH private key
18
+ * @param {string} imageTag - The tag to apply to the built image
19
+ * @param {string} cwd - The build context directory (usually project root)
20
+ * @param {Record<string, string>} buildArgs - Key/value pairs for --build-arg
21
+ * @returns {Promise<boolean>} Success
22
+ */
23
+ export async function buildRemote(ip, sshKeyPath, imageTag, cwd, buildArgs = {}) {
24
+ const s = p.spinner();
25
+ s.start(`Building image natively on ${ip}...`);
26
+
27
+ const dockerHost = `ssh://root@${ip}`;
28
+
29
+ // Create a temporary directory for our SSH wrapper
30
+ const tempBinDir = mkdtempSync(join(tmpdir(), 'vc-bin-'));
31
+ const sshWrapperPath = join(tempBinDir, 'ssh');
32
+
33
+ // Create an SSH wrapper script that forces our options. Docker uses the
34
+ // system 'ssh' command found in PATH — we prepend a temp dir containing
35
+ // this wrapper so Docker's SSH transport picks it up instead. Pass the
36
+ // key path via env var rather than string-interpolating into the script
37
+ // to keep shell-escaping concerns out of the picture (paths with spaces
38
+ // or quotes would otherwise break the wrapper).
39
+ const sshWrapper = `#!/bin/bash
40
+ exec /usr/bin/ssh -i "$VIBECARBON_SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=30 "$@"
41
+ `;
42
+ writeFileSync(sshWrapperPath, sshWrapper, { mode: 0o755 });
43
+
44
+ const env = {
45
+ ...process.env,
46
+ DOCKER_HOST: dockerHost,
47
+ DOCKER_BUILDKIT: '1',
48
+ PATH: `${tempBinDir}:${process.env.PATH}`, // Prepend our wrapper
49
+ VIBECARBON_SSH_KEY: sshKeyPath,
50
+ };
51
+
52
+ try {
53
+ // 1. Ensure the host is reachable and sshd is ready. Caller has usually
54
+ // already gone through setupServer/waitForDockerReady, so SSH is live by
55
+ // attempt 0; the fast leg [1, 1, 2, 2] catches that path quickly. Tail
56
+ // keeps 5s pacing for the rare genuinely slow-coming-up case.
57
+ // Gated under perfTimer because the SSH-probe wait is invisible to the
58
+ // outer `deploy.image.remoteBuild` timer (which only covers the docker
59
+ // build subprocess) — and analysis of iter-reliab3 showed the wrapper
60
+ // running 71s longer than the longest child timer, suggesting this
61
+ // probe loop is sometimes burning serious time we couldn't attribute.
62
+ const sshProbeT = perfTimer('deploy.image.remoteBuild.sshProbe');
63
+ const SSH_DELAYS_MS = [
64
+ 1000, 1000, 2000, 2000, 5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000, 10_000, 10_000,
65
+ ];
66
+ let sshReady = false;
67
+ for (let i = 0; i < SSH_DELAYS_MS.length; i++) {
68
+ try {
69
+ // Bare `ssh` resolves through PATH to the wrapper script written
70
+ // above (sshWrapper) which adds -i + -o BatchMode=yes etc.; the
71
+ // wrapper is the only `ssh` on PATH for this child process.
72
+ // shell-safety-ignore: ssh is the PATH-prepended wrapper, not system ssh
73
+ runCommand(['ssh', `root@${ip}`, 'echo', 'ready'], { silent: true, env });
74
+ sshReady = true;
75
+ break;
76
+ } catch {
77
+ await new Promise((r) => setTimeout(r, SSH_DELAYS_MS[i]));
78
+ }
79
+ }
80
+ sshProbeT.end();
81
+
82
+ if (!sshReady) {
83
+ s.stop('Remote SSH not reachable');
84
+ return false;
85
+ }
86
+
87
+ // 2. Run the build directly on the remote host via DOCKER_HOST=ssh://...
88
+ const args = ['docker', 'build', '-t', imageTag, '.'];
89
+
90
+ for (const [key, value] of Object.entries(buildArgs)) {
91
+ args.push('--build-arg', `${key}=${value}`);
92
+ }
93
+
94
+ // The build itself is the dominant cost on direct-mode deploys; gate it
95
+ // behind a perfTimer so cold (first-build, ~2-5min) vs warm (cached
96
+ // BuildKit, 1-3s) is attributable in the trace.
97
+ //
98
+ // runCommand without silent:true returns `false` on non-zero exit instead
99
+ // of throwing (see lib/command.js:135). Without this check, a failed
100
+ // remote build silently proceeded to `s.stop('Image built natively')`,
101
+ // returning true to the HA fan; the missing image then surfaced ~3 min
102
+ // later as `pull access denied for <project>-app` during compose up
103
+ // (compose-ha 2026-05-09T00:38 e2 failure). Treat the false return as a
104
+ // hard failure so the error path captures + reports it.
105
+ const buildT = perfTimer('deploy.image.remoteBuild');
106
+ const buildOk = runCommand(args, {
107
+ cwd,
108
+ stdio: 'inherit',
109
+ env,
110
+ timeout: 600000,
111
+ });
112
+ buildT.end();
113
+ if (buildOk === false) {
114
+ throw new Error(`docker build failed on ${ip} for tag ${imageTag}`);
115
+ }
116
+
117
+ s.stop(`Image built natively: ${imageTag}`);
118
+ return true;
119
+ } catch (error) {
120
+ s.stop(`Remote build failed`);
121
+ p.log.error(error.message);
122
+ return false;
123
+ } finally {
124
+ try {
125
+ rmSync(tempBinDir, { recursive: true, force: true });
126
+ } catch {
127
+ /* ignore cleanup errors */
128
+ }
129
+ }
130
+ }