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
package/src/add.js ADDED
@@ -0,0 +1,718 @@
1
+ /**
2
+ * Vibecarbon Add Command
3
+ *
4
+ * Adds optional services to an existing Vibecarbon project. Services
5
+ * are fetched from GitHub at runtime so users always get the latest
6
+ * templates regardless of their CLI version. -offline falls back to
7
+ * bundled templates for air-gapped environments.
8
+ *
9
+ * Interactive-by-default: bare `vibecarbon add` prompts for a feature
10
+ * to add. Positional features (`vibecarbon add observability redis`)
11
+ * skip the prompt and queue multiple installs.
12
+ *
13
+ * Form rule: vibecarbon uses single-dash flags only — see
14
+ * memory:feedback_cli_single_dash_flags. -l is gone (the bare-form
15
+ * help body already lists features).
16
+ */
17
+
18
+ import {
19
+ chmodSync,
20
+ existsSync,
21
+ mkdirSync,
22
+ readdirSync,
23
+ readFileSync,
24
+ writeFileSync,
25
+ } from 'node:fs';
26
+ import { dirname, join } from 'node:path';
27
+ import { fileURLToPath } from 'node:url';
28
+ import * as p from '@clack/prompts';
29
+
30
+ import { renderHelp } from './lib/cli/help.js';
31
+ import { parseFlags } from './lib/cli/parse-flags.js';
32
+ import { requireTTYOrFlags } from './lib/cli/tty-guard.js';
33
+ import { c, printBanner } from './lib/colors.js';
34
+ import { isHAConfigured, registerProject } from './lib/config.js';
35
+ import { fetchWithRetry } from './lib/fetch-retry.js';
36
+ import {
37
+ appendToEnv,
38
+ loadEnvVariables,
39
+ loadManifest,
40
+ saveManifest,
41
+ setEnvVar,
42
+ } from './lib/project.js';
43
+ import { assertInProjectDir } from './lib/project-guard.js';
44
+ import { generatePassword } from './lib/secrets.js';
45
+ import { VERSION } from './lib/version.js';
46
+
47
+ const __filename = fileURLToPath(import.meta.url);
48
+ const __dirname = dirname(__filename);
49
+
50
+ // GitHub repository for fetching service bundles
51
+ const GITHUB_REPO = 'hyperformant/vibecarbon';
52
+ const GITHUB_BRANCH = 'main';
53
+ const GITHUB_RAW_BASE = `https://raw.githubusercontent.com/${GITHUB_REPO}/${GITHUB_BRANCH}`;
54
+ const SERVICES_PATH = 'services';
55
+
56
+ // Local services directory (for --offline mode and development)
57
+ const LOCAL_SERVICES_DIR = join(__dirname, '..', SERVICES_PATH);
58
+
59
+ // Template variable placeholders
60
+ const PLACEHOLDERS = {
61
+ PROJECT_NAME: '{{PROJECT_NAME}}',
62
+ DB_PASSWORD: '{{DB_PASSWORD}}',
63
+ ANON_KEY: '{{ANON_KEY}}',
64
+ SERVICE_ROLE_KEY: '{{SERVICE_ROLE_KEY}}',
65
+ N8N_PASSWORD: '{{N8N_PASSWORD}}',
66
+ DOMAIN: '{{DOMAIN}}',
67
+ };
68
+
69
+ // Map service names to their env flags for dashboard visibility
70
+ // client: VITE_* flags for frontend (sidebar links, UI)
71
+ // server: flags for backend (services status monitoring)
72
+ const SERVICE_ENV_FLAGS = {
73
+ n8n: { client: 'VITE_N8N_ENABLED', server: 'N8N_ENABLED' },
74
+ metabase: { client: 'VITE_METABASE_ENABLED', server: 'METABASE_ENABLED' },
75
+ observability: { client: 'VITE_OBSERVABILITY_ENABLED', server: 'OBSERVABILITY_ENABLED' },
76
+ redis: { client: 'VITE_REDIS_ENABLED', server: 'REDIS_ENABLED' },
77
+ };
78
+
79
+ // ============================================================================
80
+ // COMMAND SPEC — single source of truth for argv parsing AND help output.
81
+ // ============================================================================
82
+
83
+ /**
84
+ * The features list is referenced by both the SPEC (so help body
85
+ * shows what's available) and `addService` (the dispatcher). Defined
86
+ * later in the file as `LOCAL_FEATURES`; lazy access via a getter so
87
+ * this module can resolve hoisting cleanly.
88
+ */
89
+ function buildSpec() {
90
+ return /** @type {import('./lib/cli/parse-flags.js').CommandSpec & { summary?: string, description?: string, examples?: Array<{ command: string, description?: string }> }} */ ({
91
+ name: 'add',
92
+ summary: 'Add a feature to a Vibecarbon project',
93
+ description: [
94
+ 'Available features:',
95
+ ...LOCAL_FEATURES.map((f) => ` ${f.name.padEnd(16)} ${f.description}`),
96
+ '',
97
+ 'External services (CI/CD, Stripe, OAuth, etc.) live in `vibecarbon configure`.',
98
+ ].join('\n'),
99
+ positional: [
100
+ {
101
+ name: 'features',
102
+ variadic: true,
103
+ optional: true,
104
+ description: 'One or more features to add (skips the feature prompt)',
105
+ },
106
+ ],
107
+ flags: [
108
+ { name: 'h', boolean: true, description: 'Show this help' },
109
+ { name: 'v', boolean: true, description: 'Show version' },
110
+ { name: 'y', boolean: true, description: 'Skip confirmation prompts' },
111
+ {
112
+ name: 'offline',
113
+ boolean: true,
114
+ description: 'Use bundled templates (for air-gapped environments)',
115
+ },
116
+ ],
117
+ examples: [
118
+ { command: 'vibecarbon add', description: 'prompts for a feature to add' },
119
+ { command: 'vibecarbon add observability', description: 'add a specific feature' },
120
+ { command: 'vibecarbon add observability redis', description: 'add multiple features' },
121
+ ],
122
+ });
123
+ }
124
+
125
+ // ============================================================================
126
+ // GITHUB FETCHING
127
+ // ============================================================================
128
+
129
+ async function fetchFromGitHub(path) {
130
+ const url = `${GITHUB_RAW_BASE}/${path}`;
131
+ try {
132
+ const response = await fetchWithRetry(url);
133
+ if (!response.ok) {
134
+ throw new Error(`HTTP ${response.status}`);
135
+ }
136
+ return await response.text();
137
+ } catch (error) {
138
+ throw new Error(`Failed to fetch ${url}: ${error.message}`);
139
+ }
140
+ }
141
+
142
+ async function fetchServiceManifest(serviceName, offline = false) {
143
+ if (offline) {
144
+ const manifestPath = join(LOCAL_SERVICES_DIR, serviceName, 'manifest.json');
145
+ if (!existsSync(manifestPath)) {
146
+ throw new Error(`Service "${serviceName}" not found in bundled services`);
147
+ }
148
+ return JSON.parse(readFileSync(manifestPath, 'utf-8'));
149
+ }
150
+
151
+ try {
152
+ const content = await fetchFromGitHub(`${SERVICES_PATH}/${serviceName}/manifest.json`);
153
+ return JSON.parse(content);
154
+ } catch (error) {
155
+ throw new Error(`Service "${serviceName}" not found: ${error.message}`);
156
+ }
157
+ }
158
+
159
+ async function fetchServiceFile(serviceName, filePath, offline = false) {
160
+ if (offline) {
161
+ const localPath = join(LOCAL_SERVICES_DIR, serviceName, filePath);
162
+ if (!existsSync(localPath)) {
163
+ throw new Error(`File not found: ${localPath}`);
164
+ }
165
+ return readFileSync(localPath, 'utf-8');
166
+ }
167
+
168
+ return await fetchFromGitHub(`${SERVICES_PATH}/${serviceName}/${filePath}`);
169
+ }
170
+
171
+ // ============================================================================
172
+ // FILE OPERATIONS
173
+ // ============================================================================
174
+
175
+ function applyVariables(content, variables) {
176
+ let result = content;
177
+ // Replace predefined placeholders
178
+ for (const [key, placeholder] of Object.entries(PLACEHOLDERS)) {
179
+ if (variables[key] !== undefined) {
180
+ result = result.replaceAll(placeholder, variables[key]);
181
+ }
182
+ }
183
+ // Also replace any {{VARIABLE_NAME}} patterns from dynamic variables
184
+ // This handles service-specific vars like METABASE_PASSWORD, REDIS_PASSWORD, etc.
185
+ for (const [key, value] of Object.entries(variables)) {
186
+ if (value !== undefined) {
187
+ result = result.replaceAll(`{{${key}}}`, value);
188
+ }
189
+ }
190
+ return result;
191
+ }
192
+
193
+ async function installServiceFile(serviceName, srcPath, destPath, variables, offline) {
194
+ const content = await fetchServiceFile(serviceName, srcPath, offline);
195
+ const processedContent = applyVariables(content, variables);
196
+
197
+ const fullDestPath = join(process.cwd(), destPath);
198
+ mkdirSync(dirname(fullDestPath), { recursive: true });
199
+ writeFileSync(fullDestPath, processedContent);
200
+
201
+ // Make shell scripts executable
202
+ if (destPath.endsWith('.sh')) {
203
+ chmodSync(fullDestPath, 0o755);
204
+ }
205
+ }
206
+
207
+ async function installServiceDirectory(serviceName, srcDir, destDir, variables, offline) {
208
+ // For directories, we need to list files
209
+ // In offline mode, we can read the directory
210
+ // In online mode, we'd need the manifest to list files
211
+
212
+ if (offline) {
213
+ const localDir = join(LOCAL_SERVICES_DIR, serviceName, srcDir);
214
+ if (!existsSync(localDir)) {
215
+ return;
216
+ }
217
+
218
+ const entries = readdirSync(localDir, { withFileTypes: true });
219
+ for (const entry of entries) {
220
+ const srcPath = join(srcDir, entry.name);
221
+ const destPath = join(destDir, entry.name);
222
+
223
+ if (entry.isDirectory()) {
224
+ await installServiceDirectory(serviceName, srcPath, destPath, variables, offline);
225
+ } else {
226
+ await installServiceFile(serviceName, srcPath, destPath, variables, offline);
227
+ }
228
+ }
229
+ } else {
230
+ // For online mode, the manifest should list all files
231
+ // We'll handle this in the main install function
232
+ throw new Error('Directory installation requires file listing in manifest');
233
+ }
234
+ }
235
+
236
+ // ============================================================================
237
+ // KUBERNETES CONFIGURATION
238
+ // ============================================================================
239
+
240
+ function updateBaseKustomization(kustomizationEntry) {
241
+ if (!kustomizationEntry) {
242
+ return;
243
+ }
244
+
245
+ const kustomizationPath = join(process.cwd(), 'k8s', 'base', 'kustomization.yaml');
246
+
247
+ if (!existsSync(kustomizationPath)) {
248
+ return;
249
+ }
250
+
251
+ let content = readFileSync(kustomizationPath, 'utf-8');
252
+ const entry = ` - ${kustomizationEntry}`;
253
+
254
+ if (content.includes(entry)) {
255
+ return;
256
+ }
257
+
258
+ // Insert before labels/commonLabels section so entry stays in resources block
259
+ const labelsIndex = content.indexOf('labels:');
260
+ const commonLabelsIndex = content.indexOf('commonLabels:');
261
+ const insertBefore = labelsIndex !== -1 ? labelsIndex : commonLabelsIndex;
262
+
263
+ if (insertBefore !== -1) {
264
+ content = `${content.slice(0, insertBefore).trimEnd()}\n${entry}\n\n${content.slice(insertBefore)}`;
265
+ } else {
266
+ content = `${content.trimEnd()}\n${entry}\n`;
267
+ }
268
+
269
+ writeFileSync(kustomizationPath, content);
270
+ }
271
+
272
+ function removeFromBaseKustomization(kustomizationEntry) {
273
+ if (!kustomizationEntry) {
274
+ return;
275
+ }
276
+
277
+ const kustomizationPath = join(process.cwd(), 'k8s', 'base', 'kustomization.yaml');
278
+
279
+ if (!existsSync(kustomizationPath)) {
280
+ return;
281
+ }
282
+
283
+ let content = readFileSync(kustomizationPath, 'utf-8');
284
+ const entry = ` - ${kustomizationEntry}`;
285
+
286
+ content = content.replace(`${entry}\n\n`, '');
287
+ content = content.replace(`${entry}\n`, '');
288
+ content = content.replace(entry, '');
289
+
290
+ writeFileSync(kustomizationPath, content);
291
+ }
292
+
293
+ // ============================================================================
294
+ // SPECIALIZED FEATURE HANDLERS
295
+ // ============================================================================
296
+
297
+ // Features with specialized handlers (not manifest-based). CI/CD used to be
298
+ // here too; it moved to `vibecarbon configure` (CI/CD is an external service,
299
+ // not a local container, so it lives with the other configure features).
300
+ const SPECIALIZED_FEATURES = ['observability'];
301
+
302
+ async function handleSpecializedFeature(featureName, options) {
303
+ const { project } = options;
304
+
305
+ switch (featureName) {
306
+ case 'observability': {
307
+ return handleObservabilityFeature(project);
308
+ }
309
+
310
+ default:
311
+ throw new Error(`Unknown specialized feature: ${featureName}`);
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Enable observability stack (Prometheus, Grafana, Loki)
317
+ * Files are already included in the project - this just enables them
318
+ */
319
+ async function handleObservabilityFeature(_project) {
320
+ const manifest = loadManifest();
321
+
322
+ // Check if already enabled
323
+ if (manifest.services?.observability) {
324
+ p.log.info('Observability is already enabled.');
325
+ return true;
326
+ }
327
+
328
+ // Check that the observability compose file exists
329
+ const observabilityCompose = join(process.cwd(), 'docker-compose.observability.yml');
330
+ if (!existsSync(observabilityCompose)) {
331
+ p.log.error('Observability files not found. Please ensure your project is up to date.');
332
+ return false;
333
+ }
334
+
335
+ const s = p.spinner();
336
+ s.start('Enabling observability stack');
337
+
338
+ // Set env flags for dashboard visibility (client + server)
339
+ setEnvVar('VITE_OBSERVABILITY_ENABLED', 'true');
340
+ setEnvVar('OBSERVABILITY_ENABLED', 'true');
341
+
342
+ // Update manifest
343
+ if (!manifest.services) manifest.services = {};
344
+ manifest.services.observability = { addedAt: new Date().toISOString() };
345
+ saveManifest(manifest);
346
+
347
+ s.stop('Observability enabled');
348
+
349
+ p.note(
350
+ [
351
+ 'Observability stack enabled:',
352
+ ` ${c.info('Prometheus')} http://prometheus.localhost`,
353
+ ` ${c.info('Grafana')} http://grafana.localhost`,
354
+ ` ${c.info('Loki')} http://loki.localhost`,
355
+ ].join('\n'),
356
+ 'Observability',
357
+ );
358
+
359
+ return true;
360
+ }
361
+
362
+ // ============================================================================
363
+ // ADD SERVICE
364
+ // ============================================================================
365
+
366
+ async function addService(serviceName, options) {
367
+ const { project } = options;
368
+ let { offline } = options;
369
+
370
+ // Check if this is a specialized feature with a custom handler
371
+ if (SPECIALIZED_FEATURES.includes(serviceName)) {
372
+ return handleSpecializedFeature(serviceName, options);
373
+ }
374
+
375
+ let manifest;
376
+ try {
377
+ manifest = await fetchServiceManifest(serviceName, offline);
378
+ } catch (error) {
379
+ // If online fetch failed, automatically try offline mode
380
+ if (!offline) {
381
+ try {
382
+ manifest = await fetchServiceManifest(serviceName, true);
383
+ offline = true; // Switch to offline for file fetching
384
+ } catch {
385
+ const suggestion = findClosestFeature(serviceName);
386
+ if (suggestion) {
387
+ p.log.error(`Service "${serviceName}" not found. Did you mean "${suggestion}"?`);
388
+ } else {
389
+ p.log.error(`Service "${serviceName}" not found.`);
390
+ }
391
+ return false;
392
+ }
393
+ } else {
394
+ const suggestion = findClosestFeature(serviceName);
395
+ if (suggestion) {
396
+ p.log.error(`${error.message} Did you mean "${suggestion}"?`);
397
+ } else {
398
+ p.log.error(error.message);
399
+ }
400
+ return false;
401
+ }
402
+ }
403
+
404
+ const projectManifest = loadManifest();
405
+
406
+ // Check if already added
407
+ if (projectManifest.services?.[serviceName]) {
408
+ p.log.warn(`Service "${serviceName}" is already added to this project.`);
409
+ return false;
410
+ }
411
+
412
+ // n8n requires Redis for queue mode — add it automatically if not already present
413
+ if (serviceName === 'n8n' && !projectManifest.services?.redis) {
414
+ p.log.info('n8n requires Redis for queue mode — adding Redis automatically');
415
+ const redisAdded = await addService('redis', options);
416
+ if (!redisAdded) {
417
+ p.log.error('Failed to add Redis (required by n8n)');
418
+ return false;
419
+ }
420
+ }
421
+
422
+ // Load existing env vars
423
+ const envVars = loadEnvVariables();
424
+
425
+ // Generate variables for template substitution
426
+ const variables = {
427
+ PROJECT_NAME: project.projectName,
428
+ DB_PASSWORD: envVars.POSTGRES_PASSWORD || envVars.DB_PASSWORD || '',
429
+ ANON_KEY: envVars.SUPABASE_ANON_KEY || '',
430
+ SERVICE_ROLE_KEY: envVars.SUPABASE_SERVICE_ROLE_KEY || '',
431
+ DOMAIN: envVars.DOMAIN || 'localhost',
432
+ };
433
+
434
+ // Generate service-specific env vars
435
+ const serviceEnvVars = {};
436
+ for (const [key, config] of Object.entries(manifest.envVars || {})) {
437
+ if (config.generate === 'password') {
438
+ serviceEnvVars[key] = generatePassword(config.length || 24);
439
+ variables[key] = serviceEnvVars[key];
440
+ } else if (config.from && envVars[config.from]) {
441
+ // Inherit from existing env var (e.g., ADMIN_EMAIL)
442
+ serviceEnvVars[key] = envVars[config.from];
443
+ variables[key] = envVars[config.from];
444
+ } else if (config.value !== undefined) {
445
+ serviceEnvVars[key] = config.value;
446
+ variables[key] = config.value;
447
+ } else if (config.default !== undefined) {
448
+ serviceEnvVars[key] = config.default;
449
+ variables[key] = config.default;
450
+ }
451
+ }
452
+
453
+ const s = p.spinner();
454
+ s.start(`Adding ${manifest.name}${offline ? ' (offline)' : ''}...`);
455
+
456
+ try {
457
+ // Install files based on manifest mappings
458
+ for (const [src, dest] of Object.entries(manifest.files || {})) {
459
+ if (src.endsWith('/')) {
460
+ // Directory mapping
461
+ await installServiceDirectory(serviceName, src, dest, variables, offline);
462
+ } else {
463
+ // File mapping
464
+ await installServiceFile(serviceName, src, dest, variables, offline);
465
+ }
466
+ }
467
+
468
+ // Update kustomization.yaml if needed
469
+ if (manifest.kustomization?.entry) {
470
+ updateBaseKustomization(manifest.kustomization.entry);
471
+ }
472
+
473
+ // Update .env.local with service env vars
474
+ if (Object.keys(serviceEnvVars).length > 0) {
475
+ appendToEnv(serviceName, serviceEnvVars);
476
+ }
477
+
478
+ // Patch k8s secrets manifest with service-specific env vars
479
+ const k8sSecretsPath = join(process.cwd(), 'k8s', 'base', 'secrets', 'secrets.yaml');
480
+ if (existsSync(k8sSecretsPath) && Object.keys(serviceEnvVars).length > 0) {
481
+ let secretsContent = readFileSync(k8sSecretsPath, 'utf-8');
482
+ for (const [key, value] of Object.entries(serviceEnvVars)) {
483
+ if (!secretsContent.includes(` ${key}:`)) {
484
+ // Append under stringData section
485
+ secretsContent = `${secretsContent.trimEnd()}\n ${key}: "${value}"\n`;
486
+ }
487
+ }
488
+ writeFileSync(k8sSecretsPath, secretsContent);
489
+ }
490
+
491
+ // Set env flags for dashboard visibility (client + server)
492
+ const envFlags = SERVICE_ENV_FLAGS[serviceName];
493
+ if (envFlags) {
494
+ if (envFlags.client) setEnvVar(envFlags.client, 'true');
495
+ if (envFlags.server) setEnvVar(envFlags.server, 'true');
496
+ }
497
+
498
+ // Update project manifest
499
+ if (!projectManifest.services) {
500
+ projectManifest.services = {};
501
+ }
502
+ projectManifest.services[serviceName] = {
503
+ addedAt: new Date().toISOString(),
504
+ version: manifest.version,
505
+ };
506
+ saveManifest(projectManifest);
507
+
508
+ s.stop(`${manifest.name} added successfully`);
509
+
510
+ // Show credentials if defined
511
+ if (manifest.credentials) {
512
+ const credentialLines = manifest.credentials.fields.map((field) => {
513
+ const value = field.envVar ? serviceEnvVars[field.envVar] : field.value;
514
+ return `${field.label}: ${value}`;
515
+ });
516
+ p.note(credentialLines.join('\n'), manifest.credentials.title);
517
+ }
518
+
519
+ return true;
520
+ } catch (error) {
521
+ s.stop(`Failed to add ${manifest.name}`);
522
+ p.log.error(error.message);
523
+ return false;
524
+ }
525
+ }
526
+
527
+ // ============================================================================
528
+ // LIST SERVICES
529
+ // ============================================================================
530
+
531
+ // Features that work locally without external dependencies
532
+ const LOCAL_FEATURES = [
533
+ { name: 'observability', description: 'Prometheus, Grafana, Loki monitoring stack' },
534
+ { name: 'n8n', description: 'Workflow automation platform' },
535
+ { name: 'metabase', description: 'Business intelligence and analytics' },
536
+ { name: 'redis', description: 'In-memory cache and session store' },
537
+ ];
538
+
539
+ // External services were previously listed here (CI/CD via GitHub Actions);
540
+ // they all moved to `vibecarbon configure` since they wire up off-stack
541
+ // providers rather than installing local containers.
542
+
543
+ const ALL_FEATURES = [...LOCAL_FEATURES];
544
+
545
+ /**
546
+ * Calculate Levenshtein distance between two strings
547
+ */
548
+ function levenshteinDistance(a, b) {
549
+ const matrix = Array.from({ length: a.length + 1 }, (_, i) =>
550
+ Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
551
+ );
552
+
553
+ for (let i = 1; i <= a.length; i++) {
554
+ for (let j = 1; j <= b.length; j++) {
555
+ matrix[i][j] =
556
+ a[i - 1] === b[j - 1]
557
+ ? matrix[i - 1][j - 1]
558
+ : Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + 1);
559
+ }
560
+ }
561
+ return matrix[a.length][b.length];
562
+ }
563
+
564
+ /**
565
+ * Find the closest matching feature name for typo correction
566
+ */
567
+ function findClosestFeature(input) {
568
+ const featureNames = ALL_FEATURES.map((f) => f.name);
569
+ let closest = null;
570
+ let minDistance = Infinity;
571
+
572
+ for (const name of featureNames) {
573
+ const distance = levenshteinDistance(input.toLowerCase(), name.toLowerCase());
574
+ // Only suggest if the distance is reasonable (less than half the word length)
575
+ if (distance < minDistance && distance <= Math.ceil(name.length / 2)) {
576
+ minDistance = distance;
577
+ closest = name;
578
+ }
579
+ }
580
+ return closest;
581
+ }
582
+
583
+ // ============================================================================
584
+ // MAIN
585
+ // ============================================================================
586
+
587
+ async function main(cliArgs) {
588
+ const SPEC = buildSpec();
589
+ const { values, positional, errors } = parseFlags(cliArgs, SPEC);
590
+
591
+ if (errors.length > 0) {
592
+ for (const e of errors) {
593
+ process.stderr.write(`${c.error('✗')} ${e}\n`);
594
+ }
595
+ process.stderr.write(`Run ${c.info('vibecarbon add -h')} for usage.\n`);
596
+ process.exit(1);
597
+ }
598
+
599
+ if (values.h) {
600
+ process.stdout.write(renderHelp(SPEC));
601
+ process.exit(0);
602
+ }
603
+ if (values.v) {
604
+ console.log(`vibecarbon add v${VERSION}`);
605
+ process.exit(0);
606
+ }
607
+
608
+ // Detect project FIRST, before the secret scan. Otherwise an accidental
609
+ // invocation from a parent directory (e.g. ~/repos) would walk every
610
+ // sibling repo and dump real tokens to the operator's scrollback before
611
+ // the friendly "not a project" message ever appears.
612
+ const projectConfig = assertInProjectDir();
613
+
614
+ // Refuse to add if the working tree contains likely secrets — `add`
615
+ // mutates files (compose includes, k8s manifests) that get pushed
616
+ // alongside whatever else is in the tree. A leak through `add`
617
+ // looks the same as a leak through `deploy`.
618
+ await refuseIfSecretsPresent();
619
+
620
+ const project = {
621
+ projectName: projectConfig.projectName,
622
+ haConfigured: isHAConfigured(),
623
+ provider: projectConfig.provider || 'hetzner',
624
+ services: projectConfig.services || {},
625
+ };
626
+
627
+ // Resolve which services to add. Positional list wins; otherwise
628
+ // prompt on TTY, fail off-TTY with the canonical message naming the
629
+ // missing positional.
630
+ /** @type {string[]} */
631
+ const seedServices = /** @type {string[]|undefined} */ (positional.features || []).map((s) =>
632
+ s.toLowerCase(),
633
+ );
634
+
635
+ if (seedServices.length === 0) {
636
+ requireTTYOrFlags({
637
+ requirements: [
638
+ {
639
+ flag: 'features',
640
+ description: 'name a feature to add (e.g. observability, redis, n8n)',
641
+ satisfied: false,
642
+ },
643
+ ],
644
+ });
645
+ }
646
+
647
+ printBanner();
648
+ p.intro(`${c.bold('vibecarbon add')} ${c.dim(`v${VERSION}`)}`);
649
+
650
+ /** @type {string[]} */
651
+ let services = seedServices;
652
+ if (services.length === 0) {
653
+ const choice = await p.select({
654
+ message: 'Which feature do you want to add?',
655
+ options: LOCAL_FEATURES.map((f) => ({
656
+ value: f.name,
657
+ label: f.name,
658
+ hint: f.description,
659
+ })),
660
+ });
661
+ if (p.isCancel(choice)) {
662
+ p.cancel('Operation cancelled.');
663
+ process.exit(0);
664
+ }
665
+ services = [/** @type {string} */ (choice)];
666
+ }
667
+
668
+ let success = true;
669
+ for (const serviceName of services) {
670
+ const result = await addService(serviceName, {
671
+ offline: !!values.offline,
672
+ project,
673
+ cliArgs: cliArgs, // Pass raw CLI args for infrastructure services
674
+ });
675
+ if (!result) {
676
+ success = false;
677
+ }
678
+ }
679
+
680
+ if (success) {
681
+ registerProject(project.projectName, process.cwd());
682
+ p.note(
683
+ ['# Restart services to apply changes:', 'vibecarbon down && vibecarbon up'].join('\n'),
684
+ 'Next steps',
685
+ );
686
+ p.outro('Services added successfully!');
687
+ } else {
688
+ p.outro('Some services could not be added.');
689
+ process.exit(1);
690
+ }
691
+ }
692
+
693
+ export async function run(args) {
694
+ await main(args);
695
+ }
696
+
697
+ async function refuseIfSecretsPresent() {
698
+ const { scanTree, formatFindings } = await import('./lib/secret-scan.js');
699
+ const findings = scanTree(process.cwd());
700
+ if (findings.length === 0) return;
701
+ process.stderr.write(`\n${c.error('✗')} Refusing to add: secrets detected.\n\n`);
702
+ process.stderr.write(formatFindings(findings));
703
+ process.exit(1);
704
+ }
705
+
706
+ // ============================================================================
707
+ // EXPORTS FOR TESTING
708
+ // ============================================================================
709
+
710
+ export {
711
+ addService,
712
+ buildSpec,
713
+ fetchServiceManifest,
714
+ loadManifest,
715
+ removeFromBaseKustomization,
716
+ saveManifest,
717
+ updateBaseKustomization,
718
+ };