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,120 @@
1
+ /**
2
+ * Task-Level Deployment State Tracker
3
+ * Manages persistence and hashing of deployment steps to enable granular resume.
4
+ */
5
+
6
+ import { createHash } from 'node:crypto';
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+
10
+ export class StateTracker {
11
+ constructor(projectName, environment) {
12
+ this.projectName = projectName;
13
+ this.environment = environment;
14
+ this.stateDir = join(process.cwd(), '.vibecarbon');
15
+ this.stateFile = join(this.stateDir, `deploy-state-${environment}.json`);
16
+ this.state = this.load();
17
+ }
18
+
19
+ /**
20
+ * Load state from disk
21
+ */
22
+ load() {
23
+ if (existsSync(this.stateFile)) {
24
+ try {
25
+ return JSON.parse(readFileSync(this.stateFile, 'utf-8'));
26
+ } catch {
27
+ return { steps: {} };
28
+ }
29
+ }
30
+ return { steps: {} };
31
+ }
32
+
33
+ /**
34
+ * Save state to disk
35
+ */
36
+ save() {
37
+ if (!existsSync(this.stateDir)) {
38
+ mkdirSync(this.stateDir, { recursive: true });
39
+ }
40
+ writeFileSync(this.stateFile, JSON.stringify(this.state, null, 2));
41
+ }
42
+
43
+ /**
44
+ * Calculate a hash for a step's inputs
45
+ * @param {object} inputs
46
+ * @returns {string}
47
+ */
48
+ calculateHash(inputs) {
49
+ const data = JSON.stringify(inputs);
50
+ return createHash('sha256').update(data).digest('hex');
51
+ }
52
+
53
+ /**
54
+ * Check if a step should be skipped based on its input hash
55
+ * @param {string} stepId
56
+ * @param {object} inputs
57
+ * @returns {boolean}
58
+ */
59
+ shouldSkip(stepId, inputs) {
60
+ const currentHash = this.calculateHash(inputs);
61
+ const stepState = this.state.steps[stepId];
62
+
63
+ if (stepState && stepState.status === 'completed' && stepState.hash === currentHash) {
64
+ return true;
65
+ }
66
+ return false;
67
+ }
68
+
69
+ /**
70
+ * Record a step as started
71
+ * @param {string} stepId
72
+ * @param {object} inputs
73
+ */
74
+ startStep(stepId, inputs) {
75
+ const hash = this.calculateHash(inputs);
76
+ this.state.steps[stepId] = {
77
+ status: 'started',
78
+ hash,
79
+ startedAt: new Date().toISOString(),
80
+ };
81
+ this.save();
82
+ }
83
+
84
+ /**
85
+ * Record a step as completed
86
+ * @param {string} stepId
87
+ * @param {object} result
88
+ */
89
+ completeStep(stepId, result = {}) {
90
+ if (!this.state.steps[stepId]) {
91
+ throw new Error(`Step ${stepId} was not started`);
92
+ }
93
+ this.state.steps[stepId].status = 'completed';
94
+ this.state.steps[stepId].completedAt = new Date().toISOString();
95
+ this.state.steps[stepId].result = result;
96
+ this.save();
97
+ }
98
+
99
+ /**
100
+ * Get the result of a completed step
101
+ * @param {string} stepId
102
+ * @returns {object|null}
103
+ */
104
+ getStepResult(stepId) {
105
+ const stepState = this.state.steps[stepId];
106
+ return stepState && stepState.status === 'completed' ? stepState.result : null;
107
+ }
108
+
109
+ /**
110
+ * Clear state for a new deployment
111
+ */
112
+ clear() {
113
+ this.state = { steps: {} };
114
+ if (existsSync(this.stateFile)) {
115
+ try {
116
+ writeFileSync(this.stateFile, JSON.stringify(this.state, null, 2));
117
+ } catch {}
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Deployment Utilities
3
+ * Shared helper functions for all deployment modes
4
+ */
5
+
6
+ import {
7
+ chmodSync,
8
+ existsSync,
9
+ mkdirSync,
10
+ mkdtempSync,
11
+ readdirSync,
12
+ readFileSync,
13
+ rmSync,
14
+ writeFileSync,
15
+ } from 'node:fs';
16
+ import { tmpdir } from 'node:os';
17
+ import { dirname, join } from 'node:path';
18
+ import { runCommand } from '../command.js';
19
+ import { getProviderClass } from '../providers/index.js';
20
+ import { escapeDotenv } from '../shell.js';
21
+
22
+ // Deployment Defaults
23
+ // Cluster-autoscaler is enabled by default with a static floor of 1 and a
24
+ // bounded ceiling of 3 (CA pool spawns 0..(max-min) workers on top of the
25
+ // static floor). See specs/2026-04-25-k3s-pivot-design.md for the design
26
+ // decision: autoscaling on by default, bounded ceiling to cap blast radius.
27
+ export const DEFAULT_WORKER_MIN = 1;
28
+ export const DEFAULT_WORKER_MAX = 3;
29
+
30
+ /**
31
+ * Get provider configuration from registry
32
+ * @param {string} providerName
33
+ * @returns {object}
34
+ */
35
+ export function getProviderConfig(providerName) {
36
+ const ProviderClass = getProviderClass(providerName);
37
+ return {
38
+ name: ProviderClass.NAME,
39
+ regions: ProviderClass.REGIONS,
40
+ serverTypes: ProviderClass.SERVER_TYPES,
41
+ defaultType: ProviderClass.DEFAULT_TYPE,
42
+ haRegions: ProviderClass.HA_REGIONS,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Normalize environment name: lowercase, and treat "production" as "prod"
48
+ * @param {string} envName
49
+ * @returns {string}
50
+ */
51
+ export function normalizeEnvName(envName) {
52
+ const normalized = envName.toLowerCase();
53
+ return normalized === 'production' ? 'prod' : normalized;
54
+ }
55
+
56
+ /**
57
+ * Branch naming: environment name = branch name, except prod uses main
58
+ * @param {string} envName
59
+ * @returns {string}
60
+ */
61
+ export function getBranchName(envName) {
62
+ return envName === 'prod' ? 'main' : envName;
63
+ }
64
+
65
+ /**
66
+ * Merge env overrides into the remote .env file by pulling it down,
67
+ * updating in-memory, and SCPing it back.
68
+ *
69
+ * @param {string} host - remote host IP
70
+ * @param {string} sshOpts - space-separated SSH options string
71
+ * @param {string} remoteDir - absolute path to the remote deployment directory
72
+ * @param {Record<string, string>} updates - key/value pairs to set in .env
73
+ */
74
+ export function mergeRemoteDotenv(host, sshOpts, remoteDir, updates) {
75
+ const dir = mkdtempSync(join(tmpdir(), 'vibecarbon-envmerge-'));
76
+ const local = join(dir, '.env');
77
+ try {
78
+ // Pull remote .env
79
+ try {
80
+ runCommand(['scp', ...sshOpts.split(' '), `root@${host}:${remoteDir}/.env`, local], {
81
+ silent: true,
82
+ });
83
+ } catch (e) {
84
+ throw new Error(
85
+ `Failed to pull ${remoteDir}/.env from ${host} — is the remote file present and SSH reachable? Underlying error: ${e?.message ?? e}`,
86
+ );
87
+ }
88
+ const existing = readFileSync(local, 'utf-8').split('\n');
89
+ const seen = new Set();
90
+ const merged = existing.map((line) => {
91
+ const m = line.match(/^([A-Z_][A-Z0-9_]*)=/);
92
+ if (m && updates[m[1]] !== undefined) {
93
+ seen.add(m[1]);
94
+ return `${m[1]}=${escapeDotenv(updates[m[1]])}`;
95
+ }
96
+ return line;
97
+ });
98
+ for (const [k, v] of Object.entries(updates)) {
99
+ if (!seen.has(k)) merged.push(`${k}=${escapeDotenv(v)}`);
100
+ }
101
+ writeFileSync(local, merged.join('\n'));
102
+ // Push merged .env back
103
+ try {
104
+ runCommand(['scp', ...sshOpts.split(' '), local, `root@${host}:${remoteDir}/.env`], {
105
+ silent: true,
106
+ });
107
+ } catch (e) {
108
+ throw new Error(
109
+ `Failed to push merged .env back to ${host}:${remoteDir}/.env — remote still has the previous contents. Underlying error: ${e?.message ?? e}`,
110
+ );
111
+ }
112
+ } finally {
113
+ rmSync(dir, { recursive: true, force: true });
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Generate SSH key pair for deployment
119
+ * @param {string} keyPath
120
+ * @returns {string} public key content
121
+ */
122
+ export function generateSSHKeyPair(keyPath) {
123
+ if (!existsSync(keyPath)) {
124
+ // Ensure parent directory exists
125
+ const keyDir = dirname(keyPath);
126
+ if (!existsSync(keyDir)) {
127
+ mkdirSync(keyDir, { recursive: true });
128
+ }
129
+ runCommand(
130
+ ['ssh-keygen', '-t', 'ed25519', '-f', keyPath, '-N', '', '-C', 'vibecarbon-deploy'],
131
+ {
132
+ silent: true,
133
+ },
134
+ );
135
+ }
136
+ // Ensure correct permissions for SSH
137
+ chmodSync(keyPath, 0o600);
138
+ chmodSync(`${keyPath}.pub`, 0o644);
139
+ return readFileSync(`${keyPath}.pub`, 'utf-8').trim();
140
+ }
141
+
142
+ /**
143
+ * Wait for SSH to become available on a host
144
+ * @param {string} host
145
+ * @param {string} sshKeyPath
146
+ * @param {number} maxAttempts
147
+ * @returns {Promise<boolean>}
148
+ */
149
+ export async function waitForSSH(host, sshKeyPath, maxAttempts = 30) {
150
+ const sshOpts = `-i "${sshKeyPath}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes`;
151
+ for (let i = 0; i < maxAttempts; i++) {
152
+ try {
153
+ runCommand(
154
+ ['ssh', ...sshOpts.split(' '), '-o', 'ConnectTimeout=5', `root@${host}`, 'echo', 'ok'],
155
+ {
156
+ stdio: 'pipe',
157
+ timeout: 10000,
158
+ },
159
+ );
160
+ return true;
161
+ } catch {
162
+ await new Promise((r) => setTimeout(r, 5000));
163
+ }
164
+ }
165
+ return false;
166
+ }
167
+
168
+ /**
169
+ * Initial server setup - copy project files needed for docker compose
170
+ * @param {string} host - remote host IP
171
+ * @param {string} sshKeyPath - path to the SSH private key
172
+ * @param {string} projectName - name of the project
173
+ * @param {object} options - setup options (observability, n8n, etc.)
174
+ */
175
+ export async function setupServerFiles(host, sshKeyPath, projectName, options = {}) {
176
+ const cwd = process.cwd();
177
+ const remoteDir = `/opt/${projectName}`;
178
+ const sshOpts = `-i "${sshKeyPath}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes`;
179
+
180
+ // Create remote directory and required subdirectories
181
+ runCommand(
182
+ [
183
+ 'ssh',
184
+ ...sshOpts.split(' '),
185
+ `root@${host}`,
186
+ 'mkdir',
187
+ '-p',
188
+ `${remoteDir}/volumes/db`,
189
+ `${remoteDir}/volumes/kong`,
190
+ `${remoteDir}/supabase/migrations`,
191
+ ],
192
+ { stdio: 'pipe' },
193
+ );
194
+
195
+ // List of files to copy
196
+ const filesToCopy = [
197
+ 'docker-compose.yml',
198
+ 'docker-compose.prod.yml',
199
+ 'Dockerfile',
200
+ '.dockerignore',
201
+ ];
202
+
203
+ // Add optional service compose files if enabled
204
+ if (options.observability) {
205
+ filesToCopy.push('docker-compose.observability.yml', 'docker-compose.observability.prod.yml');
206
+ }
207
+ if (options.n8n) {
208
+ filesToCopy.push('docker-compose.n8n.yml', 'docker-compose.n8n.prod.yml');
209
+ }
210
+ if (options.metabase) {
211
+ filesToCopy.push('docker-compose.metabase.yml', 'docker-compose.metabase.prod.yml');
212
+ }
213
+
214
+ // Copy each file that exists
215
+ for (const file of filesToCopy) {
216
+ const localPath = join(cwd, file);
217
+ if (existsSync(localPath)) {
218
+ runCommand(['scp', ...sshOpts.split(' '), localPath, `root@${host}:${remoteDir}/`], {
219
+ stdio: 'pipe',
220
+ });
221
+ }
222
+ }
223
+
224
+ // Copy all volumes/db init files (SQL + shell scripts)
225
+ const volumesDbDir = join(cwd, 'volumes', 'db');
226
+ if (existsSync(volumesDbDir)) {
227
+ for (const file of readdirSync(volumesDbDir)) {
228
+ const filePath = join(volumesDbDir, file);
229
+ if (existsSync(filePath)) {
230
+ runCommand(
231
+ ['scp', ...sshOpts.split(' '), filePath, `root@${host}:${remoteDir}/volumes/db/`],
232
+ {
233
+ stdio: 'pipe',
234
+ },
235
+ );
236
+ }
237
+ }
238
+ }
239
+
240
+ // Copy supabase migrations directory
241
+ const migrationsDir = join(cwd, 'supabase', 'migrations');
242
+ if (existsSync(migrationsDir)) {
243
+ runCommand(
244
+ [
245
+ 'scp',
246
+ ...sshOpts.split(' '),
247
+ '-r',
248
+ `${migrationsDir}/*`,
249
+ `root@${host}:${remoteDir}/supabase/migrations/`,
250
+ ],
251
+ {
252
+ stdio: 'pipe',
253
+ ignoreError: true, // handle the 2>/dev/null || true semantics
254
+ },
255
+ );
256
+ }
257
+
258
+ // Copy kong configuration
259
+ const volumesKongDir = join(cwd, 'volumes', 'kong');
260
+ if (existsSync(volumesKongDir)) {
261
+ for (const file of readdirSync(volumesKongDir)) {
262
+ const filePath = join(volumesKongDir, file);
263
+ if (existsSync(filePath)) {
264
+ runCommand(
265
+ ['scp', ...sshOpts.split(' '), filePath, `root@${host}:${remoteDir}/volumes/kong/`],
266
+ {
267
+ stdio: 'pipe',
268
+ },
269
+ );
270
+ }
271
+ }
272
+ }
273
+
274
+ // Copy local .env file if it exists
275
+ const localEnvPath = join(cwd, '.env');
276
+ if (existsSync(localEnvPath)) {
277
+ runCommand(['scp', ...sshOpts.split(' '), localEnvPath, `root@${host}:${remoteDir}/`], {
278
+ stdio: 'pipe',
279
+ });
280
+ }
281
+
282
+ // Override/append production env vars
283
+ const envOverrides = {};
284
+ if (options.githubRepo && options.envName) {
285
+ envOverrides.APP_IMAGE = `ghcr.io/${options.githubRepo}:${options.envName}`;
286
+ }
287
+ if (options.domain) {
288
+ envOverrides.DOMAIN = options.domain;
289
+ envOverrides.ACME_EMAIL = `admin@${options.domain}`;
290
+ envOverrides.SITE_URL = `https://api.${options.domain}`;
291
+ envOverrides.SUPABASE_URL = 'http://kong:8000';
292
+ }
293
+
294
+ if (Object.keys(envOverrides).length > 0) {
295
+ mergeRemoteDotenv(host, sshOpts, remoteDir, envOverrides);
296
+ }
297
+
298
+ // Copy prometheus config
299
+ const prometheusDir = join(cwd, 'prometheus');
300
+ if (options.observability && existsSync(prometheusDir)) {
301
+ runCommand(
302
+ ['ssh', ...sshOpts.split(' '), `root@${host}`, 'mkdir', '-p', `${remoteDir}/prometheus`],
303
+ { stdio: 'pipe' },
304
+ );
305
+ runCommand(
306
+ [
307
+ 'scp',
308
+ ...sshOpts.split(' '),
309
+ '-r',
310
+ `${prometheusDir}/*`,
311
+ `root@${host}:${remoteDir}/prometheus/`,
312
+ ],
313
+ {
314
+ stdio: 'pipe',
315
+ },
316
+ );
317
+ }
318
+
319
+ // Copy grafana provisioning
320
+ const grafanaDir = join(cwd, 'grafana');
321
+ if (options.observability && existsSync(grafanaDir)) {
322
+ runCommand(['scp', ...sshOpts.split(' '), '-r', grafanaDir, `root@${host}:${remoteDir}/`], {
323
+ stdio: 'pipe',
324
+ });
325
+ }
326
+
327
+ return true;
328
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Persistent deploy logger.
3
+ *
4
+ * Tees every byte written to process.stdout / process.stderr during a
5
+ * deploy invocation into ~/.vibecarbon/logs/<env>-<ts>.log. Survives
6
+ * cluster teardown and process exit so a failed deploy can be reviewed
7
+ * after the fact — including the long-running Pulumi/k3s/Flux output
8
+ * that's otherwise lost when a session ends.
9
+ *
10
+ * ANSI escape sequences (color, cursor moves, spinner frames) are
11
+ * stripped from the file so it's grep-able and reads cleanly in
12
+ * `less`/editors. The terminal still gets the original colored output.
13
+ */
14
+
15
+ import { createWriteStream, existsSync, mkdirSync } from 'node:fs';
16
+ import { homedir } from 'node:os';
17
+ import { join } from 'node:path';
18
+
19
+ // Strip ANSI CSI sequences (ESC + `[` + params + final byte) and bare CRs.
20
+ // Built from String.fromCharCode to avoid biome's no-control-characters lint
21
+ // rejecting a literal escape in the regex; equivalent to
22
+ // `/\x1b\[[0-9;?]*[a-zA-Z]|\r/g`.
23
+ const ANSI_RE = new RegExp(`${String.fromCharCode(0x1b)}\\[[0-9;?]*[a-zA-Z]|\\r`, 'g');
24
+
25
+ /**
26
+ * Wrap an async function so all stdout/stderr during its run is also
27
+ * tee'd to ~/.vibecarbon/logs/<envName>-<timestamp>.log. Returns
28
+ * whatever the wrapped function returns; rethrows after restoring.
29
+ *
30
+ * The returned promise resolves only after the underlying file stream
31
+ * has flushed and closed, so the caller can safely read the log file
32
+ * back as soon as the call settles.
33
+ *
34
+ * @param {string} envName Environment name for the log filename
35
+ * (defaults to "deploy" if missing).
36
+ * @param {() => Promise<any>} fn Async work to run.
37
+ * @returns {Promise<{result: any, logPath: string}>}
38
+ */
39
+ export async function withDeployLog(envName, fn) {
40
+ const logDir = join(homedir(), '.vibecarbon', 'logs');
41
+ if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
42
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
43
+ const safeEnv = (envName || 'deploy').replace(/[^a-zA-Z0-9_-]/g, '_');
44
+ const logPath = join(logDir, `${safeEnv}-${ts}.log`);
45
+ const stream = createWriteStream(logPath, { flags: 'a' });
46
+
47
+ const originalStdoutWrite = process.stdout.write.bind(process.stdout);
48
+ const originalStderrWrite = process.stderr.write.bind(process.stderr);
49
+
50
+ const tee = (orig) => (chunk, encoding, cb) => {
51
+ try {
52
+ const s = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString();
53
+ stream.write(s.replace(ANSI_RE, ''));
54
+ } catch {
55
+ // best-effort — never break terminal output for the sake of logging
56
+ }
57
+ return orig(chunk, encoding, cb);
58
+ };
59
+
60
+ process.stdout.write = tee(originalStdoutWrite);
61
+ process.stderr.write = tee(originalStderrWrite);
62
+
63
+ // Header so the file is self-describing.
64
+ stream.write(
65
+ `# vibecarbon deploy log\n# env: ${envName ?? '(unset)'}\n# started: ${new Date().toISOString()}\n# argv: ${process.argv.join(' ')}\n\n`,
66
+ );
67
+
68
+ // Wait for the underlying fd to actually close before resolving so
69
+ // callers can read the file immediately after the await.
70
+ const closeStream = () =>
71
+ new Promise((resolve) => {
72
+ stream.on('close', resolve);
73
+ stream.end();
74
+ });
75
+
76
+ const restore = async () => {
77
+ process.stdout.write = originalStdoutWrite;
78
+ process.stderr.write = originalStderrWrite;
79
+ await closeStream();
80
+ };
81
+
82
+ try {
83
+ const result = await fn();
84
+ await restore();
85
+ return { result, logPath };
86
+ } catch (err) {
87
+ try {
88
+ stream.write(`\n# deploy threw: ${err instanceof Error ? err.stack || err.message : err}\n`);
89
+ } catch {}
90
+ await restore();
91
+ throw err;
92
+ }
93
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Wait for a DNS A record to propagate to public resolvers.
3
+ *
4
+ * Polls Cloudflare (1.1.1.1) and Google (8.8.8.8) resolvers until at least
5
+ * one returns the expected IP, or the timeout elapses. Returns true on
6
+ * success, false if the budget runs out (caller decides whether to proceed
7
+ * or hard-fail).
8
+ *
9
+ * Why this matters for compose deploys: Traefik attempts ACME HTTP-01
10
+ * challenges immediately on container start and bursts ~7 attempts in
11
+ * ~30s. Let's Encrypt rejects challenges with "no valid A records found"
12
+ * if its own resolver sees stale or absent records (the 0.0.0.0 warm-up
13
+ * placeholder also fails this check — LE treats 0.0.0.0 as not-a-valid-
14
+ * A-record). Once Traefik gives up, acme.json has zero certs and the
15
+ * browser sees the TRAEFIK DEFAULT CERT, which is self-signed and shows
16
+ * NET::ERR_CERT_AUTHORITY_INVALID. Cold-deploy 2026-05-19 RCA against
17
+ * vibecarbon.com hit exactly this.
18
+ *
19
+ * The fix: gate compose-up on DNS propagation so Traefik's first ACME
20
+ * attempt sees real records. Polling public resolvers is the closest
21
+ * approximation we have to "LE will see this" without actually calling
22
+ * LE's resolver directly.
23
+ *
24
+ * @param {string} domain - The hostname to check (e.g. "vibecarbon.com")
25
+ * @param {string} expectedIp - The IP we expect the record to point to
26
+ * @param {number} [timeoutMs=120_000] - Total budget in ms
27
+ * @returns {Promise<boolean>} true if propagated, false if timed out
28
+ */
29
+ export async function waitForDNSPropagation(domain, expectedIp, timeoutMs = 120_000) {
30
+ const dns = await import('node:dns');
31
+ const resolver = new dns.promises.Resolver();
32
+ resolver.setServers(['1.1.1.1', '8.8.8.8']);
33
+
34
+ const deadline = Date.now() + timeoutMs;
35
+ while (Date.now() < deadline) {
36
+ try {
37
+ const addrs = await resolver.resolve4(domain);
38
+ if (addrs.includes(expectedIp)) return true;
39
+ } catch {
40
+ // NXDOMAIN / SERVFAIL / timeout — record may not exist yet or
41
+ // hasn't propagated to this resolver. Retry; cheap to fail.
42
+ }
43
+ const remaining = deadline - Date.now();
44
+ if (remaining <= 0) break;
45
+ await new Promise((r) => setTimeout(r, Math.min(5_000, remaining)));
46
+ }
47
+ return false;
48
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Shared environment resolution
3
+ * Used by backup.js, restore.js, and failover.js
4
+ */
5
+
6
+ import * as p from '@clack/prompts';
7
+ import { c } from './colors.js';
8
+
9
+ /**
10
+ * Resolve which environment the user wants to operate on.
11
+ * - If `args.env` is set, validates it against config
12
+ * - If only one environment exists, selects it automatically
13
+ * - Otherwise, prompts the user to pick one
14
+ *
15
+ * @param {{ env: string | null }} args - Parsed CLI arguments
16
+ * @param {object} projectConfig - Project configuration with environments
17
+ * @returns {Promise<{ envName: string, envConfig: object }>}
18
+ */
19
+ export async function resolveEnvironment(args, projectConfig) {
20
+ const envNames = Object.keys(projectConfig.environments || {});
21
+
22
+ if (envNames.length === 0) {
23
+ p.log.error('No deployed environments found');
24
+ p.log.info(`Run ${c.info('vibecarbon deploy')} first`);
25
+ process.exit(1);
26
+ }
27
+
28
+ let envName = args.env;
29
+
30
+ if (!envName) {
31
+ if (envNames.length === 1) {
32
+ envName = envNames[0];
33
+ } else {
34
+ envName = await p.select({
35
+ message: 'Which environment?',
36
+ options: envNames.map((name) => ({
37
+ value: name,
38
+ label: name,
39
+ hint: projectConfig.environments[name].servers?.map((s) => s.ip).join(', '),
40
+ })),
41
+ });
42
+ }
43
+ }
44
+
45
+ if (p.isCancel(envName)) {
46
+ p.cancel('Operation cancelled.');
47
+ process.exit(0);
48
+ }
49
+
50
+ const envConfig = projectConfig.environments[envName];
51
+ if (!envConfig) {
52
+ p.log.error(`Environment '${envName}' not found`);
53
+ p.log.info(`Available environments: ${envNames.join(', ')}`);
54
+ process.exit(1);
55
+ }
56
+
57
+ return { envName, envConfig };
58
+ }