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,666 @@
1
+ /**
2
+ * CI-first image setup for `vibecarbon deploy`.
3
+ *
4
+ * The deploy command doesn't build images locally — CI (GitHub Actions) does,
5
+ * publishing to GitHub Container Registry. This module bootstraps that pipeline
6
+ * on first deploy and verifies the expected image tag is available before we
7
+ * touch any infrastructure.
8
+ *
9
+ * Contract: `ensureCIImageReady()` returns `{ imageTag, githubOwner, repoName,
10
+ * ghcrPullCreds }`. If any step fails irrecoverably the caller should abort
11
+ * the deploy — there's no image to pull.
12
+ */
13
+
14
+ import { execFileSync } from 'node:child_process';
15
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
16
+ import { dirname, join } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import * as p from '@clack/prompts';
19
+ import { checkDependency, runCommand, runCommandAsync } from './command.js';
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = dirname(__filename);
23
+
24
+ // Path to the bundled workflow template that ships with the CLI.
25
+ // Note: carbon/ is copied into user projects by `vibecarbon create`, but the
26
+ // workflow may be missing or out-of-date on existing projects — we copy-on-demand
27
+ // from here so the shipped CLI version is always authoritative.
28
+ const WORKFLOW_TEMPLATE_PATH = join(
29
+ __dirname,
30
+ '..',
31
+ '..',
32
+ 'carbon',
33
+ '.github',
34
+ 'workflows',
35
+ 'vibecarbon-build.yml',
36
+ );
37
+
38
+ const PROJECT_WORKFLOW_PATH = '.github/workflows/vibecarbon-build.yml';
39
+ const PROJECT_DEPLOY_WORKFLOW_PATH = '.github/workflows/deploy.yml';
40
+
41
+ // Deploy workflow template (lives alongside the build workflow in carbon/).
42
+ const DEPLOY_WORKFLOW_TEMPLATE_PATH = join(
43
+ __dirname,
44
+ '..',
45
+ '..',
46
+ 'carbon',
47
+ '.github',
48
+ 'workflows',
49
+ 'deploy.yml',
50
+ );
51
+
52
+ /**
53
+ * Check whether CI/CD is configured for the project.
54
+ *
55
+ * Authoritative signal: `vibecarbon.json` has `cicdEnabled: true`, set by
56
+ * `vibecarbon configure` → CI/CD. Anything else returns false — direct
57
+ * deploy is the default, CI/CD is an explicit opt-in.
58
+ */
59
+ export function ciAvailable(cwd = process.cwd()) {
60
+ try {
61
+ const projectConfigPath = join(cwd, 'vibecarbon.json');
62
+ if (!existsSync(projectConfigPath)) return false;
63
+ const parsed = JSON.parse(readFileSync(projectConfigPath, 'utf-8'));
64
+ return parsed?.cicdEnabled === true;
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Resolve the build mode for a deploy.
72
+ *
73
+ * Returns one of: 'local' | 'direct' | 'push'.
74
+ *
75
+ * - 'local' — k3s + compose default: build the image on the operator's
76
+ * machine, sideload the tarball via `docker save | ssh
77
+ * docker load`. No registry round-trip. Compose runs the
78
+ * local build in parallel with `iac.upStack` (Pulumi VPS
79
+ * provisioning) so the build cost is hidden behind the
80
+ * ~109s VM-provisioning window.
81
+ * - 'direct' — compose legacy: build over `DOCKER_HOST=ssh://` against
82
+ * the new VPS. Slower (sequential after upStack) but works
83
+ * when the operator has no local Docker. Selected via
84
+ * `--direct`, or auto-fallback when `docker` is missing.
85
+ * - 'push' — compose: commit + push triggers GitHub Actions build
86
+ * and deploy. Default when CI/CD is configured.
87
+ *
88
+ * K8s/k8s-ha is always 'local'. Layer in Flux + GHA via
89
+ * `vibecarbon configure cicd <env>` after the cluster is up — that
90
+ * doesn't go through this function.
91
+ *
92
+ * Compose modes auto-detect the default. The interactive compose prompt
93
+ * (lib/deploy/prompts.js) lets the operator override by mutating
94
+ * args.direct or args.push when CI/CD is configured; this function
95
+ * honors that mutation. --yes deploys take the auto-detected default.
96
+ *
97
+ * Pre-PR-5 (2026-04-25 spec): five flags (`--direct`/`--push`/`--build-local`/
98
+ * `--gitops`/`--no-gitops`) gated this with a precedence ladder. PR 5
99
+ * collapsed them to one default per mode. The `--gitops` path moved
100
+ * to a new `vibecarbon configure cicd <env>` subcommand (PR 7).
101
+ */
102
+ export function resolveBuildMode(args = {}, cwd = process.cwd(), deployMode = null) {
103
+ const isK8s = deployMode === 'kubernetes' || deployMode === 'kubernetes-ha';
104
+ if (isK8s) return 'local';
105
+ if (args.direct && args.push) {
106
+ throw new Error('Cannot pass both direct and push build modes. Pick one.');
107
+ }
108
+ if (args.push) return 'push';
109
+ if (args.direct) return 'direct';
110
+ if (ciAvailable(cwd)) return 'push';
111
+ // Compose default: prefer 'local' when the operator has docker so we can
112
+ // run buildLocalImage in parallel with iac.upStack (saves ~30-40s off
113
+ // cold deploy by hiding the build behind VPS provisioning). Fall back to
114
+ // 'direct' (build over DOCKER_HOST=ssh://) when docker is missing —
115
+ // legacy behavior, no operator-side dependencies.
116
+ return checkDependency('docker') ? 'local' : 'direct';
117
+ }
118
+
119
+ /**
120
+ * Resolve the short git SHA for the current HEAD. Falls back to 'latest' if
121
+ * the project isn't a git repo.
122
+ */
123
+ export function getImageTag(cwd = process.cwd()) {
124
+ try {
125
+ const sha = runCommand(['git', 'rev-parse', '--short', 'HEAD'], {
126
+ silent: true,
127
+ returnOutput: true,
128
+ cwd,
129
+ })
130
+ ?.toString()
131
+ .trim();
132
+ return sha || 'latest';
133
+ } catch {
134
+ return 'latest';
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Resolve the GitHub owner/repo pair for the current working directory.
140
+ * Uses `gh repo view` — returns null if no gh remote is configured.
141
+ */
142
+ export function getGitHubRepo(cwd = process.cwd()) {
143
+ try {
144
+ const out = runCommand(['gh', 'repo', 'view', '--json', 'owner,name'], {
145
+ silent: true,
146
+ returnOutput: true,
147
+ cwd,
148
+ });
149
+ const parsed = JSON.parse(out.toString());
150
+ return { owner: parsed.owner?.login || null, name: parsed.name || null };
151
+ } catch {
152
+ return { owner: null, name: null };
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Get a GHCR-authenticated token using `gh auth token`. Returns null if gh
158
+ * isn't authenticated. Works for both public and private packages.
159
+ */
160
+ export function getGHCRToken() {
161
+ try {
162
+ const token = runCommand(['gh', 'auth', 'token'], {
163
+ silent: true,
164
+ returnOutput: true,
165
+ })
166
+ ?.toString()
167
+ .trim();
168
+ return token || null;
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Check whether a specific tag exists in ghcr.io. Uses the GitHub packages API
176
+ * via `gh api` (inherits auth), so this works for private packages.
177
+ *
178
+ * Missing `read:packages` scope causes a 403 here — we surface that as a
179
+ * thrown error rather than swallowing it as "tag not found", because the
180
+ * symptom (deploy waits 15m for an image CI already published) is miserable
181
+ * to debug otherwise.
182
+ */
183
+ export async function ghcrTagExists(owner, repo, tag) {
184
+ try {
185
+ const out = runCommand(
186
+ [
187
+ 'gh',
188
+ 'api',
189
+ `/users/${owner}/packages/container/${repo}/versions`,
190
+ '--jq',
191
+ `.[].metadata.container.tags[] | select(. == "${tag}")`,
192
+ ],
193
+ { silent: true, returnOutput: true },
194
+ );
195
+ return Boolean(out?.toString().trim());
196
+ } catch (err) {
197
+ const stderr = (err?.stderr ?? '').toString();
198
+ if (/You need at least read:packages scope/.test(stderr)) {
199
+ throw new Error(
200
+ 'gh token missing `read:packages` scope — run `gh auth refresh -h github.com -s read:packages` and retry.',
201
+ );
202
+ }
203
+ if (/HTTP 401|HTTP 403/.test(stderr)) {
204
+ throw new Error(`gh api for ghcr package versions failed: ${stderr.trim()}`);
205
+ }
206
+ // 404 on first poll (package doesn't exist yet) is legitimate "tag not found".
207
+ return false;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Commit + push any pending changes, then wait for the build workflow to
213
+ * publish the expected tag. Returns when the tag is available in ghcr.io.
214
+ *
215
+ * Poll cadence: fast (2s) for the first 60s while CI is nearly done on
216
+ * redeploys with layer cache; slow (10s) after so we don't hammer the
217
+ * GitHub API during genuine cold builds.
218
+ */
219
+ export async function waitForCIImage(owner, repo, tag, options = {}) {
220
+ // 35-min budget — first deploy on a fresh repo cold-builds pnpm install,
221
+ // runs lint+typecheck+test, then docker buildx + GHCR push. Empirically:
222
+ // - 12-18 min on a warm runner with primed buildx cache
223
+ // - 25-30 min on a cold fresh-repo path (no cache, full deps install,
224
+ // full Vite build, full Docker build)
225
+ // An earlier 25-min cap tripped both compose scenarios on a cold
226
+ // e2e run; they failed at the cap with no inline diagnostic
227
+ // surfaced. 35 min covers the cold tail; the lifecycle
228
+ // deploy step's overall timeout (40 min in
229
+ // tests/e2e/scenarios/_run-lifecycle.ts) bounds this with 5 min
230
+ // for the actual deploy after the image is ready (compose pulls + ups
231
+ // in ~2 min normally).
232
+ const { tracker, timeoutMs = 2_100_000 } = options;
233
+ const spin = (tracker ?? p).spinner();
234
+ spin.start(`Waiting for CI to publish ghcr.io/${owner}/${repo}:${tag}...`);
235
+ const start = Date.now();
236
+ const deadline = start + timeoutMs;
237
+ while (Date.now() < deadline) {
238
+ if (await ghcrTagExists(owner, repo, tag)) {
239
+ spin.stop(`Image published: ghcr.io/${owner}/${repo}:${tag}`);
240
+ return true;
241
+ }
242
+ const elapsed = Date.now() - start;
243
+ const intervalMs = elapsed < 60_000 ? 2_000 : 10_000;
244
+ await new Promise((r) => setTimeout(r, intervalMs));
245
+ }
246
+ spin.stop('CI build did not publish image within timeout');
247
+ return false;
248
+ }
249
+
250
+ /**
251
+ * Exchange a GitHub PAT for a short-lived GHCR pull token.
252
+ *
253
+ * GHCR implements the OCI Distribution Spec: unauthenticated requests to
254
+ * `/v2/...` return 401 with a `WWW-Authenticate: Bearer realm=...` challenge,
255
+ * and the realm is `https://ghcr.io/token`. We exchange our PAT for a
256
+ * pull-scoped token there using Basic auth, then use that token as Bearer
257
+ * against the manifests endpoint.
258
+ *
259
+ * Returns the pull token, or null if exchange fails.
260
+ */
261
+ async function getGhcrPullToken(owner, repo, pat) {
262
+ try {
263
+ const res = await fetch(`https://ghcr.io/token?scope=repository:${owner}/${repo}:pull`, {
264
+ headers: {
265
+ Authorization: `Basic ${Buffer.from(`${owner}:${pat}`).toString('base64')}`,
266
+ },
267
+ });
268
+ if (!res.ok) return null;
269
+ const body = await res.json();
270
+ return body.token || body.access_token || null;
271
+ } catch {
272
+ return null;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Wait for a manifest to be pullable from ghcr.io.
278
+ *
279
+ * `ghcrTagExists` (GH packages API) can report a tag present while the OCI
280
+ * manifests endpoint (what kubelet / docker actually hit on pull) still
281
+ * returns 404 for a few seconds. Polling the manifests endpoint directly
282
+ * closes that race without the old blanket 30s sleep.
283
+ *
284
+ * The OCI registry auth dance: exchange our PAT for a pull-scoped token
285
+ * against ghcr.io/token (Basic auth), then HEAD the manifest with Bearer.
286
+ * The earlier version bearer-encoded the PAT directly — GHCR rejected
287
+ * that with 401, so every probe timed out and we fell back to "proceed
288
+ * anyway" after 30s. Proper exchange makes the probe actually work.
289
+ *
290
+ * Poll every 2s, cap at `timeoutMs` (default 30s). Returns true on first
291
+ * HTTP 2xx; false on timeout (caller logs a warning but proceeds — the
292
+ * probe is best-effort).
293
+ */
294
+ export async function waitForGhcrManifest(owner, repo, tag, options = {}) {
295
+ const { timeoutMs = 30_000, intervalMs = 2_000 } = options;
296
+ const pat = getGHCRToken();
297
+ if (!pat) return false;
298
+ const pullToken = await getGhcrPullToken(owner, repo, pat);
299
+ if (!pullToken) return false;
300
+ const url = `https://ghcr.io/v2/${owner}/${repo}/manifests/${tag}`;
301
+ const headers = {
302
+ Authorization: `Bearer ${pullToken}`,
303
+ Accept:
304
+ 'application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json',
305
+ };
306
+ const deadline = Date.now() + timeoutMs;
307
+ while (Date.now() < deadline) {
308
+ try {
309
+ const res = await fetch(url, { method: 'HEAD', headers });
310
+ if (res.ok) return true;
311
+ } catch {
312
+ // Network hiccup — keep polling until the deadline.
313
+ }
314
+ await new Promise((r) => setTimeout(r, intervalMs));
315
+ }
316
+ return false;
317
+ }
318
+
319
+ /**
320
+ * Install the vibecarbon build workflow into the project if missing.
321
+ * Returns true if the workflow file was newly created (needs commit+push).
322
+ */
323
+ export function installWorkflowFile(cwd = process.cwd()) {
324
+ const target = join(cwd, PROJECT_WORKFLOW_PATH);
325
+ if (existsSync(target)) return false;
326
+ if (!existsSync(WORKFLOW_TEMPLATE_PATH)) {
327
+ throw new Error(
328
+ `Workflow template not found at ${WORKFLOW_TEMPLATE_PATH} — vibecarbon install is incomplete`,
329
+ );
330
+ }
331
+ const content = readFileSync(WORKFLOW_TEMPLATE_PATH, 'utf-8');
332
+ mkdirSync(dirname(target), { recursive: true });
333
+ writeFileSync(target, content);
334
+ return true;
335
+ }
336
+
337
+ /**
338
+ * Install `.github/workflows/deploy.yml` from the CLI-bundled template.
339
+ *
340
+ * This workflow drives the K8s push-deploy path: applies env-secrets,
341
+ * bootstraps Flux, triggers reconciliation. Only relevant for K8s deploys
342
+ * but installed unconditionally during `vibecarbon configure` → CI/CD so
343
+ * the project can flip to K8s later without a second configure step.
344
+ *
345
+ * Returns true if newly installed, false if already present (idempotent).
346
+ */
347
+ export function installDeployWorkflowFile(cwd = process.cwd()) {
348
+ const target = join(cwd, PROJECT_DEPLOY_WORKFLOW_PATH);
349
+ if (existsSync(target)) return false;
350
+ if (!existsSync(DEPLOY_WORKFLOW_TEMPLATE_PATH)) {
351
+ throw new Error(
352
+ `Deploy workflow template not found at ${DEPLOY_WORKFLOW_TEMPLATE_PATH} — vibecarbon install is incomplete`,
353
+ );
354
+ }
355
+ const content = readFileSync(DEPLOY_WORKFLOW_TEMPLATE_PATH, 'utf-8');
356
+ mkdirSync(dirname(target), { recursive: true });
357
+ writeFileSync(target, content);
358
+ return true;
359
+ }
360
+
361
+ /**
362
+ * Update Docker/K8s image references + Flux GitRepository URLs with the
363
+ * correct GitHub owner. At `vibecarbon create` time, GITHUB_OWNER defaults
364
+ * to the project name (see create.js); `configure` → CI/CD learns the real
365
+ * owner via `gh api user` (or the operator-supplied owner) and rewrites
366
+ * every place that baked in the wrong owner.
367
+ *
368
+ * Two substitution passes:
369
+ * - `ghcr.io/<wrong>/<project>` → `ghcr.io/<owner>/<project>`
370
+ * (app image refs in docker-compose, k8s Deployment + overlays)
371
+ * - `github.com/<wrong>/<project>` → `github.com/<owner>/<project>`
372
+ * (Flux GitRepository source URL in both cluster overlays)
373
+ *
374
+ * The Flux URL rewrite was missing before 2026-04-22 and caused the
375
+ * GitRepository to fail with "authentication required" — the URL owner
376
+ * was the project name, the auth token was scoped to the real repo,
377
+ * so source-controller's git clone 403'd. Flux Kustomizations then
378
+ * sat with "Source artifact not found, retrying in 30s" forever and
379
+ * deploy.yml's `kubectl wait` for vibecarbon-base hit 10m timeout.
380
+ *
381
+ * Mirrored at deploy time by rewriteOwnerAndRepo() in lib/deploy/k8s/
382
+ * gitops-deploy.js for projects that reach deploy without configuring
383
+ * CI/CD first — keep the two helpers in sync.
384
+ */
385
+ export function updateImageReferences(projectName, githubOwner, cwd = process.cwd()) {
386
+ const filesToUpdate = [
387
+ 'docker-compose.prod.yml',
388
+ 'k8s/base/app/deployment.yaml',
389
+ 'k8s/base/backup/cronjob.yaml',
390
+ 'k8s/values/supabase.values.yaml',
391
+ 'k8s/overlays/production/kustomization.yaml',
392
+ 'k8s/flux/clusters/primary/vibecarbon.yaml',
393
+ 'k8s/flux/clusters/standby/vibecarbon.yaml',
394
+ ];
395
+
396
+ // Match the main app image plus the `-db` and `-backup` suffix variants
397
+ // that supabase.values.yaml + backup/cronjob.yaml pull.
398
+ const imagePattern = new RegExp(`ghcr\\.io/[^/]+/${projectName}(-db|-backup)?\\b`, 'g');
399
+ const fluxPattern = new RegExp(`github\\.com/[^/]+/${projectName}\\b`, 'g');
400
+
401
+ for (const file of filesToUpdate) {
402
+ const filePath = join(cwd, file);
403
+ if (existsSync(filePath)) {
404
+ let content = readFileSync(filePath, 'utf-8');
405
+ content = content.replace(
406
+ imagePattern,
407
+ (_m, suffix) => `ghcr.io/${githubOwner}/${projectName}${suffix ?? ''}`,
408
+ );
409
+ content = content.replace(fluxPattern, `github.com/${githubOwner}/${projectName}`);
410
+ writeFileSync(filePath, content);
411
+ }
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Detect GitHub username from gh CLI (authenticated user). Returns null if
417
+ * gh is not installed/authenticated or the API call fails.
418
+ */
419
+ export function detectGitHubUsername() {
420
+ return runCommand('gh api user -q .login', { silent: true, ignoreError: true })?.trim() || null;
421
+ }
422
+
423
+ /**
424
+ * Commit + push the workflow file so GitHub Actions picks it up.
425
+ */
426
+ export async function commitAndPushWorkflow(cwd = process.cwd()) {
427
+ // Only commit the workflow file — don't sweep the user's working tree
428
+ runCommand(['git', 'add', PROJECT_WORKFLOW_PATH], { cwd });
429
+ runCommand(
430
+ ['git', 'commit', '-m', 'chore: add vibecarbon build workflow'],
431
+ { cwd, ignoreError: true }, // already-committed or empty is fine
432
+ );
433
+ await runCommandAsync(['git', 'push'], { cwd, silent: true });
434
+ }
435
+
436
+ /**
437
+ * Ensure a built image exists in ghcr.io for the current HEAD. Bootstraps CI
438
+ * if needed (first deploy). Returns deploy-time info:
439
+ * { imageTag, githubOwner, repoName, ghcrPullCreds }
440
+ *
441
+ * - `imageTag`: the tag to reference in k8s/compose manifests
442
+ * - `githubOwner`: the GH owner (user/org) for ghcr.io path
443
+ * - `repoName`: the GH repo name (= ghcr package name)
444
+ * - `ghcrPullCreds`: { owner, token } for kubelet/docker login (null if user
445
+ * opts for a public package)
446
+ */
447
+ // Per-process cache of `gh auth status` — the call costs ~500ms and the result
448
+ // never changes within a single deploy invocation.
449
+ let _ghAuthChecked = false;
450
+
451
+ export async function ensureCIImageReady(options = {}) {
452
+ const { yes = false, tracker, cwd = process.cwd() } = options;
453
+
454
+ if (!checkDependency('gh', 'GitHub CLI')) {
455
+ throw new Error(
456
+ '`gh` CLI is required. Install from https://cli.github.com/ and run `gh auth login`.',
457
+ );
458
+ }
459
+ if (!_ghAuthChecked) {
460
+ // `gh auth status` prints to stderr even on success; return code is the signal.
461
+ try {
462
+ runCommand(['gh', 'auth', 'status'], { silent: true });
463
+ _ghAuthChecked = true;
464
+ } catch {
465
+ throw new Error('`gh` is not authenticated. Run `gh auth login` and re-run deploy.');
466
+ }
467
+ }
468
+
469
+ const repo = getGitHubRepo(cwd);
470
+ if (!repo.owner || !repo.name) {
471
+ throw new Error(
472
+ 'No GitHub remote detected. Run `gh repo create` (or `gh repo fork`) in this project first.',
473
+ );
474
+ }
475
+
476
+ // Install workflow if missing — first-deploy case
477
+ const installed = installWorkflowFile(cwd);
478
+ if (installed) {
479
+ p.log.info(`Installed ${PROJECT_WORKFLOW_PATH}`);
480
+ const shouldPush =
481
+ yes ||
482
+ (await p.confirm({
483
+ message: 'Commit + push the workflow to trigger the first image build?',
484
+ initialValue: true,
485
+ }));
486
+ if (p.isCancel(shouldPush)) {
487
+ p.cancel('Operation cancelled.');
488
+ process.exit(0);
489
+ }
490
+ if (!shouldPush) {
491
+ p.log.error('Workflow push cancelled — deploy requires a CI-built image.');
492
+ process.exit(1);
493
+ }
494
+ await commitAndPushWorkflow(cwd);
495
+ }
496
+
497
+ const imageTag = getImageTag(cwd);
498
+ const tagExists = await ghcrTagExists(repo.owner, repo.name, imageTag);
499
+ if (!tagExists) {
500
+ p.log.info(`ghcr.io/${repo.owner}/${repo.name}:${imageTag} not yet built — waiting for CI...`);
501
+ // Brand-new repos don't auto-trigger workflows that were added in the
502
+ // same initial commit (a known GitHub Actions quirk). The push event
503
+ // is delivered before the workflow file is indexed, so the run is
504
+ // silently skipped. E2E test runs hit this every time because
505
+ // setup-repo creates the throwaway repo + pushes the initial commit
506
+ // in one shot. Explicitly dispatching here is a no-op when the push
507
+ // already triggered it (concurrency group cancels duplicates), and a
508
+ // safety net when the push didn't.
509
+ //
510
+ // Indexing the new workflow file takes 30-60s on GitHub's side; an
511
+ // immediate `gh workflow run` returns 404 "could not find workflow"
512
+ // until then. Retry every 10s for up to 90s — long enough to cover
513
+ // the indexing window, short enough to fail fast if something else
514
+ // is wrong.
515
+ let dispatched = false;
516
+ for (let attempt = 0; attempt < 9; attempt++) {
517
+ try {
518
+ execFileSync('gh', ['workflow', 'run', 'vibecarbon-build.yml', '--ref', 'main'], {
519
+ cwd,
520
+ stdio: ['ignore', 'pipe', 'pipe'],
521
+ });
522
+ dispatched = true;
523
+ break;
524
+ } catch {
525
+ // Not yet indexed — wait and retry.
526
+ await new Promise((r) => setTimeout(r, 10_000));
527
+ }
528
+ }
529
+ if (!dispatched) {
530
+ p.log.warn(
531
+ 'Could not dispatch vibecarbon-build.yml after 90s — relying on push trigger if it fires.',
532
+ );
533
+ }
534
+ const ok = await waitForCIImage(repo.owner, repo.name, imageTag, { tracker });
535
+ if (!ok) {
536
+ // Inline the build workflow's state so the operator isn't sent to
537
+ // `gh run list` — in e2e-test runs the throwaway repo is
538
+ // deleted seconds after this throws, so post-hoc `gh run view`
539
+ // 404s. Mirrors the same pattern in waitForLatestWorkflowRun
540
+ // (src/lib/github-environments.js). Each gh probe is wrapped
541
+ // independently so a single failure (e.g., empty run list) doesn't
542
+ // erase the rest of the diagnostic — the whole point of this block
543
+ // is to ALWAYS leave a trace of why CI didn't publish.
544
+ const safeGhJson = (argv) => {
545
+ try {
546
+ const out = execFileSync('gh', argv, {
547
+ cwd,
548
+ encoding: 'utf-8',
549
+ stdio: ['ignore', 'pipe', 'ignore'],
550
+ });
551
+ return { ok: true, value: out, parsed: JSON.parse(out) };
552
+ } catch (err) {
553
+ return {
554
+ ok: false,
555
+ error: err instanceof Error ? err.message.split('\n')[0] : String(err),
556
+ };
557
+ }
558
+ };
559
+ const detailLines = [];
560
+ // (1) Did the workflow_dispatch even register?
561
+ detailLines.push(`dispatched=${dispatched ? 'true' : 'false (push fallback only)'}`);
562
+ // (2) Are there any runs at all? List the most-recent build runs.
563
+ const listResult = safeGhJson([
564
+ 'run',
565
+ 'list',
566
+ '--workflow',
567
+ 'vibecarbon-build.yml',
568
+ '--limit',
569
+ '3',
570
+ '--json',
571
+ 'databaseId,status,conclusion,url,createdAt,event',
572
+ ]);
573
+ if (!listResult.ok) {
574
+ detailLines.push(`gh run list failed: ${listResult.error}`);
575
+ } else if (!listResult.parsed || listResult.parsed.length === 0) {
576
+ detailLines.push(
577
+ 'gh run list returned 0 runs — workflow_dispatch never started a run ' +
578
+ '(workflow file likely not yet indexed when push event fired). ' +
579
+ 'Check `gh workflow list` and re-trigger with `gh workflow run vibecarbon-build.yml`.',
580
+ );
581
+ } else {
582
+ for (const run of listResult.parsed) {
583
+ detailLines.push(
584
+ `run ${run.databaseId} event=${run.event} status=${run.status}` +
585
+ `${run.conclusion ? ` conclusion=${run.conclusion}` : ''} created=${run.createdAt} url=${run.url}`,
586
+ );
587
+ }
588
+ // (3) Detail the most-recent run's job/step state.
589
+ const run = listResult.parsed[0];
590
+ if (run.status === 'completed' && run.conclusion !== 'success') {
591
+ const logResult = (() => {
592
+ try {
593
+ const out = execFileSync(
594
+ 'gh',
595
+ ['run', 'view', String(run.databaseId), '--log-failed'],
596
+ { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] },
597
+ );
598
+ return { ok: true, value: out };
599
+ } catch (err) {
600
+ return {
601
+ ok: false,
602
+ error: err instanceof Error ? err.message.split('\n')[0] : String(err),
603
+ };
604
+ }
605
+ })();
606
+ if (logResult.ok) {
607
+ const tail =
608
+ logResult.value.length > 5000
609
+ ? `…\n${logResult.value.slice(-5000)}`
610
+ : logResult.value;
611
+ detailLines.push(`--- build failing-step log (last 5KB) ---\n${tail}`);
612
+ } else {
613
+ detailLines.push(`gh run view --log-failed failed: ${logResult.error}`);
614
+ }
615
+ } else {
616
+ const jobsResult = safeGhJson([
617
+ 'run',
618
+ 'view',
619
+ String(run.databaseId),
620
+ '--json',
621
+ 'status,conclusion,jobs,url',
622
+ ]);
623
+ if (jobsResult.ok) {
624
+ const info = jobsResult.parsed;
625
+ const jobs = (info.jobs ?? [])
626
+ .map((j) => {
627
+ const steps = (j.steps ?? [])
628
+ .map((s) => ` ${s.conclusion ?? s.status} ${s.name}`)
629
+ .join('\n');
630
+ return ` job: ${j.name} [${j.status}${j.conclusion ? `/${j.conclusion}` : ''}]\n${steps}`;
631
+ })
632
+ .join('\n');
633
+ detailLines.push(
634
+ `most-recent run still ${info.status} @ ${info.url}:\n${jobs || ' (no jobs)'}`,
635
+ );
636
+ } else {
637
+ detailLines.push(`gh run view jobs failed: ${jobsResult.error}`);
638
+ }
639
+ }
640
+ }
641
+ const detail = `\n--- waitForCIImage diagnostics ---\n${detailLines.join('\n')}`;
642
+ throw new Error(
643
+ `CI did not publish ghcr.io/${repo.owner}/${repo.name}:${imageTag} in time.${detail}`,
644
+ );
645
+ }
646
+ } else {
647
+ p.log.success(`Using image ghcr.io/${repo.owner}/${repo.name}:${imageTag}`);
648
+ }
649
+
650
+ const token = getGHCRToken();
651
+ const ghcrPullCreds = token ? { owner: repo.owner, token } : null;
652
+
653
+ return {
654
+ imageTag,
655
+ githubOwner: repo.owner,
656
+ repoName: repo.name,
657
+ ghcrPullCreds,
658
+ };
659
+ }
660
+
661
+ /**
662
+ * Build the fully-qualified image reference for a ghcr.io-hosted app image.
663
+ */
664
+ export function buildImageRef(owner, repo, tag) {
665
+ return `ghcr.io/${owner}/${repo}:${tag}`.toLowerCase();
666
+ }