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/upgrade.js ADDED
@@ -0,0 +1,721 @@
1
+ /**
2
+ * Vibecarbon Upgrade Command
3
+ * Upgrades infrastructure files in an existing project to match the latest template
4
+ *
5
+ * Usage:
6
+ * vibecarbon upgrade # Interactive upgrade
7
+ * vibecarbon upgrade --dry-run # Preview changes without applying
8
+ * vibecarbon upgrade --force # Replace all files (creates .upgrade-backup copies)
9
+ * vibecarbon upgrade -y # Auto-accept safe replacements, skip merge files
10
+ * vibecarbon upgrade --help # Show help
11
+ */
12
+
13
+ import {
14
+ chmodSync,
15
+ copyFileSync,
16
+ existsSync,
17
+ mkdirSync,
18
+ readFileSync,
19
+ writeFileSync,
20
+ } from 'node:fs';
21
+ import { dirname, join } from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+ import * as p from '@clack/prompts';
24
+ import { hashContent, hashFile } from './lib/checksum.js';
25
+ import { renderHelp } from './lib/cli/help.js';
26
+ import { parseFlags } from './lib/cli/parse-flags.js';
27
+ import { c, printBanner } from './lib/colors.js';
28
+ import { runCommandAsync } from './lib/command.js';
29
+ import { requireLicense } from './lib/licensing/index.js';
30
+ import { mergePackageJson } from './lib/merge-package-json.js';
31
+ import {
32
+ adaptDockerfileForPackageManager,
33
+ getPackageManagerVersion,
34
+ } from './lib/package-manager.js';
35
+ import {
36
+ detectPackageManager,
37
+ loadEnvVariables,
38
+ loadManifest,
39
+ saveManifest,
40
+ } from './lib/project.js';
41
+ import { assertInProjectDir } from './lib/project-guard.js';
42
+ import { getFilePolicy, getUpgradeableFiles } from './lib/upgrade-policy.js';
43
+ import { VERSION } from './lib/version.js';
44
+
45
+ const __filename = fileURLToPath(import.meta.url);
46
+ const __dirname = dirname(__filename);
47
+ const TEMPLATE_DIR = join(__dirname, '..', 'carbon');
48
+
49
+ // Template variable placeholders (same as create.js)
50
+ const PLACEHOLDERS = {
51
+ PROJECT_NAME: '{{PROJECT_NAME}}',
52
+ GITHUB_OWNER: '{{GITHUB_OWNER}}',
53
+ DB_PASSWORD: '{{DB_PASSWORD}}',
54
+ JWT_SECRET: '{{JWT_SECRET}}',
55
+ ANON_KEY: '{{ANON_KEY}}',
56
+ SERVICE_ROLE_KEY: '{{SERVICE_ROLE_KEY}}',
57
+ REALTIME_SECRET: '{{REALTIME_SECRET}}',
58
+ LOGFLARE_API_KEY: '{{LOGFLARE_API_KEY}}',
59
+ VAULT_ENC_KEY: '{{VAULT_ENC_KEY}}',
60
+ PG_META_CRYPTO_KEY: '{{PG_META_CRYPTO_KEY}}',
61
+ DB_ENC_KEY: '{{DB_ENC_KEY}}',
62
+ SITE_URL: '{{SITE_URL}}',
63
+ ADMIN_EMAIL: '{{ADMIN_EMAIL}}',
64
+ ADMIN_PASSWORD: '{{ADMIN_PASSWORD}}',
65
+ ADMIN_PASSWORD_HASH: '{{ADMIN_PASSWORD_HASH}}',
66
+ };
67
+
68
+ // Mirrors SECRET_PLACEHOLDERS in src/create.js — placeholders that must never
69
+ // be substituted into checked-in files (deploy code patches them at runtime
70
+ // from .env.local). Keep in sync with create.js.
71
+ const SECRET_PLACEHOLDERS = new Set([
72
+ 'DB_PASSWORD',
73
+ 'JWT_SECRET',
74
+ 'ANON_KEY',
75
+ 'SERVICE_ROLE_KEY',
76
+ 'REALTIME_SECRET',
77
+ 'LOGFLARE_API_KEY',
78
+ 'VAULT_ENC_KEY',
79
+ 'PG_META_CRYPTO_KEY',
80
+ 'DB_ENC_KEY',
81
+ 'ADMIN_PASSWORD',
82
+ ]);
83
+
84
+ // Binary file extensions — copy raw, skip placeholder replacement
85
+ const BINARY_EXTENSIONS = new Set([
86
+ '.png',
87
+ '.jpg',
88
+ '.jpeg',
89
+ '.gif',
90
+ '.ico',
91
+ '.svg',
92
+ '.woff',
93
+ '.woff2',
94
+ '.ttf',
95
+ '.eot',
96
+ '.otf',
97
+ '.zip',
98
+ '.tar',
99
+ '.gz',
100
+ ]);
101
+
102
+ // Files that contain {{PLACEHOLDER}} strings as their own substitution keys
103
+ // (not vibecarbon template variables). Must be copied raw without replacement.
104
+ const RAW_COPY_FILES = new Set(['scripts/generate-dev-configs.sh']);
105
+
106
+ // Optional-feature file prefixes — only upgrade these if they already exist in the project.
107
+ // These are added by `vibecarbon add` or `vibecarbon deploy`, not during `create`.
108
+ const OPTIONAL_FILE_PREFIXES = ['docker-compose.n8n.', 'docker-compose.metabase.'];
109
+
110
+ // ============================================================================
111
+ // COMMAND SPEC — single source of truth for argv parsing AND help output.
112
+ // ============================================================================
113
+
114
+ /** @type {import('./lib/cli/parse-flags.js').CommandSpec & { summary?: string, description?: string, examples?: Array<{ command: string, description?: string }> }} */
115
+ const SPEC = {
116
+ name: 'upgrade',
117
+ summary: 'Update infrastructure files to the latest template',
118
+ description: [
119
+ "Compares your project's infrastructure files against the latest template",
120
+ 'and applies updates. User source code (src/**, content/**, migrations/**)',
121
+ 'is never touched.',
122
+ '',
123
+ 'Files are classified into three categories:',
124
+ " Safe Auto-replaced if you haven't modified them",
125
+ ' Merge Always shown for review (docker-compose.yml, package.json, etc.)',
126
+ ' Never Your code, never touched (src/**, supabase/**, .env, etc.)',
127
+ ].join('\n'),
128
+ flags: [
129
+ { name: 'h', boolean: true, description: 'Show this help' },
130
+ { name: 'v', boolean: true, description: 'Show version' },
131
+ { name: 'y', boolean: true, description: 'Auto-accept safe replacements, skip merge files' },
132
+ { name: 'dry', boolean: true, description: 'Preview changes without applying' },
133
+ {
134
+ name: 'force',
135
+ boolean: true,
136
+ description: 'Replace all files (creates .upgrade-backup copies)',
137
+ },
138
+ ],
139
+ examples: [
140
+ { command: 'vibecarbon upgrade -dry', description: 'preview what would change' },
141
+ { command: 'vibecarbon upgrade', description: 'interactive upgrade' },
142
+ {
143
+ command: 'vibecarbon upgrade -y',
144
+ description: 'non-interactive: update safe files, skip merge files',
145
+ },
146
+ ],
147
+ };
148
+
149
+ // ============================================================================
150
+ // VARIABLE RECONSTRUCTION
151
+ // ============================================================================
152
+
153
+ /**
154
+ * Reconstruct template variables from the project's .env.local
155
+ * Maps environment variable names back to template placeholder names
156
+ *
157
+ * @param {string} cwd - Project directory
158
+ * @returns {object} - Variables keyed by placeholder name (without braces)
159
+ */
160
+ function reconstructVariables(cwd) {
161
+ const env = loadEnvVariables(cwd);
162
+ const manifest = loadManifest(cwd);
163
+
164
+ const variables = {
165
+ PROJECT_NAME: env.PROJECT_NAME || env.VITE_PROJECT_NAME || '',
166
+ GITHUB_OWNER: manifest.services?.cicd?.owner || env.PROJECT_NAME || '',
167
+ DB_PASSWORD: env.DB_PASSWORD || env.POSTGRES_PASSWORD || '',
168
+ JWT_SECRET: env.JWT_SECRET || '',
169
+ ANON_KEY: env.SUPABASE_ANON_KEY || '',
170
+ SERVICE_ROLE_KEY: env.SUPABASE_SERVICE_ROLE_KEY || '',
171
+ REALTIME_SECRET: env.REALTIME_SECRET || env.SECRET_KEY_BASE || '',
172
+ LOGFLARE_API_KEY: env.LOGFLARE_API_KEY || '',
173
+ VAULT_ENC_KEY: env.VAULT_ENC_KEY || '',
174
+ PG_META_CRYPTO_KEY: env.PG_META_CRYPTO_KEY || '',
175
+ DB_ENC_KEY: (env.DB_ENC_KEY || '').slice(0, 16), // Realtime aes_128_ecb requires exactly 16 bytes
176
+ SITE_URL: env.SITE_URL || 'http://localhost:5173',
177
+ ADMIN_EMAIL: env.ADMIN_EMAIL || '',
178
+ ADMIN_PASSWORD: env.ADMIN_PASSWORD || '',
179
+ // ADMIN_PASSWORD_HASH can't be reconstructed, but only appears in NEVER files
180
+ ADMIN_PASSWORD_HASH: '',
181
+ };
182
+
183
+ return variables;
184
+ }
185
+
186
+ // ============================================================================
187
+ // TEMPLATE RESOLUTION
188
+ // ============================================================================
189
+
190
+ /**
191
+ * Check if a file path has a binary extension
192
+ *
193
+ * @param {string} filePath
194
+ * @returns {boolean}
195
+ */
196
+ function isBinaryFile(filePath) {
197
+ const ext = filePath.substring(filePath.lastIndexOf('.'));
198
+ return BINARY_EXTENSIONS.has(ext);
199
+ }
200
+
201
+ /**
202
+ * Read a template file and apply placeholder replacement
203
+ *
204
+ * @param {string} templateRelPath - Relative path within TEMPLATE_DIR
205
+ * @param {object} variables - Template variables
206
+ * @returns {string} - Resolved content
207
+ */
208
+ function resolveTemplate(templateRelPath, variables) {
209
+ const fullPath = join(TEMPLATE_DIR, templateRelPath);
210
+ let content = readFileSync(fullPath, 'utf-8');
211
+
212
+ for (const [key, placeholder] of Object.entries(PLACEHOLDERS)) {
213
+ if (SECRET_PLACEHOLDERS.has(key)) continue;
214
+ if (variables[key] !== undefined && variables[key] !== '') {
215
+ content = content.replaceAll(placeholder, variables[key]);
216
+ }
217
+ }
218
+
219
+ return content;
220
+ }
221
+
222
+ // ============================================================================
223
+ // DIFF DISPLAY
224
+ // ============================================================================
225
+
226
+ /**
227
+ * Generate a simple unified-style diff between two strings
228
+ * Shows only changed lines with context
229
+ *
230
+ * @param {string} oldContent
231
+ * @param {string} newContent
232
+ * @returns {string}
233
+ */
234
+ function simpleDiff(oldContent, newContent) {
235
+ const oldLines = oldContent.split('\n');
236
+ const newLines = newContent.split('\n');
237
+ const output = [];
238
+ const maxLines = Math.max(oldLines.length, newLines.length);
239
+
240
+ for (let i = 0; i < maxLines; i++) {
241
+ const oldLine = oldLines[i];
242
+ const newLine = newLines[i];
243
+
244
+ if (oldLine === undefined) {
245
+ output.push(c.success(`+ ${newLine}`));
246
+ } else if (newLine === undefined) {
247
+ output.push(c.error(`- ${oldLine}`));
248
+ } else if (oldLine !== newLine) {
249
+ output.push(c.error(`- ${oldLine}`));
250
+ output.push(c.success(`+ ${newLine}`));
251
+ }
252
+ }
253
+
254
+ // Limit output to avoid flooding terminal
255
+ if (output.length > 40) {
256
+ return `${output.slice(0, 40).join('\n')}\n${c.dim(`... and ${output.length - 40} more lines`)}`;
257
+ }
258
+
259
+ return output.join('\n');
260
+ }
261
+
262
+ // ============================================================================
263
+ // MAIN UPGRADE LOGIC
264
+ // ============================================================================
265
+
266
+ async function main(cliArgs) {
267
+ const { values, errors } = parseFlags(cliArgs, SPEC);
268
+
269
+ if (errors.length > 0) {
270
+ for (const e of errors) {
271
+ process.stderr.write(`${c.error('✗')} ${e}\n`);
272
+ }
273
+ process.stderr.write(`Run ${c.info('vibecarbon upgrade -h')} for usage.\n`);
274
+ process.exit(1);
275
+ }
276
+
277
+ if (values.h) {
278
+ process.stdout.write(renderHelp(SPEC));
279
+ process.exit(0);
280
+ }
281
+ if (values.v) {
282
+ console.log(`vibecarbon v${VERSION}`);
283
+ process.exit(0);
284
+ }
285
+
286
+ // Build the legacy `args` shape that the orchestration code reads.
287
+ // dryRun (camelCase) is the historical name; preserve it.
288
+ const args = {
289
+ dryRun: !!values.dry,
290
+ force: !!values.force,
291
+ yes: !!values.y,
292
+ };
293
+
294
+ // Detect project
295
+ assertInProjectDir();
296
+
297
+ // Require a paid license (Diamond or Fullerene)
298
+ requireLicense('upgrade');
299
+
300
+ const cwd = process.cwd();
301
+ const manifest = loadManifest(cwd);
302
+ const currentTemplateVersion = manifest.templateVersion || '0.0.0';
303
+ const storedChecksums = manifest.fileChecksums || {};
304
+ const isBootstrap = currentTemplateVersion === '0.0.0';
305
+
306
+ printBanner();
307
+ p.intro(`${c.bold('vibecarbon upgrade')} ${c.dim(`v${VERSION}`)}`);
308
+
309
+ if (isBootstrap) {
310
+ p.log.info(
311
+ `First upgrade — no stored checksums found. Files will be compared against the current template.`,
312
+ );
313
+ } else {
314
+ p.log.info(`Template: ${c.dim(currentTemplateVersion)} → ${c.bold(VERSION)}`);
315
+ }
316
+
317
+ // Warn about dirty git state
318
+ try {
319
+ const { execFileSync } = await import('node:child_process');
320
+ const gitStatus = execFileSync('git', ['status', '--porcelain'], {
321
+ cwd,
322
+ encoding: 'utf-8',
323
+ timeout: 5000,
324
+ }).trim();
325
+ if (gitStatus) {
326
+ p.log.warn(
327
+ 'You have uncommitted changes. Consider committing first so the upgrade is easily revertable.',
328
+ );
329
+ }
330
+ } catch {
331
+ // Not a git repo or git not available — no warning needed
332
+ }
333
+
334
+ // Warn about running containers
335
+ try {
336
+ const { execFileSync } = await import('node:child_process');
337
+ const ps = execFileSync('docker', ['compose', 'ps', '--format', 'json'], {
338
+ cwd,
339
+ encoding: 'utf-8',
340
+ timeout: 10000,
341
+ }).trim();
342
+ if (ps) {
343
+ p.log.warn(
344
+ `Docker containers are running. Consider running ${c.info('vibecarbon down')} first.`,
345
+ );
346
+ }
347
+ } catch {
348
+ // Docker not available or not running — fine
349
+ }
350
+
351
+ // Reconstruct template variables
352
+ const s = p.spinner();
353
+ s.start('Scanning infrastructure files');
354
+
355
+ const variables = reconstructVariables(cwd);
356
+ const upgradeableFiles = getUpgradeableFiles(TEMPLATE_DIR);
357
+
358
+ // Classify each file
359
+ const autoUpdated = []; // { path, reason: 'updated' | 'added' }
360
+ const unchanged = []; // files with no diff
361
+ const needsReview = []; // { path, oldContent, newContent }
362
+
363
+ for (const relPath of upgradeableFiles) {
364
+ const policy = getFilePolicy(relPath);
365
+ const projectFilePath = join(cwd, relPath);
366
+ const templateFilePath = join(TEMPLATE_DIR, relPath);
367
+ const fileExists = existsSync(projectFilePath);
368
+
369
+ // Skip optional-feature files that don't exist in the project yet.
370
+ // These are added by `vibecarbon add` or `vibecarbon deploy`, not during upgrade.
371
+ if (!fileExists && OPTIONAL_FILE_PREFIXES.some((prefix) => relPath.startsWith(prefix))) {
372
+ continue;
373
+ }
374
+
375
+ // Handle binary files
376
+ if (isBinaryFile(relPath)) {
377
+ if (policy === 'safe' && !fileExists) {
378
+ autoUpdated.push({ path: relPath, reason: 'added' });
379
+ if (!args.dryRun) {
380
+ mkdirSync(dirname(projectFilePath), { recursive: true });
381
+ copyFileSync(templateFilePath, projectFilePath);
382
+ }
383
+ }
384
+ continue;
385
+ }
386
+
387
+ // Resolve template with variables (or raw-copy for files with their own {{PLACEHOLDER}} tokens)
388
+ let newContent;
389
+ try {
390
+ if (RAW_COPY_FILES.has(relPath)) {
391
+ newContent = readFileSync(join(TEMPLATE_DIR, relPath), 'utf-8');
392
+ } else {
393
+ newContent = resolveTemplate(relPath, variables);
394
+ }
395
+ } catch {
396
+ // Template file can't be read — skip
397
+ continue;
398
+ }
399
+
400
+ // Smart-merge package.json: update template dep versions, add new template deps,
401
+ // preserve user-added deps and fields, merge pnpm config (overrides, etc.)
402
+ if (relPath === 'package.json') {
403
+ try {
404
+ const pm = detectPackageManager(cwd);
405
+ const templatePkg = JSON.parse(newContent);
406
+ const userPkg = fileExists ? JSON.parse(readFileSync(projectFilePath, 'utf-8')) : {};
407
+
408
+ const merged = fileExists ? mergePackageJson(userPkg, templatePkg) : templatePkg;
409
+
410
+ if (variables.PROJECT_NAME) merged.name = variables.PROJECT_NAME;
411
+ merged.packageManager = getPackageManagerVersion(pm);
412
+ if (pm !== 'pnpm' && merged.scripts) {
413
+ const run = pm === 'npm' ? 'npm run' : pm;
414
+ for (const [key, value] of Object.entries(merged.scripts)) {
415
+ if (typeof value === 'string' && value.includes('pnpm ')) {
416
+ merged.scripts[key] = value.replaceAll('pnpm ', `${run} `);
417
+ }
418
+ }
419
+ }
420
+ newContent = `${JSON.stringify(merged, null, 2)}\n`;
421
+ } catch {
422
+ // If merge fails, use the raw resolved template content
423
+ }
424
+ }
425
+
426
+ const newHash = hashContent(newContent);
427
+
428
+ if (!fileExists) {
429
+ // New file in template — add it
430
+ if (policy === 'safe') {
431
+ autoUpdated.push({ path: relPath, reason: 'added' });
432
+ if (!args.dryRun) {
433
+ mkdirSync(dirname(projectFilePath), { recursive: true });
434
+ writeFileSync(projectFilePath, newContent);
435
+ if (relPath.endsWith('.sh')) chmodSync(projectFilePath, 0o755);
436
+ }
437
+ } else if (policy === 'merge') {
438
+ needsReview.push({ path: relPath, oldContent: null, newContent, reason: 'new' });
439
+ }
440
+ continue;
441
+ }
442
+
443
+ // File exists — compare
444
+ const currentContent = readFileSync(projectFilePath, 'utf-8');
445
+ const currentHash = hashContent(currentContent);
446
+
447
+ // No change needed
448
+ if (currentHash === newHash) {
449
+ unchanged.push(relPath);
450
+ continue;
451
+ }
452
+
453
+ const storedHash = storedChecksums[relPath];
454
+ const userModified = storedHash ? currentHash !== storedHash : true;
455
+
456
+ if (args.force) {
457
+ // Force mode: replace everything, backup modified files
458
+ if (userModified) {
459
+ if (!args.dryRun) {
460
+ writeFileSync(`${projectFilePath}.upgrade-backup`, currentContent);
461
+ }
462
+ }
463
+ autoUpdated.push({ path: relPath, reason: 'updated' });
464
+ if (!args.dryRun) {
465
+ writeFileSync(projectFilePath, newContent);
466
+ if (relPath.endsWith('.sh')) chmodSync(projectFilePath, 0o755);
467
+ }
468
+ continue;
469
+ }
470
+
471
+ if (policy === 'safe') {
472
+ if (!userModified) {
473
+ // Not modified by user — safe to auto-replace
474
+ autoUpdated.push({ path: relPath, reason: 'updated' });
475
+ if (!args.dryRun) {
476
+ writeFileSync(projectFilePath, newContent);
477
+ if (relPath.endsWith('.sh')) chmodSync(projectFilePath, 0o755);
478
+ }
479
+ } else if (isBootstrap) {
480
+ // Bootstrap: can't tell if user modified — compare with template
481
+ // If the current content differs, prompt for safe files too
482
+ needsReview.push({
483
+ path: relPath,
484
+ oldContent: currentContent,
485
+ newContent,
486
+ reason: 'unknown',
487
+ });
488
+ } else {
489
+ // User modified a safe file — still needs review
490
+ needsReview.push({
491
+ path: relPath,
492
+ oldContent: currentContent,
493
+ newContent,
494
+ reason: 'modified',
495
+ });
496
+ }
497
+ } else if (policy === 'merge') {
498
+ // Merge files always need review
499
+ needsReview.push({
500
+ path: relPath,
501
+ oldContent: currentContent,
502
+ newContent,
503
+ reason: 'modified',
504
+ });
505
+ }
506
+ }
507
+
508
+ s.stop('Scan complete');
509
+
510
+ // Display results
511
+ if (autoUpdated.length > 0) {
512
+ p.log.success(`${c.bold('AUTO-UPDATED')} (unmodified by you):`);
513
+ for (const { path: filePath, reason } of autoUpdated) {
514
+ const icon = reason === 'added' ? '+' : '\u2713';
515
+ console.log(` ${c.success(icon)} ${filePath}${args.dryRun ? c.dim(' (dry run)') : ''}`);
516
+ }
517
+ }
518
+
519
+ if (unchanged.length > 0) {
520
+ p.log.info(`${unchanged.length} files already up to date`);
521
+ }
522
+
523
+ // Handle files that need review
524
+ let reviewedCount = 0;
525
+ let skippedReviewCount = 0;
526
+ let packageJsonUpdated = autoUpdated.some((f) => f.path === 'package.json');
527
+
528
+ if (needsReview.length > 0 && !args.dryRun) {
529
+ if (args.yes) {
530
+ // Non-interactive: skip all merge/review files
531
+ p.log.info(`${needsReview.length} files need review — skipped (non-interactive mode)`);
532
+ for (const { path: filePath, newContent } of needsReview) {
533
+ const projectFilePath = join(cwd, filePath);
534
+ mkdirSync(dirname(projectFilePath), { recursive: true });
535
+ writeFileSync(`${projectFilePath}.upgrade-new`, newContent);
536
+ }
537
+ p.log.info(
538
+ `New versions saved as ${c.info('.upgrade-new')} files. Review and apply manually.`,
539
+ );
540
+ skippedReviewCount = needsReview.length;
541
+ } else {
542
+ // Interactive: prompt for each file
543
+ p.log.step(`${c.bold('NEEDS REVIEW')} (${needsReview.length} files):`);
544
+
545
+ for (const { path: filePath, oldContent, newContent, reason } of needsReview) {
546
+ const projectFilePath = join(cwd, filePath);
547
+ const reasonText =
548
+ reason === 'new'
549
+ ? c.info('new in template')
550
+ : reason === 'unknown'
551
+ ? c.dim('no stored checksum')
552
+ : c.dim('you modified this');
553
+
554
+ const action = await p.select({
555
+ message: `${filePath} (${reasonText})`,
556
+ options: [
557
+ { value: 'diff', label: 'View diff first' },
558
+ { value: 'replace', label: 'Replace (backup as .upgrade-backup)' },
559
+ { value: 'new', label: 'Save new version as .upgrade-new' },
560
+ { value: 'skip', label: 'Skip' },
561
+ ],
562
+ initialValue: 'new',
563
+ });
564
+
565
+ if (p.isCancel(action)) {
566
+ p.cancel('Operation cancelled.');
567
+ process.exit(0);
568
+ }
569
+
570
+ if (action === 'diff') {
571
+ // Show diff, then re-prompt
572
+ if (oldContent) {
573
+ console.log(`\n${c.dim(`--- ${filePath} (current)`)}`);
574
+ console.log(`${c.dim(`+++ ${filePath} (template)`)}`);
575
+ console.log(simpleDiff(oldContent, newContent));
576
+ console.log();
577
+ } else {
578
+ console.log(`\n${c.dim('(new file)')}`);
579
+ const preview = newContent.split('\n').slice(0, 20).join('\n');
580
+ console.log(preview);
581
+ if (newContent.split('\n').length > 20) {
582
+ console.log(c.dim(`... ${newContent.split('\n').length - 20} more lines`));
583
+ }
584
+ console.log();
585
+ }
586
+
587
+ // Re-prompt after viewing diff
588
+ const action2 = await p.select({
589
+ message: `${filePath}`,
590
+ options: [
591
+ { value: 'replace', label: 'Replace (backup as .upgrade-backup)' },
592
+ { value: 'new', label: 'Save new version as .upgrade-new' },
593
+ { value: 'skip', label: 'Skip' },
594
+ ],
595
+ initialValue: 'replace',
596
+ });
597
+
598
+ if (p.isCancel(action2)) {
599
+ p.cancel('Operation cancelled.');
600
+ process.exit(0);
601
+ }
602
+
603
+ if (action2 === 'replace') {
604
+ if (oldContent) writeFileSync(`${projectFilePath}.upgrade-backup`, oldContent);
605
+ mkdirSync(dirname(projectFilePath), { recursive: true });
606
+ writeFileSync(projectFilePath, newContent);
607
+ if (filePath.endsWith('.sh')) chmodSync(projectFilePath, 0o755);
608
+ if (filePath === 'package.json') packageJsonUpdated = true;
609
+ reviewedCount++;
610
+ } else if (action2 === 'new') {
611
+ mkdirSync(dirname(projectFilePath), { recursive: true });
612
+ writeFileSync(`${projectFilePath}.upgrade-new`, newContent);
613
+ skippedReviewCount++;
614
+ } else {
615
+ skippedReviewCount++;
616
+ }
617
+ } else if (action === 'replace') {
618
+ if (oldContent) writeFileSync(`${projectFilePath}.upgrade-backup`, oldContent);
619
+ mkdirSync(dirname(projectFilePath), { recursive: true });
620
+ writeFileSync(projectFilePath, newContent);
621
+ if (filePath.endsWith('.sh')) chmodSync(projectFilePath, 0o755);
622
+ if (filePath === 'package.json') packageJsonUpdated = true;
623
+ reviewedCount++;
624
+ } else if (action === 'new') {
625
+ mkdirSync(dirname(projectFilePath), { recursive: true });
626
+ writeFileSync(`${projectFilePath}.upgrade-new`, newContent);
627
+ skippedReviewCount++;
628
+ } else {
629
+ skippedReviewCount++;
630
+ }
631
+ }
632
+ }
633
+ } else if (needsReview.length > 0 && args.dryRun) {
634
+ p.log.step(`${c.bold('NEEDS REVIEW')} (${needsReview.length} files):`);
635
+ for (const { path: filePath, reason } of needsReview) {
636
+ const reasonText =
637
+ reason === 'new'
638
+ ? 'new in template'
639
+ : reason === 'unknown'
640
+ ? 'no stored checksum'
641
+ : 'modified by you';
642
+ console.log(` ${c.dim('?')} ${filePath} ${c.dim(`(${reasonText})`)}`);
643
+ }
644
+ }
645
+
646
+ // Re-apply package manager adaptations to upgraded files.
647
+ // The template is always pnpm-based; bun/npm projects need re-adaptation.
648
+ if (!args.dryRun) {
649
+ const pm = detectPackageManager(cwd);
650
+ if (pm !== 'pnpm') {
651
+ adaptDockerfileForPackageManager(cwd, pm);
652
+ }
653
+ }
654
+
655
+ // Regenerate the lockfile so it stays in sync with the updated package.json.
656
+ // Without this, `--frozen-lockfile` in the Dockerfile will fail on deploy.
657
+ if (!args.dryRun && packageJsonUpdated) {
658
+ const pm = detectPackageManager(cwd);
659
+ const installCmd = {
660
+ pnpm: ['pnpm', 'install'],
661
+ npm: ['npm', 'install'],
662
+ bun: ['bun', 'install'],
663
+ }[pm] || ['pnpm', 'install'];
664
+
665
+ const s2 = p.spinner();
666
+ s2.start('Regenerating lockfile…');
667
+ try {
668
+ await runCommandAsync(installCmd, { cwd, silent: true });
669
+ s2.stop(`${c.success('✓')} Lockfile updated`);
670
+ } catch {
671
+ s2.stop('');
672
+ p.log.warn('Failed to regenerate lockfile — run install manually before deploying');
673
+ }
674
+ }
675
+
676
+ // Update manifest with new checksums and template version (unless dry run)
677
+ if (!args.dryRun) {
678
+ const newChecksums = {};
679
+
680
+ // Recompute checksums for all upgradeable files that now exist
681
+ for (const relPath of upgradeableFiles) {
682
+ if (isBinaryFile(relPath)) continue;
683
+ const projectFilePath = join(cwd, relPath);
684
+ if (existsSync(projectFilePath)) {
685
+ try {
686
+ newChecksums[relPath] = hashFile(projectFilePath);
687
+ } catch {
688
+ // Skip files that can't be read
689
+ }
690
+ }
691
+ }
692
+
693
+ manifest.templateVersion = VERSION;
694
+ manifest.fileChecksums = newChecksums;
695
+ saveManifest(manifest, cwd);
696
+ }
697
+
698
+ // Summary
699
+ const totalUpdated = autoUpdated.length + reviewedCount;
700
+ const totalReview = skippedReviewCount;
701
+
702
+ if (args.dryRun) {
703
+ p.outro(
704
+ `Dry run complete — ${autoUpdated.length} would update, ${needsReview.length} need review, ${unchanged.length} unchanged`,
705
+ );
706
+ } else {
707
+ p.outro(
708
+ `Upgrade complete (${totalUpdated} updated, ${totalReview} need review, ${unchanged.length} unchanged)\n Template: ${currentTemplateVersion} → ${VERSION}`,
709
+ );
710
+ }
711
+ }
712
+
713
+ export async function run(args) {
714
+ await main(args);
715
+ }
716
+
717
+ // ============================================================================
718
+ // EXPORTS FOR TESTING
719
+ // ============================================================================
720
+
721
+ export { reconstructVariables, resolveTemplate, SPEC };