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,735 @@
1
+ /**
2
+ * `vibecarbon diagnose [env] [section]`
3
+ *
4
+ * One-command full cluster state dump. Writes a timestamped report to
5
+ * ~/.vibecarbon/diag-<env>-<ts>.txt and tees a summary to stdout. Companion
6
+ * to `vibecarbon shell` — when something is wrong, this is the single
7
+ * command that gathers every signal that's
8
+ * helped diagnose past failures (node IPs, CoreDNS config, Flux state,
9
+ * pod-level DNS probes, hcloud resource state).
10
+ *
11
+ * Sections:
12
+ * nodes — kubectl get nodes -o wide + ssh systemctl status k3s
13
+ * + journalctl -u k3s + ip addr + dmesg via SSH
14
+ * pods — kubectl get pods -A, describe non-Running pods, logs for
15
+ * crashlooping pods
16
+ * network — CoreDNS configmap + logs, Flannel + kube-proxy logs,
17
+ * hcloud-ccm + hcloud-csi logs, in-cluster busybox DNS probe
18
+ * flux — kubectl -n flux-system get gitrepository,kustomization,
19
+ * helmrelease + describe + controller logs
20
+ * hcloud — server/network/firewall/floating-ip/PG/SSH-key list
21
+ * all — every section (default)
22
+ *
23
+ * Each probe is wrapped so a failure in one section doesn't prevent the
24
+ * others from running — partial output is far more useful than nothing.
25
+ */
26
+
27
+ import { execFileSync } from 'node:child_process';
28
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
29
+ import { homedir } from 'node:os';
30
+ import { join } from 'node:path';
31
+ import { renderHelp } from './lib/cli/help.js';
32
+ import { parseFlags } from './lib/cli/parse-flags.js';
33
+ import { c } from './lib/colors.js';
34
+ import { loadProjectConfig } from './lib/config.js';
35
+ import { ensureOperatorIpAccess } from './lib/operator-ip.js';
36
+ import { assertInProjectDir } from './lib/project-guard.js';
37
+
38
+ // Sections that run as part of `diagnose all`. Egress is intentionally
39
+ // excluded — its netshoot image pull + multi-probe battery can take 30-60s,
40
+ // which is fine for an explicit `diagnose <env> egress` invocation but
41
+ // disproportionate for the default sweep.
42
+ const SECTIONS = ['nodes', 'pods', 'network', 'flux', 'hcloud'];
43
+ const OPTIONAL_SECTIONS = ['egress'];
44
+ const ALL_VALID_SECTIONS = [...SECTIONS, ...OPTIONAL_SECTIONS];
45
+
46
+ /** @type {import('./lib/cli/parse-flags.js').CommandSpec & { summary?: string, description?: string, examples?: Array<{ command: string, description?: string }> }} */
47
+ const SPEC = {
48
+ name: 'diagnose',
49
+ summary: 'Dump full cluster state for an environment',
50
+ description: [
51
+ 'Streams sections to stdout AND writes the full report to:',
52
+ ' ~/.vibecarbon/diag-<env>-<timestamp>.txt',
53
+ ].join('\n'),
54
+ positional: [
55
+ {
56
+ name: 'env',
57
+ optional: true,
58
+ description: 'Environment to inspect (default: prod)',
59
+ },
60
+ {
61
+ name: 'section',
62
+ optional: true,
63
+ description: `One of: ${ALL_VALID_SECTIONS.join(', ')}, all (default: all — excludes egress)`,
64
+ },
65
+ ],
66
+ flags: [{ name: 'h', boolean: true, description: 'Show this help' }],
67
+ examples: [
68
+ { command: 'vibecarbon diagnose e3', description: 'full dump (excludes heavy egress probe)' },
69
+ { command: 'vibecarbon diagnose e3 network', description: 'CoreDNS + CCM + DNS probe' },
70
+ {
71
+ command: 'vibecarbon diagnose e3 egress',
72
+ description: 'netshoot DNS + MTU + route battery',
73
+ },
74
+ { command: 'vibecarbon diagnose e3 flux', description: 'just Flux state' },
75
+ ],
76
+ };
77
+
78
+ function loadHcloudToken() {
79
+ const credPath = join(homedir(), '.vibecarbon', 'credentials.json');
80
+ if (!existsSync(credPath)) return null;
81
+ try {
82
+ const data = JSON.parse(readFileSync(credPath, 'utf-8'));
83
+ return data?.hetzner?.apiToken ?? data?.hetznerApiToken ?? null;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Run a command, returning { ok, stdout, stderr }. Never throws —
91
+ * failures get captured into stderr/exitCode so a single broken command
92
+ * doesn't break the whole report.
93
+ */
94
+ function safeRun(cmd, args, env) {
95
+ try {
96
+ const out = execFileSync(cmd, args, {
97
+ env,
98
+ encoding: 'utf-8',
99
+ stdio: ['ignore', 'pipe', 'pipe'],
100
+ timeout: 60_000,
101
+ });
102
+ return { ok: true, stdout: out, stderr: '' };
103
+ } catch (err) {
104
+ return {
105
+ ok: false,
106
+ stdout: err?.stdout?.toString() ?? '',
107
+ stderr: err?.stderr?.toString() ?? err?.message ?? String(err),
108
+ };
109
+ }
110
+ }
111
+
112
+ class Report {
113
+ constructor() {
114
+ this.lines = [];
115
+ }
116
+ section(title) {
117
+ const banner = `\n${'='.repeat(72)}\n ${title}\n${'='.repeat(72)}\n`;
118
+ this.lines.push(banner);
119
+ console.log(banner);
120
+ }
121
+ subsection(title) {
122
+ const banner = `\n--- ${title} ---`;
123
+ this.lines.push(banner);
124
+ console.log(banner);
125
+ }
126
+ command(label, cmd, args) {
127
+ this.lines.push(`\n# $ ${cmd} ${args.join(' ')} (${label})`);
128
+ console.log(c.dim(`# $ ${cmd} ${args.join(' ')}`));
129
+ }
130
+ output(text) {
131
+ if (!text) return;
132
+ this.lines.push(text.trimEnd());
133
+ process.stdout.write(text);
134
+ if (!text.endsWith('\n')) process.stdout.write('\n');
135
+ }
136
+ warn(text) {
137
+ const line = `${c.warning('!')} ${text}`;
138
+ this.lines.push(`! ${text}`);
139
+ console.log(line);
140
+ }
141
+ toString() {
142
+ return this.lines.join('\n');
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Run a command, push it to the report with a label, and dump output.
148
+ */
149
+ function probe(report, label, cmd, args, env) {
150
+ report.command(label, cmd, args);
151
+ const r = safeRun(cmd, args, env);
152
+ if (r.ok) {
153
+ report.output(r.stdout);
154
+ } else {
155
+ report.output(r.stdout);
156
+ report.warn(`${cmd} ${args.join(' ')} failed: ${r.stderr.split('\n')[0]}`);
157
+ }
158
+ }
159
+
160
+ function diagnoseHcloud(report, env) {
161
+ report.section('HCLOUD RESOURCES');
162
+ for (const kind of [
163
+ 'server',
164
+ 'network',
165
+ 'firewall',
166
+ 'floating-ip',
167
+ 'placement-group',
168
+ 'ssh-key',
169
+ ]) {
170
+ probe(report, kind, 'hcloud', [kind, 'list'], env);
171
+ }
172
+ }
173
+
174
+ function diagnoseNodes(report, env, kubeconfig, sshKey, controlplaneIp) {
175
+ report.section('NODES');
176
+ probe(
177
+ report,
178
+ 'nodes wide',
179
+ 'kubectl',
180
+ ['--kubeconfig', kubeconfig, 'get', 'nodes', '-o', 'wide'],
181
+ env,
182
+ );
183
+ probe(report, 'node describe', 'kubectl', ['--kubeconfig', kubeconfig, 'describe', 'nodes'], env);
184
+ if (existsSync(sshKey) && controlplaneIp) {
185
+ const sshArgs = (cmd) => [
186
+ '-i',
187
+ sshKey,
188
+ '-o',
189
+ 'StrictHostKeyChecking=accept-new',
190
+ '-o',
191
+ 'BatchMode=yes',
192
+ '-o',
193
+ 'ConnectTimeout=10',
194
+ `root@${controlplaneIp}`,
195
+ cmd,
196
+ ];
197
+ probe(report, 'k3s service status', 'ssh', sshArgs('systemctl status k3s --no-pager'), env);
198
+ probe(
199
+ report,
200
+ 'k3s journal (last 50)',
201
+ 'ssh',
202
+ sshArgs('journalctl -u k3s --no-pager -n 50'),
203
+ env,
204
+ );
205
+ probe(
206
+ report,
207
+ 'containerd containers',
208
+ 'ssh',
209
+ sshArgs('k3s ctr containers ls 2>&1 | head -20'),
210
+ env,
211
+ );
212
+ probe(report, 'host addresses', 'ssh', sshArgs('ip addr show'), env);
213
+ probe(report, 'host dmesg (tail)', 'ssh', sshArgs('dmesg | tail -30'), env);
214
+ } else {
215
+ report.warn(
216
+ `ssh skipped — sshKey=${sshKey} (${existsSync(sshKey) ? 'found' : 'MISSING'}) controlplaneIp=${controlplaneIp ?? '(none)'}`,
217
+ );
218
+ }
219
+ }
220
+
221
+ function diagnosePods(report, env, kubeconfig) {
222
+ report.section('PODS');
223
+ probe(
224
+ report,
225
+ 'pods all-ns wide',
226
+ 'kubectl',
227
+ ['--kubeconfig', kubeconfig, 'get', 'pods', '-A', '-o', 'wide'],
228
+ env,
229
+ );
230
+ probe(
231
+ report,
232
+ 'non-running pods',
233
+ 'kubectl',
234
+ [
235
+ '--kubeconfig',
236
+ kubeconfig,
237
+ 'get',
238
+ 'pods',
239
+ '-A',
240
+ '--field-selector=status.phase!=Running,status.phase!=Succeeded',
241
+ ],
242
+ env,
243
+ );
244
+ // Describe pods in non-running state
245
+ const listResult = safeRun(
246
+ 'kubectl',
247
+ [
248
+ '--kubeconfig',
249
+ kubeconfig,
250
+ 'get',
251
+ 'pods',
252
+ '-A',
253
+ '--field-selector=status.phase!=Running,status.phase!=Succeeded',
254
+ '-o',
255
+ 'jsonpath={range .items[*]}{.metadata.namespace} {.metadata.name}{"\\n"}{end}',
256
+ ],
257
+ env,
258
+ );
259
+ if (listResult.ok) {
260
+ const pairs = listResult.stdout
261
+ .trim()
262
+ .split('\n')
263
+ .filter(Boolean)
264
+ .map((l) => l.split(' '));
265
+ for (const [ns, name] of pairs) {
266
+ if (!ns || !name) continue;
267
+ report.subsection(`${ns}/${name}`);
268
+ probe(
269
+ report,
270
+ 'describe',
271
+ 'kubectl',
272
+ ['--kubeconfig', kubeconfig, '-n', ns, 'describe', `pod/${name}`],
273
+ env,
274
+ );
275
+ probe(
276
+ report,
277
+ 'logs (current)',
278
+ 'kubectl',
279
+ ['--kubeconfig', kubeconfig, '-n', ns, 'logs', name, '--tail=40'],
280
+ env,
281
+ );
282
+ probe(
283
+ report,
284
+ 'logs (previous)',
285
+ 'kubectl',
286
+ ['--kubeconfig', kubeconfig, '-n', ns, 'logs', name, '--previous', '--tail=40'],
287
+ env,
288
+ );
289
+ }
290
+ }
291
+ }
292
+
293
+ function diagnoseNetwork(report, env, kubeconfig) {
294
+ report.section('NETWORK');
295
+ probe(
296
+ report,
297
+ 'CoreDNS configmap',
298
+ 'kubectl',
299
+ ['--kubeconfig', kubeconfig, '-n', 'kube-system', 'get', 'cm', 'coredns', '-o', 'yaml'],
300
+ env,
301
+ );
302
+ probe(
303
+ report,
304
+ 'kube-system pods',
305
+ 'kubectl',
306
+ ['--kubeconfig', kubeconfig, '-n', 'kube-system', 'get', 'pods', '-o', 'wide'],
307
+ env,
308
+ );
309
+ for (const labelSel of [
310
+ { name: 'CoreDNS', sel: 'k8s-app=kube-dns' },
311
+ { name: 'Flannel', sel: 'app=flannel' },
312
+ { name: 'kube-proxy', sel: 'k8s-app=kube-proxy' },
313
+ { name: 'hcloud-CCM', sel: 'app.kubernetes.io/name=hcloud-cloud-controller-manager' },
314
+ {
315
+ name: 'hcloud-CSI controller',
316
+ sel: 'app.kubernetes.io/component=controller,app.kubernetes.io/name=hcloud-csi',
317
+ },
318
+ ]) {
319
+ report.subsection(`${labelSel.name} logs`);
320
+ const list = safeRun(
321
+ 'kubectl',
322
+ [
323
+ '--kubeconfig',
324
+ kubeconfig,
325
+ '-n',
326
+ 'kube-system',
327
+ 'get',
328
+ 'pods',
329
+ '-l',
330
+ labelSel.sel,
331
+ '-o',
332
+ 'jsonpath={.items[*].metadata.name}',
333
+ ],
334
+ env,
335
+ );
336
+ if (list.ok && list.stdout.trim()) {
337
+ for (const pod of list.stdout.trim().split(/\s+/)) {
338
+ probe(
339
+ report,
340
+ pod,
341
+ 'kubectl',
342
+ ['--kubeconfig', kubeconfig, '-n', 'kube-system', 'logs', pod, '--tail=30'],
343
+ env,
344
+ );
345
+ }
346
+ } else {
347
+ report.warn(`No pods found matching ${labelSel.sel}`);
348
+ }
349
+ }
350
+ // In-cluster DNS + egress probe
351
+ report.subsection('busybox in-cluster probe (nslookup + wget)');
352
+ const probeName = `vc-diag-${Math.random().toString(36).slice(2, 8)}`;
353
+ probe(
354
+ report,
355
+ 'pod probe',
356
+ 'kubectl',
357
+ [
358
+ '--kubeconfig',
359
+ kubeconfig,
360
+ '-n',
361
+ 'kube-system',
362
+ 'run',
363
+ probeName,
364
+ '--image=busybox:1.36',
365
+ '--restart=Never',
366
+ '--rm',
367
+ '--attach',
368
+ '--timeout=60s',
369
+ '--command',
370
+ '--',
371
+ 'sh',
372
+ '-c',
373
+ 'echo "--- /etc/resolv.conf ---"; cat /etc/resolv.conf; echo; echo "--- nslookup github.com (cluster DNS) ---"; nslookup github.com 2>&1 | head -10; echo; echo "--- nslookup github.com @1.1.1.1 ---"; nslookup github.com 1.1.1.1 2>&1 | head -10; echo; echo "--- wget https://github.com (5s) ---"; wget -qO- --timeout=5 https://github.com 2>&1 | head -3 || echo wget-failed',
374
+ ],
375
+ env,
376
+ );
377
+ }
378
+
379
+ /**
380
+ * Deep DNS + pod-network egress battery. Runs a netshoot pod with sleep,
381
+ * execs a multi-layered probe script, then deletes the pod. Distinguishes
382
+ * between "DNS broken", "pod-network broken", "MTU mismatch", and "external
383
+ * egress broken" — the four classes of bug that masquerade as the same
384
+ * "i/o timeout to github.com" symptom that bit us 2026-04-24.
385
+ *
386
+ * Heavy enough (~30s netshoot pull, ~60s battery) that it's excluded from
387
+ * `diagnose all`. Run explicitly when network is suspect:
388
+ * vibecarbon diagnose <env> egress
389
+ */
390
+ function diagnoseEgress(report, env, kubeconfig, sshKey, controlplaneIp) {
391
+ report.section('EGRESS (DNS + pod-network battery)');
392
+
393
+ // CCM env vars: HCLOUD_NETWORK should resolve via secretKeyRef:hcloud/network.
394
+ // If it's empty here the route-controller is silently disabled — see
395
+ // hcloud-cloud-controller-manager #115/#630.
396
+ report.subsection('CCM env vars (HCLOUD_NETWORK present?)');
397
+ probe(
398
+ report,
399
+ 'CCM deploy env',
400
+ 'kubectl',
401
+ [
402
+ '--kubeconfig',
403
+ kubeconfig,
404
+ '-n',
405
+ 'kube-system',
406
+ 'get',
407
+ 'deploy',
408
+ 'hcloud-cloud-controller-manager',
409
+ '-o',
410
+ 'jsonpath={range .spec.template.spec.containers[*].env[*]}{.name}\t{.value}\t{.valueFrom.secretKeyRef.name}/{.valueFrom.secretKeyRef.key}{"\\n"}{end}',
411
+ ],
412
+ env,
413
+ );
414
+
415
+ // CoreDNS Corefile — forwarders are the upstream resolver pod-DNS depends on.
416
+ report.subsection('CoreDNS Corefile');
417
+ probe(
418
+ report,
419
+ 'coredns Corefile',
420
+ 'kubectl',
421
+ [
422
+ '--kubeconfig',
423
+ kubeconfig,
424
+ '-n',
425
+ 'kube-system',
426
+ 'get',
427
+ 'cm',
428
+ 'coredns',
429
+ '-o',
430
+ 'jsonpath={.data.Corefile}',
431
+ ],
432
+ env,
433
+ );
434
+
435
+ // Node-level state: routes, addresses, links, resolv.conf. If the host
436
+ // can't egress, no pod can either.
437
+ if (existsSync(sshKey) && controlplaneIp) {
438
+ report.subsection('node-level routes / addresses / links / resolv.conf');
439
+ const sshArgs = (cmd) => [
440
+ '-i',
441
+ sshKey,
442
+ '-o',
443
+ 'StrictHostKeyChecking=accept-new',
444
+ '-o',
445
+ 'BatchMode=yes',
446
+ '-o',
447
+ 'ConnectTimeout=10',
448
+ `root@${controlplaneIp}`,
449
+ cmd,
450
+ ];
451
+ probe(report, 'host routes', 'ssh', sshArgs('ip route'), env);
452
+ probe(report, 'host addresses', 'ssh', sshArgs('ip addr'), env);
453
+ probe(report, 'host links', 'ssh', sshArgs('ip link'), env);
454
+ probe(report, 'host /etc/resolv.conf', 'ssh', sshArgs('cat /etc/resolv.conf'), env);
455
+ } else {
456
+ report.warn(
457
+ `host node probes skipped — sshKey=${existsSync(sshKey) ? 'found' : 'MISSING'} controlplaneIp=${controlplaneIp ?? '(none)'}`,
458
+ );
459
+ }
460
+
461
+ // Pod-level battery — the part that actually distinguishes failure modes.
462
+ // Each line is labeled so a failed step localizes the layer:
463
+ // A fails → cluster DNS (CoreDNS) broken
464
+ // B fails → external DNS reachability (node→1.1.1.1) broken
465
+ // C fails → public-IP egress broken (CNI masquerade / default route)
466
+ // D fails → github.com unreachable by IP (egress to that AS broken)
467
+ // E fails → DNS-driven egress broken specifically (combo of A or B)
468
+ // I fails → MTU clamp not applied (vxlan-on-Hetzner=1450); fragments dropped
469
+ //
470
+ // We can't use `kubectl run --rm --attach` with a one-shot script — it
471
+ // truncates stdout when the container exits quickly (validated against a
472
+ // live cluster). Use the create+wait+exec+delete pattern: create with a
473
+ // long sleep, wait for Ready, exec the battery (stdout is captured cleanly),
474
+ // delete in finally-ish fashion (probe() never throws so cleanup always runs).
475
+ report.subsection('netshoot pod battery (DNS + curl + MTU)');
476
+ const podName = `vc-egress-${Math.random().toString(36).slice(2, 8)}`;
477
+ const battery = [
478
+ 'set +e',
479
+ 'echo "## /etc/resolv.conf"; cat /etc/resolv.conf; echo',
480
+ 'echo "## A. dig @cluster-DNS (10.96.0.10) github.com"; dig +time=3 +tries=1 @10.96.0.10 github.com +short; echo "exit=$?"; echo',
481
+ 'echo "## B. dig @1.1.1.1 github.com"; dig +time=3 +tries=1 @1.1.1.1 github.com +short; echo "exit=$?"; echo',
482
+ 'echo "## C. curl https://1.1.1.1 (public IP)"; curl -m5 -sI https://1.1.1.1 -o /dev/null -w "http=%{http_code} t=%{time_total}s\\n" || echo FAIL_PUBLIC; echo',
483
+ 'echo "## D. curl github.com via direct IP (bypass DNS)"; curl -m5 -skI --resolve github.com:443:140.82.121.4 https://github.com/ -o /dev/null -w "http=%{http_code} t=%{time_total}s\\n" || echo FAIL_GITHUB_IP; echo',
484
+ 'echo "## E. curl https://github.com (DNS-driven)"; curl -m5 -sI https://github.com -o /dev/null -w "http=%{http_code} t=%{time_total}s\\n" || echo FAIL_GITHUB_DNS; echo',
485
+ 'echo "## F. curl https://raw.githubusercontent.com (Flux source target)"; curl -m5 -sI https://raw.githubusercontent.com -o /dev/null -w "http=%{http_code} t=%{time_total}s\\n" || echo FAIL_RAWGH; echo',
486
+ 'echo "## G. ip route"; ip route; echo',
487
+ 'echo "## H. ip -br link (MTU on each iface)"; ip -br link; echo',
488
+ 'echo "## I. ping -M do -s 1372 1.1.1.1 (vxlan-MTU 1400 OK)"; ping -M do -s 1372 -c1 -W2 1.1.1.1 2>&1 | tail -2; echo',
489
+ 'echo "## J. ping -M do -s 1444 1.1.1.1 (full-MTU 1472 — expect FAIL on Hetzner private)"; ping -M do -s 1444 -c1 -W2 1.1.1.1 2>&1 | tail -2; echo',
490
+ ].join('; ');
491
+ probe(
492
+ report,
493
+ 'create pod',
494
+ 'kubectl',
495
+ [
496
+ '--kubeconfig',
497
+ kubeconfig,
498
+ '-n',
499
+ 'default',
500
+ 'run',
501
+ podName,
502
+ '--image=nicolaka/netshoot',
503
+ '--restart=Never',
504
+ '--command',
505
+ '--',
506
+ 'sleep',
507
+ '300',
508
+ ],
509
+ env,
510
+ );
511
+ probe(
512
+ report,
513
+ 'wait pod ready',
514
+ 'kubectl',
515
+ [
516
+ '--kubeconfig',
517
+ kubeconfig,
518
+ '-n',
519
+ 'default',
520
+ 'wait',
521
+ `pod/${podName}`,
522
+ '--for=condition=Ready',
523
+ '--timeout=180s',
524
+ ],
525
+ env,
526
+ );
527
+ probe(
528
+ report,
529
+ 'exec battery',
530
+ 'kubectl',
531
+ ['--kubeconfig', kubeconfig, '-n', 'default', 'exec', podName, '--', 'sh', '-c', battery],
532
+ env,
533
+ );
534
+ probe(
535
+ report,
536
+ 'cleanup pod',
537
+ 'kubectl',
538
+ [
539
+ '--kubeconfig',
540
+ kubeconfig,
541
+ '-n',
542
+ 'default',
543
+ 'delete',
544
+ 'pod',
545
+ podName,
546
+ '--wait=false',
547
+ '--grace-period=1',
548
+ ],
549
+ env,
550
+ );
551
+ }
552
+
553
+ function diagnoseFlux(report, env, kubeconfig) {
554
+ report.section('FLUX');
555
+ probe(
556
+ report,
557
+ 'flux resources',
558
+ 'kubectl',
559
+ [
560
+ '--kubeconfig',
561
+ kubeconfig,
562
+ '-n',
563
+ 'flux-system',
564
+ 'get',
565
+ 'gitrepository,kustomization,helmrelease',
566
+ ],
567
+ env,
568
+ );
569
+ probe(
570
+ report,
571
+ 'flux events',
572
+ 'kubectl',
573
+ ['--kubeconfig', kubeconfig, '-n', 'flux-system', 'get', 'events', '--sort-by=.lastTimestamp'],
574
+ env,
575
+ );
576
+ for (const controller of [
577
+ 'source-controller',
578
+ 'kustomize-controller',
579
+ 'helm-controller',
580
+ 'notification-controller',
581
+ ]) {
582
+ report.subsection(`${controller} logs`);
583
+ probe(
584
+ report,
585
+ controller,
586
+ 'kubectl',
587
+ [
588
+ '--kubeconfig',
589
+ kubeconfig,
590
+ '-n',
591
+ 'flux-system',
592
+ 'logs',
593
+ `deployment/${controller}`,
594
+ '--tail=40',
595
+ ],
596
+ env,
597
+ );
598
+ }
599
+ // Describe the GitRepository — this is where stale-URL / auth errors appear
600
+ probe(
601
+ report,
602
+ 'GitRepository describe',
603
+ 'kubectl',
604
+ ['--kubeconfig', kubeconfig, '-n', 'flux-system', 'describe', 'gitrepository'],
605
+ env,
606
+ );
607
+ }
608
+
609
+ function readControlplaneIp(envName) {
610
+ const projConfigPath = join(process.cwd(), '.vibecarbon.json');
611
+ if (!existsSync(projConfigPath)) return null;
612
+ try {
613
+ const data = JSON.parse(readFileSync(projConfigPath, 'utf-8'));
614
+ const envCfg = data?.environments?.[envName];
615
+ if (!envCfg) return null;
616
+ const masterServer = envCfg.servers?.find((s) => s.name === 'master' || s.name === 'primary');
617
+ return masterServer?.ip ?? null;
618
+ } catch {
619
+ return null;
620
+ }
621
+ }
622
+
623
+ export async function run(args) {
624
+ const { values, positional, errors } = parseFlags(args, SPEC);
625
+
626
+ if (errors.length > 0) {
627
+ for (const e of errors) {
628
+ process.stderr.write(`${c.error('✗')} ${e}\n`);
629
+ }
630
+ process.stderr.write(`Run ${c.info('vibecarbon diagnose -h')} for usage.\n`);
631
+ process.exit(1);
632
+ }
633
+
634
+ if (values.h) {
635
+ process.stdout.write(renderHelp(SPEC));
636
+ process.exit(0);
637
+ }
638
+
639
+ // Project guard runs first so an accidental `vibecarbon diagnose` from
640
+ // a parent directory emits the canonical message instead of stumbling
641
+ // through kubectl probes against a missing kubeconfig.
642
+ assertInProjectDir();
643
+
644
+ const envName = /** @type {string|undefined} */ (positional.env) ?? 'prod';
645
+ const requested = /** @type {string|undefined} */ (positional.section) ?? 'all';
646
+ if (!ALL_VALID_SECTIONS.includes(requested) && requested !== 'all') {
647
+ console.error(
648
+ c.error(`Unknown section "${requested}". Valid: ${ALL_VALID_SECTIONS.join(', ')}, all.`),
649
+ );
650
+ process.exit(1);
651
+ }
652
+ const sectionsToRun = requested === 'all' ? SECTIONS : [requested];
653
+
654
+ const cwd = process.cwd();
655
+ const kubeconfig = join(cwd, '.vibecarbon', `kubeconfig-${envName}`);
656
+ const sshKey = join(cwd, '.vibecarbon', `ssh-${envName}`);
657
+ const controlplaneIp = readControlplaneIp(envName);
658
+
659
+ const hcloudToken = loadHcloudToken();
660
+ const env = { ...process.env };
661
+ if (hcloudToken) env.HCLOUD_TOKEN = hcloudToken;
662
+
663
+ // Auto-detect operator IP and ensure firewall lets us in. Diagnose runs
664
+ // many kubectl + ssh probes; if the firewall blocks our IP every probe
665
+ // hangs to its timeout. Refresh lastUsedAt and apply on first miss.
666
+ const projectConfig = loadProjectConfig();
667
+ if (projectConfig && hcloudToken) {
668
+ try {
669
+ const isHA = !!projectConfig.environments?.[envName]?.ha?.enabled;
670
+ const result = await ensureOperatorIpAccess({
671
+ projectConfig,
672
+ environment: envName,
673
+ isHA,
674
+ apiToken: hcloudToken,
675
+ yes: false,
676
+ onMessage: (msg) => console.error(`${c.info('i')} ${msg}`),
677
+ });
678
+ if (result.added) {
679
+ console.error(`${c.success('✓')} Firewall updated: SSH/k8s API now allow ${result.cidr}`);
680
+ }
681
+ } catch (err) {
682
+ console.error(`${c.warning('!')} Operator-IP check failed: ${err.message}`);
683
+ console.error(
684
+ ' Continuing — kubectl/SSH probes may time out if your IP is not allowlisted.',
685
+ );
686
+ }
687
+ }
688
+
689
+ const report = new Report();
690
+
691
+ // Header so the saved report is self-describing
692
+ const ts = new Date().toISOString();
693
+ const header = [
694
+ '# vibecarbon diagnose',
695
+ `# env: ${envName}`,
696
+ `# sections: ${sectionsToRun.join(', ')}`,
697
+ `# timestamp: ${ts}`,
698
+ `# cwd: ${cwd}`,
699
+ `# kubeconfig: ${kubeconfig} ${existsSync(kubeconfig) ? '(found)' : '(MISSING)'}`,
700
+ `# sshKey: ${sshKey} ${existsSync(sshKey) ? '(found)' : '(MISSING)'}`,
701
+ `# masterIP: ${controlplaneIp ?? '(unknown)'}`,
702
+ `# hcloud: ${hcloudToken ? 'authenticated' : 'NOT AUTHENTICATED'}`,
703
+ ].join('\n');
704
+ console.log(header);
705
+ report.lines.push(header);
706
+
707
+ const includesHcloud = sectionsToRun.includes('hcloud');
708
+ const needsKubectl = sectionsToRun.some((s) =>
709
+ ['nodes', 'pods', 'network', 'flux', 'egress'].includes(s),
710
+ );
711
+
712
+ if (includesHcloud) diagnoseHcloud(report, env);
713
+ if (needsKubectl) {
714
+ if (!existsSync(kubeconfig)) {
715
+ report.warn(
716
+ `Skipping kubectl-based sections: kubeconfig not found at ${kubeconfig}. Run a deploy first.`,
717
+ );
718
+ } else {
719
+ if (sectionsToRun.includes('nodes'))
720
+ diagnoseNodes(report, env, kubeconfig, sshKey, controlplaneIp);
721
+ if (sectionsToRun.includes('pods')) diagnosePods(report, env, kubeconfig);
722
+ if (sectionsToRun.includes('network')) diagnoseNetwork(report, env, kubeconfig);
723
+ if (sectionsToRun.includes('egress'))
724
+ diagnoseEgress(report, env, kubeconfig, sshKey, controlplaneIp);
725
+ if (sectionsToRun.includes('flux')) diagnoseFlux(report, env, kubeconfig);
726
+ }
727
+ }
728
+
729
+ // Persist
730
+ const diagDir = join(homedir(), '.vibecarbon');
731
+ if (!existsSync(diagDir)) mkdirSync(diagDir, { recursive: true });
732
+ const outFile = join(diagDir, `diag-${envName}-${ts.replace(/[:.]/g, '-')}.txt`);
733
+ writeFileSync(outFile, report.toString());
734
+ console.log(`\n${c.success('✓')} Report saved: ${outFile}`);
735
+ }