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,381 @@
1
+ /**
2
+ * Operator-IP detection and management for the Hetzner Cloud firewall's
3
+ * SSH (port 22) and Kubernetes API (port 6443) rules.
4
+ *
5
+ * Architecture: a project-level list of operator CIDRs persisted in
6
+ * .vibecarbon.json under `operatorCidrs`. Every interactive command that
7
+ * needs SSH or k8s API access calls `ensureOperatorIpAccess` near the top
8
+ * of its run() — if the operator's current public IP isn't already covered
9
+ * by an entry in the list, the helper detects it, appends it, and patches
10
+ * the live Hetzner firewall via the Cloud API. Subsequent commands from
11
+ * the same IP are silent.
12
+ *
13
+ * Non-interactive (--yes) flows skip auto-detect and require either an
14
+ * already-populated list or an explicit ALLOWED_SSH_IPS env var, since CI
15
+ * runners have ephemeral IPs that would otherwise pollute the list.
16
+ *
17
+ * Exports:
18
+ * - detectOperatorIp() — primary + fallback HTTP detector
19
+ * - cidrFromIp(ip, version) — formats /32 or /128
20
+ * - loadOperatorCidrs / addCidr / removeCidr / pruneCidrs / refreshLastUsed
21
+ * - findMatchingCidr — checks if an IP is contained by any persisted CIDR
22
+ * - applyToFirewall — patches the live Hetzner firewall via the Cloud API
23
+ * - ensureOperatorIpAccess — the all-in-one helper for command run()s
24
+ */
25
+
26
+ import { saveProjectConfig } from './config.js';
27
+ import { fetchWithRetry } from './fetch-retry.js';
28
+
29
+ // ============================================================================
30
+ // IP DETECTION
31
+ // ============================================================================
32
+
33
+ const PRIMARY_DETECTOR = 'https://api.ipify.org';
34
+ const FALLBACK_DETECTOR = 'https://icanhazip.com';
35
+
36
+ /**
37
+ * Resolve the operator's current public IP. Returns { ip, version: 4|6 }.
38
+ * Throws if both detectors fail.
39
+ */
40
+ export async function detectOperatorIp({ fetcher = fetchWithRetry } = {}) {
41
+ const errors = [];
42
+ for (const url of [PRIMARY_DETECTOR, FALLBACK_DETECTOR]) {
43
+ try {
44
+ const res = await fetcher(url, { method: 'GET' });
45
+ if (!res.ok) {
46
+ errors.push(`${url}: HTTP ${res.status}`);
47
+ continue;
48
+ }
49
+ const ip = (await res.text()).trim();
50
+ const version = ip.includes(':') ? 6 : 4;
51
+ if (!isValidIp(ip, version)) {
52
+ errors.push(`${url}: invalid response "${ip}"`);
53
+ continue;
54
+ }
55
+ return { ip, version };
56
+ } catch (err) {
57
+ errors.push(`${url}: ${err.message || err}`);
58
+ }
59
+ }
60
+ throw new Error(`Unable to detect operator IP — ${errors.join('; ')}`);
61
+ }
62
+
63
+ function isValidIp(ip, version) {
64
+ if (version === 4) {
65
+ if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) return false;
66
+ return ip.split('.').every((p) => {
67
+ const n = Number(p);
68
+ return Number.isInteger(n) && n >= 0 && n <= 255;
69
+ });
70
+ }
71
+ // IPv6 — must contain colons and only hex chars
72
+ return /^[0-9a-fA-F:]+$/.test(ip) && ip.includes(':');
73
+ }
74
+
75
+ export function cidrFromIp(ip, version) {
76
+ return `${ip}/${version === 6 ? 128 : 32}`;
77
+ }
78
+
79
+ // ============================================================================
80
+ // CIDR MATCHING
81
+ // ============================================================================
82
+
83
+ /**
84
+ * Find the first persisted CIDR entry that contains the given IP, or null.
85
+ */
86
+ export function findMatchingCidr(list, ip, version) {
87
+ for (const entry of list) {
88
+ if (cidrContainsIp(entry.cidr, ip, version)) return entry;
89
+ }
90
+ return null;
91
+ }
92
+
93
+ export function cidrContainsIp(cidr, ip, version) {
94
+ const slashIdx = cidr.indexOf('/');
95
+ if (slashIdx < 0) return false;
96
+ const base = cidr.slice(0, slashIdx);
97
+ const bits = Number(cidr.slice(slashIdx + 1));
98
+ if (!Number.isInteger(bits) || bits < 0) return false;
99
+
100
+ const cidrIsV6 = base.includes(':');
101
+ if (cidrIsV6 !== (version === 6)) return false;
102
+
103
+ if (version === 4) {
104
+ if (bits > 32) return false;
105
+ const baseNum = ipv4ToInt(base);
106
+ const ipNum = ipv4ToInt(ip);
107
+ if (baseNum === null || ipNum === null) return false;
108
+ if (bits === 0) return true;
109
+ if (bits === 32) return baseNum === ipNum;
110
+ const mask = (-1 << (32 - bits)) >>> 0;
111
+ return (baseNum & mask) === (ipNum & mask);
112
+ }
113
+
114
+ if (bits > 128) return false;
115
+ const baseNum = ipv6ToBigInt(base);
116
+ const ipNum = ipv6ToBigInt(ip);
117
+ if (baseNum === null || ipNum === null) return false;
118
+ if (bits === 0) return true;
119
+ if (bits === 128) return baseNum === ipNum;
120
+ const shift = BigInt(128 - bits);
121
+ return baseNum >> shift === ipNum >> shift;
122
+ }
123
+
124
+ function ipv4ToInt(ip) {
125
+ const parts = ip.split('.');
126
+ if (parts.length !== 4) return null;
127
+ let n = 0;
128
+ for (const p of parts) {
129
+ const num = Number(p);
130
+ if (!Number.isInteger(num) || num < 0 || num > 255) return null;
131
+ n = (n << 8) | num;
132
+ }
133
+ return n >>> 0;
134
+ }
135
+
136
+ function ipv6ToBigInt(ip) {
137
+ const halves = ip.split('::');
138
+ if (halves.length > 2) return null;
139
+ const head = halves[0] ? halves[0].split(':') : [];
140
+ const tail = halves.length === 2 && halves[1] ? halves[1].split(':') : [];
141
+ if (head.length + tail.length > 8) return null;
142
+ if (halves.length === 1 && head.length !== 8) return null;
143
+ const fillCount = halves.length === 2 ? 8 - head.length - tail.length : 0;
144
+ const groups = [...head, ...new Array(fillCount).fill('0'), ...tail];
145
+ if (groups.length !== 8) return null;
146
+ let result = 0n;
147
+ for (const g of groups) {
148
+ if (!/^[0-9a-fA-F]{1,4}$/.test(g)) return null;
149
+ result = (result << 16n) | BigInt(Number.parseInt(g, 16));
150
+ }
151
+ return result;
152
+ }
153
+
154
+ // ============================================================================
155
+ // CIDR LIST MANAGEMENT
156
+ // ============================================================================
157
+
158
+ export function loadOperatorCidrs(projectConfig) {
159
+ return projectConfig?.operatorCidrs ?? [];
160
+ }
161
+
162
+ /**
163
+ * Append a CIDR to the list, or refresh lastUsedAt if it's already present.
164
+ * Returns the new list (input is not mutated).
165
+ */
166
+ export function addCidr(list, cidr, now = new Date()) {
167
+ const nowIso = now.toISOString();
168
+ const idx = list.findIndex((e) => e.cidr === cidr);
169
+ if (idx >= 0) {
170
+ return list.map((e, i) => (i === idx ? { ...e, lastUsedAt: nowIso } : e));
171
+ }
172
+ return [...list, { cidr, addedAt: nowIso, lastUsedAt: nowIso }];
173
+ }
174
+
175
+ export function removeCidr(list, cidr) {
176
+ return list.filter((e) => e.cidr !== cidr);
177
+ }
178
+
179
+ /**
180
+ * Drop entries whose lastUsedAt is older than maxAgeDays.
181
+ */
182
+ export function pruneCidrs(list, now = new Date(), maxAgeDays = 90) {
183
+ const cutoff = now.getTime() - maxAgeDays * 86_400_000;
184
+ return list.filter((e) => {
185
+ const t = new Date(e.lastUsedAt).getTime();
186
+ return Number.isFinite(t) && t >= cutoff;
187
+ });
188
+ }
189
+
190
+ export function refreshLastUsed(list, cidr, now = new Date()) {
191
+ const nowIso = now.toISOString();
192
+ return list.map((e) => (e.cidr === cidr ? { ...e, lastUsedAt: nowIso } : e));
193
+ }
194
+
195
+ // ============================================================================
196
+ // HETZNER FIREWALL UPDATE
197
+ // ============================================================================
198
+
199
+ const SSH_PORT = '22';
200
+ const K8S_API_PORT = '6443';
201
+
202
+ /**
203
+ * Patch the SSH (port 22) and Kubernetes API (port 6443) rules on each of
204
+ * the named environments' firewalls to use the given CIDR list as their
205
+ * source_ips. Other rules (HTTP/HTTPS, internal cluster traffic) are left
206
+ * unchanged.
207
+ *
208
+ * environments is an array of full env identifiers — for HA, pass
209
+ * ['<env>-primary', '<env>-standby']; for single, pass ['<env>'].
210
+ *
211
+ * Returns the names of firewalls that were updated. Firewalls that don't
212
+ * exist (env not yet deployed) are skipped silently.
213
+ */
214
+ export async function applyToFirewall({ projectName, environments, operatorCidrs, apiToken }) {
215
+ const cidrs = operatorCidrs.map((e) => e.cidr);
216
+ if (cidrs.length === 0) {
217
+ throw new Error('Refusing to update firewall with empty CIDR list (would lock everyone out).');
218
+ }
219
+ if (!apiToken) {
220
+ throw new Error('Hetzner API token required to update firewall.');
221
+ }
222
+
223
+ const updated = [];
224
+ for (const env of environments) {
225
+ const fwName = `${projectName}-${env}-firewall`;
226
+ const fw = await findFirewallByName(apiToken, fwName);
227
+ if (!fw) continue;
228
+ const newRules = fw.rules.map((rule) => {
229
+ if (rule.direction === 'in' && (rule.port === SSH_PORT || rule.port === K8S_API_PORT)) {
230
+ return { ...rule, source_ips: cidrs };
231
+ }
232
+ return rule;
233
+ });
234
+ await setFirewallRules(apiToken, fw.id, newRules);
235
+ updated.push(fwName);
236
+ }
237
+ return updated;
238
+ }
239
+
240
+ async function findFirewallByName(apiToken, name) {
241
+ const res = await fetchWithRetry(
242
+ `https://api.hetzner.cloud/v1/firewalls?name=${encodeURIComponent(name)}`,
243
+ { headers: { Authorization: `Bearer ${apiToken}` } },
244
+ );
245
+ if (!res.ok) return null;
246
+ const { firewalls } = await res.json();
247
+ return firewalls?.[0] ?? null;
248
+ }
249
+
250
+ async function setFirewallRules(apiToken, firewallId, rules) {
251
+ const res = await fetchWithRetry(
252
+ `https://api.hetzner.cloud/v1/firewalls/${firewallId}/actions/set_rules`,
253
+ {
254
+ method: 'POST',
255
+ headers: {
256
+ Authorization: `Bearer ${apiToken}`,
257
+ 'Content-Type': 'application/json',
258
+ },
259
+ body: JSON.stringify({ rules }),
260
+ },
261
+ );
262
+ if (!res.ok) {
263
+ const body = await res.text().catch(() => '');
264
+ throw new Error(`Hetzner firewall set_rules failed (${res.status}): ${body}`);
265
+ }
266
+ }
267
+
268
+ // ============================================================================
269
+ // AUTO-ADD HELPER
270
+ // ============================================================================
271
+
272
+ /**
273
+ * Ensure the current operator's IP is covered by the project's CIDR list,
274
+ * persisting + applying to the live firewall if not.
275
+ *
276
+ * Inputs:
277
+ * projectConfig — loaded .vibecarbon.json (must include projectName and
278
+ * environments[<environment>] for the apply step to fire)
279
+ * environment — user-facing env name
280
+ * isHA — whether to update both '<env>-primary' and '<env>-standby'
281
+ * firewalls (vs just '<env>')
282
+ * apiToken — Hetzner Cloud API token (from ~/.vibecarbon/credentials.json)
283
+ * yes — non-interactive mode flag
284
+ * onMessage — optional callback invoked with a single info string when a
285
+ * new CIDR is added (caller wires this to its logger)
286
+ * cwd — passed to saveProjectConfig
287
+ *
288
+ * Returns { added, cidr, list, fromEnv }:
289
+ * added — true if the list was modified this call
290
+ * cidr — the CIDR that was added (or matched, when added=false)
291
+ * list — the updated list
292
+ * fromEnv — true if the list was bootstrapped from ALLOWED_SSH_IPS env var
293
+ */
294
+ export async function ensureOperatorIpAccess({
295
+ projectConfig,
296
+ environment,
297
+ isHA = false,
298
+ apiToken,
299
+ yes = false,
300
+ onMessage,
301
+ cwd,
302
+ }) {
303
+ const list = loadOperatorCidrs(projectConfig);
304
+
305
+ if (yes) {
306
+ if (list.length > 0) {
307
+ return { added: false, cidr: null, list };
308
+ }
309
+ const envCidrs = parseAllowedSshIpsEnv(process.env.ALLOWED_SSH_IPS);
310
+ if (envCidrs.length === 0) {
311
+ throw new Error(
312
+ 'No operator CIDRs configured. Run `vibecarbon deploy` interactively first, or set ALLOWED_SSH_IPS="1.2.3.4/32,5.6.7.8/32".',
313
+ );
314
+ }
315
+ const now = new Date();
316
+ const newList = envCidrs.map((cidr) => ({
317
+ cidr,
318
+ addedAt: now.toISOString(),
319
+ lastUsedAt: now.toISOString(),
320
+ }));
321
+ persist(projectConfig, newList, cwd);
322
+ return { added: true, cidr: envCidrs.join(','), list: newList, fromEnv: true };
323
+ }
324
+
325
+ // Interactive path
326
+ let ip;
327
+ let version;
328
+ try {
329
+ ({ ip, version } = await detectOperatorIp());
330
+ } catch (err) {
331
+ throw new Error(
332
+ `${err.message}. Pass ALLOWED_SSH_IPS=... or run \`vibecarbon access add <cidr>\` manually.`,
333
+ );
334
+ }
335
+
336
+ const match = findMatchingCidr(list, ip, version);
337
+ if (match) {
338
+ const refreshed = refreshLastUsed(list, match.cidr);
339
+ persist(projectConfig, refreshed, cwd);
340
+ return { added: false, cidr: match.cidr, list: refreshed };
341
+ }
342
+
343
+ const newCidr = cidrFromIp(ip, version);
344
+ if (onMessage) onMessage(`Detected your IP: ${newCidr} — adding to access list...`);
345
+ const updatedList = addCidr(list, newCidr);
346
+ persist(projectConfig, updatedList, cwd);
347
+
348
+ // Apply to Hetzner firewall when the env is already deployed (firewall
349
+ // exists). For first-deploy bootstrap, the deploy flow itself will create
350
+ // the firewall with the right CIDRs from the persisted list.
351
+ const envDeployed = !!projectConfig?.environments?.[environment];
352
+ if (envDeployed && apiToken) {
353
+ const environments = isHA
354
+ ? [`${environment}-primary`, `${environment}-standby`]
355
+ : [environment];
356
+ await applyToFirewall({
357
+ projectName: projectConfig.projectName,
358
+ environments,
359
+ operatorCidrs: updatedList,
360
+ apiToken,
361
+ });
362
+ }
363
+
364
+ return { added: true, cidr: newCidr, list: updatedList };
365
+ }
366
+
367
+ function persist(projectConfig, newList, cwd) {
368
+ if (!projectConfig) return;
369
+ const updated = { ...projectConfig, operatorCidrs: newList };
370
+ // Mutate in place so callers holding a reference see the new list.
371
+ projectConfig.operatorCidrs = newList;
372
+ saveProjectConfig(updated, cwd);
373
+ }
374
+
375
+ export function parseAllowedSshIpsEnv(envVar) {
376
+ if (!envVar) return [];
377
+ return envVar
378
+ .split(',')
379
+ .map((s) => s.trim())
380
+ .filter(Boolean);
381
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Package manager adaptation utilities
3
+ *
4
+ * Shared between create.js (initial project setup) and upgrade.js
5
+ * (re-apply adaptations after template files are replaced).
6
+ */
7
+
8
+ import { readFileSync, writeFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { runCommand } from './command.js';
11
+
12
+ export function getPackageManagerVersion(pm) {
13
+ // Try to detect installed version, fall back to sensible defaults
14
+ // The packageManager field requires full semver (e.g., "pnpm@9.0.0" not "pnpm@9")
15
+ try {
16
+ const version = runCommand([pm, '--version'], {
17
+ encoding: 'utf-8',
18
+ stdio: ['pipe', 'pipe', 'pipe'],
19
+ }).trim();
20
+ // Validate it looks like a semver version
21
+ if (/^\d+\.\d+\.\d+/.test(version)) {
22
+ return `${pm}@${version}`;
23
+ }
24
+ } catch {
25
+ // Fall through to defaults
26
+ }
27
+
28
+ // Default versions (full semver required)
29
+ switch (pm) {
30
+ case 'npm':
31
+ return 'npm@10.0.0';
32
+ case 'bun':
33
+ return 'bun@1.0.0';
34
+ default:
35
+ return 'pnpm@9.15.4';
36
+ }
37
+ }
38
+
39
+ export function adaptDockerfileForPackageManager(projectDir, packageManager) {
40
+ if (packageManager === 'pnpm') return;
41
+
42
+ const dockerfilePath = join(projectDir, 'Dockerfile');
43
+ let content = readFileSync(dockerfilePath, 'utf-8');
44
+ const version = getPackageManagerVersion(packageManager); // e.g. "npm@10.0.0"
45
+
46
+ const lockfiles = { npm: 'package-lock.json', bun: 'bun.lock' };
47
+ const lockfile = lockfiles[packageManager];
48
+
49
+ // Package manager install line. Both halves rewrite together: the
50
+ // `command -v pnpm` check and the `npm install -g pnpm@9` fallback. If
51
+ // we leave `command -v pnpm` in place for bun/npm projects, the check
52
+ // always misses (no pnpm in the runtime image), forcing the fallback to
53
+ // run every build — functional but logically wrong (it installs bun
54
+ // even though bun was already installed by a prior layer).
55
+ if (packageManager === 'bun') {
56
+ content = content.replaceAll(
57
+ 'command -v pnpm >/dev/null 2>&1 || npm install -g pnpm@9',
58
+ 'command -v bun >/dev/null 2>&1 || npm install -g bun',
59
+ );
60
+ } else if (packageManager === 'npm') {
61
+ // npm is always present in node:* images, so the check is a no-op.
62
+ // Replace the whole line with a comment-and-true so the layer still
63
+ // exists (preserving the cache key) but does no real work.
64
+ content = content.replaceAll(
65
+ 'command -v pnpm >/dev/null 2>&1 || npm install -g pnpm@9',
66
+ 'true # npm is preinstalled in node:* base image',
67
+ );
68
+ } else {
69
+ content = content.replaceAll('npm install -g pnpm@9', `npm install -g ${version}`);
70
+ }
71
+
72
+ // lockfile COPY lines
73
+ content = content.replaceAll('pnpm-lock.yaml', lockfile);
74
+
75
+ // prod install (replace before general install — more specific match first)
76
+ const prodInstallMap = {
77
+ npm: 'npm ci --omit=dev',
78
+ bun: 'bun install --frozen-lockfile --production',
79
+ };
80
+ content = content.replaceAll(
81
+ 'pnpm install --frozen-lockfile --prod',
82
+ prodInstallMap[packageManager],
83
+ );
84
+
85
+ // install commands
86
+ const installMap = {
87
+ npm: 'npm ci',
88
+ bun: 'bun install --frozen-lockfile',
89
+ };
90
+ content = content.replaceAll('pnpm install --frozen-lockfile', installMap[packageManager]);
91
+
92
+ // BuildKit cache mount path. The template hardcodes pnpm's store location;
93
+ // without this rewrite, bun and npm projects mount an empty cache at pnpm's
94
+ // path and never read from their own caches — every install pays full
95
+ // network cost. Fixing it is ~30-60s off warm-cache rebuilds for non-pnpm.
96
+ const cacheMountTargets = {
97
+ npm: '/root/.npm',
98
+ bun: '/root/.bun/install/cache',
99
+ };
100
+ content = content.replace(
101
+ /--mount=type=cache,id=pnpm,target=\/root\/\.local\/share\/pnpm\/store/g,
102
+ `--mount=type=cache,id=${packageManager},target=${cacheMountTargets[packageManager]}`,
103
+ );
104
+
105
+ // build commands
106
+ const run = packageManager === 'npm' ? 'npm run' : packageManager;
107
+ content = content.replaceAll('pnpm build:client', `${run} build:client`);
108
+ content = content.replaceAll('pnpm build:server', `${run} build:server`);
109
+
110
+ writeFileSync(dockerfilePath, content);
111
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Lightweight performance-timing helper for deploy-path sub-steps.
3
+ *
4
+ * Enabled via `VIBECARBON_PERF=1`. When enabled, every `perfAsync` /
5
+ * `perfTimer` call emits a single stderr line in the form:
6
+ *
7
+ * [perf] <stage.name> <ms>ms [<optional-note>]
8
+ *
9
+ * The e2e harness captures this and the post-run reporter
10
+ * aggregates every sub-step across all scenarios, sorted descending,
11
+ * so we can see exactly where the critical path spends its time.
12
+ *
13
+ * Writing to stderr (never stdout) keeps @clack/prompts spinners on
14
+ * stdout undisturbed.
15
+ *
16
+ * When the env var is unset, all of these are near-zero overhead:
17
+ * one Date.now() call and a boolean guard.
18
+ */
19
+
20
+ const enabled =
21
+ process.env.VIBECARBON_PERF === '1' ||
22
+ process.env.VIBECARBON_PERF === 'true' ||
23
+ process.env.VIBECARBON_PERF === 'yes';
24
+
25
+ /**
26
+ * Returns whether perf tracing is enabled. Cheap; call at boundaries
27
+ * you don't want to pay for when perf is off.
28
+ */
29
+ export function perfEnabled() {
30
+ return enabled;
31
+ }
32
+
33
+ /**
34
+ * Open a manually-controlled timer. Call .end(note?) to emit the
35
+ * measurement. Always returns the ms duration whether or not perf
36
+ * tracing is enabled so callers can persist timings elsewhere.
37
+ */
38
+ export function perfTimer(name) {
39
+ const start = Date.now();
40
+ let ended = false;
41
+ return {
42
+ end(note) {
43
+ if (ended) return 0;
44
+ ended = true;
45
+ const ms = Date.now() - start;
46
+ if (enabled) {
47
+ const suffix = note ? ` ${note}` : '';
48
+ process.stderr.write(`[perf] ${name} ${ms}ms${suffix}\n`);
49
+ }
50
+ return ms;
51
+ },
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Wrap a promise-returning function in a perf timer. The returned
57
+ * promise resolves/rejects identically to the original; timing is
58
+ * captured on both paths (success notes nothing, failure notes
59
+ * `(failed)` so a reader can see which branch blew the budget).
60
+ */
61
+ export async function perfAsync(name, fn) {
62
+ const t = perfTimer(name);
63
+ try {
64
+ const result = await fn();
65
+ t.end();
66
+ return result;
67
+ } catch (err) {
68
+ t.end('(failed)');
69
+ throw err;
70
+ }
71
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Shared "are we in a Vibecarbon project?" guard.
3
+ *
4
+ * Every CLI subcommand that mutates or reads a project's tree must call
5
+ * `assertInProjectDir()` as its FIRST action — before banners, secret
6
+ * scans, prompt-clears, or anything else that touches the cwd. An
7
+ * accidental invocation from a parent directory (e.g. ~/repos) would
8
+ * otherwise descend into every sibling repo, dump real tokens to the
9
+ * scrollback (when a secret-scan runs), or print a confusing fatal
10
+ * error before any "not in a project" message ever appears.
11
+ *
12
+ * The check is deliberately strict: a Vibecarbon project always has
13
+ * `docker-compose.yml` at its root (templated by `vibecarbon create`),
14
+ * even for cloud-only k8s deploys. Requiring both `.vibecarbon.json`
15
+ * (loadProjectConfig, with package.json fallback) AND `docker-compose.yml`
16
+ * means the Vibecarbon source repo itself — which has a package.json but
17
+ * no docker-compose.yml — is correctly rejected.
18
+ */
19
+
20
+ import * as p from '@clack/prompts';
21
+ import { hasDockerCompose, loadProjectConfig } from './config.js';
22
+
23
+ /**
24
+ * Verify the current working directory is a Vibecarbon project. On
25
+ * failure, print the canonical error/info pair and exit 1. Returns the
26
+ * loaded project config so callers don't have to reload it.
27
+ *
28
+ * @param {string} [cwd] - Working directory (defaults to process.cwd())
29
+ * @returns {object} - Project config from `loadProjectConfig`
30
+ */
31
+ export function assertInProjectDir(cwd = process.cwd()) {
32
+ const projectConfig = loadProjectConfig(cwd);
33
+ if (!projectConfig || !hasDockerCompose(cwd)) {
34
+ p.log.error('Not in a Vibecarbon project directory.');
35
+ p.log.info('Run this command from within a project created with `vibecarbon create`.');
36
+ process.exit(1);
37
+ }
38
+ return projectConfig;
39
+ }