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,164 @@
1
+ /**
2
+ * Cloud Provider Registry
3
+ *
4
+ * Provides a unified interface for accessing cloud providers.
5
+ * Currently supports Hetzner Cloud, with abstraction for future providers.
6
+ *
7
+ * To add a new provider:
8
+ * 1. Create a new provider class extending BaseProvider
9
+ * 2. Import and register it in PROVIDERS below
10
+ */
11
+
12
+ import { BaseProvider } from './base.js';
13
+ import { HetznerProvider } from './hetzner.js';
14
+
15
+ /**
16
+ * Registry of available cloud providers
17
+ * @type {Object<string, typeof BaseProvider>}
18
+ */
19
+ export const PROVIDERS = {
20
+ hetzner: HetznerProvider,
21
+ // Future providers:
22
+ // digitalocean: DigitalOceanProvider,
23
+ // vultr: VultrProvider,
24
+ // linode: LinodeProvider,
25
+ };
26
+
27
+ /**
28
+ * Get a provider instance
29
+ * @param {string} providerName - Provider name (e.g., 'hetzner')
30
+ * @param {string} apiToken - API token for authentication
31
+ * @returns {BaseProvider} Provider instance
32
+ * @throws {Error} If provider not found
33
+ */
34
+ export function getProvider(providerName, apiToken) {
35
+ const Provider = PROVIDERS[providerName.toLowerCase()];
36
+
37
+ if (!Provider) {
38
+ const available = Object.keys(PROVIDERS).join(', ');
39
+ throw new Error(`Unknown provider: ${providerName}. Available providers: ${available}`);
40
+ }
41
+
42
+ return new Provider(apiToken);
43
+ }
44
+
45
+ /**
46
+ * Get a provider class (not instantiated)
47
+ * @param {string} providerName - Provider name
48
+ * @returns {typeof BaseProvider} Provider class
49
+ * @throws {Error} If provider not found
50
+ */
51
+ export function getProviderClass(providerName) {
52
+ const Provider = PROVIDERS[providerName.toLowerCase()];
53
+
54
+ if (!Provider) {
55
+ const available = Object.keys(PROVIDERS).join(', ');
56
+ throw new Error(`Unknown provider: ${providerName}. Available providers: ${available}`);
57
+ }
58
+
59
+ return Provider;
60
+ }
61
+
62
+ /**
63
+ * List all available providers with their details
64
+ * @returns {Array<{id: string, name: string, regions: object, serverTypes: object}>}
65
+ */
66
+ export function listProviders() {
67
+ return Object.entries(PROVIDERS).map(([id, Provider]) => ({
68
+ id,
69
+ name: Provider.NAME,
70
+ regions: Provider.REGIONS,
71
+ serverTypes: Provider.SERVER_TYPES,
72
+ defaultType: Provider.DEFAULT_TYPE,
73
+ haRegions: Provider.HA_REGIONS,
74
+ }));
75
+ }
76
+
77
+ /**
78
+ * Get list of provider IDs
79
+ * @returns {string[]}
80
+ */
81
+ export function getProviderIds() {
82
+ return Object.keys(PROVIDERS);
83
+ }
84
+
85
+ /**
86
+ * Check if a provider exists
87
+ * @param {string} providerName - Provider name to check
88
+ * @returns {boolean}
89
+ */
90
+ export function hasProvider(providerName) {
91
+ return providerName.toLowerCase() in PROVIDERS;
92
+ }
93
+
94
+ /**
95
+ * Get the default provider ID
96
+ * @returns {string}
97
+ */
98
+ export function getDefaultProvider() {
99
+ return 'hetzner';
100
+ }
101
+
102
+ /**
103
+ * Validate provider configuration
104
+ * @param {string} providerName - Provider name
105
+ * @param {object} config - Configuration to validate
106
+ * @param {string} [config.region] - Region to validate
107
+ * @param {string} [config.serverType] - Server type to validate
108
+ * @returns {{valid: boolean, errors: string[]}}
109
+ */
110
+ export function validateProviderConfig(providerName, config) {
111
+ const errors = [];
112
+
113
+ const Provider = PROVIDERS[providerName.toLowerCase()];
114
+ if (!Provider) {
115
+ return { valid: false, errors: [`Unknown provider: ${providerName}`] };
116
+ }
117
+
118
+ if (config.region && !(config.region in Provider.REGIONS)) {
119
+ const available = Object.keys(Provider.REGIONS).join(', ');
120
+ errors.push(`Invalid region "${config.region}". Available regions: ${available}`);
121
+ }
122
+
123
+ if (config.serverType && !(config.serverType in Provider.SERVER_TYPES)) {
124
+ const available = Object.keys(Provider.SERVER_TYPES).join(', ');
125
+ errors.push(`Invalid server type "${config.serverType}". Available types: ${available}`);
126
+ }
127
+
128
+ return { valid: errors.length === 0, errors };
129
+ }
130
+
131
+ /**
132
+ * Format provider regions for display (e.g., in CLI prompts)
133
+ * @param {string} providerName - Provider name
134
+ * @returns {Array<{value: string, label: string}>}
135
+ */
136
+ export function formatRegionsForSelect(providerName) {
137
+ const Provider = PROVIDERS[providerName.toLowerCase()];
138
+ if (!Provider) return [];
139
+
140
+ return Object.entries(Provider.REGIONS).map(([id, name]) => ({
141
+ value: id,
142
+ label: `${id} - ${name}`,
143
+ }));
144
+ }
145
+
146
+ /**
147
+ * Format provider server types for display
148
+ * @param {string} providerName - Provider name
149
+ * @returns {Array<{value: string, label: string, hint: string}>}
150
+ */
151
+ export function formatServerTypesForSelect(providerName) {
152
+ const Provider = PROVIDERS[providerName.toLowerCase()];
153
+ if (!Provider) return [];
154
+
155
+ return Object.entries(Provider.SERVER_TYPES).map(([id, spec]) => ({
156
+ value: id,
157
+ label: id,
158
+ hint: `${spec.vcpu} vCPU, ${spec.ram}GB RAM, ${spec.disk}GB SSD - ${spec.price}`,
159
+ }));
160
+ }
161
+
162
+ // Re-export base class and providers for direct access
163
+ export { BaseProvider } from './base.js';
164
+ export { HetznerProvider } from './hetzner.js';
package/src/lib/s3.js ADDED
@@ -0,0 +1,510 @@
1
+ /**
2
+ * Vibecarbon Add S3 Command
3
+ * Adds Hetzner Object Storage (S3) to Supabase Storage backend
4
+ *
5
+ * Usage:
6
+ * vibecarbon add s3 # Interactive setup
7
+ * vibecarbon add s3 --help # Show help
8
+ */
9
+
10
+ import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
11
+ import { dirname, join } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import * as p from '@clack/prompts';
14
+
15
+ import { c, printBanner } from './colors.js';
16
+ import { hasDockerCompose, loadProjectConfig } from './config.js';
17
+ import { loadManifest, saveManifest } from './project.js';
18
+ import { HetznerS3Provider, sanitizeBucketName } from './providers/hetzner-s3.js';
19
+ import { VERSION } from './version.js';
20
+
21
+ // Supported S3 providers (will expand when we add more cloud providers)
22
+ const S3_PROVIDERS = {
23
+ hetzner: {
24
+ name: 'Hetzner Object Storage',
25
+ providerClass: HetznerS3Provider,
26
+ },
27
+ // Future: digitalocean, vultr, etc.
28
+ };
29
+
30
+ const __filename = fileURLToPath(import.meta.url);
31
+ const __dirname = dirname(__filename);
32
+
33
+ // Local services directory
34
+ const SERVICES_DIR = join(__dirname, '..', '..', 'services');
35
+
36
+ // ============================================================================
37
+ // CLI ARGUMENT PARSING
38
+ // ============================================================================
39
+
40
+ function parseArgs(args) {
41
+ const parsed = {
42
+ help: false,
43
+ region: null,
44
+ ha: false,
45
+ };
46
+
47
+ for (let i = 0; i < args.length; i++) {
48
+ const arg = args[i];
49
+ const nextArg = args[i + 1];
50
+
51
+ if (arg === '--help' || arg === '-h') {
52
+ parsed.help = true;
53
+ } else if (arg === '--region' || arg === '-r') {
54
+ parsed.region = nextArg;
55
+ i++;
56
+ } else if (arg === '--ha') {
57
+ parsed.ha = true;
58
+ }
59
+ }
60
+
61
+ return parsed;
62
+ }
63
+
64
+ function showHelp() {
65
+ console.log(`
66
+ ${c.bold('Vibecarbon Add S3')} - Add Hetzner Object Storage
67
+
68
+ ${c.bold('USAGE')}
69
+ vibecarbon add s3 [options]
70
+
71
+ ${c.bold('OPTIONS')}
72
+ -r, --region <region> S3 region (fsn1, nbg1, hel1)
73
+ --ha Enable HA mode with cross-region replication
74
+ -h, --help Show this help message
75
+
76
+ ${c.bold('EXAMPLES')}
77
+ ${c.dim('# Interactive setup')}
78
+ vibecarbon add s3
79
+
80
+ ${c.dim('# Specify region')}
81
+ vibecarbon add s3 --region fsn1
82
+
83
+ ${c.dim('# HA setup with cross-region sync')}
84
+ vibecarbon add s3 --ha
85
+
86
+ ${c.bold('NOTES')}
87
+ - S3 credentials must be created in Hetzner Console first
88
+ - Buckets are created automatically based on project name
89
+ - For HA, buckets are created in two regions with rclone sync
90
+ `);
91
+ }
92
+
93
+ // ============================================================================
94
+ // ENVIRONMENT UPDATES
95
+ // ============================================================================
96
+
97
+ function appendS3ToEnv(s3Config, isHA = false) {
98
+ const envFiles = ['.env.local', '.env'];
99
+
100
+ for (const filename of envFiles) {
101
+ const envPath = join(process.cwd(), filename);
102
+ if (!existsSync(envPath)) continue;
103
+
104
+ const content = readFileSync(envPath, 'utf-8');
105
+
106
+ // Check if S3 section already exists
107
+ if (content.includes('# S3 STORAGE')) {
108
+ continue;
109
+ }
110
+
111
+ let s3Section;
112
+ if (isHA) {
113
+ s3Section = `
114
+
115
+ # =============================================================================
116
+ # S3 STORAGE (Hetzner Object Storage - HA)
117
+ # =============================================================================
118
+
119
+ S3_ACCESS_KEY="${s3Config.accessKey}"
120
+ S3_SECRET_KEY="${s3Config.secretKey}"
121
+ S3_PRIMARY_BUCKET="${s3Config.primaryBucket}"
122
+ S3_STANDBY_BUCKET="${s3Config.standbyBucket}"
123
+ S3_PRIMARY_ENDPOINT="${s3Config.primaryEndpoint}"
124
+ S3_STANDBY_ENDPOINT="${s3Config.standbyEndpoint}"
125
+ S3_PRIMARY_REGION="${s3Config.primaryRegion}"
126
+ S3_STANDBY_REGION="${s3Config.standbyRegion}"
127
+ S3_SYNC_INTERVAL="${s3Config.syncInterval || '5'}"
128
+ `;
129
+ } else {
130
+ s3Section = `
131
+
132
+ # =============================================================================
133
+ # S3 STORAGE (Hetzner Object Storage)
134
+ # =============================================================================
135
+
136
+ S3_ACCESS_KEY="${s3Config.accessKey}"
137
+ S3_SECRET_KEY="${s3Config.secretKey}"
138
+ S3_BUCKET="${s3Config.bucket}"
139
+ S3_ENDPOINT="${s3Config.endpoint}"
140
+ S3_REGION="${s3Config.region}"
141
+ `;
142
+ }
143
+
144
+ writeFileSync(envPath, content.trimEnd() + s3Section);
145
+ }
146
+ }
147
+
148
+ // ============================================================================
149
+ // COMPOSE FILE INSTALLATION
150
+ // ============================================================================
151
+
152
+ function installComposeOverlay(isHA = false) {
153
+ const srcFile = isHA ? 'docker-compose.s3-ha.yml' : 'docker-compose.s3.yml';
154
+ const srcPath = join(SERVICES_DIR, 's3', 'compose', srcFile);
155
+ const destPath = join(process.cwd(), srcFile);
156
+
157
+ if (!existsSync(srcPath)) {
158
+ throw new Error(`Service file not found: ${srcPath}`);
159
+ }
160
+
161
+ copyFileSync(srcPath, destPath);
162
+ return destPath;
163
+ }
164
+
165
+ // ============================================================================
166
+ // PACKAGE.JSON SCRIPTS
167
+ // ============================================================================
168
+
169
+ function updatePackageJsonScripts(isHA = false) {
170
+ const packageJsonPath = join(process.cwd(), 'package.json');
171
+ if (!existsSync(packageJsonPath)) return;
172
+
173
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
174
+
175
+ if (!packageJson.scripts) {
176
+ packageJson.scripts = {};
177
+ }
178
+
179
+ // Detect package manager from packageManager field for cross-script references
180
+ const pm = packageJson.packageManager?.split('@')[0] || 'pnpm';
181
+ const run = pm === 'npm' ? 'npm run' : pm;
182
+
183
+ if (isHA) {
184
+ packageJson.scripts['docker:up:s3'] =
185
+ 'docker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.s3-ha.yml up -d';
186
+ packageJson.scripts['dev:start:s3'] = `${run} docker:up:s3 && ${run} db:migrate && ${run} dev`;
187
+ } else {
188
+ packageJson.scripts['docker:up:s3'] =
189
+ 'docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d';
190
+ packageJson.scripts['dev:start:s3'] = `${run} docker:up:s3 && ${run} db:migrate && ${run} dev`;
191
+ }
192
+
193
+ writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
194
+ }
195
+
196
+ // ============================================================================
197
+ // INSTRUCTIONS
198
+ // ============================================================================
199
+
200
+ function showS3Instructions() {
201
+ const instructions = `
202
+ ${c.bold('To use S3 storage, create credentials in Hetzner Console:')}
203
+
204
+ 1. Go to ${c.info('https://console.hetzner.cloud/')}
205
+ 2. Select your project (or create one)
206
+ 3. Click ${c.bold('Object Storage')} in the sidebar
207
+ 4. Click ${c.bold('Create Credentials')}
208
+ 5. Copy the ${c.bold('Access Key')} and ${c.bold('Secret Key')} (secret shown only once!)
209
+ `;
210
+
211
+ p.note(instructions.trim(), 'Hetzner Object Storage Setup');
212
+ }
213
+
214
+ // ============================================================================
215
+ // MAIN FLOW
216
+ // ============================================================================
217
+
218
+ async function addS3(options) {
219
+ const { project, args } = options;
220
+ const isHA = args.ha || project.haConfigured;
221
+ const cloudProvider = project.provider || 'hetzner';
222
+
223
+ // Check if the provider's S3 service is supported
224
+ const s3Provider = S3_PROVIDERS[cloudProvider];
225
+ if (!s3Provider) {
226
+ p.log.error(`S3 storage is not yet supported for provider: ${cloudProvider}`);
227
+ p.log.info('Currently supported: Hetzner Object Storage');
228
+ p.log.info('Support for DigitalOcean Spaces, Vultr Object Storage coming soon.');
229
+ return false;
230
+ }
231
+
232
+ const S3ProviderClass = s3Provider.providerClass;
233
+ const s3ProviderName = s3Provider.name;
234
+
235
+ // Check if S3 is already configured
236
+ const manifest = loadManifest();
237
+ if (manifest.services?.s3) {
238
+ p.log.warn('S3 storage is already configured for this project.');
239
+ return false;
240
+ }
241
+
242
+ // Show provider-specific instructions
243
+ p.log.info(`Using ${c.bold(s3ProviderName)} (detected from deployment config)`);
244
+ showS3Instructions();
245
+
246
+ // Prompt for credentials
247
+ const accessKey = await p.text({
248
+ message: `Enter your ${s3ProviderName} Access Key`,
249
+ validate: (v) => (v.length < 10 ? 'Access Key is required' : undefined),
250
+ });
251
+
252
+ if (p.isCancel(accessKey)) {
253
+ p.cancel('Operation cancelled.');
254
+ return false;
255
+ }
256
+
257
+ const secretKey = await p.password({
258
+ message: `Enter your ${s3ProviderName} Secret Key`,
259
+ validate: (v) => (v.length < 10 ? 'Secret Key is required' : undefined),
260
+ });
261
+
262
+ if (p.isCancel(secretKey)) {
263
+ p.cancel('Operation cancelled.');
264
+ return false;
265
+ }
266
+
267
+ // Region selection
268
+ const regionOptions = Object.entries(S3ProviderClass.REGIONS).map(([id, name]) => ({
269
+ value: id,
270
+ label: `${id} - ${name}`,
271
+ }));
272
+
273
+ let primaryRegion, standbyRegion;
274
+
275
+ if (isHA) {
276
+ primaryRegion = await p.select({
277
+ message: 'Select PRIMARY region for S3 bucket',
278
+ options: regionOptions,
279
+ initialValue: args.region || 'fsn1',
280
+ });
281
+
282
+ if (p.isCancel(primaryRegion)) {
283
+ p.cancel('Operation cancelled.');
284
+ return false;
285
+ }
286
+
287
+ // Filter out primary region for standby selection
288
+ const standbyOptions = regionOptions.filter((r) => r.value !== primaryRegion);
289
+ standbyRegion = await p.select({
290
+ message: 'Select STANDBY region for S3 bucket',
291
+ options: standbyOptions,
292
+ initialValue: primaryRegion === 'fsn1' ? 'hel1' : 'fsn1',
293
+ });
294
+
295
+ if (p.isCancel(standbyRegion)) {
296
+ p.cancel('Operation cancelled.');
297
+ return false;
298
+ }
299
+ } else {
300
+ primaryRegion = await p.select({
301
+ message: 'Select region for S3 bucket',
302
+ options: regionOptions,
303
+ initialValue: args.region || 'fsn1',
304
+ });
305
+
306
+ if (p.isCancel(primaryRegion)) {
307
+ p.cancel('Operation cancelled.');
308
+ return false;
309
+ }
310
+ }
311
+
312
+ // Validate credentials
313
+ const s = p.spinner();
314
+ s.start('Verifying S3 credentials...');
315
+
316
+ const provider = new S3ProviderClass(accessKey, secretKey, primaryRegion);
317
+ const validation = await provider.validateCredentials();
318
+
319
+ if (!validation.valid) {
320
+ s.stop(`Credential validation failed: ${validation.error}`);
321
+ return false;
322
+ }
323
+
324
+ s.stop('Credentials verified');
325
+
326
+ // Create bucket(s)
327
+ const baseBucketName = sanitizeBucketName(project.projectName);
328
+
329
+ let s3Config;
330
+
331
+ if (isHA) {
332
+ // Create primary bucket
333
+ s.start(`Creating primary bucket: ${baseBucketName}-primary`);
334
+ const primaryBucket = await provider.createBucket(`${baseBucketName}-primary`);
335
+
336
+ if (primaryBucket.created) {
337
+ s.stop(`Primary bucket created: ${primaryBucket.name}`);
338
+ } else {
339
+ s.stop(`Primary bucket exists: ${primaryBucket.name}`);
340
+ }
341
+
342
+ // Configure CORS for primary
343
+ s.start('Configuring CORS for primary bucket...');
344
+ await provider.configureCORS(`${baseBucketName}-primary`);
345
+ s.stop('CORS configured');
346
+
347
+ // Create standby bucket in different region
348
+ const standbyProvider = new S3ProviderClass(accessKey, secretKey, standbyRegion);
349
+ s.start(`Creating standby bucket: ${baseBucketName}-standby`);
350
+ const standbyBucket = await standbyProvider.createBucket(`${baseBucketName}-standby`);
351
+
352
+ if (standbyBucket.created) {
353
+ s.stop(`Standby bucket created: ${standbyBucket.name}`);
354
+ } else {
355
+ s.stop(`Standby bucket exists: ${standbyBucket.name}`);
356
+ }
357
+
358
+ // Configure CORS for standby
359
+ s.start('Configuring CORS for standby bucket...');
360
+ await standbyProvider.configureCORS(`${baseBucketName}-standby`);
361
+ s.stop('CORS configured');
362
+
363
+ s3Config = {
364
+ accessKey,
365
+ secretKey,
366
+ primaryBucket: `${baseBucketName}-primary`,
367
+ standbyBucket: `${baseBucketName}-standby`,
368
+ primaryEndpoint: S3ProviderClass.getEndpointForRegion(primaryRegion),
369
+ standbyEndpoint: S3ProviderClass.getEndpointForRegion(standbyRegion),
370
+ primaryRegion,
371
+ standbyRegion,
372
+ syncInterval: '5',
373
+ };
374
+ } else {
375
+ // Single bucket setup
376
+ s.start(`Creating bucket: ${baseBucketName}`);
377
+ const bucket = await provider.createBucket(baseBucketName);
378
+
379
+ if (bucket.created) {
380
+ s.stop(`Bucket created: ${bucket.name}`);
381
+ } else {
382
+ s.stop(`Bucket exists: ${bucket.name}`);
383
+ }
384
+
385
+ // Configure CORS
386
+ s.start('Configuring CORS...');
387
+ await provider.configureCORS(baseBucketName);
388
+ s.stop('CORS configured');
389
+
390
+ s3Config = {
391
+ accessKey,
392
+ secretKey,
393
+ bucket: baseBucketName,
394
+ endpoint: provider.getEndpoint(),
395
+ region: primaryRegion,
396
+ };
397
+ }
398
+
399
+ // Install compose overlay
400
+ s.start('Installing docker-compose overlay...');
401
+ const composeFile = installComposeOverlay(isHA);
402
+ s.stop(`Created: ${composeFile.replace(`${process.cwd()}/`, '')}`);
403
+
404
+ // Update .env files
405
+ s.start('Updating environment files...');
406
+ appendS3ToEnv(s3Config, isHA);
407
+ s.stop('Environment files updated');
408
+
409
+ // Update package.json scripts
410
+ s.start('Adding npm scripts...');
411
+ updatePackageJsonScripts(isHA);
412
+ s.stop('Scripts added');
413
+
414
+ // Update project manifest
415
+ manifest.services = manifest.services || {};
416
+ manifest.services.s3 = {
417
+ addedAt: new Date().toISOString(),
418
+ version: '1.0.0',
419
+ provider: cloudProvider, // Cloud provider (hetzner, digitalocean, etc.)
420
+ s3Service: s3ProviderName, // S3 service name (Hetzner Object Storage, etc.)
421
+ ha: isHA,
422
+ region: primaryRegion,
423
+ ...(isHA ? { standbyRegion } : {}),
424
+ };
425
+ saveManifest(manifest);
426
+
427
+ // Show summary
428
+ if (isHA) {
429
+ p.note(
430
+ [
431
+ `Primary: ${s3Config.primaryBucket} (${primaryRegion})`,
432
+ `Standby: ${s3Config.standbyBucket} (${standbyRegion})`,
433
+ `Sync: Every ${s3Config.syncInterval} minutes via rclone`,
434
+ '',
435
+ `Endpoint: ${s3Config.primaryEndpoint}`,
436
+ ].join('\n'),
437
+ 'S3 HA Storage Configured',
438
+ );
439
+ } else {
440
+ p.note(
441
+ [
442
+ `Bucket: ${s3Config.bucket}`,
443
+ `Region: ${primaryRegion}`,
444
+ `Endpoint: ${s3Config.endpoint}`,
445
+ ].join('\n'),
446
+ 'S3 Storage Configured',
447
+ );
448
+ }
449
+
450
+ return true;
451
+ }
452
+
453
+ // ============================================================================
454
+ // MAIN
455
+ // ============================================================================
456
+
457
+ async function main(cliArgs) {
458
+ const args = parseArgs(cliArgs);
459
+
460
+ if (args.help) {
461
+ showHelp();
462
+ process.exit(0);
463
+ }
464
+
465
+ // Detect project
466
+ const projectConfig = loadProjectConfig();
467
+ if (!projectConfig || !hasDockerCompose()) {
468
+ p.log.error('Not in a Vibecarbon project directory.');
469
+ p.log.info('Run this command from within a project created with `vibecarbon create`.');
470
+ process.exit(1);
471
+ }
472
+ const project = { projectName: projectConfig.projectName };
473
+
474
+ printBanner();
475
+ p.intro(`${c.bold('vibecarbon add s3')} ${c.dim(`v${VERSION}`)}`);
476
+
477
+ try {
478
+ const success = await addS3({ project, args });
479
+
480
+ if (success) {
481
+ p.note(
482
+ ['# Restart services to apply changes:', 'vibecarbon down && vibecarbon up'].join('\n'),
483
+ 'Next steps',
484
+ );
485
+ p.outro('S3 storage added successfully!');
486
+ } else {
487
+ p.outro('S3 setup was not completed.');
488
+ process.exit(1);
489
+ }
490
+ } catch (error) {
491
+ p.log.error(`Failed to add S3: ${error.message}`);
492
+ process.exit(1);
493
+ }
494
+ }
495
+
496
+ export async function run(args) {
497
+ await main(args);
498
+ }
499
+
500
+ // ============================================================================
501
+ // EXPORTS FOR TESTING
502
+ // ============================================================================
503
+
504
+ export {
505
+ appendS3ToEnv,
506
+ installComposeOverlay,
507
+ parseArgs,
508
+ sanitizeBucketName,
509
+ updatePackageJsonScripts,
510
+ };