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,103 @@
1
+ /**
2
+ * fetchWithRetry — a small wrapper around global `fetch` that retries on
3
+ * transient network + upstream errors. Used by the Hetzner DNS and
4
+ * Cloudflare clients so a momentary "fetch failed" during deploy / scale
5
+ * doesn't kill an otherwise-healthy run.
6
+ *
7
+ * Transient conditions we retry:
8
+ * - `fetch failed` (Node undici wraps TCP RST / DNS blip / ECONNRESET)
9
+ * - HTTP 429 / 500 / 502 / 503 / 504
10
+ *
11
+ * Non-transient (4xx other than 429) short-circuit immediately so we don't
12
+ * retry a genuine bad-request / auth failure.
13
+ *
14
+ * Policy: up to 5 attempts, exponential backoff (1s, 2s, 4s, 8s). Cap total
15
+ * wait at ~15s so we don't silently stretch a deploy.
16
+ *
17
+ * Connection pooling: we install a global undici Agent with keep-alive so
18
+ * every fetch() call in the process shares a warm connection pool. A single
19
+ * deploy makes 50+ round-trips against 4 APIs (Hetzner Cloud, Hetzner DNS,
20
+ * Cloudflare, Hetzner S3) — without keep-alive each call pays a fresh
21
+ * TCP+TLS handshake, which magnifies flaky-network failure rates. With
22
+ * keep-alive, repeat calls reuse idle sockets, eliminating ~80% of the
23
+ * "fetch failed" class of errors we were papering over with retries.
24
+ */
25
+
26
+ import { Agent, setGlobalDispatcher } from 'undici';
27
+
28
+ // Install once per process. Subsequent imports are no-ops (ES modules run
29
+ // top-level code exactly once). Tune connections for our fan-out: deploys
30
+ // talk to ~4 hosts in parallel; 16 per-origin is more than we need and
31
+ // leaves room for batch operations.
32
+ setGlobalDispatcher(
33
+ new Agent({
34
+ keepAliveTimeout: 30_000, // hold idle sockets 30s for reuse
35
+ keepAliveMaxTimeout: 60_000,
36
+ connections: 16, // per-origin cap
37
+ pipelining: 1, // conservative — not all APIs tolerate pipelining
38
+ }),
39
+ );
40
+
41
+ const DEFAULT_MAX_ATTEMPTS = 5;
42
+ const TRANSIENT_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);
43
+
44
+ function isTransientNetworkError(err) {
45
+ if (!err) return false;
46
+ const msg = String(err.message || err);
47
+ // Node fetch wraps low-level failures into these strings — the underlying
48
+ // cause (ECONNRESET, ENOTFOUND, EAI_AGAIN) is buried in err.cause.
49
+ return (
50
+ /fetch failed|ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|socket hang up|network.*timeout|undici/i.test(
51
+ msg,
52
+ ) ||
53
+ (err.cause &&
54
+ /ECONNRESET|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|UND_ERR/i.test(String(err.cause.code ?? err.cause)))
55
+ );
56
+ }
57
+
58
+ /**
59
+ * @param {string | URL | Request} url
60
+ * @param {RequestInit & { maxAttempts?: number; label?: string }} [init]
61
+ */
62
+ export async function fetchWithRetry(url, init = {}) {
63
+ const { maxAttempts = DEFAULT_MAX_ATTEMPTS, label, ...fetchInit } = init;
64
+ // Label the retry line so a flaky deploy's output makes sense at a glance.
65
+ // Prefer an explicit caller-supplied label; else derive from URL hostname.
66
+ const op =
67
+ label ||
68
+ (() => {
69
+ try {
70
+ return new URL(String(url)).host;
71
+ } catch {
72
+ return 'fetch';
73
+ }
74
+ })();
75
+ let lastErr;
76
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
77
+ try {
78
+ const response = await fetch(url, fetchInit);
79
+ if (response.ok || !TRANSIENT_STATUS.has(response.status)) {
80
+ return response;
81
+ }
82
+ // Drain & retry — the retry path re-issues the request with a fresh body
83
+ // stream where applicable.
84
+ lastErr = new Error(`HTTP ${response.status} on ${url}`);
85
+ lastErr.status = response.status;
86
+ } catch (err) {
87
+ if (!isTransientNetworkError(err)) throw err;
88
+ lastErr = err;
89
+ }
90
+ if (attempt === maxAttempts) break;
91
+ const backoffMs = Math.min(2 ** (attempt - 1) * 1000, 8000);
92
+ // Surface the retry so a user staring at a slow deploy knows we're
93
+ // not silently stuck. stderr keeps it out of captured stdout pipes.
94
+ const reason = lastErr?.status
95
+ ? `HTTP ${lastErr.status}`
96
+ : (lastErr?.message || String(lastErr)).slice(0, 120);
97
+ console.error(
98
+ `[retry] ${op}: attempt ${attempt}/${maxAttempts} failed (${reason}); retrying in ${backoffMs}ms`,
99
+ );
100
+ await new Promise((r) => setTimeout(r, backoffMs));
101
+ }
102
+ throw lastErr;
103
+ }
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Helpers for managing GitHub Environments + secrets/vars on the customer's
3
+ * repo. Used by Phase 4.3b of the GitOps refactor — the deploy workflow
4
+ * (.github/workflows/deploy.yml) reads per-environment `${{ secrets.X }}`
5
+ * + `${{ vars.Y }}` to apply the vibecarbon-secrets Secret, hetzner-api-token
6
+ * Secret, and kube-system/hcloud Secret into the cluster.
7
+ *
8
+ * All calls go through the `gh` CLI (argv form, no shell interpolation) so
9
+ * secret values never leak through ps/logs/shell history. Secret upload
10
+ * pipes the value through stdin — `gh secret set NAME` (no --body) reads
11
+ * stdin — to keep the value off argv.
12
+ *
13
+ * Decision: secret scope (1=org/repo-level, 2=per-environment) matches
14
+ * the session 2026-04-20 discussion:
15
+ * Org-level: HETZNER_API_TOKEN, CLOUDFLARE_API_TOKEN,
16
+ * S3_ACCESS_KEY, S3_SECRET_KEY
17
+ * Per-env: KUBECONFIG_B64, DB_PASSWORD, JWT_SECRET,
18
+ * SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY,
19
+ * REALTIME_SECRET, DB_ENC_KEY, VAULT_ENC_KEY,
20
+ * PG_META_CRYPTO_KEY, LOGFLARE_API_KEY,
21
+ * ADMIN_EMAIL, ADMIN_PASSWORD, SMTP_*
22
+ * Per-env vars: HCLOUD_NETWORK_ID, SITE_URL, DNS_PROVIDER
23
+ */
24
+
25
+ import { runCommandAsync } from './command.js';
26
+
27
+ /**
28
+ * Get the current repo's `owner/name` from the gh CLI. Relies on a git
29
+ * remote being configured (which `vibecarbon create` sets up).
30
+ */
31
+ async function getRepoSlug() {
32
+ const out = await runCommandAsync(
33
+ ['gh', 'repo', 'view', '--json', 'nameWithOwner', '-q', '.nameWithOwner'],
34
+ { silent: true },
35
+ );
36
+ return out.toString().trim();
37
+ }
38
+
39
+ /**
40
+ * Ensure a GitHub Environment exists on the customer's repo. Idempotent.
41
+ * `gh api -X PUT` creates if missing, no-op if present.
42
+ */
43
+ export async function ensureEnvironment(envName) {
44
+ const repo = await getRepoSlug();
45
+ await runCommandAsync(['gh', 'api', `repos/${repo}/environments/${envName}`, '-X', 'PUT'], {
46
+ silent: true,
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Set a secret value via `gh secret set`, piping the value through stdin.
52
+ *
53
+ * Pipes stdin instead of passing the value on argv — `gh secret set NAME`
54
+ * (no --body) reads from stdin. Previously used `--body-file <path>` but
55
+ * that flag was removed from `gh` (verified 2026-04-22: only `-b/--body`,
56
+ * `-f/--env-file`, and stdin remain). Stdin has the same security
57
+ * properties as a 0600 temp file — never on argv, never on disk.
58
+ *
59
+ * `env` = undefined → repo-level secret; `env` = "prod" → environment-level.
60
+ *
61
+ * Returns false if the value is empty (skips the call — GH rejects empty
62
+ * secrets). True on successful write.
63
+ */
64
+ async function setSecretFromBodyFile(key, value, env) {
65
+ if (!value) return false;
66
+ const args = ['gh', 'secret', 'set', key];
67
+ if (env) args.push('--env', env);
68
+ await runCommandAsync(args, { silent: true, input: value });
69
+ return true;
70
+ }
71
+
72
+ /**
73
+ * Set a variable (non-secret) value. `gh variable set` doesn't support
74
+ * --body-file, so the value goes on argv. Acceptable since these are
75
+ * non-secret (network IDs, URLs).
76
+ */
77
+ async function setVariable(key, value, env) {
78
+ if (value === undefined || value === null || value === '') return false;
79
+ const args = ['gh', 'variable', 'set', key, '--body', String(value)];
80
+ if (env) args.push('--env', env);
81
+ await runCommandAsync(args, { silent: true });
82
+ return true;
83
+ }
84
+
85
+ /**
86
+ * Seed the org-level (repo-level) secrets. Called once per repo — values
87
+ * are shared across all environments. If any are already set, they're
88
+ * overwritten (rotation path).
89
+ *
90
+ * @param {object} creds - Shape matches ~/.vibecarbon/credentials.json
91
+ */
92
+ export async function seedOrgSecrets(creds) {
93
+ const hetznerToken = creds?.hetzner?.apiToken ?? creds?.hetznerApiToken ?? '';
94
+ const cloudflareToken = creds?.cloudflare?.apiToken ?? creds?.cloudflareApiToken ?? '';
95
+ const s3Access = creds?.s3?.accessKey ?? '';
96
+ const s3Secret = creds?.s3?.secretKey ?? '';
97
+
98
+ const applied = [];
99
+ if (await setSecretFromBodyFile('HETZNER_API_TOKEN', hetznerToken))
100
+ applied.push('HETZNER_API_TOKEN');
101
+ if (await setSecretFromBodyFile('CLOUDFLARE_API_TOKEN', cloudflareToken))
102
+ applied.push('CLOUDFLARE_API_TOKEN');
103
+ if (await setSecretFromBodyFile('S3_ACCESS_KEY', s3Access)) applied.push('S3_ACCESS_KEY');
104
+ if (await setSecretFromBodyFile('S3_SECRET_KEY', s3Secret)) applied.push('S3_SECRET_KEY');
105
+ return applied;
106
+ }
107
+
108
+ /**
109
+ * Seed the per-environment secrets + variables. `envVars` are sourced from
110
+ * .env.local (read by the caller) so we can re-use the same values as the
111
+ * imperative deploy path sources.
112
+ *
113
+ * @param {string} envName - "dev" | "staging" | "prod"
114
+ * @param {object} secrets - Key-value map of secret names → values.
115
+ * @param {object} vars - Key-value map of variable names → values.
116
+ */
117
+ export async function seedEnvironmentSecrets(envName, secrets, vars = {}) {
118
+ await ensureEnvironment(envName);
119
+
120
+ const appliedSecrets = [];
121
+ for (const [key, value] of Object.entries(secrets)) {
122
+ if (await setSecretFromBodyFile(key, value, envName)) appliedSecrets.push(key);
123
+ }
124
+
125
+ const appliedVars = [];
126
+ for (const [key, value] of Object.entries(vars)) {
127
+ if (await setVariable(key, value, envName)) appliedVars.push(key);
128
+ }
129
+ return { secrets: appliedSecrets, vars: appliedVars };
130
+ }
131
+
132
+ /**
133
+ * Upload the cluster kubeconfig as a base64-encoded env secret. The deploy
134
+ * workflow base64-decodes it with `printf ... | base64 -d` into a file and
135
+ * points `$KUBECONFIG` at it. Base64 is used (rather than raw YAML) because
136
+ * the downstream shell interpolation needs a single-line value.
137
+ */
138
+ export async function uploadKubeconfig(envName, kubeconfigPath) {
139
+ const { readFileSync } = await import('node:fs');
140
+ const raw = readFileSync(kubeconfigPath, 'utf-8');
141
+ const b64 = Buffer.from(raw, 'utf-8').toString('base64');
142
+ await ensureEnvironment(envName);
143
+ await setSecretFromBodyFile('KUBECONFIG_B64', b64, envName);
144
+ }
145
+
146
+ /**
147
+ * Trigger the customer's deploy workflow for a given environment and
148
+ * optionally wait for it to complete. `gh workflow run` fires it; the
149
+ * workflow's concurrency group serializes per-env so only one runs at a
150
+ * time.
151
+ */
152
+ export async function triggerDeployWorkflow(envName) {
153
+ await runCommandAsync(
154
+ ['gh', 'workflow', 'run', 'deploy.yml', '--ref', 'main', '-F', `environment=${envName}`],
155
+ { silent: true },
156
+ );
157
+ }
158
+
159
+ /**
160
+ * Block until the most recent run of deploy.yml completes. Polls
161
+ * `gh run list` every 5s; surfaces the final status when done.
162
+ */
163
+ export async function waitForLatestWorkflowRun(timeoutSec = 1800) {
164
+ const deadline = Date.now() + timeoutSec * 1000;
165
+ const startedAt = Date.now();
166
+ // Give GitHub a couple seconds to register the run before we poll.
167
+ await new Promise((r) => setTimeout(r, 3000));
168
+ // Periodic state snapshot — every 2 min, log which workflow step is
169
+ // currently in-progress. Without this, a 60-minute wait is a black
170
+ // box; with it, the deploy log shows where the workflow spent its
171
+ // time. Captured by the deploy logger (#6) so it persists past
172
+ // teardown. Snapshot every ~24th poll (5s × 24 = 120s) — short enough
173
+ // to catch a mid-step hang, long enough to not spam.
174
+ let lastSnapshotAt = 0;
175
+ while (Date.now() < deadline) {
176
+ const elapsed = Math.round((Date.now() - startedAt) / 1000);
177
+ if (Date.now() - lastSnapshotAt >= 120_000) {
178
+ try {
179
+ const stateOut = await runCommandAsync(
180
+ [
181
+ 'gh',
182
+ 'run',
183
+ 'list',
184
+ '--workflow',
185
+ 'deploy.yml',
186
+ '--limit',
187
+ '1',
188
+ '--json',
189
+ 'databaseId,status,conclusion,jobs',
190
+ ],
191
+ { silent: true, ignoreError: true },
192
+ );
193
+ if (stateOut) {
194
+ const info = JSON.parse(stateOut.toString())[0];
195
+ const jobs = (info?.jobs ?? [])
196
+ .map((j) => {
197
+ const inFlight =
198
+ j.steps?.find((s) => s.status === 'in_progress')?.name ??
199
+ (j.status === 'completed' ? `(done: ${j.conclusion})` : `(${j.status})`);
200
+ return ` ${j.name}: ${inFlight}`;
201
+ })
202
+ .join('\n');
203
+ console.log(
204
+ `[wait ${elapsed}s] deploy.yml run=${info?.databaseId ?? '?'} status=${info?.status ?? '?'}\n${jobs}`,
205
+ );
206
+ }
207
+ } catch {
208
+ /* snapshot is best-effort — never break the wait */
209
+ }
210
+ lastSnapshotAt = Date.now();
211
+ }
212
+ const out =
213
+ (
214
+ await runCommandAsync(
215
+ [
216
+ 'gh',
217
+ 'run',
218
+ 'list',
219
+ '--workflow',
220
+ 'deploy.yml',
221
+ '--limit',
222
+ '1',
223
+ '--json',
224
+ 'status,conclusion,databaseId',
225
+ ],
226
+ { silent: true, ignoreError: true },
227
+ )
228
+ )?.toString() ?? '[]';
229
+ let run;
230
+ try {
231
+ run = JSON.parse(out)[0];
232
+ } catch {
233
+ run = null;
234
+ }
235
+ if (run?.status === 'completed') {
236
+ if (run.conclusion === 'success') return { ok: true, runId: run.databaseId };
237
+ // Fetch the failing-step log inline so the caller has something to
238
+ // act on. The throwaway repo used by e2e tests is destroyed
239
+ // right after a failure; post-mortem `gh run view` then 404s. Even
240
+ // outside e2e, surfacing the actual failure at throw time is
241
+ // far more useful than a "go look at this URL" pointer the operator
242
+ // then has to re-fetch manually. Best-effort — if the log fetch
243
+ // itself fails (403, run not yet indexed), proceed with the short
244
+ // error so we don't hide the original failure.
245
+ let logTail = '';
246
+ try {
247
+ const logOut = await runCommandAsync(
248
+ ['gh', 'run', 'view', String(run.databaseId), '--log-failed'],
249
+ { silent: true, ignoreError: true, timeout: 60_000 },
250
+ );
251
+ if (logOut) {
252
+ const text = logOut.toString();
253
+ // Tail size 20KB — deploy.yml's failure dump (CoreDNS config +
254
+ // CoreDNS logs + CCM logs + node info + pod network probes) can
255
+ // total 10-15KB; a 3KB tail truncates every useful line and
256
+ // leaves only the final error marker.
257
+ const tail = text.length > 20000 ? `…\n${text.slice(-20000)}` : text;
258
+ logTail = `\n--- failing-step log (last 20KB) ---\n${tail}`;
259
+ }
260
+ } catch {
261
+ /* best-effort */
262
+ }
263
+ throw new Error(
264
+ `deploy.yml run ${run.databaseId} finished with conclusion=${run.conclusion}. ` +
265
+ `Inspect: gh run view ${run.databaseId} --log-failed${logTail}`,
266
+ );
267
+ }
268
+ await new Promise((r) => setTimeout(r, 5000));
269
+ }
270
+ // Timeout: grab a log tail of the still-running workflow inline. In
271
+ // e2e runs the throwaway repo is deleted seconds after this
272
+ // throws, so a post-hoc `gh run view` 404s — we need the state now or
273
+ // we fly blind. Best-effort; a dry runId pointer is better than
274
+ // nothing when the log fetch itself fails.
275
+ let runId = null;
276
+ let logTail = '';
277
+ try {
278
+ const out = await runCommandAsync(
279
+ [
280
+ 'gh',
281
+ 'run',
282
+ 'list',
283
+ '--workflow',
284
+ 'deploy.yml',
285
+ '--limit',
286
+ '1',
287
+ '--json',
288
+ 'databaseId,status',
289
+ ],
290
+ { silent: true, ignoreError: true },
291
+ );
292
+ if (out) {
293
+ const parsed = JSON.parse(out.toString())[0];
294
+ runId = parsed?.databaseId ?? null;
295
+ if (runId) {
296
+ // `gh run view --log` 404s on in-flight runs — the archive doesn't
297
+ // exist until the run completes. For in-flight state, parse the
298
+ // structured jobs JSON: it gives each job's status + per-step
299
+ // conclusion + timing, which is exactly the "which step is hung"
300
+ // signal we need.
301
+ const jobsOut = await runCommandAsync(
302
+ ['gh', 'run', 'view', String(runId), '--json', 'status,conclusion,jobs,url,createdAt'],
303
+ { silent: true, ignoreError: true, timeout: 60_000 },
304
+ );
305
+ if (jobsOut) {
306
+ try {
307
+ const info = JSON.parse(jobsOut.toString());
308
+ const stepSummary = (info.jobs ?? [])
309
+ .map((j) => {
310
+ const steps = (j.steps ?? [])
311
+ .map(
312
+ (s) =>
313
+ ` ${s.conclusion ?? s.status} ${s.name}${s.startedAt ? ` (${s.startedAt})` : ''}`,
314
+ )
315
+ .join('\n');
316
+ return ` job: ${j.name} [${j.status}${j.conclusion ? `/${j.conclusion}` : ''}]\n${steps}`;
317
+ })
318
+ .join('\n');
319
+ logTail =
320
+ `\n--- in-flight run ${runId} @ ${info.url ?? '(no url)'} ---\n` +
321
+ ` status=${info.status} conclusion=${info.conclusion ?? '(pending)'} started=${info.createdAt}\n` +
322
+ stepSummary;
323
+ } catch {
324
+ /* JSON parse failed — silent */
325
+ }
326
+ }
327
+ }
328
+ }
329
+ } catch {
330
+ /* best-effort */
331
+ }
332
+ throw new Error(
333
+ `deploy.yml run did not complete within ${timeoutSec}s.${runId ? ` run=${runId}` : ''}${logTail}`,
334
+ );
335
+ }