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,52 @@
1
+ /**
2
+ * Shared helpers for loading + rendering the k3s cloud-init scripts in
3
+ * `carbon/cloud-init/k3s/` (master-init.sh / supabase-init.sh /
4
+ * worker-init.sh).
5
+ *
6
+ * Used by:
7
+ * - `src/lib/iac/programs/hetzner-k8s.js` to render user_data baked
8
+ * into the master/supabase/static-worker servers at Pulumi up.
9
+ * - `src/lib/deploy/k8s/k3s.js` (`renderCAClusterConfig`) to render
10
+ * the worker-init.sh body that cluster-autoscaler stuffs into its
11
+ * `cluster-config.json` Secret. CA spawns workers on demand and
12
+ * base64-encodes this body into user_data on each `ServerCreate`.
13
+ */
14
+
15
+ import { readFileSync } from 'node:fs';
16
+ import { dirname, join } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = dirname(__filename);
21
+
22
+ // Cloud-init scripts live in carbon/cloud-init/k3s/.
23
+ // Relative to this file: src/lib/iac/ → ../../../carbon/cloud-init/k3s
24
+ const CLOUD_INIT_DIR = join(__dirname, '../../../carbon/cloud-init/k3s');
25
+
26
+ /**
27
+ * Read a cloud-init script template by basename (e.g. 'master-init.sh').
28
+ * Returns the raw template — `${var}` placeholders are NOT yet
29
+ * substituted; pass to `renderScript()` to substitute.
30
+ *
31
+ * @param {string} name Basename inside carbon/cloud-init/k3s/.
32
+ * @returns {string} UTF-8 contents of the script.
33
+ */
34
+ export function loadCloudInit(name) {
35
+ return readFileSync(join(CLOUD_INIT_DIR, name), 'utf-8');
36
+ }
37
+
38
+ /**
39
+ * Substitute ${var} placeholders in a cloud-init script template with
40
+ * values from a JS object. Used to render the master/supabase/worker
41
+ * cloud-init scripts at deploy time.
42
+ *
43
+ * @param {string} tmpl
44
+ * @param {Record<string, unknown>} vars
45
+ * @returns {string}
46
+ */
47
+ export function renderScript(tmpl, vars) {
48
+ return Object.entries(vars).reduce(
49
+ (acc, [k, v]) => acc.replace(new RegExp(`\\$\\{${k}\\}`, 'g'), String(v ?? '')),
50
+ tmpl,
51
+ );
52
+ }
@@ -0,0 +1,325 @@
1
+ /**
2
+ * Pulumi-based Infrastructure-as-Code runtime for Vibecarbon.
3
+ *
4
+ * Entry point for Pulumi's Automation API. Programs live under
5
+ * `src/lib/iac/programs/` and are invoked from here. State is stored in
6
+ * the user's Hetzner S3 bucket by default; we fall back to a local file
7
+ * backend when no S3 is configured.
8
+ *
9
+ * Design notes:
10
+ * - We use inline programs (Automation API, no Pulumi.yaml files) so there's
11
+ * nothing for users to manage on disk. Stack state lives in the backend;
12
+ * the program code lives in this npm package.
13
+ * - One project name ("vibecarbon"), many stacks (one per environment like
14
+ * "prod", "staging"). HA deploys use two stacks: "<env>-primary" and
15
+ * "<env>-standby".
16
+ * - Pulumi's own secrets provider isn't used — secrets stay in
17
+ * ~/.vibecarbon/credentials.json; we pass them to programs as plain config.
18
+ */
19
+
20
+ import { mkdirSync } from 'node:fs';
21
+ import { homedir } from 'node:os';
22
+ import { join } from 'node:path';
23
+ import { LocalWorkspace } from '@pulumi/pulumi/automation/index.js';
24
+
25
+ const PULUMI_PROJECT = 'vibecarbon';
26
+ const LOCAL_STATE_DIR = join(homedir(), '.vibecarbon', 'pulumi-state');
27
+
28
+ // Pulumi's file:// backend requires the target directory to exist before
29
+ // `stack select` runs, otherwise the first scenario to race for a stack
30
+ // fails with `error: unable to open bucket file:///... no such file or
31
+ // directory`. Create it at module load so every call path sees it.
32
+ mkdirSync(LOCAL_STATE_DIR, { recursive: true });
33
+
34
+ /**
35
+ * Compute the Pulumi backend URL. Prefers Hetzner S3 when s3Config is
36
+ * provided; otherwise falls back to a local file:// backend under
37
+ * ~/.vibecarbon/pulumi-state. Pulumi Cloud is opt-in via
38
+ * PULUMI_BACKEND_URL env var before invoking vibecarbon.
39
+ *
40
+ * Hetzner S3 uses path-style URLs via the Hetzner-provided endpoint, so we
41
+ * pass both `endpoint` and `s3ForcePathStyle=true` in the query string.
42
+ */
43
+ export function resolveBackendUrl(s3Config) {
44
+ if (process.env.PULUMI_BACKEND_URL) {
45
+ return process.env.PULUMI_BACKEND_URL;
46
+ }
47
+ if (s3Config?.bucket && s3Config?.endpoint) {
48
+ const endpointHost = s3Config.endpoint.replace(/^https?:\/\//, '');
49
+ const region = s3Config.region || 'nbg1';
50
+ return `s3://${s3Config.bucket}?endpoint=${endpointHost}&region=${region}&s3ForcePathStyle=true`;
51
+ }
52
+ return `file://${LOCAL_STATE_DIR}`;
53
+ }
54
+
55
+ /**
56
+ * Build the env var bag Pulumi needs. Hetzner provider reads HCLOUD_TOKEN;
57
+ * S3 backend reads AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY; our programs
58
+ * read any additional config we pass through `stack.setConfig(...)`.
59
+ */
60
+ function buildEnv(options) {
61
+ const env = { ...process.env };
62
+ if (options.hcloudToken) env.HCLOUD_TOKEN = options.hcloudToken;
63
+ if (options.s3Config?.accessKey) env.AWS_ACCESS_KEY_ID = options.s3Config.accessKey;
64
+ if (options.s3Config?.secretKey) env.AWS_SECRET_ACCESS_KEY = options.s3Config.secretKey;
65
+ // Disable Pulumi's telemetry for solo-hosted users. No data leaves the box.
66
+ env.PULUMI_SKIP_UPDATE_CHECK = 'true';
67
+ // Inline programs don't need a passphrase — but Pulumi insists on one for
68
+ // the secrets provider. A deterministic empty value keeps the automation
69
+ // API happy without us caring about secrets encryption at the state level
70
+ // (we never store secrets in Pulumi state; they flow in at program time).
71
+ env.PULUMI_CONFIG_PASSPHRASE = options.configPassphrase || '';
72
+ return env;
73
+ }
74
+
75
+ /**
76
+ * Create or select a stack with the given name + inline program. Returns
77
+ * the Stack instance (caller drives up/destroy/refresh).
78
+ */
79
+ export async function getOrCreateStack(stackName, program, options = {}) {
80
+ const backendUrl = resolveBackendUrl(options.s3Config);
81
+ const envVars = buildEnv(options);
82
+
83
+ const stack = await LocalWorkspace.createOrSelectStack(
84
+ {
85
+ stackName,
86
+ projectName: PULUMI_PROJECT,
87
+ program,
88
+ },
89
+ {
90
+ projectSettings: {
91
+ name: PULUMI_PROJECT,
92
+ runtime: 'nodejs',
93
+ backend: { url: backendUrl },
94
+ },
95
+ envVars,
96
+ },
97
+ );
98
+
99
+ return stack;
100
+ }
101
+
102
+ /**
103
+ * Pre-initialize a Pulumi S3 backend by creating a no-op stack and removing
104
+ * it. The first `pulumi stack select` against a freshly-created S3 bucket
105
+ * fails with `could not list bucket: blob (code=NotFound): NoSuchKey`
106
+ * because the backend hasn't laid down its `.pulumi/meta.yaml` yet. When
107
+ * `deployK8sHA` launches primary + standby `deployK3s` in parallel against
108
+ * the SAME bucket, both threads race the bucket-init and both see the
109
+ * NoSuchKey error, killing the deploy in <20s. Calling this once
110
+ * sequentially before the parallel block writes the backend metadata so
111
+ * subsequent stack ops succeed.
112
+ *
113
+ * Idempotent: if the bucket is already initialized, the no-op stack is
114
+ * created and removed cleanly. If it fails (e.g., bucket access issue),
115
+ * we log and proceed — the parallel deploys will surface the underlying
116
+ * problem with a clearer error.
117
+ *
118
+ * @param {object} options - Same shape as getOrCreateStack options
119
+ * (s3Config, hcloudToken, configPassphrase). The stack name we use is
120
+ * internal (`__init`); callers don't see it.
121
+ */
122
+ export async function initBackend(options = {}) {
123
+ const initStackName = '__init';
124
+ // No-op program — returns nothing. We don't run `up` so this never
125
+ // executes Hetzner API calls. The act of `createStack` writing the
126
+ // stack's checkpoint file to S3 forces the backend to lay down its
127
+ // metadata.
128
+ const noopProgram = async () => ({});
129
+ try {
130
+ const stack = await getOrCreateStack(initStackName, noopProgram, options);
131
+ // Remove it so it doesn't leak into stack listings.
132
+ await stack.workspace.removeStack(initStackName).catch(() => {});
133
+ } catch (err) {
134
+ console.error(`[pulumi] backend pre-init warning: ${err.message?.split('\n')[0] || err}`);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Run `pulumi up` and return the raw outputs. Streams Pulumi logs to an
140
+ * optional `onOutput` sink so callers can forward progress to a spinner.
141
+ */
142
+ export async function upStack(stackName, program, options = {}) {
143
+ const stack = await getOrCreateStack(stackName, program, options);
144
+ // Clear any stale update lock before `up` — when a prior run aborted
145
+ // mid-flight (Ctrl-C, scenario timeout, background-task stop), Pulumi's
146
+ // file backend leaves a `.lock.*` in the bucket and subsequent `up`
147
+ // calls fail with:
148
+ //
149
+ // error: the stack is currently locked by 1 lock(s). Either wait for
150
+ // the other process(es) to end or delete the lock file with
151
+ // `pulumi cancel`.
152
+ //
153
+ // Since e2e runs one scenario per stack at a time, it's always
154
+ // safe to clear. `stack.cancel()` is idempotent (no-op if no lock).
155
+ try {
156
+ await stack.cancel();
157
+ } catch {
158
+ /* no lock — ignore */
159
+ }
160
+
161
+ // Refresh state before `up` to purge pending operations from a crashed
162
+ // prior run. Without this, Pulumi's `up` with "N pending operations
163
+ // from previous deployment" warnings can trigger a delete-before-replace
164
+ // on resources that don't actually exist in the cloud, hitting provider
165
+ // panics (e.g., hcloud@1.32.1 nil-deref on Firewall Delete). `refresh`
166
+ // reconciles state with live cloud resources and clears the pending-op
167
+ // markers. Idempotent — no-op when state already matches reality.
168
+ //
169
+ // Skip on truly-empty stacks: a freshly-created stack has only the
170
+ // bookkeeping `pulumi:pulumi:Stack` resource (no real cloud resources
171
+ // to reconcile). Refresh still pays plugin spawn + cloud API listing,
172
+ // ~3-8s on cold compose deploys for nothing. exportStack() failure
173
+ // falls through to the safer always-refresh path.
174
+ let needsRefresh = true;
175
+ try {
176
+ const state = await stack.exportStack();
177
+ const resources = state?.deployment?.resources ?? [];
178
+ needsRefresh = resources.some((r) => r.type !== 'pulumi:pulumi:Stack');
179
+ } catch {
180
+ /* exportStack failed — fall through and refresh as before */
181
+ }
182
+ if (needsRefresh) {
183
+ try {
184
+ await stack.refresh({ onOutput: options.onOutput, color: 'never' });
185
+ } catch (err) {
186
+ // refresh can fail if the state file is completely corrupt or a
187
+ // resource is unreachable. Log and proceed — `up` may still succeed
188
+ // or surface a cleaner error.
189
+ console.error(`[pulumi] pre-up refresh warning: ${err.message?.split('\n')[0] || err}`);
190
+ }
191
+ }
192
+
193
+ const result = await stack.up({
194
+ onOutput: options.onOutput,
195
+ color: 'never',
196
+ });
197
+ // Flatten outputs: { foo: { value: 'bar', secret: false } } → { foo: 'bar' }
198
+ const outputs = {};
199
+ for (const [key, { value }] of Object.entries(result.outputs || {})) {
200
+ outputs[key] = value;
201
+ }
202
+ return { outputs, summary: result.summary };
203
+ }
204
+
205
+ /**
206
+ * Run `pulumi destroy` and remove the stack. Idempotent — if the stack
207
+ * doesn't exist the function resolves without error.
208
+ */
209
+ export async function destroyStack(stackName, program, options = {}) {
210
+ let stack;
211
+ try {
212
+ stack = await getOrCreateStack(stackName, program, options);
213
+ } catch (err) {
214
+ // Stack doesn't exist yet — treat as a no-op so destroy is idempotent.
215
+ if (/no stack named/i.test(err.message || '')) return { destroyed: false };
216
+ throw err;
217
+ }
218
+ // Refresh first so destroy tolerates resources that were already reaped
219
+ // out-of-band (e.g. Hetzner server deleted by a prior partial destroy or
220
+ // by direct API cleanup). Without this, hcloud returns 404 and pulumi
221
+ // treats it as a hard error — surfaced in orphan-stack cleanup as
222
+ // "server not found (not_found, ...)".
223
+ try {
224
+ await stack.refresh({ onOutput: options.onOutput, color: 'never' });
225
+ } catch (err) {
226
+ console.error(`[pulumi] pre-destroy refresh warning: ${err.message?.split('\n')[0] || err}`);
227
+ }
228
+ await stack.destroy({ onOutput: options.onOutput, color: 'never' });
229
+ await stack.workspace.removeStack(stackName);
230
+ return { destroyed: true };
231
+ }
232
+
233
+ /**
234
+ * Read the current outputs of a stack without running an update. Used by
235
+ * commands like `status`, `scale`, `failover` that need infra identifiers
236
+ * (IPs, IDs) but shouldn't drift the state.
237
+ */
238
+ export async function getStackOutputs(stackName, program, options = {}) {
239
+ const stack = await getOrCreateStack(stackName, program, options);
240
+ const raw = await stack.outputs();
241
+ const outputs = {};
242
+ for (const [key, { value }] of Object.entries(raw)) {
243
+ outputs[key] = value;
244
+ }
245
+ return outputs;
246
+ }
247
+
248
+ /**
249
+ * Classify a prior-outputs probe for the k3sToken probe-and-replay path.
250
+ *
251
+ * `scale.js` reads the prior stack outputs before constructing the real
252
+ * Pulumi program so it can replay the deploy-time `k3sToken` and avoid
253
+ * destructive userData drift (see the master-replace defense). The probe
254
+ * has three observable outcomes:
255
+ *
256
+ * - `recovered`: outputs has a non-empty k3sToken — replay it
257
+ * - `empty`: outputs object exists but k3sToken is missing or empty —
258
+ * legitimate when the stack is fresh (no prior `up`), but
259
+ * also what we'd see if the deploy somehow skipped the
260
+ * output declaration. Caller must not silently mint a
261
+ * fresh token; the master-replace defense catches the
262
+ * destructive case.
263
+ * - `errored`: the probe call itself threw — backend unreachable,
264
+ * expired creds, etc. Caller should surface loudly.
265
+ *
266
+ * Pure function; the Pulumi state read happens upstream.
267
+ *
268
+ * @param {{ outputs?: Record<string, unknown> | null, error?: Error | null }} probe
269
+ * @returns {{ status: 'recovered' | 'empty' | 'errored', priorK3sToken?: string, reason: string }}
270
+ */
271
+ export function classifyK3sTokenProbe({ outputs, error } = {}) {
272
+ if (error) {
273
+ const msg = error.message?.split('\n')[0] || String(error);
274
+ return { status: 'errored', reason: `probe threw: ${msg}` };
275
+ }
276
+ if (!outputs || typeof outputs !== 'object') {
277
+ return { status: 'empty', reason: 'no outputs returned' };
278
+ }
279
+ const token = outputs.k3sToken;
280
+ if (typeof token === 'string' && token.length > 0) {
281
+ return { status: 'recovered', priorK3sToken: token, reason: 'k3sToken present' };
282
+ }
283
+ const keys = Object.keys(outputs);
284
+ return {
285
+ status: 'empty',
286
+ reason:
287
+ keys.length === 0
288
+ ? 'outputs object empty (fresh stack)'
289
+ : `outputs missing k3sToken (keys=${keys.join(',')})`,
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Refresh the stack's view of reality without applying program changes.
295
+ * Useful before a partial scale operation.
296
+ */
297
+ export async function refreshStack(stackName, program, options = {}) {
298
+ const stack = await getOrCreateStack(stackName, program, options);
299
+ await stack.refresh({ onOutput: options.onOutput, color: 'never' });
300
+ }
301
+
302
+ /**
303
+ * List stack names that exist in the configured backend for this project.
304
+ * Used by `destroy` to surface orphans (stacks without a matching envConfig).
305
+ * Returns an empty list if no backend is reachable yet.
306
+ */
307
+ export async function listStacks(options = {}) {
308
+ const backendUrl = resolveBackendUrl(options.s3Config);
309
+ const envVars = buildEnv(options);
310
+ try {
311
+ const workspace = await LocalWorkspace.create({
312
+ projectSettings: {
313
+ name: PULUMI_PROJECT,
314
+ runtime: 'nodejs',
315
+ backend: { url: backendUrl },
316
+ },
317
+ envVars,
318
+ });
319
+ const stacks = await workspace.listStacks();
320
+ return stacks.map((s) => s.name);
321
+ } catch {
322
+ // No backend yet (fresh install) — no orphans possible.
323
+ return [];
324
+ }
325
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Pulumi program for a single Hetzner Docker Compose VPS.
3
+ *
4
+ * Much simpler than the k8s program — one server, one firewall, optional
5
+ * floating IP. Cloud-init installs Docker via the shipped docker-ce-setup.yaml
6
+ * (carbon/cloud-init/), which is reused as-is (no templating needed).
7
+ *
8
+ * Outputs: serverIp, serverId, firewallId, sshKeyId.
9
+ */
10
+
11
+ import { readFileSync } from 'node:fs';
12
+ import { dirname, join } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import * as hcloud from '@pulumi/hcloud';
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = dirname(__filename);
18
+
19
+ const CLOUD_INIT_PATH = join(__dirname, '../../../../carbon/cloud-init/docker-ce-setup.yaml');
20
+
21
+ /**
22
+ * @typedef {Object} ComposeStackConfig
23
+ * @property {string} projectName
24
+ * @property {string} environment
25
+ * @property {string} sshPublicKey
26
+ * @property {string} [existingSshKeyId]
27
+ * @property {string} location
28
+ * @property {string} serverType
29
+ * @property {Record<string,string>} [labels]
30
+ * @property {string[]} [allowedSshIps]
31
+ */
32
+
33
+ /**
34
+ * @param {ComposeStackConfig} config
35
+ * @returns {() => Promise<Record<string, unknown>>}
36
+ */
37
+ export function buildHetznerComposeProgram(config) {
38
+ const name = `${config.projectName}-${config.environment}`;
39
+ const labels = {
40
+ project: config.projectName,
41
+ environment: config.environment,
42
+ ...(config.labels ?? {}),
43
+ };
44
+ // Operator-IP firewall lock (H-2). The deploy flow populates this from
45
+ // projectConfig.operatorCidrs; an empty list = nothing can SSH in. We
46
+ // refuse to build a program that would silently apply an open default.
47
+ if (!config.allowedSshIps?.length) {
48
+ throw new Error(
49
+ 'allowedSshIps required (no default; pass operatorCidrs from .vibecarbon.json or set ALLOWED_SSH_IPS=...)',
50
+ );
51
+ }
52
+ const allowedSshIps = config.allowedSshIps;
53
+ const userData = readFileSync(CLOUD_INIT_PATH, 'utf-8');
54
+
55
+ return async () => {
56
+ let sshKeyId;
57
+ if (config.existingSshKeyId) {
58
+ sshKeyId = config.existingSshKeyId;
59
+ } else {
60
+ // Name includes region so parallel or re-deploys across regions don't
61
+ // collide in Hetzner's per-project SSH-key namespace.
62
+ const sshKey = new hcloud.SshKey('ssh-key', {
63
+ name: `${name}-${config.location}-key`,
64
+ publicKey: config.sshPublicKey,
65
+ labels,
66
+ });
67
+ sshKeyId = sshKey.id;
68
+ }
69
+
70
+ const firewall = new hcloud.Firewall(
71
+ 'firewall',
72
+ {
73
+ name: `${name}-firewall`,
74
+ labels,
75
+ rules: [
76
+ {
77
+ direction: 'in',
78
+ protocol: 'tcp',
79
+ port: '22',
80
+ sourceIps: allowedSshIps,
81
+ description: 'SSH',
82
+ },
83
+ {
84
+ direction: 'in',
85
+ protocol: 'tcp',
86
+ port: '80',
87
+ sourceIps: ['0.0.0.0/0', '::/0'],
88
+ description: 'HTTP',
89
+ },
90
+ {
91
+ direction: 'in',
92
+ protocol: 'tcp',
93
+ port: '443',
94
+ sourceIps: ['0.0.0.0/0', '::/0'],
95
+ description: 'HTTPS',
96
+ },
97
+ ],
98
+ },
99
+ // Upstream hcloud-go@1.32.1 bug: the Firewall `Update` code path
100
+ // nil-pointer-panics when rules change on a live firewall. Forcing
101
+ // full replacement avoids the Update call site entirely. Recreate
102
+ // is cheap (~2s).
103
+ { replaceOnChanges: ['*'], deleteBeforeReplace: true },
104
+ );
105
+
106
+ const server = new hcloud.Server('server', {
107
+ name,
108
+ serverType: config.serverType,
109
+ // Hetzner-maintained app image: Ubuntu LTS with docker + the
110
+ // docker-compose plugin pre-installed. Matches the contract in
111
+ // carbon/cloud-init/docker-ce-setup.yaml ("VPS boot time in parallel
112
+ // with Hetzner's default docker-ce app"). Plain `ubuntu-24.04` leaves
113
+ // the VM without docker and the deploy hits `docker: command not
114
+ // found` on the first `docker compose up` call.
115
+ image: 'docker-ce',
116
+ location: config.location,
117
+ sshKeys: [sshKeyId],
118
+ firewallIds: [firewall.id.apply((id) => Number.parseInt(id, 10))],
119
+ labels: { ...labels, role: 'compose' },
120
+ userData,
121
+ });
122
+
123
+ return {
124
+ serverIp: server.ipv4Address,
125
+ serverId: server.id.apply((id) => String(id)),
126
+ firewallId: firewall.id.apply((id) => String(id)),
127
+ sshKeyId: typeof sshKeyId === 'string' ? sshKeyId : sshKeyId.apply((id) => String(id)),
128
+ };
129
+ };
130
+ }