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,967 @@
1
+ /**
2
+ * Vibecarbon Configure Command
3
+ * Interactive wizard for configuring external services (billing, OAuth, SMTP, etc.)
4
+ *
5
+ * Guides the user through selecting which features to enable and entering
6
+ * the required credentials. Writes all values to .env.local and .env.
7
+ *
8
+ * Usage:
9
+ * vibecarbon configure # Interactive configuration wizard
10
+ * vibecarbon configure cicd <env> # Layer GitOps (Flux + GHA) on a deployed k8s cluster
11
+ * vibecarbon configure -h # Show help
12
+ */
13
+
14
+ import { existsSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import * as p from '@clack/prompts';
17
+
18
+ import { renderHelp } from './lib/cli/help.js';
19
+ import { parseFlags } from './lib/cli/parse-flags.js';
20
+ import { c, printBanner } from './lib/colors.js';
21
+ import { loadCredentials, loadProjectConfig } from './lib/config.js';
22
+ import { requireLicense } from './lib/licensing/index.js';
23
+ import { buildGitAddArgv, loadEnvVariables, setEnvVar } from './lib/project.js';
24
+ import { assertInProjectDir } from './lib/project-guard.js';
25
+ import { VERSION } from './lib/version.js';
26
+
27
+ /** @type {import('./lib/cli/parse-flags.js').CommandSpec & { summary?: string, description?: string, examples?: Array<{ command: string, description?: string }> }} */
28
+ const SPEC = {
29
+ name: 'configure',
30
+ summary: 'Configure external services (billing, OAuth, SMTP, CI/CD)',
31
+ description: [
32
+ 'Interactive wizard for configuring external services in your Vibecarbon',
33
+ 'project. Walks you through enabling features and entering API keys,',
34
+ 'then writes everything to .env.local and .env.',
35
+ '',
36
+ 'Configured credentials are deployed automatically when you run',
37
+ '`vibecarbon deploy`.',
38
+ '',
39
+ 'SUBCOMMAND',
40
+ ' cicd <env> Layer Flux + GitHub Actions wiring onto a deployed',
41
+ ' k8s/k8s-ha cluster. Diamond/Fullerene only.',
42
+ '',
43
+ 'CONFIGURABLE FEATURES',
44
+ ' CI/CD GitHub Actions build + deploy workflows',
45
+ ' Billing Stripe, Paddle, or Polar payment processing',
46
+ ' Google OAuth Sign in with Google',
47
+ ' Microsoft OAuth Sign in with Microsoft',
48
+ ' SMTP / Email Transactional emails (Resend, Mailgun, SES, etc.)',
49
+ ' Plausible Analytics Privacy-friendly web analytics',
50
+ ].join('\n'),
51
+ flags: [
52
+ { name: 'h', boolean: true, description: 'Show this help' },
53
+ { name: 'v', boolean: true, description: 'Show version' },
54
+ ],
55
+ examples: [
56
+ {
57
+ command: 'vibecarbon configure',
58
+ description: 'Configure a new project after creation',
59
+ },
60
+ {
61
+ command: 'vibecarbon configure cicd prod',
62
+ description: 'Layer GitOps onto a deployed k8s cluster',
63
+ },
64
+ ],
65
+ };
66
+
67
+ // ============================================================================
68
+ // FEATURE PROMPT FUNCTIONS
69
+ // ============================================================================
70
+
71
+ /**
72
+ * Helper: prompt for a text value, showing current value as placeholder.
73
+ * Returns the entered value, or the current value if user pressed Enter on a non-empty placeholder.
74
+ */
75
+ async function promptText(message, currentValue, options = {}) {
76
+ const placeholder = currentValue || options.placeholder || '';
77
+ const defaultValue = currentValue || options.defaultValue || undefined;
78
+ const result = await p.text({
79
+ message,
80
+ placeholder: placeholder ? `${placeholder}` : undefined,
81
+ defaultValue,
82
+ validate: options.validate,
83
+ });
84
+ if (p.isCancel(result)) return null;
85
+ return result;
86
+ }
87
+
88
+ function requireNonEmpty(label) {
89
+ return (value) => {
90
+ if (!value?.trim()) return `${label} is required`;
91
+ return undefined;
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Helper: prompt for a secret value (masked input).
97
+ */
98
+ async function promptSecret(message, currentValue) {
99
+ const result = await p.password({
100
+ message: currentValue ? `${message} ${c.dim('(press Enter to keep current)')}` : message,
101
+ validate: (v) => {
102
+ if (!v && !currentValue) return 'This field is required';
103
+ },
104
+ });
105
+ if (p.isCancel(result)) return null;
106
+ // If user pressed Enter with no input and there's a current value, keep it
107
+ return result || currentValue || '';
108
+ }
109
+
110
+ // ---- Billing ----
111
+
112
+ // Webhook endpoint that all billing providers point at. The unified handler
113
+ // (carbon/src/server/routes/webhooks/billing.ts) dispatches based on the
114
+ // active provider; Stripe also keeps /api/webhooks/stripe for back-compat.
115
+ const BILLING_WEBHOOK_PATH = '/api/webhooks/billing';
116
+
117
+ async function promptBilling(env) {
118
+ p.log.info(c.bold('Billing Configuration'));
119
+ p.log.info(c.dim('Choose your payment provider and enter API credentials.'));
120
+
121
+ const currentProvider = env.BILLING_PROVIDER || 'stripe';
122
+ const provider = await p.select({
123
+ message: 'Billing provider',
124
+ options: [
125
+ { value: 'stripe', label: 'Stripe', hint: 'most popular' },
126
+ { value: 'paddle', label: 'Paddle', hint: 'merchant of record' },
127
+ { value: 'polar', label: 'Polar', hint: 'open-source focused' },
128
+ ],
129
+ initialValue: currentProvider,
130
+ });
131
+ if (p.isCancel(provider)) return null;
132
+
133
+ const vars = { BILLING_PROVIDER: provider };
134
+
135
+ if (provider === 'stripe') {
136
+ p.note(
137
+ [
138
+ `${c.bold('1. Secret key')}`,
139
+ ` ${c.info('https://dashboard.stripe.com/apikeys')}`,
140
+ ` Use ${c.dim('sk_test_...')} for testing or ${c.dim('sk_live_...')} in production.`,
141
+ '',
142
+ `${c.bold('2. Webhook')} (Workbench → Webhooks → Create an event destination)`,
143
+ ` ${c.info('https://dashboard.stripe.com/webhooks')}`,
144
+ ` • Endpoint URL: ${c.dim(`https://<your-domain>${BILLING_WEBHOOK_PATH}`)}`,
145
+ ' • Events to send:',
146
+ ' checkout.session.completed',
147
+ ' customer.subscription.created',
148
+ ' customer.subscription.updated',
149
+ ' customer.subscription.deleted',
150
+ ' invoice.payment_failed',
151
+ ` • After creating, click ${c.dim('Reveal')} to copy the ${c.dim('whsec_...')} signing secret.`,
152
+ '',
153
+ `${c.bold('3. Price IDs')}`,
154
+ ` ${c.info('https://dashboard.stripe.com/products')}`,
155
+ ' Create a Startup and Pro product, then copy each recurring',
156
+ ` price's ${c.dim('price_...')} ID.`,
157
+ ].join('\n'),
158
+ 'Stripe setup',
159
+ );
160
+
161
+ const secretKey = await promptSecret('Stripe secret key (sk_...)', env.STRIPE_SECRET_KEY);
162
+ if (secretKey === null) return null;
163
+ vars.STRIPE_SECRET_KEY = secretKey;
164
+
165
+ const webhookSecret = await promptSecret(
166
+ 'Stripe webhook secret (whsec_...)',
167
+ env.STRIPE_WEBHOOK_SECRET,
168
+ );
169
+ if (webhookSecret === null) return null;
170
+ vars.STRIPE_WEBHOOK_SECRET = webhookSecret;
171
+
172
+ const priceStarter = await promptText(
173
+ 'Stripe price ID for Startup plan (price_...)',
174
+ env.STRIPE_PRICE_STARTER,
175
+ );
176
+ if (priceStarter === null) return null;
177
+ vars.STRIPE_PRICE_STARTER = priceStarter;
178
+
179
+ const pricePro = await promptText(
180
+ 'Stripe price ID for Pro plan (price_...)',
181
+ env.STRIPE_PRICE_PRO,
182
+ );
183
+ if (pricePro === null) return null;
184
+ vars.STRIPE_PRICE_PRO = pricePro;
185
+ } else if (provider === 'paddle') {
186
+ p.note(
187
+ [
188
+ `${c.bold('1. API key')} (Developer Tools → Authentication → API keys → New API key)`,
189
+ ` ${c.info('https://vendors.paddle.com')}`,
190
+ ' Generate a new key with read + write scope.',
191
+ '',
192
+ `${c.bold('2. Webhook')} (Developer Tools → Notifications → New destination)`,
193
+ ` ${c.info('https://vendors.paddle.com')}`,
194
+ ` • Endpoint URL: ${c.dim(`https://<your-domain>${BILLING_WEBHOOK_PATH}`)}`,
195
+ ' • Events to send:',
196
+ ' transaction.completed',
197
+ ' transaction.payment_failed',
198
+ ' subscription.created',
199
+ ' subscription.updated',
200
+ ' subscription.canceled',
201
+ ' subscription.past_due',
202
+ ' • Copy the secret key shown after creating the destination.',
203
+ '',
204
+ `${c.bold('3. Price IDs')} (Catalog → Products → pick product → Prices)`,
205
+ ` ${c.info('https://vendors.paddle.com')}`,
206
+ ' Create a Startup and Pro product, then copy each recurring',
207
+ ` price's ${c.dim('pri_...')} ID.`,
208
+ ].join('\n'),
209
+ 'Paddle setup',
210
+ );
211
+
212
+ const apiKey = await promptSecret('Paddle API key', env.PADDLE_API_KEY);
213
+ if (apiKey === null) return null;
214
+ vars.PADDLE_API_KEY = apiKey;
215
+
216
+ const webhookSecret = await promptSecret('Paddle webhook secret', env.PADDLE_WEBHOOK_SECRET);
217
+ if (webhookSecret === null) return null;
218
+ vars.PADDLE_WEBHOOK_SECRET = webhookSecret;
219
+
220
+ const environment = await p.select({
221
+ message: 'Paddle environment',
222
+ options: [
223
+ { value: 'sandbox', label: 'Sandbox', hint: 'for testing' },
224
+ { value: 'production', label: 'Production', hint: 'live payments' },
225
+ ],
226
+ initialValue: env.PADDLE_ENVIRONMENT || 'sandbox',
227
+ });
228
+ if (p.isCancel(environment)) return null;
229
+ vars.PADDLE_ENVIRONMENT = environment;
230
+
231
+ const priceStarter = await promptText(
232
+ 'Paddle price ID for Startup plan',
233
+ env.PADDLE_PRICE_STARTER,
234
+ );
235
+ if (priceStarter === null) return null;
236
+ vars.PADDLE_PRICE_STARTER = priceStarter;
237
+
238
+ const pricePro = await promptText('Paddle price ID for Pro plan', env.PADDLE_PRICE_PRO);
239
+ if (pricePro === null) return null;
240
+ vars.PADDLE_PRICE_PRO = pricePro;
241
+ } else if (provider === 'polar') {
242
+ p.note(
243
+ [
244
+ `${c.bold('1. Access token')} (Settings → Developers → New access token)`,
245
+ ` ${c.info('https://polar.sh/dashboard/<org>/settings')}`,
246
+ '',
247
+ `${c.bold('2. Webhook')} (Settings → Webhooks → Add Endpoint)`,
248
+ ` ${c.info('https://polar.sh/dashboard/<org>/settings/webhooks')}`,
249
+ ` • Endpoint URL: ${c.dim(`https://<your-domain>${BILLING_WEBHOOK_PATH}`)}`,
250
+ ' • Events to send:',
251
+ ' checkout.created',
252
+ ' subscription.created',
253
+ ' subscription.updated',
254
+ ' subscription.canceled',
255
+ ' subscription.revoked',
256
+ ' • Copy the secret shown after creating the endpoint.',
257
+ '',
258
+ `${c.bold('3. Organization ID')}`,
259
+ ` Shown in the dashboard URL after ${c.dim('/dashboard/')}.`,
260
+ '',
261
+ `${c.bold('4. Price IDs')}`,
262
+ ` ${c.info('https://polar.sh/dashboard/<org>/products')}`,
263
+ ].join('\n'),
264
+ 'Polar setup',
265
+ );
266
+
267
+ const accessToken = await promptSecret('Polar access token', env.POLAR_ACCESS_TOKEN);
268
+ if (accessToken === null) return null;
269
+ vars.POLAR_ACCESS_TOKEN = accessToken;
270
+
271
+ const webhookSecret = await promptSecret('Polar webhook secret', env.POLAR_WEBHOOK_SECRET);
272
+ if (webhookSecret === null) return null;
273
+ vars.POLAR_WEBHOOK_SECRET = webhookSecret;
274
+
275
+ const orgId = await promptText('Polar organization ID', env.POLAR_ORGANIZATION_ID);
276
+ if (orgId === null) return null;
277
+ vars.POLAR_ORGANIZATION_ID = orgId;
278
+
279
+ const priceStarter = await promptText(
280
+ 'Polar price ID for Startup plan',
281
+ env.POLAR_PRICE_STARTER,
282
+ );
283
+ if (priceStarter === null) return null;
284
+ vars.POLAR_PRICE_STARTER = priceStarter;
285
+
286
+ const pricePro = await promptText('Polar price ID for Pro plan', env.POLAR_PRICE_PRO);
287
+ if (pricePro === null) return null;
288
+ vars.POLAR_PRICE_PRO = pricePro;
289
+ }
290
+
291
+ return vars;
292
+ }
293
+
294
+ // ---- Google OAuth ----
295
+
296
+ // Supabase Auth handles the OAuth dance; providers redirect to its callback,
297
+ // not to your app. Show the user the exact URI to paste so they don't guess.
298
+ function supabaseCallbackUrl(env) {
299
+ const base = (env.SUPABASE_URL || '').replace(/\/$/, '');
300
+ if (!base) return c.dim('<SUPABASE_URL>/auth/v1/callback');
301
+ return c.dim(`${base}/auth/v1/callback`);
302
+ }
303
+
304
+ async function promptGoogleOAuth(env) {
305
+ p.log.info(c.bold('Google OAuth'));
306
+
307
+ p.note(
308
+ [
309
+ `${c.bold('1. Configure consent screen')} (one-time prerequisite)`,
310
+ ` ${c.info('https://console.cloud.google.com/auth/branding')}`,
311
+ ' Google Auth Platform → Branding. Add app name + support email.',
312
+ '',
313
+ `${c.bold('2. Create OAuth client')} (Clients → Create client)`,
314
+ ` ${c.info('https://console.cloud.google.com/auth/clients')}`,
315
+ ` Application type: ${c.dim('Web application')}.`,
316
+ '',
317
+ `${c.bold('3. Authorized redirect URI')}`,
318
+ ` ${supabaseCallbackUrl(env)}`,
319
+ ' (paste this exact URL — Google must match it character-for-character)',
320
+ '',
321
+ `${c.bold('4. Copy the Client ID and Client secret')}`,
322
+ ` ${c.dim('The secret is only visible at creation — copy it immediately.')}`,
323
+ ].join('\n'),
324
+ 'Google OAuth setup',
325
+ );
326
+
327
+ const clientId = await promptText('Google Client ID', env.GOOGLE_CLIENT_ID);
328
+ if (clientId === null) return null;
329
+
330
+ const clientSecret = await promptSecret('Google Client Secret', env.GOOGLE_CLIENT_SECRET);
331
+ if (clientSecret === null) return null;
332
+
333
+ return {
334
+ GOOGLE_ENABLED: 'true',
335
+ GOOGLE_CLIENT_ID: clientId,
336
+ GOOGLE_CLIENT_SECRET: clientSecret,
337
+ };
338
+ }
339
+
340
+ // ---- Microsoft OAuth ----
341
+
342
+ async function promptMicrosoftOAuth(env) {
343
+ p.log.info(c.bold('Microsoft OAuth'));
344
+
345
+ p.note(
346
+ [
347
+ `${c.bold('1. Register an app')} (App registrations → New registration)`,
348
+ ` ${c.info('https://entra.microsoft.com')}`,
349
+ ' Microsoft Entra ID → App registrations → New registration.',
350
+ '',
351
+ `${c.bold('2. Redirect URI')} (platform: Web)`,
352
+ ` ${supabaseCallbackUrl(env)}`,
353
+ '',
354
+ `${c.bold('3. Client secret')} (Certificates & secrets → New client secret)`,
355
+ ` ${c.dim('Copy the Value (not the Secret ID) — Entra only shows it once.')}`,
356
+ '',
357
+ `${c.bold('4. Tenant ID')}`,
358
+ ` Use ${c.dim('common')} to allow any Microsoft account, or paste your`,
359
+ " directory's tenant UUID for single-tenant apps.",
360
+ ].join('\n'),
361
+ 'Microsoft OAuth setup',
362
+ );
363
+
364
+ const clientId = await promptText('Microsoft Client ID', env.MICROSOFT_CLIENT_ID);
365
+ if (clientId === null) return null;
366
+
367
+ const clientSecret = await promptSecret('Microsoft Client Secret', env.MICROSOFT_CLIENT_SECRET);
368
+ if (clientSecret === null) return null;
369
+
370
+ const tenantId = await promptText('Microsoft Tenant ID', env.MICROSOFT_TENANT_ID, {
371
+ placeholder: 'common',
372
+ defaultValue: 'common',
373
+ });
374
+ if (tenantId === null) return null;
375
+
376
+ return {
377
+ MICROSOFT_ENABLED: 'true',
378
+ MICROSOFT_CLIENT_ID: clientId,
379
+ MICROSOFT_CLIENT_SECRET: clientSecret,
380
+ MICROSOFT_TENANT_ID: tenantId,
381
+ };
382
+ }
383
+
384
+ // ---- SMTP / Email ----
385
+
386
+ async function promptSmtp(env) {
387
+ p.log.info(c.bold('SMTP / Email'));
388
+ p.log.info(c.dim('Used for password resets, verification, and transactional emails.'));
389
+
390
+ p.note(
391
+ [
392
+ 'Pick a provider, create an SMTP credential, then paste the values below.',
393
+ '',
394
+ `${c.bold('Resend')}`,
395
+ ` ${c.info('https://resend.com/api-keys')}`,
396
+ ` host ${c.dim('smtp.resend.com')} • port ${c.dim('587')} • user ${c.dim('resend')} • pass = API key`,
397
+ '',
398
+ `${c.bold('Mailgun')}`,
399
+ ` ${c.info('https://app.mailgun.com/app/sending/domains')}`,
400
+ ` host ${c.dim('smtp.mailgun.org')} • port ${c.dim('587')}`,
401
+ '',
402
+ `${c.bold('AWS SES')}`,
403
+ ` ${c.info('https://console.aws.amazon.com/ses/home#/smtp-settings')}`,
404
+ ` region-specific host • port ${c.dim('587')}`,
405
+ '',
406
+ `${c.bold('Postmark')}`,
407
+ ` ${c.info('https://account.postmarkapp.com/servers')}`,
408
+ ` host ${c.dim('smtp.postmarkapp.com')} • port ${c.dim('587')}`,
409
+ '',
410
+ `${c.bold('Verify your sender domain')} in the provider before going live,`,
411
+ 'or production emails will land in spam (or get rejected outright).',
412
+ ].join('\n'),
413
+ 'SMTP setup',
414
+ );
415
+
416
+ const host = await promptText('SMTP host', env.SMTP_HOST, {
417
+ placeholder: 'smtp.resend.com',
418
+ validate: requireNonEmpty('SMTP host'),
419
+ });
420
+ if (host === null) return null;
421
+
422
+ const port = await promptText('SMTP port', env.SMTP_PORT || '587', {
423
+ placeholder: '587',
424
+ });
425
+ if (port === null) return null;
426
+
427
+ const user = await promptText('SMTP username', env.SMTP_USER, {
428
+ validate: requireNonEmpty('SMTP username'),
429
+ });
430
+ if (user === null) return null;
431
+
432
+ const pass = await promptSecret('SMTP password', env.SMTP_PASS);
433
+ if (pass === null) return null;
434
+
435
+ const adminEmail = await promptText('Sender email address (from)', env.SMTP_ADMIN_EMAIL, {
436
+ placeholder: 'noreply@yourdomain.com',
437
+ validate: requireNonEmpty('Sender email'),
438
+ });
439
+ if (adminEmail === null) return null;
440
+
441
+ const senderDefault = env.PROJECT_NAME || 'My App';
442
+ const senderName = await promptText('Sender display name', env.SMTP_SENDER_NAME, {
443
+ placeholder: senderDefault,
444
+ defaultValue: senderDefault,
445
+ });
446
+ if (senderName === null) return null;
447
+
448
+ return {
449
+ SMTP_HOST: host,
450
+ SMTP_PORT: port,
451
+ SMTP_USER: user,
452
+ SMTP_PASS: pass,
453
+ SMTP_ADMIN_EMAIL: adminEmail,
454
+ SMTP_SENDER_NAME: senderName,
455
+ };
456
+ }
457
+
458
+ // ---- Plausible Analytics ----
459
+
460
+ async function promptPlausible(env) {
461
+ p.log.info(c.bold('Plausible Analytics'));
462
+
463
+ p.note(
464
+ [
465
+ `${c.bold('1. Add your site')}`,
466
+ ` ${c.info('https://plausible.io/sites/new')}`,
467
+ ` Enter your domain ${c.dim('(without https://)')} — this becomes the value below.`,
468
+ '',
469
+ `${c.bold('2. Script URL')}`,
470
+ ` Defaults to ${c.dim('https://plausible.io/js/script.js')}.`,
471
+ ' Self-hosting Plausible? Point it at your own instance, e.g.',
472
+ ` ${c.dim('https://analytics.yourdomain.com/js/script.js')}.`,
473
+ ].join('\n'),
474
+ 'Plausible setup',
475
+ );
476
+
477
+ const domain = await promptText('Your site domain (e.g., myapp.com)', env.VITE_PLAUSIBLE_DOMAIN);
478
+ if (domain === null) return null;
479
+
480
+ const scriptUrl = await promptText(
481
+ 'Plausible script URL',
482
+ env.VITE_PLAUSIBLE_SCRIPT_URL || 'https://plausible.io/js/script.js',
483
+ );
484
+ if (scriptUrl === null) return null;
485
+
486
+ return {
487
+ VITE_PLAUSIBLE_DOMAIN: domain,
488
+ VITE_PLAUSIBLE_SCRIPT_URL: scriptUrl,
489
+ };
490
+ }
491
+
492
+ // (i18n moved to admin Settings → Localization card — language list lives in
493
+ // app_settings.enabled_languages, not .env.local. See migration
494
+ // 00005_localization_languages.sql.)
495
+
496
+ /**
497
+ * CI/CD setup — bootstraps the project's GitHub repo (if missing), installs
498
+ * the canonical GHA workflows, rewrites image references with the real owner,
499
+ * and records the opt-in flag. Absorbs everything the legacy `vibecarbon add
500
+ * cicd` did; that command is gone (CI/CD is an external service, so it lives
501
+ * here in `configure`, not in `add` which is for local containers).
502
+ *
503
+ * Flow:
504
+ * 1. Verify `gh` CLI is installed + authenticated
505
+ * 2. Resolve owner: existing remote → use it; otherwise prompt + create repo
506
+ * via `gh repo create` (with `git init` + initial commit if needed)
507
+ * 3. Rewrite ghcr.io / github.com image refs in compose + k8s manifests
508
+ * 4. Install `.github/workflows/vibecarbon-build.yml` + `deploy.yml`
509
+ * 5. Set `cicdEnabled: true` in `vibecarbon.json`
510
+ *
511
+ * After this, `vibecarbon deploy` for compose prompts Direct/Push per-deploy
512
+ * (push becomes the default once CI/CD is configured). K8s and K8s-HA deploys
513
+ * are local-first via deployK3s and ignore CI/CD entirely — this feature is
514
+ * purely additive: it adds the git-push deploy path for compose, and lays
515
+ * down GHA workflow files in the repo for teams that want them.
516
+ *
517
+ * Org-level secret seeding (Hetzner/Cloudflare/S3 tokens) is NOT done here —
518
+ * the first push-mode compose deploy (or first `-mode k8s` deploy) still runs
519
+ * `seedOrgSecrets` via `deployK8sGitOps`. Kept that way so secrets only land
520
+ * in GitHub when the user actually deploys, not when they merely configure.
521
+ */
522
+ async function promptCicd() {
523
+ p.log.info(c.bold('CI/CD (GitHub Actions)'));
524
+ p.log.info(
525
+ c.dim(
526
+ 'Bootstraps the GitHub repo (if needed) and installs build + deploy workflows so this project can ship via git push. Optional in all modes — k8s/k8s-ha deploys are local-first and never need CI/CD; compose deploys use git-push when configured, otherwise direct.',
527
+ ),
528
+ );
529
+
530
+ const {
531
+ detectGitHubUsername,
532
+ getGitHubRepo,
533
+ installDeployWorkflowFile,
534
+ installWorkflowFile,
535
+ updateImageReferences,
536
+ } = await import('./lib/ci-setup.js');
537
+ const { checkDependency, runCommand } = await import('./lib/command.js');
538
+
539
+ if (!checkDependency('gh', 'GitHub CLI')) {
540
+ p.log.error(
541
+ '`gh` CLI is required. Install from https://cli.github.com/, then run `gh auth login`.',
542
+ );
543
+ return null;
544
+ }
545
+ try {
546
+ runCommand(['gh', 'auth', 'status'], { silent: true });
547
+ } catch {
548
+ p.log.error(
549
+ '`gh` is not authenticated. Run `gh auth login` and re-run `vibecarbon configure`.',
550
+ );
551
+ return null;
552
+ }
553
+
554
+ const cwd = process.cwd();
555
+ const projectConfig = loadProjectConfig(cwd);
556
+ const projectName = projectConfig?.projectName;
557
+ if (!projectName) {
558
+ p.log.error('vibecarbon.json missing or has no projectName. Run `vibecarbon create` first.');
559
+ return null;
560
+ }
561
+
562
+ // Resolve owner: prefer an existing origin remote; otherwise bootstrap one.
563
+ let { owner: githubOwner, name: repoName } = getGitHubRepo(cwd);
564
+ const s = p.spinner();
565
+
566
+ if (githubOwner && repoName) {
567
+ p.log.info(`GitHub repo detected: ${c.bold(`${githubOwner}/${repoName}`)}`);
568
+ } else {
569
+ p.log.info('No GitHub origin remote — creating one now.');
570
+ const detected = detectGitHubUsername();
571
+ const ownerInput = await p.text({
572
+ message: 'GitHub username or organization:',
573
+ placeholder: detected || 'your-username',
574
+ initialValue: detected || '',
575
+ validate: (value) => {
576
+ if (!value) return 'GitHub username is required';
577
+ if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(value))
578
+ return 'Invalid GitHub username format';
579
+ },
580
+ });
581
+ if (p.isCancel(ownerInput)) return null;
582
+ githubOwner = ownerInput;
583
+
584
+ const visibility = await p.select({
585
+ message: 'Repository visibility',
586
+ options: [
587
+ { value: 'private', label: 'Private', hint: 'recommended' },
588
+ { value: 'public', label: 'Public' },
589
+ ],
590
+ initialValue: 'private',
591
+ });
592
+ if (p.isCancel(visibility)) return null;
593
+
594
+ if (!existsSync(join(cwd, '.git'))) {
595
+ s.start('Initializing git repository');
596
+ runCommand(['git', 'init'], { silent: true, cwd });
597
+ s.stop('Git repository initialized');
598
+ }
599
+ const hasCommits = runCommand(['git', 'rev-parse', 'HEAD'], {
600
+ silent: true,
601
+ ignoreError: true,
602
+ cwd,
603
+ });
604
+ if (!hasCommits) {
605
+ s.start('Creating initial commit');
606
+ runCommand(buildGitAddArgv(), { silent: true, ignoreError: true, cwd });
607
+ runCommand(['git', 'commit', '-m', 'Initial commit - Vibecarbon project'], {
608
+ silent: true,
609
+ cwd,
610
+ });
611
+ s.stop('Initial commit created');
612
+ }
613
+
614
+ s.start(`Creating GitHub repository: ${githubOwner}/${projectName}`);
615
+ try {
616
+ // projectName is basename-validated at create time; visibility is a literal.
617
+ runCommand(
618
+ ['gh', 'repo', 'create', projectName, `--${visibility}`, '--source=.', '--remote=origin'],
619
+ { silent: true, timeout: 60000, cwd },
620
+ );
621
+ s.stop('GitHub repository created');
622
+ } catch (err) {
623
+ s.stop(`Failed to create GitHub repository: ${err.message}`);
624
+ return null;
625
+ }
626
+ repoName = projectName;
627
+ }
628
+
629
+ // Rewrite ghcr.io / github.com refs in compose + k8s manifests so they
630
+ // point at the real owner. Idempotent — re-running with the same owner
631
+ // is a no-op.
632
+ s.start('Updating Docker / Flux image references');
633
+ updateImageReferences(projectName, githubOwner, cwd);
634
+ s.stop('Image references updated');
635
+
636
+ try {
637
+ s.start('Installing .github/workflows/vibecarbon-build.yml');
638
+ const installedBuild = installWorkflowFile(cwd);
639
+ s.stop(installedBuild ? 'Build workflow installed' : 'Build workflow already present');
640
+
641
+ s.start('Installing .github/workflows/deploy.yml');
642
+ const installedDeploy = installDeployWorkflowFile(cwd);
643
+ s.stop(installedDeploy ? 'Deploy workflow installed' : 'Deploy workflow already present');
644
+ } catch (err) {
645
+ s.stop(`Failed to install workflow files: ${err.message}`);
646
+ return null;
647
+ }
648
+
649
+ // Record opt-in on the project so `ciAvailable()` detects CI/CD without
650
+ // shelling out to `gh` on every deploy.
651
+ const { saveProjectConfig: savePc } = await import('./lib/config.js');
652
+ if (projectConfig) {
653
+ savePc({ ...projectConfig, cicdEnabled: true }, cwd);
654
+ p.log.success('Marked CI/CD as enabled in vibecarbon.json');
655
+ } else {
656
+ p.log.warn(
657
+ 'vibecarbon.json not found — workflows installed but the CI/CD flag could not be persisted. Subsequent deploys may require `--push` explicitly.',
658
+ );
659
+ }
660
+
661
+ p.log.info(
662
+ c.dim(
663
+ 'Next: `git add .github/workflows && git commit && git push` to register the workflows with GitHub.',
664
+ ),
665
+ );
666
+
667
+ // No env vars to merge — side-effects only.
668
+ return {};
669
+ }
670
+
671
+ // ============================================================================
672
+ // FEATURE DEFINITIONS
673
+ // ============================================================================
674
+
675
+ const FEATURES = [
676
+ {
677
+ value: 'cicd',
678
+ label: 'CI/CD',
679
+ hint: 'GitHub Actions build + deploy workflows (optional; enables git-push deploys)',
680
+ promptFn: promptCicd,
681
+ },
682
+ {
683
+ value: 'billing',
684
+ label: 'Billing',
685
+ hint: 'Stripe, Paddle, or Polar',
686
+ promptFn: promptBilling,
687
+ },
688
+ {
689
+ value: 'google-oauth',
690
+ label: 'Google OAuth',
691
+ hint: 'Sign in with Google',
692
+ promptFn: promptGoogleOAuth,
693
+ },
694
+ {
695
+ value: 'microsoft-oauth',
696
+ label: 'Microsoft OAuth',
697
+ hint: 'Sign in with Microsoft',
698
+ promptFn: promptMicrosoftOAuth,
699
+ },
700
+ {
701
+ value: 'smtp',
702
+ label: 'SMTP / Email',
703
+ hint: 'password resets, verification, transactional emails',
704
+ promptFn: promptSmtp,
705
+ },
706
+ {
707
+ value: 'plausible',
708
+ label: 'Plausible Analytics',
709
+ hint: 'privacy-friendly web analytics',
710
+ promptFn: promptPlausible,
711
+ },
712
+ ];
713
+
714
+ // ============================================================================
715
+ // MAIN
716
+ // ============================================================================
717
+
718
+ async function main(cliArgs) {
719
+ const { values, errors } = parseFlags(cliArgs, SPEC);
720
+
721
+ if (errors.length > 0) {
722
+ for (const e of errors) {
723
+ process.stderr.write(`${c.error('✗')} ${e}\n`);
724
+ }
725
+ process.stderr.write(`Run ${c.info('vibecarbon configure -h')} for usage.\n`);
726
+ process.exit(1);
727
+ }
728
+
729
+ if (values.h) {
730
+ process.stdout.write(renderHelp(SPEC));
731
+ process.exit(0);
732
+ }
733
+
734
+ if (values.v) {
735
+ console.log(`vibecarbon v${VERSION}`);
736
+ process.exit(0);
737
+ }
738
+
739
+ // Detect vibecarbon project
740
+ const cwd = process.cwd();
741
+ assertInProjectDir(cwd);
742
+
743
+ printBanner();
744
+ p.intro(`${c.bold('vibecarbon configure')} ${c.dim(`v${VERSION}`)}`);
745
+
746
+ // Load current env values
747
+ const env = loadEnvVariables(cwd);
748
+
749
+ // Feature selection — opt-in: nothing selected by default. Most users only
750
+ // want to configure one or two features per run; default-on forced them to
751
+ // uncheck the rest, which was tedious and hid the intent of the prompt.
752
+ const selected = await p.multiselect({
753
+ message: 'Which features would you like to configure?',
754
+ options: FEATURES.map((f) => ({
755
+ value: f.value,
756
+ label: f.label,
757
+ hint: f.hint,
758
+ })),
759
+ required: false,
760
+ initialValues: [],
761
+ });
762
+
763
+ if (p.isCancel(selected)) {
764
+ p.cancel('Operation cancelled.');
765
+ process.exit(0);
766
+ }
767
+
768
+ if (selected.length === 0) {
769
+ p.log.warn('No features selected.');
770
+ p.outro('Nothing to configure.');
771
+ return;
772
+ }
773
+
774
+ // Collect credentials for each selected feature
775
+ const allVars = {};
776
+ const configuredFeatures = [];
777
+
778
+ for (const featureValue of selected) {
779
+ const feature = FEATURES.find((f) => f.value === featureValue);
780
+ const result = await feature.promptFn(env);
781
+
782
+ if (result === null) {
783
+ p.log.warn(`Skipped ${feature.label} (cancelled)`);
784
+ continue;
785
+ }
786
+
787
+ Object.assign(allVars, result);
788
+ configuredFeatures.push(feature.label);
789
+ }
790
+
791
+ if (configuredFeatures.length === 0) {
792
+ p.outro('No features configured.');
793
+ return;
794
+ }
795
+
796
+ // Show summary
797
+ const summaryLines = configuredFeatures.map((f) => ` ${c.success('+')} ${f}`).join('\n');
798
+ p.note(summaryLines, 'Features to configure');
799
+
800
+ // Write all values
801
+ const s = p.spinner();
802
+ s.start('Writing configuration');
803
+
804
+ for (const [key, value] of Object.entries(allVars)) {
805
+ setEnvVar(key, value, cwd);
806
+ }
807
+
808
+ s.stop('Configuration saved to .env.local and .env');
809
+
810
+ p.outro(
811
+ `Configured ${configuredFeatures.length} feature${configuredFeatures.length === 1 ? '' : 's'}. Run ${c.info('vibecarbon deploy')} to push to production.`,
812
+ );
813
+ }
814
+
815
+ // ============================================================================
816
+ // `vibecarbon configure cicd <env>` subcommand
817
+ // ============================================================================
818
+
819
+ /**
820
+ * Layer GitOps (Flux + GitHub Actions wiring) onto a deployed k8s cluster.
821
+ *
822
+ * Pre-PR-5 this was the body of the `--gitops` deploy flag (see
823
+ * orchestrator.js's now-deleted handover block). PR 7 lifted it into a
824
+ * standalone subcommand so:
825
+ * - The deploy path stays local-first by default (no Flux unless the
826
+ * operator explicitly opts in).
827
+ * - Operators can layer GitOps onto an existing cluster without
828
+ * re-deploying.
829
+ * - Tier gating happens at the entry point (Diamond/Fullerene only)
830
+ * instead of being a flag-on-deploy concern.
831
+ *
832
+ * Pre-conditions:
833
+ * - Project is initialized (`.vibecarbon.json` exists).
834
+ * - The named environment was deployed via `vibecarbon deploy -mode k8s`
835
+ * (the kubeconfig at `.vibecarbon/kubeconfig-<env>` must exist).
836
+ * - `gh` CLI is authenticated and the project has a GitHub origin.
837
+ * - License is Diamond or higher.
838
+ */
839
+ export async function runConfigureCicd(envName) {
840
+ // Project guard runs before banner so an accidental `vibecarbon
841
+ // configure cicd` from a parent directory emits the canonical message.
842
+ const cwd = process.cwd();
843
+ const projectConfig = assertInProjectDir(cwd);
844
+
845
+ requireLicense('configure cicd');
846
+ printBanner();
847
+ p.intro(`${c.bold('vibecarbon configure cicd')} ${c.dim(`v${VERSION}`)}`);
848
+
849
+ if (!envName) {
850
+ p.log.error('Missing environment argument. Usage: vibecarbon configure cicd <env>');
851
+ process.exit(1);
852
+ }
853
+
854
+ const envConfig = projectConfig.environments?.[envName];
855
+ if (!envConfig) {
856
+ p.log.error(
857
+ `Environment "${envName}" not found in vibecarbon.json. Deploy it first with \`vibecarbon deploy ${envName} -mode k8s\`.`,
858
+ );
859
+ process.exit(1);
860
+ }
861
+
862
+ // GitOps only makes sense for k8s clusters. Compose deploys go through
863
+ // the build-workflow path that promptCicd already handles via the
864
+ // interactive wizard's "CI/CD" feature.
865
+ const isK8s =
866
+ envConfig.deployMode === 'kubernetes' ||
867
+ envConfig.deployMode === 'kubernetes-ha' ||
868
+ envConfig.deployMode === 'k8s' ||
869
+ envConfig.deployMode === 'k8s-ha';
870
+ if (!isK8s) {
871
+ p.log.error(
872
+ `Environment "${envName}" is ${envConfig.deployMode || 'unknown'} mode. configure cicd only applies to k8s / k8s-ha deploys.`,
873
+ );
874
+ p.log.info(
875
+ 'For compose CI/CD, run `vibecarbon configure` (no subcommand) and pick CI/CD from the menu.',
876
+ );
877
+ process.exit(1);
878
+ }
879
+
880
+ const kubeconfigPath = join(cwd, '.vibecarbon', `kubeconfig-${envName}`);
881
+ if (!existsSync(kubeconfigPath)) {
882
+ p.log.error(
883
+ `Kubeconfig not found at ${kubeconfigPath}. Deploy the cluster first: vibecarbon deploy ${envName} -mode k8s`,
884
+ );
885
+ process.exit(1);
886
+ }
887
+
888
+ // Stage 1: install build + deploy workflow files locally + flip cicdEnabled.
889
+ // Reuses the same code path that the interactive wizard invokes for the
890
+ // CI/CD feature so behavior stays consistent across entry points.
891
+ p.log.step('Stage 1: install GitHub Actions workflows + mark cicdEnabled');
892
+ const projectSide = await promptCicd();
893
+ if (projectSide === null) {
894
+ p.log.error('Workflow installation aborted. Fix the gh-CLI / origin issues above and re-run.');
895
+ process.exit(1);
896
+ }
897
+
898
+ // Stage 2: layer Flux + GH Environment + workflow trigger onto the live
899
+ // cluster. This is the body that orchestrator.js used to run inline when
900
+ // --gitops was passed (deleted in PR 5).
901
+ p.log.step('Stage 2: install Flux + seed GitHub Environment + trigger first reconcile');
902
+ const { deployK8sGitOps } = await import('./lib/deploy/k8s/gitops-deploy.js');
903
+ const { getImageTag } = await import('./lib/ci-setup.js');
904
+ const creds = loadCredentials();
905
+ const imageTag = getImageTag(cwd);
906
+
907
+ // HA clusters split into <env>-primary and <env>-standby kubeconfigs;
908
+ // each needs its own GitOps handover. The orchestrator used to iterate
909
+ // over `[{suffix:'primary'}, {suffix:'standby'}]` in HA, single in non-HA.
910
+ const isHA = envConfig.deployMode === 'kubernetes-ha' || envConfig.deployMode === 'k8s-ha';
911
+ const targets = isHA
912
+ ? [
913
+ { suffix: 'primary', envName: `${envName}-primary` },
914
+ { suffix: 'standby', envName: `${envName}-standby` },
915
+ ]
916
+ : [{ suffix: null, envName }];
917
+
918
+ for (const target of targets) {
919
+ const targetKubeconfig = join(cwd, '.vibecarbon', `kubeconfig-${target.envName}`);
920
+ if (!existsSync(targetKubeconfig)) {
921
+ p.log.warn(
922
+ `Kubeconfig for ${target.envName} not found at ${targetKubeconfig} — skipping. Re-deploy that cluster with vibecarbon deploy first.`,
923
+ );
924
+ continue;
925
+ }
926
+ const subEnvConfig = projectConfig.environments?.[target.envName] ?? envConfig;
927
+ p.log.info(`Configuring GitOps for ${c.bold(target.envName)}`);
928
+ await deployK8sGitOps({
929
+ projectDir: cwd,
930
+ environment: target.envName,
931
+ domain: subEnvConfig.domain || envConfig.domain,
932
+ projectName: projectConfig.projectName,
933
+ kubeconfigPath: targetKubeconfig,
934
+ networkId: subEnvConfig.networkId || envConfig.networkId,
935
+ dnsProvider: subEnvConfig.dnsProvider || envConfig.dnsProvider,
936
+ creds,
937
+ imageTag,
938
+ s3BackupBucket: subEnvConfig.backupBucket || envConfig.backupBucket,
939
+ });
940
+ }
941
+
942
+ p.outro(c.success(`GitOps layered onto ${envName}. Future commits to main reconcile via Flux.`));
943
+ }
944
+
945
+ // ============================================================================
946
+ // RUN FUNCTION (called by CLI entry point)
947
+ // ============================================================================
948
+
949
+ export async function run(args) {
950
+ // Subcommand routing: `vibecarbon configure cicd <env>` skips the
951
+ // interactive feature menu and runs the GitOps add-on flow directly.
952
+ // Any other first-positional arg falls through to the wizard's parser
953
+ // (which currently only handles -h / -v).
954
+ if (args[0] === 'cicd') {
955
+ return runConfigureCicd(args[1]);
956
+ }
957
+ await main(args);
958
+ }
959
+
960
+ // ============================================================================
961
+ // TEST EXPORTS
962
+ // ============================================================================
963
+
964
+ export const __test__ = {
965
+ FEATURES,
966
+ SPEC,
967
+ };