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/status.js ADDED
@@ -0,0 +1,981 @@
1
+ /**
2
+ * Vibecarbon Status Command
3
+ * Shows project and deployment status (read-only, non-interactive)
4
+ *
5
+ * Usage:
6
+ * vibecarbon status # Show all status info
7
+ * vibecarbon status -env prod # Show specific environment
8
+ * vibecarbon status -json # Machine-readable output
9
+ *
10
+ * Local-dev checks are context-sensitive: skipped when stdout is not a
11
+ * TTY OR when `-json` is set (CI / scripting paths don't want them),
12
+ * and skipped when run outside a project directory (registry-only mode).
13
+ */
14
+
15
+ import { existsSync, readFileSync } from 'node:fs';
16
+ import * as p from '@clack/prompts';
17
+ import { renderHelp } from './lib/cli/help.js';
18
+ import { parseFlags } from './lib/cli/parse-flags.js';
19
+ import { c, printBanner } from './lib/colors.js';
20
+ import { runCommand } from './lib/command.js';
21
+ import { cleanStaleProjects, loadGlobalRegistry, loadProjectConfig } from './lib/config.js';
22
+ import { HetznerProvider } from './lib/providers/hetzner.js';
23
+ import { VERSION } from './lib/version.js';
24
+
25
+ /** @type {import('./lib/cli/parse-flags.js').CommandSpec & { summary?: string, description?: string, examples?: Array<{ command: string, description?: string }> }} */
26
+ const SPEC = {
27
+ name: 'status',
28
+ summary: 'Show project & deployment status',
29
+ description: [
30
+ 'Read-only summary of project + environment health.',
31
+ '',
32
+ 'MODES',
33
+ ' Run from outside a project: Shows summary of all registered projects',
34
+ ' Run from inside a project: Shows detailed environment status',
35
+ '',
36
+ 'ENVIRONMENT VARIABLES',
37
+ ' HETZNER_API_TOKEN Enables live server status checks',
38
+ ].join('\n'),
39
+ flags: [
40
+ { name: 'h', boolean: true, description: 'Show this help' },
41
+ { name: 'v', boolean: true, description: 'Show version' },
42
+ { name: 'env', value: '<name>', description: 'Show only a specific environment' },
43
+ { name: 'json', boolean: true, description: 'Machine-readable JSON output' },
44
+ ],
45
+ examples: [
46
+ { command: 'vibecarbon status', description: 'Show full project status' },
47
+ { command: 'vibecarbon status -env prod', description: 'Show only production' },
48
+ { command: 'vibecarbon status -json', description: 'JSON output for CI / scripting' },
49
+ ],
50
+ };
51
+
52
+ // ============================================================================
53
+ // UTILITY FUNCTIONS
54
+ // ============================================================================
55
+
56
+ function formatRelativeTime(isoDate) {
57
+ if (!isoDate) return '';
58
+ const now = Date.now();
59
+ const then = new Date(isoDate).getTime();
60
+ const diffMs = now - then;
61
+
62
+ if (diffMs < 0) return 'in the future';
63
+
64
+ const seconds = Math.floor(diffMs / 1000);
65
+ const minutes = Math.floor(seconds / 60);
66
+ const hours = Math.floor(minutes / 60);
67
+ const days = Math.floor(hours / 24);
68
+ const weeks = Math.floor(days / 7);
69
+ const months = Math.floor(days / 30);
70
+
71
+ if (months > 0) return `${months} month${months === 1 ? '' : 's'} ago`;
72
+ if (weeks > 0) return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
73
+ if (days > 0) return `${days} day${days === 1 ? '' : 's'} ago`;
74
+ if (hours > 0) return `${hours} hour${hours === 1 ? '' : 's'} ago`;
75
+ if (minutes > 0) return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;
76
+ return 'just now';
77
+ }
78
+
79
+ function getBranchName(envName) {
80
+ return envName === 'prod' ? 'main' : envName;
81
+ }
82
+
83
+ // ============================================================================
84
+ // HEALTH CHECK FUNCTIONS
85
+ // ============================================================================
86
+
87
+ async function checkHttpEndpoint(url, timeout = 2000) {
88
+ const start = Date.now();
89
+ try {
90
+ const controller = new AbortController();
91
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
92
+ const response = await fetch(url, { method: 'GET', signal: controller.signal });
93
+ clearTimeout(timeoutId);
94
+ const latencyMs = Date.now() - start;
95
+
96
+ let data = null;
97
+ try {
98
+ data = await response.json();
99
+ } catch {
100
+ // Not JSON, that's fine
101
+ }
102
+
103
+ return {
104
+ ok: response.status >= 200 && response.status < 400,
105
+ status: response.status,
106
+ latencyMs,
107
+ data,
108
+ };
109
+ } catch (error) {
110
+ const latencyMs = Date.now() - start;
111
+ const message = error instanceof Error ? error.message : 'Unknown error';
112
+ const isTimeout = message.includes('abort');
113
+ return {
114
+ ok: false,
115
+ status: null,
116
+ latencyMs,
117
+ error: isTimeout ? `timeout after ${timeout}ms` : message,
118
+ };
119
+ }
120
+ }
121
+
122
+ // Note: execSync calls below use only hardcoded commands (no user input),
123
+ // matching the pattern in deploy.js and destroy.js throughout this codebase.
124
+
125
+ async function checkDockerContainers(projectName) {
126
+ const services = [
127
+ {
128
+ name: 'PostgreSQL',
129
+ container: 'db',
130
+ healthUrl: 'http://localhost:8000/rest/v1/',
131
+ acceptCodes: [200, 401],
132
+ },
133
+ {
134
+ name: 'Kong Gateway',
135
+ container: 'kong',
136
+ healthUrl: 'http://localhost:8000/',
137
+ acceptCodes: [404],
138
+ },
139
+ {
140
+ name: 'Auth (GoTrue)',
141
+ container: 'auth',
142
+ healthUrl: 'http://localhost:8000/auth/v1/health',
143
+ acceptCodes: [200, 401],
144
+ },
145
+ {
146
+ name: 'REST (PostgREST)',
147
+ container: 'rest',
148
+ healthUrl: 'http://localhost:8000/rest/v1/',
149
+ acceptCodes: [200, 401],
150
+ },
151
+ {
152
+ name: 'Realtime',
153
+ container: 'realtime',
154
+ healthUrl: 'http://localhost:8000/realtime/v1/',
155
+ acceptCodes: [200, 401, 403, 426],
156
+ },
157
+ {
158
+ name: 'Storage',
159
+ container: 'storage',
160
+ healthUrl: 'http://localhost:8000/storage/v1/status',
161
+ acceptCodes: [200, 401],
162
+ },
163
+ {
164
+ name: 'Studio',
165
+ container: 'studio',
166
+ healthUrl: 'http://studio.localhost/',
167
+ acceptCodes: [200, 307],
168
+ },
169
+ {
170
+ name: 'Meta',
171
+ container: 'meta',
172
+ healthUrl: 'http://localhost:8000/pg/',
173
+ acceptCodes: [200, 401],
174
+ },
175
+ ];
176
+
177
+ // Get list of running containers
178
+ let runningContainers = new Set();
179
+ try {
180
+ const output =
181
+ runCommand(['docker', 'ps', '--format', '{{.Names}}'], {
182
+ silent: true,
183
+ encoding: 'utf-8',
184
+ timeout: 5000,
185
+ ignoreError: true,
186
+ }) || '';
187
+ const prefix = projectName ? `${projectName}-` : null;
188
+ runningContainers = new Set(
189
+ output
190
+ .split('\n')
191
+ .map((name) => name.trim())
192
+ .filter(Boolean)
193
+ .filter((name) => !prefix || name.startsWith(prefix))
194
+ .map((name) => (prefix ? name.slice(prefix.length) : name.replace(/^[^-]+-/, ''))),
195
+ );
196
+ } catch {
197
+ return [];
198
+ }
199
+
200
+ if (runningContainers.size === 0) return [];
201
+
202
+ // Check port offset from env
203
+ let portOffset = 0;
204
+ try {
205
+ const envFiles = ['.env.local', '.env'];
206
+ for (const file of envFiles) {
207
+ if (existsSync(file)) {
208
+ const content = readFileSync(file, 'utf-8');
209
+ const match = content.match(/^DEV_PORT_OFFSET=["']?(\d+)["']?/m);
210
+ if (match) {
211
+ portOffset = Number.parseInt(match[1], 10);
212
+ }
213
+ break;
214
+ }
215
+ }
216
+ } catch {
217
+ // Use defaults
218
+ }
219
+
220
+ // Adjust ports if offset
221
+ if (portOffset > 0) {
222
+ for (const svc of services) {
223
+ svc.healthUrl = svc.healthUrl.replace(':8000', `:${8000 + portOffset}`);
224
+ }
225
+ }
226
+
227
+ // Only check services whose containers are running
228
+ const activeServices = services.filter((svc) => runningContainers.has(svc.container));
229
+
230
+ const results = await Promise.allSettled(
231
+ activeServices.map(async (svc) => {
232
+ const start = Date.now();
233
+ try {
234
+ const controller = new AbortController();
235
+ const timeoutId = setTimeout(() => controller.abort(), 2000);
236
+ const response = await fetch(svc.healthUrl, { method: 'GET', signal: controller.signal });
237
+ clearTimeout(timeoutId);
238
+ const latencyMs = Date.now() - start;
239
+ const isHealthy = svc.acceptCodes
240
+ ? svc.acceptCodes.includes(response.status)
241
+ : response.status >= 200 && response.status < 400;
242
+
243
+ return {
244
+ name: svc.name,
245
+ container: svc.container,
246
+ health: isHealthy ? 'healthy' : 'unhealthy',
247
+ latencyMs,
248
+ };
249
+ } catch {
250
+ return {
251
+ name: svc.name,
252
+ container: svc.container,
253
+ health: 'unhealthy',
254
+ latencyMs: Date.now() - start,
255
+ };
256
+ }
257
+ }),
258
+ );
259
+
260
+ return results.map((r) =>
261
+ r.status === 'fulfilled'
262
+ ? r.value
263
+ : { name: '?', container: '?', health: 'unknown', latencyMs: 0 },
264
+ );
265
+ }
266
+
267
+ function getPortConfig() {
268
+ const defaults = { vite: 5173, api: 3000 };
269
+ try {
270
+ const envFiles = ['.env.local', '.env'];
271
+ for (const file of envFiles) {
272
+ if (existsSync(file)) {
273
+ const content = readFileSync(file, 'utf-8');
274
+ const getEnvValue = (key) => {
275
+ const match = content.match(new RegExp(`^${key}=["']?([^"'\\n]+)["']?`, 'm'));
276
+ return match ? match[1] : null;
277
+ };
278
+
279
+ const portOffset = Number.parseInt(getEnvValue('DEV_PORT_OFFSET') || '0', 10);
280
+ const vitePort = getEnvValue('DEV_VITE_PORT') || String(5173 + portOffset);
281
+ const apiPort = getEnvValue('DEV_API_PORT') || String(3000 + portOffset);
282
+
283
+ return { vite: Number.parseInt(vitePort, 10), api: Number.parseInt(apiPort, 10) };
284
+ }
285
+ }
286
+ } catch {
287
+ // Fall through to defaults
288
+ }
289
+ return defaults;
290
+ }
291
+
292
+ async function checkLocalDev(projectName) {
293
+ const ports = getPortConfig();
294
+
295
+ const [apiCheck, viteCheck, dockerServices] = await Promise.allSettled([
296
+ checkHttpEndpoint(`http://localhost:${ports.api}/api/health`, 2000),
297
+ checkHttpEndpoint(`http://localhost:${ports.vite}`, 2000),
298
+ checkDockerContainers(projectName),
299
+ ]);
300
+
301
+ const api =
302
+ apiCheck.status === 'fulfilled' ? apiCheck.value : { ok: false, error: 'check failed' };
303
+ const vite =
304
+ viteCheck.status === 'fulfilled' ? viteCheck.value : { ok: false, error: 'check failed' };
305
+ const docker = dockerServices.status === 'fulfilled' ? dockerServices.value : [];
306
+
307
+ return {
308
+ api: { running: api.ok, latencyMs: api.latencyMs, port: ports.api, data: api.data },
309
+ vite: { running: vite.ok, port: ports.vite },
310
+ docker,
311
+ };
312
+ }
313
+
314
+ async function checkRemoteHealth(domain) {
315
+ const url = `https://${domain}/api/health`;
316
+ const result = await checkHttpEndpoint(url, 5000);
317
+ return {
318
+ url,
319
+ ok: result.ok,
320
+ status: result.status,
321
+ latencyMs: result.latencyMs,
322
+ data: result.data,
323
+ error: result.error,
324
+ };
325
+ }
326
+
327
+ async function getServerInfo(serverId) {
328
+ const apiToken = process.env.HETZNER_API_TOKEN;
329
+ if (!apiToken) return null;
330
+
331
+ try {
332
+ const response = await fetch(`https://api.hetzner.cloud/v1/servers/${serverId}`, {
333
+ headers: { Authorization: `Bearer ${apiToken}` },
334
+ signal: AbortSignal.timeout(5000),
335
+ });
336
+
337
+ if (!response.ok) return null;
338
+
339
+ const { server } = await response.json();
340
+ return {
341
+ status: server.status,
342
+ serverType: server.server_type?.name || null,
343
+ };
344
+ } catch {
345
+ return null;
346
+ }
347
+ }
348
+
349
+ function checkGitSync(envName, envConfig) {
350
+ const deployedCommit = envConfig.deployedCommit;
351
+ if (!deployedCommit) {
352
+ return { branch: getBranchName(envName), current: null, deployed: null, commitsAhead: null };
353
+ }
354
+
355
+ const branch = getBranchName(envName);
356
+
357
+ try {
358
+ const current = runCommand(['git', 'rev-parse', 'HEAD'], {
359
+ encoding: 'utf-8',
360
+ stdio: ['pipe', 'pipe', 'pipe'],
361
+ }).trim();
362
+
363
+ let commitsAhead = null;
364
+ try {
365
+ const count = runCommand(['git', 'rev-list', '--count', `${deployedCommit}..HEAD`], {
366
+ encoding: 'utf-8',
367
+ stdio: ['pipe', 'pipe', 'pipe'],
368
+ }).trim();
369
+ commitsAhead = Number.parseInt(count, 10);
370
+ } catch {
371
+ // Deployed commit might not be in local history
372
+ }
373
+
374
+ let deployedMessage = null;
375
+ try {
376
+ deployedMessage = runCommand(['git', 'log', '--format=%s', '-1', deployedCommit], {
377
+ encoding: 'utf-8',
378
+ stdio: ['pipe', 'pipe', 'pipe'],
379
+ }).trim();
380
+ } catch {
381
+ // Commit might not be locally available
382
+ }
383
+
384
+ let currentMessage = null;
385
+ try {
386
+ currentMessage = runCommand(['git', 'log', '--format=%s', '-1', 'HEAD'], {
387
+ encoding: 'utf-8',
388
+ stdio: ['pipe', 'pipe', 'pipe'],
389
+ }).trim();
390
+ } catch {
391
+ // Ignore
392
+ }
393
+
394
+ return {
395
+ branch,
396
+ current: { sha: current, message: currentMessage },
397
+ deployed: { sha: deployedCommit, message: deployedMessage },
398
+ commitsAhead,
399
+ };
400
+ } catch {
401
+ return {
402
+ branch,
403
+ current: null,
404
+ deployed: { sha: deployedCommit, message: null },
405
+ commitsAhead: null,
406
+ };
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Calculate monthly cost for a single environment.
412
+ * Includes servers, floating IP, CSI volumes, and S3 storage.
413
+ */
414
+ function calculateEnvCost(envConfig) {
415
+ const { INFRA_COSTS, VOLUME_SIZES, SERVER_TYPES } = HetznerProvider;
416
+ let cost = 0;
417
+
418
+ // Floating IP
419
+ if (envConfig.floatingIp) {
420
+ cost += INFRA_COSTS.floatingIp;
421
+ }
422
+
423
+ // Servers
424
+ const servers = envConfig.servers || [];
425
+ for (const server of servers) {
426
+ const serverType = server.serverType || server.type || 'cpx11';
427
+ const spec = SERVER_TYPES[serverType];
428
+ if (spec) {
429
+ const priceMatch = spec.price.match(/[\d.]+/);
430
+ if (priceMatch) cost += Number.parseFloat(priceMatch[0]);
431
+ }
432
+ }
433
+
434
+ // CSI Volumes (base supabase PVCs are always provisioned)
435
+ if (servers.length > 0) {
436
+ let volumeGb = VOLUME_SIZES.base;
437
+ const services = envConfig.services || {};
438
+ if (envConfig.observability || services.observability) volumeGb += VOLUME_SIZES.observability;
439
+ if (services.n8n) volumeGb += VOLUME_SIZES.n8n;
440
+ if (services.metabase) volumeGb += VOLUME_SIZES.metabase;
441
+ if (services.redis) volumeGb += VOLUME_SIZES.redis;
442
+ cost += volumeGb * INFRA_COSTS.volumePerGb;
443
+ }
444
+
445
+ // S3 Object Storage
446
+ if (envConfig.s3) {
447
+ cost += INFRA_COSTS.s3MinBucket;
448
+ }
449
+
450
+ return cost;
451
+ }
452
+
453
+ function calculateMonthlyCost(environments) {
454
+ let total = 0;
455
+ for (const [, envConfig] of Object.entries(environments)) {
456
+ total += calculateEnvCost(envConfig);
457
+ }
458
+ return total > 0 ? `\u20AC${total.toFixed(2)}/mo` : null;
459
+ }
460
+
461
+ // ============================================================================
462
+ // RENDERING FUNCTIONS
463
+ // ============================================================================
464
+
465
+ function renderLocalDev(data) {
466
+ const lines = [];
467
+
468
+ // API server
469
+ const apiStatus = data.api.running
470
+ ? `${c.success('\u25cf')} ${c.success('Running')} ${c.dim(`${data.api.latencyMs}ms`)}`
471
+ : `${c.dim('\u25cb')} ${c.dim('Stopped')}`;
472
+ const apiLabel = `API Server (port ${data.api.port})`;
473
+ lines.push(`${c.dim(apiLabel.padEnd(30))}${apiStatus}`);
474
+
475
+ // Vite dev server
476
+ const viteStatus = data.vite.running
477
+ ? `${c.success('\u25cf')} ${c.success('Running')}`
478
+ : `${c.dim('\u25cb')} ${c.dim('Stopped')}`;
479
+ const viteLabel = `Vite Dev Server (port ${data.vite.port})`;
480
+ lines.push(`${c.dim(viteLabel.padEnd(30))}${viteStatus}`);
481
+
482
+ // Docker services
483
+ if (data.docker.length > 0) {
484
+ const healthyCount = data.docker.filter((s) => s.health === 'healthy').length;
485
+ const total = data.docker.length;
486
+ const dockerSummary =
487
+ healthyCount === total
488
+ ? c.success(`\u25cf ${healthyCount}/${total} healthy`)
489
+ : c.warning(`\u25cf ${healthyCount}/${total} healthy`);
490
+ lines.push(`${c.dim('Docker Services'.padEnd(30))}${dockerSummary}`);
491
+
492
+ for (const svc of data.docker) {
493
+ const icon =
494
+ svc.health === 'healthy'
495
+ ? c.success('\u25cf')
496
+ : svc.health === 'unhealthy'
497
+ ? c.error('\u25cf')
498
+ : c.dim('\u25cb');
499
+ const status = svc.health === 'healthy' ? c.dim('healthy') : c.error(svc.health);
500
+ const latency = svc.latencyMs ? c.dim(`${svc.latencyMs}ms`) : '';
501
+ lines.push(` ${c.dim(svc.name.padEnd(28))}${icon} ${status} ${latency}`);
502
+ }
503
+ } else {
504
+ lines.push(`${c.dim('Docker Services'.padEnd(30))}${c.dim('not running')}`);
505
+ }
506
+
507
+ p.note(lines.join('\n'), 'Local Development');
508
+ }
509
+
510
+ function renderEnvironment(envName, envConfig, checks) {
511
+ const lines = [];
512
+
513
+ // Provider
514
+ const providerName =
515
+ envConfig.provider === 'hetzner' ? 'Hetzner Cloud' : envConfig.provider || 'unknown';
516
+ lines.push(`${c.dim('Provider')} ${providerName}`);
517
+
518
+ // Region
519
+ if (envConfig.region) {
520
+ const regionDesc = HetznerProvider.REGIONS[envConfig.region] || '';
521
+ const regionDisplay = regionDesc
522
+ ? `${envConfig.region} ${c.dim(`(${regionDesc})`)}`
523
+ : envConfig.region;
524
+ lines.push(`${c.dim('Region')} ${regionDisplay}`);
525
+ }
526
+
527
+ // Domain
528
+ if (envConfig.domain) {
529
+ lines.push(`${c.dim('Domain')} ${c.info(envConfig.domain)}`);
530
+ }
531
+
532
+ // Floating IP
533
+ if (envConfig.floatingIp) {
534
+ lines.push(`${c.dim('Floating IP')} ${envConfig.floatingIp}`);
535
+ }
536
+
537
+ // S3 Bucket
538
+ if (envConfig.s3?.bucket) {
539
+ const s3Region = envConfig.s3.region ? ` ${c.dim(`(${envConfig.s3.region})`)}` : '';
540
+ lines.push(`${c.dim('S3 Bucket')} ${envConfig.s3.bucket}${s3Region}`);
541
+ }
542
+
543
+ // Deployed date
544
+ if (envConfig.deployedAt) {
545
+ const relative = formatRelativeTime(envConfig.deployedAt);
546
+ const dateStr = new Date(envConfig.deployedAt)
547
+ .toISOString()
548
+ .replace('T', ' ')
549
+ .replace(/\.\d+Z/, ' UTC');
550
+ lines.push(`${c.dim('Deployed')} ${dateStr} ${c.dim(`(${relative})`)}`);
551
+ }
552
+
553
+ // HA
554
+ if (envConfig.ha) {
555
+ const failoverRegion = envConfig.secondaryRegion || envConfig.failoverRegion;
556
+ const haDisplay = failoverRegion
557
+ ? `${c.success('Enabled')} ${c.dim(`(failover: ${failoverRegion})`)}`
558
+ : c.success('Enabled');
559
+ lines.push(`${c.dim('HA')} ${haDisplay}`);
560
+ }
561
+
562
+ // Servers
563
+ const servers = envConfig.servers || [];
564
+ if (servers.length > 0) {
565
+ lines.push('');
566
+ lines.push(c.bold('Servers'));
567
+ for (const server of servers) {
568
+ const serverInfo = checks.serverInfo?.[server.id];
569
+ const configType = server.serverType || server.type || null;
570
+ let statusStr;
571
+ if (serverInfo) {
572
+ const icon = serverInfo.status === 'running' ? c.success('\u25cf') : c.error('\u25cf');
573
+ const typeLabel = serverInfo.serverType || configType || '';
574
+ statusStr = `${icon} ${serverInfo.status === 'running' ? c.success('running') : c.error(serverInfo.status)} ${c.dim(typeLabel)}`;
575
+ } else if (configType) {
576
+ statusStr = c.dim(configType);
577
+ } else {
578
+ statusStr = c.dim('\u2013');
579
+ }
580
+ lines.push(
581
+ ` ${c.info((server.name || '').padEnd(16))} ${(server.ip || '').padEnd(15)} ${statusStr}`,
582
+ );
583
+ }
584
+ }
585
+
586
+ // Remote health
587
+ if (envConfig.domain && checks.remoteHealth) {
588
+ lines.push('');
589
+ lines.push(c.bold('Health'));
590
+ lines.push(` ${c.dim(checks.remoteHealth.url)}`);
591
+ if (checks.remoteHealth.ok) {
592
+ const data = checks.remoteHealth.data;
593
+ let details = '';
594
+ if (data && typeof data === 'object') {
595
+ const parts = [];
596
+ if (data.database) parts.push(`db: ${data.database}`);
597
+ if (data.supabase) parts.push(`supabase: ${data.supabase}`);
598
+ if (data.status) parts.push(data.status);
599
+ if (parts.length > 0) details = c.dim(` (${parts.join(', ')})`);
600
+ }
601
+ lines.push(
602
+ ` ${c.success('\u25cf')} ${c.success('healthy')} ${c.dim(`${checks.remoteHealth.latencyMs}ms`)}${details}`,
603
+ );
604
+ } else {
605
+ const errMsg = checks.remoteHealth.error || `HTTP ${checks.remoteHealth.status}`;
606
+ lines.push(` ${c.error('\u25cf')} ${c.error('unhealthy')} ${c.dim(`(${errMsg})`)}`);
607
+ }
608
+ }
609
+
610
+ // Services
611
+ const enabledServices = [];
612
+ if (envConfig.observability) enabledServices.push('observability');
613
+ if (envConfig.n8n) enabledServices.push('n8n');
614
+ if (envConfig.metabase) enabledServices.push('metabase');
615
+ if (envConfig.ha) enabledServices.push('ha');
616
+ if (envConfig.cicd) enabledServices.push('cicd');
617
+ if (envConfig.services) {
618
+ for (const [key, val] of Object.entries(envConfig.services)) {
619
+ if (val && !enabledServices.includes(key)) enabledServices.push(key);
620
+ }
621
+ }
622
+ if (enabledServices.length > 0) {
623
+ lines.push(
624
+ `${c.dim('Services')} ${enabledServices.map((s) => c.info(s)).join(c.dim(', '))}`,
625
+ );
626
+ }
627
+
628
+ // Cost estimate
629
+ if (servers.length > 0) {
630
+ const cost = calculateEnvCost(envConfig);
631
+ if (cost > 0) {
632
+ lines.push(`${c.dim('Est. Cost')} ${c.boldYellow(`\u20AC${cost.toFixed(2)}/mo`)}`);
633
+ }
634
+ }
635
+
636
+ // Git sync
637
+ if (checks.gitSync) {
638
+ lines.push('');
639
+ lines.push(c.bold('Git Sync'));
640
+ const gs = checks.gitSync;
641
+ if (gs.deployed?.sha) {
642
+ const shortSha = gs.deployed.sha.substring(0, 7);
643
+ const msg = gs.deployed.message ? ` ${c.dim(`"${gs.deployed.message}"`)}` : '';
644
+ lines.push(` ${c.dim('Deployed')} ${c.boldCyan(shortSha)}${msg}`);
645
+ } else {
646
+ lines.push(` ${c.dim('No deployed commit recorded (redeploy to enable tracking)')}`);
647
+ }
648
+ if (gs.current?.sha && gs.deployed?.sha) {
649
+ const shortSha = gs.current.sha.substring(0, 7);
650
+ const msg = gs.current.message ? ` ${c.dim(`"${gs.current.message}"`)}` : '';
651
+ const ahead =
652
+ gs.commitsAhead !== null && gs.commitsAhead > 0
653
+ ? c.warning(` (+${gs.commitsAhead} commit${gs.commitsAhead === 1 ? '' : 's'} ahead)`)
654
+ : gs.commitsAhead === 0
655
+ ? c.success(' (up to date)')
656
+ : '';
657
+ lines.push(` ${c.dim('Current')} ${c.boldCyan(shortSha)}${msg}${ahead}`);
658
+ }
659
+ }
660
+
661
+ p.note(lines.join('\n'), `Environment: ${envName}`);
662
+ }
663
+
664
+ function renderSummary(allData) {
665
+ const lines = [];
666
+
667
+ // Environments
668
+ const envCount = Object.keys(allData.environments || {}).length;
669
+ const unhealthyCount = Object.values(allData.environments || {}).filter(
670
+ (e) => e.checks?.remoteHealth && !e.checks.remoteHealth.ok,
671
+ ).length;
672
+
673
+ if (envCount > 0) {
674
+ const envStr = `${envCount} deployed`;
675
+ const unhealthyStr =
676
+ unhealthyCount > 0
677
+ ? `, ${c.error(`${unhealthyCount} unhealthy`)}`
678
+ : `, ${c.success('0 unhealthy')}`;
679
+ lines.push(`${c.dim('Environments')} ${envStr}${unhealthyStr}`);
680
+ } else {
681
+ lines.push(`${c.dim('Environments')} ${c.dim('none deployed')}`);
682
+ }
683
+
684
+ // Local dev
685
+ if (allData.localDev) {
686
+ const ld = allData.localDev;
687
+ const parts = [];
688
+ if (ld.api.running) parts.push('API');
689
+ if (ld.vite.running) parts.push('Vite');
690
+ const dockerHealthy = ld.docker.filter((s) => s.health === 'healthy').length;
691
+ if (ld.docker.length > 0) parts.push(`Docker ${dockerHealthy}/${ld.docker.length}`);
692
+
693
+ if (
694
+ parts.length > 0 &&
695
+ ld.api.running &&
696
+ ld.vite.running &&
697
+ dockerHealthy === ld.docker.length &&
698
+ ld.docker.length > 0
699
+ ) {
700
+ lines.push(`${c.dim('Local Dev')} ${c.success('All services running')}`);
701
+ } else if (parts.length > 0) {
702
+ lines.push(`${c.dim('Local Dev')} ${parts.join(', ')}`);
703
+ } else {
704
+ lines.push(`${c.dim('Local Dev')} ${c.dim('not running')}`);
705
+ }
706
+ }
707
+
708
+ // Total cost
709
+ const totalCost = calculateMonthlyCost(
710
+ Object.fromEntries(Object.entries(allData.environments || {}).map(([k, v]) => [k, v.config])),
711
+ );
712
+ if (totalCost) {
713
+ lines.push(`${c.dim('Est. Total')} ${c.boldYellow(totalCost)}`);
714
+ }
715
+
716
+ p.note(lines.join('\n'), 'Summary');
717
+ }
718
+
719
+ // ============================================================================
720
+ // GLOBAL STATUS (outside a project directory)
721
+ // ============================================================================
722
+
723
+ async function showGlobalStatus(args) {
724
+ cleanStaleProjects();
725
+ const registry = loadGlobalRegistry();
726
+
727
+ if (args.json) {
728
+ // Enrich each project with config data
729
+ const enriched = registry.projects.map((entry) => {
730
+ const config = loadProjectConfig(entry.path);
731
+ const envNames = config?.environments ? Object.keys(config.environments) : [];
732
+ return { ...entry, environments: envNames };
733
+ });
734
+ console.log(JSON.stringify({ mode: 'global', projects: enriched }, null, 2));
735
+ return;
736
+ }
737
+
738
+ printBanner();
739
+ p.intro(`${c.bold('vibecarbon status')} ${c.dim(`v${VERSION}`)}`);
740
+
741
+ if (registry.projects.length === 0) {
742
+ p.log.info('No registered projects.');
743
+ p.log.info(`Get started: ${c.info('vibecarbon create my-app')}`);
744
+ return;
745
+ }
746
+
747
+ p.log.info(
748
+ `${registry.projects.length} registered project${registry.projects.length === 1 ? '' : 's'}`,
749
+ );
750
+
751
+ for (const entry of registry.projects) {
752
+ const config = loadProjectConfig(entry.path);
753
+ const envNames = config?.environments ? Object.keys(config.environments) : [];
754
+ const envDisplay = envNames.length > 0 ? envNames.join(', ') : c.dim('none');
755
+
756
+ console.log('');
757
+ console.log(` ${c.bold(entry.name)}`);
758
+ console.log(` Path ${c.dim(entry.path)}`);
759
+ console.log(` Environments ${envDisplay}`);
760
+ if (entry.updatedAt) {
761
+ console.log(` Last updated ${formatRelativeTime(entry.updatedAt)}`);
762
+ }
763
+ }
764
+
765
+ console.log('');
766
+ p.log.info(
767
+ `${c.dim('Run')} ${c.info('vibecarbon status')} ${c.dim('from inside a project for detailed info.')}`,
768
+ );
769
+ }
770
+
771
+ // ============================================================================
772
+ // MAIN
773
+ // ============================================================================
774
+
775
+ async function main() {
776
+ const argv = process.argv.slice(2);
777
+ const { values, errors } = parseFlags(argv, SPEC);
778
+
779
+ if (errors.length > 0) {
780
+ for (const e of errors) {
781
+ process.stderr.write(`${c.error('✗')} ${e}\n`);
782
+ }
783
+ process.stderr.write(`Run ${c.info('vibecarbon status -h')} for usage.\n`);
784
+ process.exit(1);
785
+ }
786
+
787
+ if (values.h) {
788
+ process.stdout.write(renderHelp(SPEC));
789
+ process.exit(0);
790
+ }
791
+
792
+ if (values.v) {
793
+ console.log(`vibecarbon-status v${VERSION}`);
794
+ process.exit(0);
795
+ }
796
+
797
+ // Translate to legacy args shape so the rest of main() doesn't need
798
+ // to change. `noLocal` is now context-sensitive: skip local-dev checks
799
+ // when output is JSON (the consumer is a script, not a human) or when
800
+ // stdout isn't a TTY (CI / piped). Operators who want explicit local
801
+ // checks can still get them by running on a TTY without `-json`.
802
+ const args = {
803
+ env: /** @type {string|null} */ (values.env),
804
+ json: !!values.json,
805
+ noLocal: !!values.json || !process.stdout.isTTY,
806
+ };
807
+
808
+ let cwd;
809
+ try {
810
+ cwd = process.cwd();
811
+ } catch {
812
+ if (args.json) {
813
+ console.log(JSON.stringify({ error: 'Current working directory does not exist' }));
814
+ } else {
815
+ console.error(`\n${c.error('Error:')} Current working directory does not exist.`);
816
+ }
817
+ process.exit(1);
818
+ }
819
+
820
+ const projectConfig = loadProjectConfig(cwd);
821
+ if (!projectConfig) {
822
+ await showGlobalStatus(args);
823
+ return;
824
+ }
825
+
826
+ const environments = projectConfig.environments || {};
827
+
828
+ // Filter to specific environment if requested
829
+ let envEntries = Object.entries(environments);
830
+ if (args.env) {
831
+ const filtered = envEntries.filter(([name]) => name === args.env);
832
+ if (filtered.length === 0) {
833
+ if (args.json) {
834
+ console.log(JSON.stringify({ error: `Environment '${args.env}' not found` }));
835
+ } else {
836
+ printBanner();
837
+ p.intro(`${c.bold('vibecarbon status')} ${c.dim(`v${VERSION}`)}`);
838
+ p.log.error(`Environment '${args.env}' not found`);
839
+ if (envEntries.length > 0) {
840
+ p.log.info(`Available environments: ${envEntries.map(([n]) => n).join(', ')}`);
841
+ }
842
+ }
843
+ process.exit(1);
844
+ }
845
+ envEntries = filtered;
846
+ }
847
+
848
+ // Gather all data in parallel
849
+ const allData = {
850
+ projectName: projectConfig.projectName,
851
+ localDev: null,
852
+ environments: {},
853
+ };
854
+
855
+ // Local dev check (skipped in JSON / non-TTY contexts — see args above).
856
+ const localDevPromise = args.noLocal
857
+ ? Promise.resolve(null)
858
+ : checkLocalDev(projectConfig.projectName);
859
+
860
+ // Environment checks in parallel
861
+ const envChecksPromise = Promise.allSettled(
862
+ envEntries.map(async ([envName, envConfig]) => {
863
+ const checks = {};
864
+
865
+ // Remote health check
866
+ if (envConfig.domain) {
867
+ checks.remoteHealth = await checkRemoteHealth(envConfig.domain);
868
+ }
869
+
870
+ // Server info from Hetzner API
871
+ const servers = envConfig.servers || [];
872
+ if (servers.length > 0 && process.env.HETZNER_API_TOKEN) {
873
+ const serverInfoResults = await Promise.allSettled(servers.map((s) => getServerInfo(s.id)));
874
+ checks.serverInfo = {};
875
+ servers.forEach((s, i) => {
876
+ const result = serverInfoResults[i];
877
+ if (result.status === 'fulfilled' && result.value) {
878
+ checks.serverInfo[s.id] = result.value;
879
+ }
880
+ });
881
+ }
882
+
883
+ // Git sync
884
+ checks.gitSync = checkGitSync(envName, envConfig);
885
+
886
+ return { envName, config: envConfig, checks };
887
+ }),
888
+ );
889
+
890
+ const [localDevResult, envChecksResults] = await Promise.all([localDevPromise, envChecksPromise]);
891
+
892
+ allData.localDev = localDevResult;
893
+
894
+ for (const result of envChecksResults) {
895
+ if (result.status === 'fulfilled') {
896
+ const { envName, config, checks } = result.value;
897
+ allData.environments[envName] = { config, checks };
898
+ }
899
+ }
900
+
901
+ // Output
902
+ if (args.json) {
903
+ console.log(JSON.stringify(allData, null, 2));
904
+ process.exit(0);
905
+ }
906
+
907
+ // Rendered output
908
+ printBanner();
909
+ p.intro(`${c.bold('vibecarbon status')} ${c.dim(`v${VERSION}`)}`);
910
+ p.log.info(`Project: ${c.bold(projectConfig.projectName)}`);
911
+
912
+ // Operator-IP access (H-2). One-line summary: how many CIDRs are in the
913
+ // allowlist. We don't auto-detect here — status is a passive read of
914
+ // local files and HTTP probes; firing api.ipify.org on every status
915
+ // call would be surprising. Pointer to `vibecarbon access` is enough.
916
+ const cidrs = projectConfig.operatorCidrs ?? [];
917
+ if (cidrs.length === 0) {
918
+ p.log.warn(
919
+ `Access: no operator CIDRs configured — run ${c.info('vibecarbon access add <cidr>')} or set ALLOWED_SSH_IPS before deploy.`,
920
+ );
921
+ } else {
922
+ p.log.info(
923
+ `Access: ${cidrs.length} CIDR${cidrs.length === 1 ? '' : 's'} in allowlist — see ${c.info('vibecarbon access')} for details.`,
924
+ );
925
+ }
926
+
927
+ // Local dev
928
+ if (allData.localDev) {
929
+ renderLocalDev(allData.localDev);
930
+ }
931
+
932
+ // Environments
933
+ for (const [envName, envData] of Object.entries(allData.environments)) {
934
+ renderEnvironment(envName, envData.config, envData.checks);
935
+ }
936
+
937
+ if (envEntries.length === 0 && !args.env) {
938
+ p.log.info(
939
+ `${c.dim('No environments deployed yet. Run')} ${c.info('vibecarbon deploy')} ${c.dim('to get started.')}`,
940
+ );
941
+ }
942
+
943
+ // Summary
944
+ renderSummary(allData);
945
+ p.outro(c.dim(`Status as of ${new Date().toISOString()}`));
946
+ }
947
+
948
+ // ============================================================================
949
+ // RUN FUNCTION (called by CLI entry point)
950
+ // ============================================================================
951
+
952
+ export async function run(args) {
953
+ const originalArgv = process.argv;
954
+ process.argv = ['node', 'status', ...args];
955
+ try {
956
+ await main();
957
+ } finally {
958
+ process.argv = originalArgv;
959
+ }
960
+ }
961
+
962
+ // ============================================================================
963
+ // EXPORTS FOR TESTING
964
+ // ============================================================================
965
+
966
+ export {
967
+ calculateEnvCost,
968
+ calculateMonthlyCost,
969
+ checkDockerContainers,
970
+ checkGitSync,
971
+ checkHttpEndpoint,
972
+ checkLocalDev,
973
+ checkRemoteHealth,
974
+ formatRelativeTime,
975
+ getBranchName,
976
+ getServerInfo,
977
+ main,
978
+ SPEC,
979
+ showGlobalStatus,
980
+ VERSION,
981
+ };