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.
- package/LICENSE +663 -0
- package/README.md +188 -0
- package/carbon/.claude/hooks/task-completed-gate.sh +19 -0
- package/carbon/.claude/hooks/teammate-idle-gate.sh +29 -0
- package/carbon/.claude/settings.json +29 -0
- package/carbon/.cursor/rules/vibecarbon.mdc +6 -0
- package/carbon/.dockerignore +57 -0
- package/carbon/.env.example +219 -0
- package/carbon/.github/copilot-instructions.md +3 -0
- package/carbon/.github/workflows/deploy.yml +346 -0
- package/carbon/.github/workflows/vibecarbon-build.yml +81 -0
- package/carbon/.windsurfrules +74 -0
- package/carbon/AGENTS.md +422 -0
- package/carbon/CLAUDE.md +3 -0
- package/carbon/DEVELOPMENT.md +187 -0
- package/carbon/Dockerfile +98 -0
- package/carbon/LICENSE +21 -0
- package/carbon/PRODUCTION.md +364 -0
- package/carbon/README.md +193 -0
- package/carbon/TESTING.md +138 -0
- package/carbon/backup/Dockerfile +75 -0
- package/carbon/backup/backup.sh +95 -0
- package/carbon/backup/compose-backup.sh +140 -0
- package/carbon/biome.json +83 -0
- package/carbon/cloud-init/docker-ce-setup.yaml +55 -0
- package/carbon/cloud-init/k3s/master-init.sh +215 -0
- package/carbon/cloud-init/k3s/supabase-init.sh +167 -0
- package/carbon/cloud-init/k3s/worker-init.sh +147 -0
- package/carbon/components.json +24 -0
- package/carbon/content/blog/authentication-guide.mdx +34 -0
- package/carbon/content/blog/getting-started.mdx +41 -0
- package/carbon/content/changelog/v0-1-0.mdx +40 -0
- package/carbon/content/docs/analytics.mdx +90 -0
- package/carbon/content/docs/authentication.mdx +231 -0
- package/carbon/content/docs/background-jobs.mdx +116 -0
- package/carbon/content/docs/cli.mdx +630 -0
- package/carbon/content/docs/database.mdx +236 -0
- package/carbon/content/docs/deployment.mdx +227 -0
- package/carbon/content/docs/development.mdx +238 -0
- package/carbon/content/docs/environments.mdx +84 -0
- package/carbon/content/docs/getting-started.mdx +112 -0
- package/carbon/content/docs/legal/privacy-policy.mdx +89 -0
- package/carbon/content/docs/legal/terms-of-service.mdx +99 -0
- package/carbon/content/docs/optional-services.mdx +160 -0
- package/carbon/db/Dockerfile +23 -0
- package/carbon/docker-compose.dev-init.yml +5 -0
- package/carbon/docker-compose.metabase.override.yml +14 -0
- package/carbon/docker-compose.metabase.prod.yml +28 -0
- package/carbon/docker-compose.metabase.yml +84 -0
- package/carbon/docker-compose.n8n.override.yml +14 -0
- package/carbon/docker-compose.n8n.prod.yml +31 -0
- package/carbon/docker-compose.n8n.yml +97 -0
- package/carbon/docker-compose.observability.override.yml +18 -0
- package/carbon/docker-compose.observability.prod.yml +28 -0
- package/carbon/docker-compose.observability.yml +125 -0
- package/carbon/docker-compose.override.yml +28 -0
- package/carbon/docker-compose.prod.yml +294 -0
- package/carbon/docker-compose.yml +508 -0
- package/carbon/docker-entrypoint.sh +12 -0
- package/carbon/ha/activate-standby.sh +52 -0
- package/carbon/ha/primary-init.sql +36 -0
- package/carbon/ha/standby-init.sh +74 -0
- package/carbon/k8s/LICENSE +99 -0
- package/carbon/k8s/README.md +272 -0
- package/carbon/k8s/base/app/deployment.yaml +130 -0
- package/carbon/k8s/base/app/hpa.yaml +44 -0
- package/carbon/k8s/base/app/kustomization.yaml +10 -0
- package/carbon/k8s/base/app/network-policy.yaml +124 -0
- package/carbon/k8s/base/app/pdb.yaml +14 -0
- package/carbon/k8s/base/app/rbac.yaml +36 -0
- package/carbon/k8s/base/app/service.yaml +16 -0
- package/carbon/k8s/base/backup/cronjob.yaml +120 -0
- package/carbon/k8s/base/backup/kustomization.yaml +7 -0
- package/carbon/k8s/base/backup/network-policy.yaml +31 -0
- package/carbon/k8s/base/backup/rbac.yaml +7 -0
- package/carbon/k8s/base/cluster-autoscaler/deployment.yaml +103 -0
- package/carbon/k8s/base/cluster-autoscaler/kustomization.yaml +17 -0
- package/carbon/k8s/base/cluster-autoscaler/rbac.yaml +109 -0
- package/carbon/k8s/base/config/configmap.yaml +48 -0
- package/carbon/k8s/base/config/kustomization.yaml +8 -0
- package/carbon/k8s/base/hetzner-ccm/kustomization.yaml +9 -0
- package/carbon/k8s/base/hetzner-csi/kustomization.yaml +9 -0
- package/carbon/k8s/base/kustomization.yaml +43 -0
- package/carbon/k8s/base/namespace.yaml +7 -0
- package/carbon/k8s/base/network-policies.yaml +95 -0
- package/carbon/k8s/base/registry/kustomization.yaml +5 -0
- package/carbon/k8s/base/registry/local-registry.yaml +193 -0
- package/carbon/k8s/base/traefik/certificate.yaml +21 -0
- package/carbon/k8s/base/traefik/configmap.yaml +13 -0
- package/carbon/k8s/base/traefik/deployment.yaml +165 -0
- package/carbon/k8s/base/traefik/ingressroute.yaml +141 -0
- package/carbon/k8s/base/traefik/kustomization.yaml +11 -0
- package/carbon/k8s/base/traefik/middleware.yaml +109 -0
- package/carbon/k8s/base/traefik/network-policy.yaml +92 -0
- package/carbon/k8s/base/traefik/service.yaml +38 -0
- package/carbon/k8s/flux/README.md +49 -0
- package/carbon/k8s/flux/clusters/primary/vibecarbon.yaml +128 -0
- package/carbon/k8s/flux/clusters/standby/vibecarbon.yaml +83 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-release.yaml +38 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-repository.yaml +16 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/kustomization.yaml +10 -0
- package/carbon/k8s/gitops/supabase/helm-release.yaml +66 -0
- package/carbon/k8s/gitops/supabase/helm-repository.yaml +18 -0
- package/carbon/k8s/gitops/supabase/kustomization.yaml +33 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-cloudflare.yaml +51 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-hetzner.yaml +59 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-manual.yaml +43 -0
- package/carbon/k8s/infra/cert-manager-resources/kustomization.yaml +15 -0
- package/carbon/k8s/infra/kustomization.yaml +21 -0
- package/carbon/k8s/infra/traefik-crds/kustomization.yaml +10 -0
- package/carbon/k8s/overlays/local/configmap.yaml +23 -0
- package/carbon/k8s/overlays/local/ingressroute-studio.yaml +104 -0
- package/carbon/k8s/overlays/local/kustomization.yaml +215 -0
- package/carbon/k8s/overlays/local/namespace.yaml +8 -0
- package/carbon/k8s/overlays/local/secrets.yaml +32 -0
- package/carbon/k8s/overlays/production/kustomization.yaml +34 -0
- package/carbon/k8s/test-local.sh +318 -0
- package/carbon/k8s/values/supabase.values.yaml +133 -0
- package/carbon/package.json +154 -0
- package/carbon/runtime.Dockerfile +17 -0
- package/carbon/scripts/_dev-jwt.mjs +45 -0
- package/carbon/scripts/check-rls.ts +53 -0
- package/carbon/scripts/dev-init.js +191 -0
- package/carbon/scripts/dev.js +191 -0
- package/carbon/scripts/docker-down.js +63 -0
- package/carbon/scripts/docker-logs.js +59 -0
- package/carbon/scripts/docker-up.js +222 -0
- package/carbon/scripts/generate-dev-configs.sh +131 -0
- package/carbon/scripts/generate-rss.ts +116 -0
- package/carbon/scripts/generate-sitemap.ts +102 -0
- package/carbon/scripts/k8s-apply.js +71 -0
- package/carbon/scripts/k8s-delete.js +75 -0
- package/carbon/scripts/lib/manifest.js +176 -0
- package/carbon/scripts/secret-scan.mjs +278 -0
- package/carbon/scripts/validate-dev-configs.sh +101 -0
- package/carbon/src/client/App.tsx +202 -0
- package/carbon/src/client/assets/hyperformant-dark.svg +29 -0
- package/carbon/src/client/assets/hyperformant-light.svg +29 -0
- package/carbon/src/client/assets/logos/aider.svg +1 -0
- package/carbon/src/client/assets/logos/antigravity.svg +1 -0
- package/carbon/src/client/assets/logos/bolt.svg +1 -0
- package/carbon/src/client/assets/logos/claude-code.svg +1 -0
- package/carbon/src/client/assets/logos/copilot.svg +1 -0
- package/carbon/src/client/assets/logos/cursor.svg +1 -0
- package/carbon/src/client/assets/logos/gemini-cli.svg +1 -0
- package/carbon/src/client/assets/logos/lovable.svg +1 -0
- package/carbon/src/client/assets/logos/openai-codex.svg +1 -0
- package/carbon/src/client/assets/logos/v0.svg +1 -0
- package/carbon/src/client/assets/logos/windsurf.svg +1 -0
- package/carbon/src/client/assets/vibecarbon-icon.svg +19 -0
- package/carbon/src/client/assets/vibecarbon-logo-dark.svg +29 -0
- package/carbon/src/client/assets/vibecarbon-logo-light.svg +29 -0
- package/carbon/src/client/components/AIIntegrationSection.tsx +120 -0
- package/carbon/src/client/components/AppSidebar.tsx +760 -0
- package/carbon/src/client/components/ArchitectureSection.tsx +46 -0
- package/carbon/src/client/components/CTAFooter.tsx +59 -0
- package/carbon/src/client/components/ComparisonSection.tsx +132 -0
- package/carbon/src/client/components/ContentPanel.tsx +66 -0
- package/carbon/src/client/components/ContentSkeleton.tsx +21 -0
- package/carbon/src/client/components/ErrorBoundary.tsx +46 -0
- package/carbon/src/client/components/FAQSection.tsx +76 -0
- package/carbon/src/client/components/FileUpload.tsx +210 -0
- package/carbon/src/client/components/HeaderActions.tsx +17 -0
- package/carbon/src/client/components/Hero.tsx +608 -0
- package/carbon/src/client/components/ImpersonationBanner.tsx +31 -0
- package/carbon/src/client/components/LanguageSwitcher.tsx +67 -0
- package/carbon/src/client/components/Logo.tsx +87 -0
- package/carbon/src/client/components/LogoStrip.tsx +76 -0
- package/carbon/src/client/components/MetricsStrip.tsx +48 -0
- package/carbon/src/client/components/Nav.tsx +195 -0
- package/carbon/src/client/components/NewsletterSignup.tsx +147 -0
- package/carbon/src/client/components/NotificationDrawer.tsx +171 -0
- package/carbon/src/client/components/PageHeader.tsx +24 -0
- package/carbon/src/client/components/PlanGate.tsx +36 -0
- package/carbon/src/client/components/PricingSection.tsx +371 -0
- package/carbon/src/client/components/SEO.tsx +34 -0
- package/carbon/src/client/components/SmoothScroll.tsx +45 -0
- package/carbon/src/client/components/TechStackSection.tsx +165 -0
- package/carbon/src/client/components/WorkflowSection.tsx +604 -0
- package/carbon/src/client/components/admin/DockerLogs.tsx +276 -0
- package/carbon/src/client/components/admin/PerformanceMetrics.tsx +230 -0
- package/carbon/src/client/components/admin/ServiceCard.tsx +45 -0
- package/carbon/src/client/components/admin/ServicesStatus.tsx +296 -0
- package/carbon/src/client/components/architecture/ArchitectureDiagram.tsx +413 -0
- package/carbon/src/client/components/auth/AuthProvider.tsx +466 -0
- package/carbon/src/client/components/effects/FilmGrainOverlay.tsx +35 -0
- package/carbon/src/client/components/effects/FresnelEdge.tsx +49 -0
- package/carbon/src/client/components/effects/GlowTracker.tsx +46 -0
- package/carbon/src/client/components/effects/SparkBurst.tsx +145 -0
- package/carbon/src/client/components/effects/VGlowEffect.tsx +83 -0
- package/carbon/src/client/components/effects/index.ts +2 -0
- package/carbon/src/client/components/layouts/SidebarLayout.tsx +20 -0
- package/carbon/src/client/components/scroll/ScrollSection.tsx +76 -0
- package/carbon/src/client/components/scroll/ScrollytellingProvider.tsx +81 -0
- package/carbon/src/client/components/scroll/index.ts +2 -0
- package/carbon/src/client/components/ui/accordion.tsx +71 -0
- package/carbon/src/client/components/ui/alert-dialog.tsx +162 -0
- package/carbon/src/client/components/ui/alert.tsx +73 -0
- package/carbon/src/client/components/ui/aspect-ratio.tsx +22 -0
- package/carbon/src/client/components/ui/avatar.tsx +91 -0
- package/carbon/src/client/components/ui/badge.tsx +50 -0
- package/carbon/src/client/components/ui/breadcrumb.tsx +100 -0
- package/carbon/src/client/components/ui/button-group.tsx +78 -0
- package/carbon/src/client/components/ui/button.tsx +120 -0
- package/carbon/src/client/components/ui/calendar.tsx +182 -0
- package/carbon/src/client/components/ui/card.tsx +85 -0
- package/carbon/src/client/components/ui/carousel.tsx +227 -0
- package/carbon/src/client/components/ui/chart.tsx +357 -0
- package/carbon/src/client/components/ui/checkbox.tsx +27 -0
- package/carbon/src/client/components/ui/collapsible.tsx +15 -0
- package/carbon/src/client/components/ui/command.tsx +178 -0
- package/carbon/src/client/components/ui/context-menu.tsx +233 -0
- package/carbon/src/client/components/ui/dialog.tsx +132 -0
- package/carbon/src/client/components/ui/drawer.tsx +118 -0
- package/carbon/src/client/components/ui/dropdown-menu.tsx +242 -0
- package/carbon/src/client/components/ui/empty.tsx +94 -0
- package/carbon/src/client/components/ui/field.tsx +226 -0
- package/carbon/src/client/components/ui/hover-card.tsx +44 -0
- package/carbon/src/client/components/ui/input-group.tsx +146 -0
- package/carbon/src/client/components/ui/input-otp.tsx +83 -0
- package/carbon/src/client/components/ui/input.tsx +20 -0
- package/carbon/src/client/components/ui/item.tsx +188 -0
- package/carbon/src/client/components/ui/kbd.tsx +26 -0
- package/carbon/src/client/components/ui/label.tsx +21 -0
- package/carbon/src/client/components/ui/menubar.tsx +250 -0
- package/carbon/src/client/components/ui/navigation-menu.tsx +155 -0
- package/carbon/src/client/components/ui/pagination.tsx +102 -0
- package/carbon/src/client/components/ui/popover.tsx +75 -0
- package/carbon/src/client/components/ui/progress.tsx +66 -0
- package/carbon/src/client/components/ui/radio-group.tsx +36 -0
- package/carbon/src/client/components/ui/resizable.tsx +46 -0
- package/carbon/src/client/components/ui/scroll-area.tsx +48 -0
- package/carbon/src/client/components/ui/select.tsx +186 -0
- package/carbon/src/client/components/ui/separator.tsx +21 -0
- package/carbon/src/client/components/ui/sheet.tsx +124 -0
- package/carbon/src/client/components/ui/sidebar.tsx +702 -0
- package/carbon/src/client/components/ui/skeleton.tsx +13 -0
- package/carbon/src/client/components/ui/slider.tsx +57 -0
- package/carbon/src/client/components/ui/sonner.tsx +45 -0
- package/carbon/src/client/components/ui/spinner.tsx +15 -0
- package/carbon/src/client/components/ui/switch.tsx +30 -0
- package/carbon/src/client/components/ui/table.tsx +89 -0
- package/carbon/src/client/components/ui/tabs.tsx +73 -0
- package/carbon/src/client/components/ui/textarea.tsx +18 -0
- package/carbon/src/client/components/ui/toggle-group.tsx +87 -0
- package/carbon/src/client/components/ui/toggle.tsx +44 -0
- package/carbon/src/client/components/ui/tooltip.tsx +56 -0
- package/carbon/src/client/hooks/api/index.ts +14 -0
- package/carbon/src/client/hooks/api/useAuthSettings.ts +80 -0
- package/carbon/src/client/hooks/api/useSubscription.ts +87 -0
- package/carbon/src/client/hooks/use-mobile.ts +21 -0
- package/carbon/src/client/hooks/useMousePosition.ts +91 -0
- package/carbon/src/client/hooks/useNotifications.tsx +124 -0
- package/carbon/src/client/hooks/useOrganizationMembers.ts +127 -0
- package/carbon/src/client/hooks/useOrganizations.tsx +230 -0
- package/carbon/src/client/hooks/usePackageManager.ts +69 -0
- package/carbon/src/client/hooks/useReducedMotion.ts +20 -0
- package/carbon/src/client/hooks/useRunningServices.ts +114 -0
- package/carbon/src/client/hooks/useScrollProgress.ts +56 -0
- package/carbon/src/client/index.css +467 -0
- package/carbon/src/client/index.html +56 -0
- package/carbon/src/client/lib/admin-services.ts +151 -0
- package/carbon/src/client/lib/api.ts +32 -0
- package/carbon/src/client/lib/blog.ts +35 -0
- package/carbon/src/client/lib/changelog.ts +33 -0
- package/carbon/src/client/lib/docs-search.ts +101 -0
- package/carbon/src/client/lib/docs.ts +37 -0
- package/carbon/src/client/lib/i18n.ts +32 -0
- package/carbon/src/client/lib/supabase.ts +72 -0
- package/carbon/src/client/lib/tailwind-colors.ts +357 -0
- package/carbon/src/client/lib/theme.ts +117 -0
- package/carbon/src/client/lib/utils.ts +22 -0
- package/carbon/src/client/locales/de.json +529 -0
- package/carbon/src/client/locales/en.json +461 -0
- package/carbon/src/client/locales/es.json +529 -0
- package/carbon/src/client/locales/fr.json +529 -0
- package/carbon/src/client/locales/pt.json +529 -0
- package/carbon/src/client/main.tsx +56 -0
- package/carbon/src/client/mdx.d.ts +13 -0
- package/carbon/src/client/pages/ApiDocs.tsx +76 -0
- package/carbon/src/client/pages/AuthCallback.tsx +34 -0
- package/carbon/src/client/pages/Blog.tsx +167 -0
- package/carbon/src/client/pages/Changelog.tsx +171 -0
- package/carbon/src/client/pages/Charts.tsx +388 -0
- package/carbon/src/client/pages/Checkout.tsx +227 -0
- package/carbon/src/client/pages/Contact.tsx +174 -0
- package/carbon/src/client/pages/Dashboard.tsx +368 -0
- package/carbon/src/client/pages/Docs.tsx +372 -0
- package/carbon/src/client/pages/ForgotPassword.tsx +111 -0
- package/carbon/src/client/pages/Home.tsx +187 -0
- package/carbon/src/client/pages/Legal.tsx +100 -0
- package/carbon/src/client/pages/Login.tsx +408 -0
- package/carbon/src/client/pages/MFAVerify.tsx +156 -0
- package/carbon/src/client/pages/NotFound.tsx +21 -0
- package/carbon/src/client/pages/Onboarding.tsx +246 -0
- package/carbon/src/client/pages/ResetPassword.tsx +200 -0
- package/carbon/src/client/pages/UIComponents.tsx +390 -0
- package/carbon/src/client/pages/admin/ContactSubmissions.tsx +220 -0
- package/carbon/src/client/pages/admin/Dashboard.tsx +24 -0
- package/carbon/src/client/pages/admin/Infrastructure.tsx +257 -0
- package/carbon/src/client/pages/admin/Jobs.tsx +225 -0
- package/carbon/src/client/pages/admin/Logs.tsx +18 -0
- package/carbon/src/client/pages/admin/Newsletter.tsx +300 -0
- package/carbon/src/client/pages/admin/Notifications.tsx +603 -0
- package/carbon/src/client/pages/admin/Organizations.tsx +306 -0
- package/carbon/src/client/pages/admin/Settings.tsx +314 -0
- package/carbon/src/client/pages/admin/Theme.tsx +465 -0
- package/carbon/src/client/pages/admin/Users.tsx +365 -0
- package/carbon/src/client/pages/api-docs.css +156 -0
- package/carbon/src/client/pages/organizations/Details.tsx +200 -0
- package/carbon/src/client/pages/organizations/Members.tsx +402 -0
- package/carbon/src/client/pages/settings/Billing.tsx +473 -0
- package/carbon/src/client/pages/settings/Profile.tsx +160 -0
- package/carbon/src/client/pages/settings/Security.tsx +341 -0
- package/carbon/src/client/public/favicon.svg +19 -0
- package/carbon/src/client/public/robots.txt +8 -0
- package/carbon/src/server/billing/index.ts +104 -0
- package/carbon/src/server/billing/provider.ts +81 -0
- package/carbon/src/server/billing/providers/paddle.ts +314 -0
- package/carbon/src/server/billing/providers/polar.ts +325 -0
- package/carbon/src/server/billing/providers/stripe.ts +233 -0
- package/carbon/src/server/emails/templates.ts +116 -0
- package/carbon/src/server/index.ts +554 -0
- package/carbon/src/server/lib/auth.ts +6 -0
- package/carbon/src/server/lib/email.ts +64 -0
- package/carbon/src/server/lib/env.ts +112 -0
- package/carbon/src/server/lib/errors.ts +21 -0
- package/carbon/src/server/lib/logger.ts +17 -0
- package/carbon/src/server/lib/rate-limiter.ts +288 -0
- package/carbon/src/server/lib/request.ts +34 -0
- package/carbon/src/server/lib/stripe.ts +42 -0
- package/carbon/src/server/lib/supabase.ts +65 -0
- package/carbon/src/server/middleware/requirePlan.ts +80 -0
- package/carbon/src/server/routes/_internal/services-status.ts +958 -0
- package/carbon/src/server/routes/_internal/verify-role.ts +185 -0
- package/carbon/src/server/routes/health.ts +48 -0
- package/carbon/src/server/routes/v1/admin/contact.ts +128 -0
- package/carbon/src/server/routes/v1/admin/jobs.ts +171 -0
- package/carbon/src/server/routes/v1/admin/newsletter.ts +237 -0
- package/carbon/src/server/routes/v1/auth.ts +390 -0
- package/carbon/src/server/routes/v1/billing.ts +718 -0
- package/carbon/src/server/routes/v1/contact.ts +93 -0
- package/carbon/src/server/routes/v1/index.ts +1333 -0
- package/carbon/src/server/routes/v1/newsletter.ts +181 -0
- package/carbon/src/server/routes/v1/performance.ts +157 -0
- package/carbon/src/server/routes/v1/stats.ts +170 -0
- package/carbon/src/server/routes/v1/theme.ts +106 -0
- package/carbon/src/server/routes/webhooks/billing.ts +376 -0
- package/carbon/src/server/routes/webhooks/stripe.ts +276 -0
- package/carbon/src/server/types.ts +11 -0
- package/carbon/src/shared/pricing.ts +155 -0
- package/carbon/src/shared/types.ts +338 -0
- package/carbon/supabase/migrations/00001_init.sql +717 -0
- package/carbon/supabase/migrations/00002_theme_settings.sql +13 -0
- package/carbon/supabase/migrations/00003_pg_cron.sql +121 -0
- package/carbon/supabase/migrations/00004_contact_newsletter.sql +81 -0
- package/carbon/supabase/migrations/00005_localization_languages.sql +22 -0
- package/carbon/supabase/seed.sql +16 -0
- package/carbon/tests/_helpers/app.ts +45 -0
- package/carbon/tests/_helpers/env.ts +37 -0
- package/carbon/tests/_helpers/factories.ts +69 -0
- package/carbon/tests/_helpers/jwt.ts +23 -0
- package/carbon/tests/_helpers/setup-integration.ts +20 -0
- package/carbon/tests/_helpers/setup-rtl.ts +12 -0
- package/carbon/tests/component/ErrorBoundary.test.tsx +53 -0
- package/carbon/tests/component/use-auth-settings.test.tsx +119 -0
- package/carbon/tests/integration/server/routes/contact.test.ts +162 -0
- package/carbon/tests/integration/server/routes/health.test.ts +61 -0
- package/carbon/tests/structural/i18n-parity.test.ts +42 -0
- package/carbon/tests/unit/client/utils.test.ts +49 -0
- package/carbon/tests/unit/shared/pricing.test.ts +93 -0
- package/carbon/tsconfig.json +27 -0
- package/carbon/tsconfig.server.json +27 -0
- package/carbon/tsconfig.test.json +9 -0
- package/carbon/vite.config.ts +110 -0
- package/carbon/vitest.config.ts +74 -0
- package/carbon/volumes/db/jwt.sql +57 -0
- package/carbon/volumes/db/metabase-init.sh +29 -0
- package/carbon/volumes/db/n8n-init.sh +25 -0
- package/carbon/volumes/db/realtime.sql +33 -0
- package/carbon/volumes/db/roles.sql +93 -0
- package/carbon/volumes/db/set-passwords.sh +12 -0
- package/carbon/volumes/db/super-admin.dev.sql +113 -0
- package/carbon/volumes/db/super-admin.generated.sql +113 -0
- package/carbon/volumes/db/super-admin.sql +114 -0
- package/carbon/volumes/grafana/dashboards/logs.json +179 -0
- package/carbon/volumes/grafana/dashboards/overview.json +523 -0
- package/carbon/volumes/grafana/dashboards/postgresql.json +337 -0
- package/carbon/volumes/grafana/dashboards.dev/logs.json +179 -0
- package/carbon/volumes/grafana/dashboards.dev/overview.json +156 -0
- package/carbon/volumes/grafana/dashboards.dev/postgresql.json +337 -0
- package/carbon/volumes/grafana/provisioning/dashboards/dashboards.dev.yml +16 -0
- package/carbon/volumes/grafana/provisioning/dashboards/dashboards.yml +16 -0
- package/carbon/volumes/grafana/provisioning/datasources/datasources.yml +43 -0
- package/carbon/volumes/kong/docker-entrypoint.sh +9 -0
- package/carbon/volumes/kong/kong.yml +208 -0
- package/carbon/volumes/loki/loki-config.yml +58 -0
- package/carbon/volumes/n8n/hooks.js +131 -0
- package/carbon/volumes/n8n/scripts/setup.sh +66 -0
- package/carbon/volumes/prometheus/prometheus.yml +43 -0
- package/carbon/volumes/promtail/promtail-config.yml +64 -0
- package/carbon/volumes/traefik/middlewares.dev.yml +83 -0
- package/carbon/volumes/traefik/middlewares.yml +95 -0
- package/carbon/volumes/traefik/vite-dev.yml +20 -0
- package/package.json +95 -0
- package/src/access.js +354 -0
- package/src/activate.js +187 -0
- package/src/add.js +718 -0
- package/src/backup.js +786 -0
- package/src/cli.js +350 -0
- package/src/configure.js +967 -0
- package/src/console.js +155 -0
- package/src/create.js +1499 -0
- package/src/deploy.js +311 -0
- package/src/destroy.js +2033 -0
- package/src/diagnose.js +735 -0
- package/src/down.js +80 -0
- package/src/failover.js +1032 -0
- package/src/lib/backup-s3.js +179 -0
- package/src/lib/build.js +33 -0
- package/src/lib/checksum.js +28 -0
- package/src/lib/ci-setup.js +666 -0
- package/src/lib/cli/help.js +129 -0
- package/src/lib/cli/parse-flags.js +160 -0
- package/src/lib/cli/select-action.js +65 -0
- package/src/lib/cli/select-environment.js +108 -0
- package/src/lib/cli/tty-guard.js +75 -0
- package/src/lib/cloudflare.js +447 -0
- package/src/lib/colors.js +52 -0
- package/src/lib/command.js +361 -0
- package/src/lib/config.js +359 -0
- package/src/lib/cost.js +103 -0
- package/src/lib/deploy/bundle.js +231 -0
- package/src/lib/deploy/compose/acme-verify.js +121 -0
- package/src/lib/deploy/compose/build-args.js +78 -0
- package/src/lib/deploy/compose/ha.js +1874 -0
- package/src/lib/deploy/compose/index.js +1294 -0
- package/src/lib/deploy/compose/reconcile.js +74 -0
- package/src/lib/deploy/github.js +382 -0
- package/src/lib/deploy/image.js +191 -0
- package/src/lib/deploy/index.js +119 -0
- package/src/lib/deploy/k8s/LICENSE +99 -0
- package/src/lib/deploy/k8s/gitops-deploy.js +298 -0
- package/src/lib/deploy/k8s/ha/index.js +1000 -0
- package/src/lib/deploy/k8s/index.js +62 -0
- package/src/lib/deploy/k8s/k3s.js +2515 -0
- package/src/lib/deploy/orchestrator.js +1401 -0
- package/src/lib/deploy/prompts.js +545 -0
- package/src/lib/deploy/remote-build.js +130 -0
- package/src/lib/deploy/state.js +120 -0
- package/src/lib/deploy/utils.js +328 -0
- package/src/lib/deploy-logger.js +93 -0
- package/src/lib/dns-propagation.js +48 -0
- package/src/lib/environment.js +58 -0
- package/src/lib/fetch-retry.js +103 -0
- package/src/lib/github-environments.js +335 -0
- package/src/lib/hetzner-dns.js +377 -0
- package/src/lib/hetzner-guided-setup.js +275 -0
- package/src/lib/host-keys.js +37 -0
- package/src/lib/iac/cloud-init.js +52 -0
- package/src/lib/iac/index.js +325 -0
- package/src/lib/iac/programs/hetzner-compose.js +130 -0
- package/src/lib/iac/programs/hetzner-k8s.js +320 -0
- package/src/lib/kubectl.js +81 -0
- package/src/lib/licensing/index.js +259 -0
- package/src/lib/licensing/tiers.js +181 -0
- package/src/lib/licensing/validator.js +171 -0
- package/src/lib/merge-package-json.js +90 -0
- package/src/lib/operator-ip.js +381 -0
- package/src/lib/package-manager.js +111 -0
- package/src/lib/perf.js +71 -0
- package/src/lib/project-guard.js +39 -0
- package/src/lib/project.js +334 -0
- package/src/lib/providers/base.js +276 -0
- package/src/lib/providers/hetzner-s3.js +656 -0
- package/src/lib/providers/hetzner.js +755 -0
- package/src/lib/providers/index.js +164 -0
- package/src/lib/s3.js +510 -0
- package/src/lib/secret-scan.js +583 -0
- package/src/lib/secrets.js +63 -0
- package/src/lib/server-types.js +195 -0
- package/src/lib/shell.js +91 -0
- package/src/lib/ssh.js +241 -0
- package/src/lib/tracker.js +242 -0
- package/src/lib/upgrade-policy.js +170 -0
- package/src/lib/validators.js +105 -0
- package/src/lib/version.js +5 -0
- package/src/remove.js +292 -0
- package/src/reset.js +97 -0
- package/src/restore.js +871 -0
- package/src/scale.js +1734 -0
- package/src/shell.js +222 -0
- package/src/status.js +981 -0
- package/src/up.js +264 -0
- package/src/upgrade.js +721 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Type Selection Utilities
|
|
3
|
+
*
|
|
4
|
+
* Shared logic for building server type selection prompts used by
|
|
5
|
+
* both the deploy and scale commands.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { HetznerProvider } from './providers/hetzner.js';
|
|
9
|
+
|
|
10
|
+
// Minimum server specs for a full Compose stack (Supabase + app + Traefik + socket-proxy)
|
|
11
|
+
export const COMPOSE_MIN_RAM_GB = 4;
|
|
12
|
+
|
|
13
|
+
// Curated K8s deployment profiles — sensible combinations of master + supabase + worker.
|
|
14
|
+
// Each profile has ARM (cax) and x86 (cpx) variants; the correct one is chosen per region.
|
|
15
|
+
export const K8S_PROFILES = [
|
|
16
|
+
{
|
|
17
|
+
name: 'starter',
|
|
18
|
+
label: 'Starter',
|
|
19
|
+
hint: 'Good for MVPs and low-traffic apps',
|
|
20
|
+
arm: { master: 'cax11', supabase: 'cax21', worker: 'cax11' },
|
|
21
|
+
x86: { master: 'cpx11', supabase: 'cpx21', worker: 'cpx11' },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'production',
|
|
25
|
+
label: 'Production',
|
|
26
|
+
hint: 'Recommended for production workloads',
|
|
27
|
+
arm: { master: 'cax21', supabase: 'cax31', worker: 'cax21' },
|
|
28
|
+
x86: { master: 'cpx21', supabase: 'cpx31', worker: 'cpx21' },
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'enterprise',
|
|
32
|
+
label: 'Enterprise',
|
|
33
|
+
hint: 'High-traffic apps with large databases',
|
|
34
|
+
arm: { master: 'cax31', supabase: 'cax41', worker: 'cax31' },
|
|
35
|
+
x86: { master: 'cpx31', supabase: 'cpx41', worker: 'cpx31' },
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse a price string like "€7.99/mo" into a number.
|
|
41
|
+
*/
|
|
42
|
+
export function parsePrice(priceStr) {
|
|
43
|
+
const match = (priceStr || '').match(/[\d.]+/);
|
|
44
|
+
return match ? Number.parseFloat(match[0]) : 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Format a server type label with aligned columns.
|
|
49
|
+
* Pre-computes column widths from the full list so values line up.
|
|
50
|
+
*/
|
|
51
|
+
function buildAlignedLabels(types) {
|
|
52
|
+
const nameW = Math.max(...types.map((t) => t.name.length));
|
|
53
|
+
const vcpuW = Math.max(...types.map((t) => String(t.vcpu).length));
|
|
54
|
+
const ramW = Math.max(...types.map((t) => String(t.ram).length));
|
|
55
|
+
const diskW = Math.max(...types.map((t) => String(t.disk).length));
|
|
56
|
+
|
|
57
|
+
return new Map(
|
|
58
|
+
types.map((t) => [
|
|
59
|
+
t.name,
|
|
60
|
+
`${t.name.padEnd(nameW)} - ${String(t.vcpu).padStart(vcpuW)} vCPU, ${String(t.ram).padStart(ramW)}GB RAM, ${String(t.disk).padStart(diskW)}GB`,
|
|
61
|
+
]),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build select options for Compose server type prompt.
|
|
67
|
+
* Disables types that are too small, marks minimum and recommended.
|
|
68
|
+
*/
|
|
69
|
+
export function buildComposeTypeOptions(regionTypes, defaultType) {
|
|
70
|
+
const { INFRA_COSTS } = HetznerProvider;
|
|
71
|
+
const filtered = regionTypes.filter((t) => !t.cpuType || t.cpuType === 'shared');
|
|
72
|
+
const labels = buildAlignedLabels(filtered);
|
|
73
|
+
|
|
74
|
+
return filtered.map((t) => {
|
|
75
|
+
const tooSmall = t.ram < COMPOSE_MIN_RAM_GB;
|
|
76
|
+
const isMinimum = !tooSmall && t.ram === COMPOSE_MIN_RAM_GB && t.disk <= 40;
|
|
77
|
+
const isRecommended = t.name === defaultType;
|
|
78
|
+
|
|
79
|
+
const serverCost = parsePrice(t.price);
|
|
80
|
+
const totalCost = serverCost + INFRA_COSTS.s3MinBucket;
|
|
81
|
+
|
|
82
|
+
let hint = '';
|
|
83
|
+
if (tooSmall) {
|
|
84
|
+
hint = `need ${COMPOSE_MIN_RAM_GB}GB+ RAM for Supabase`;
|
|
85
|
+
} else if (isMinimum) {
|
|
86
|
+
hint = `~€${totalCost.toFixed(0)}/mo total — minimum`;
|
|
87
|
+
} else if (isRecommended) {
|
|
88
|
+
hint = `~€${totalCost.toFixed(0)}/mo total — recommended`;
|
|
89
|
+
} else {
|
|
90
|
+
hint = `~€${totalCost.toFixed(0)}/mo total`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
value: t.name,
|
|
95
|
+
label: labels.get(t.name),
|
|
96
|
+
hint,
|
|
97
|
+
disabled: tooSmall,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build select options for K8s cluster size profiles.
|
|
104
|
+
* Calculates total monthly cost including all provisioned infra.
|
|
105
|
+
*/
|
|
106
|
+
export function buildK8sProfileOptions(_region, regionTypes, secondaryRegion) {
|
|
107
|
+
const { INFRA_COSTS, VOLUME_SIZES, SERVER_TYPES } = HetznerProvider;
|
|
108
|
+
|
|
109
|
+
// Determine available types (intersection of both regions for HA)
|
|
110
|
+
const availableNames = new Set(regionTypes.map((t) => t.name));
|
|
111
|
+
let effectiveAvailable = availableNames;
|
|
112
|
+
if (secondaryRegion) {
|
|
113
|
+
const secondaryNames = new Set(
|
|
114
|
+
HetznerProvider.getServerTypesForRegion(secondaryRegion).map((t) => t.name),
|
|
115
|
+
);
|
|
116
|
+
effectiveAvailable = new Set([...availableNames].filter((n) => secondaryNames.has(n)));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const hasArm = effectiveAvailable.has('cax11');
|
|
120
|
+
const regionMultiplier = secondaryRegion ? 2 : 1;
|
|
121
|
+
|
|
122
|
+
// Per-cluster fixed costs: floating IP + base CSI volumes
|
|
123
|
+
const fixedPerCluster = INFRA_COSTS.floatingIp + VOLUME_SIZES.base * INFRA_COSTS.volumePerGb;
|
|
124
|
+
|
|
125
|
+
return K8S_PROFILES.map((profile) => {
|
|
126
|
+
const variant = hasArm ? profile.arm : profile.x86;
|
|
127
|
+
|
|
128
|
+
// Check all types in this profile are available
|
|
129
|
+
const allAvailable = [variant.master, variant.supabase, variant.worker].every((t) =>
|
|
130
|
+
effectiveAvailable.has(t),
|
|
131
|
+
);
|
|
132
|
+
if (!allAvailable) return null;
|
|
133
|
+
|
|
134
|
+
const serverCost =
|
|
135
|
+
parsePrice(SERVER_TYPES[variant.master]?.price) +
|
|
136
|
+
parsePrice(SERVER_TYPES[variant.supabase]?.price) +
|
|
137
|
+
parsePrice(SERVER_TYPES[variant.worker]?.price);
|
|
138
|
+
|
|
139
|
+
const totalCost = (serverCost + fixedPerCluster) * regionMultiplier + INFRA_COSTS.s3MinBucket;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
value: profile.name,
|
|
143
|
+
label: `${profile.label} ~€${totalCost.toFixed(0)}/mo`,
|
|
144
|
+
hint: `${profile.hint} (${variant.master} + ${variant.supabase} + ${variant.worker})`,
|
|
145
|
+
_variant: variant,
|
|
146
|
+
};
|
|
147
|
+
})
|
|
148
|
+
.filter(Boolean)
|
|
149
|
+
.concat([
|
|
150
|
+
{
|
|
151
|
+
value: 'advanced',
|
|
152
|
+
label: 'Advanced',
|
|
153
|
+
hint: 'Choose each node type individually',
|
|
154
|
+
_variant: null,
|
|
155
|
+
},
|
|
156
|
+
]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Detect which K8s profile matches the given server types.
|
|
161
|
+
* Returns the profile name or 'advanced' if no match.
|
|
162
|
+
*/
|
|
163
|
+
export function detectCurrentProfile(masterType, supabaseType, workerType) {
|
|
164
|
+
for (const profile of K8S_PROFILES) {
|
|
165
|
+
for (const arch of ['arm', 'x86']) {
|
|
166
|
+
const v = profile[arch];
|
|
167
|
+
if (v.master === masterType && v.supabase === supabaseType && v.worker === workerType) {
|
|
168
|
+
return profile.name;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return 'advanced';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Build a flat list of server type select options.
|
|
177
|
+
* Used for K8s Advanced mode individual node selection and the scale command.
|
|
178
|
+
*
|
|
179
|
+
* @param {Array} regionTypes - from HetznerProvider.getServerTypesForRegion()
|
|
180
|
+
* @param {object} [options]
|
|
181
|
+
* @param {boolean} [options.filterSharedCpu=true] - filter to shared CPU types only
|
|
182
|
+
* @returns {Array<{value: string, label: string}>}
|
|
183
|
+
*/
|
|
184
|
+
export function buildSimpleTypeOptions(regionTypes, { filterSharedCpu = true } = {}) {
|
|
185
|
+
const filtered = filterSharedCpu
|
|
186
|
+
? regionTypes.filter((t) => !t.cpuType || t.cpuType === 'shared')
|
|
187
|
+
: regionTypes;
|
|
188
|
+
|
|
189
|
+
const labels = buildAlignedLabels(filtered);
|
|
190
|
+
|
|
191
|
+
return filtered.map((t) => ({
|
|
192
|
+
value: t.name,
|
|
193
|
+
label: labels.get(t.name),
|
|
194
|
+
}));
|
|
195
|
+
}
|
package/src/lib/shell.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell / dotenv / SQL / YAML escaping helpers.
|
|
3
|
+
*
|
|
4
|
+
* All helpers produce values safe to paste into the corresponding sink.
|
|
5
|
+
* None of them perform any validation — callers must validate input before
|
|
6
|
+
* reaching a sink that a hostile value could corrupt.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* POSIX shell single-quote escape. Wraps value in single quotes and
|
|
11
|
+
* handles embedded single quotes via close-reopen (`'\''`).
|
|
12
|
+
*/
|
|
13
|
+
export function shEscape(value) {
|
|
14
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Dotenv single-quoted form. Same encoding as shEscape — dotenv parsers
|
|
19
|
+
* that support single-quoted values treat the content literally.
|
|
20
|
+
*
|
|
21
|
+
* NOTE: The output is valid for dotenv-package parsers that support
|
|
22
|
+
* multi-line single-quoted values (e.g. the `dotenv` npm package).
|
|
23
|
+
* It is NOT safe to `source` in bash if the value contains a literal
|
|
24
|
+
* newline — bash single-quoted strings do not span lines the same way.
|
|
25
|
+
*/
|
|
26
|
+
export function escapeDotenv(value) {
|
|
27
|
+
return shEscape(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Decode a single value from its dotenv-on-disk form back to the raw
|
|
32
|
+
* string we'd have passed to `escapeDotenv`. Handles the three shapes
|
|
33
|
+
* we emit / accept:
|
|
34
|
+
*
|
|
35
|
+
* - `'POSIX-single-quoted'` (escapeDotenv output, with `'\''` for embedded `'`)
|
|
36
|
+
* - `"double-quoted"` (legacy `.env` files; no escape-sequence interpretation)
|
|
37
|
+
* - bare unquoted (legacy `.env` files; treated as the literal value)
|
|
38
|
+
*
|
|
39
|
+
* Inverse of `escapeDotenv` for any string that didn't contain a literal newline
|
|
40
|
+
* inside an unquoted value.
|
|
41
|
+
*/
|
|
42
|
+
export function unescapeDotenv(raw) {
|
|
43
|
+
if (raw == null) return '';
|
|
44
|
+
const v = String(raw);
|
|
45
|
+
if (v.startsWith("'") && v.endsWith("'") && v.length >= 2) {
|
|
46
|
+
return v.slice(1, -1).replace(/'\\''/g, "'");
|
|
47
|
+
}
|
|
48
|
+
if (v.startsWith('"') && v.endsWith('"') && v.length >= 2) {
|
|
49
|
+
return v.slice(1, -1);
|
|
50
|
+
}
|
|
51
|
+
return v;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse the full text of a `.env`-style file into a flat key→value map,
|
|
56
|
+
* decoding any escapeDotenv-quoted values back to their raw form.
|
|
57
|
+
*
|
|
58
|
+
* Skips blank lines and `#`-prefixed comments. Lines that don't match
|
|
59
|
+
* `KEY=VALUE` (e.g. `export FOO=bar` or stray text) are also ignored —
|
|
60
|
+
* we only care about the canonical `KEY=VALUE` shape that `renderBundle`
|
|
61
|
+
* itself produces. Multi-line single-quoted values are NOT supported here;
|
|
62
|
+
* we only emit single-line values from `escapeDotenv`, so the on-disk
|
|
63
|
+
* shape we read back is always single-line.
|
|
64
|
+
*/
|
|
65
|
+
export function parseDotenv(text) {
|
|
66
|
+
const out = {};
|
|
67
|
+
if (!text) return out;
|
|
68
|
+
for (const line of String(text).split('\n')) {
|
|
69
|
+
if (!line || line.startsWith('#')) continue;
|
|
70
|
+
const m = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/);
|
|
71
|
+
if (!m) continue;
|
|
72
|
+
out[m[1]] = unescapeDotenv(m[2]);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Postgres SQL single-quote escape. Returns a complete SQL string literal
|
|
79
|
+
* (including outer single quotes), ready to paste anywhere a SQL string
|
|
80
|
+
* literal is expected.
|
|
81
|
+
*/
|
|
82
|
+
export function escapeSql(value) {
|
|
83
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* YAML-safe encoding via JSON string literal.
|
|
88
|
+
*/
|
|
89
|
+
export function escapeYaml(value) {
|
|
90
|
+
return JSON.stringify(String(value));
|
|
91
|
+
}
|
package/src/lib/ssh.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared SSH/SCP/kubectl helpers.
|
|
3
|
+
*
|
|
4
|
+
* All helpers use argv (spawn) form via runCommand — no local shell
|
|
5
|
+
* interpolation. The remote shell still parses whatever reaches it, so
|
|
6
|
+
* callers that build commands from dynamic data should either:
|
|
7
|
+
* 1. Use sshRun with an argv array (preferred — no remote shell parsing
|
|
8
|
+
* of dynamic tokens), or
|
|
9
|
+
* 2. Use sshRunScript to execute a bash script SCP'd as a file.
|
|
10
|
+
*
|
|
11
|
+
* Host-key checking: first-provision callers pass { firstConnect: true }
|
|
12
|
+
* which switches StrictHostKeyChecking to accept-new (TOFU). All other
|
|
13
|
+
* calls use strict checking against a per-environment known_hosts file.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { runCommand } from './command.js';
|
|
20
|
+
import { knownHostsPath } from './host-keys.js';
|
|
21
|
+
import { shEscape } from './shell.js';
|
|
22
|
+
|
|
23
|
+
// k3s writes its kubeconfig here; kubectl doesn't find it by default in SSH sessions.
|
|
24
|
+
const K3S_KUBECONFIG = '/etc/rancher/k3s/k3s.yaml';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the SSH key path for an environment.
|
|
28
|
+
* HA deployments store a shared SSH key under the base env name
|
|
29
|
+
* (e.g., deploy_key_prod). Internal cluster names use -primary / -standby
|
|
30
|
+
* suffixes for filesystem resources.
|
|
31
|
+
*
|
|
32
|
+
* K8s single-cluster deploys use a different filename convention
|
|
33
|
+
* (`ssh-<env>` — see deployK3s in src/lib/deploy/k8s/k3s.js). We probe
|
|
34
|
+
* that path too so backup/restore/scale/failover work for k8s
|
|
35
|
+
* single-cluster without changing the deploy-side filename. (Compose,
|
|
36
|
+
* compose-HA, and k8s-HA all use `deploy_key_<env>`.)
|
|
37
|
+
*/
|
|
38
|
+
export function getSSHKeyPath(environment) {
|
|
39
|
+
const baseEnv = environment.replace(/-(primary|standby)$/, '');
|
|
40
|
+
const candidates = [
|
|
41
|
+
join(process.cwd(), '.vibecarbon', `deploy_key_${baseEnv}`),
|
|
42
|
+
join(process.cwd(), '.vibecarbon', `deploy_key_${environment}`),
|
|
43
|
+
join(process.cwd(), '.vibecarbon', `ssh-${baseEnv}`),
|
|
44
|
+
join(process.cwd(), '.vibecarbon', `ssh-${environment}`),
|
|
45
|
+
];
|
|
46
|
+
for (const path of candidates) {
|
|
47
|
+
if (existsSync(path)) return path;
|
|
48
|
+
}
|
|
49
|
+
// Nothing exists — fall back to the literal env-name path so caller
|
|
50
|
+
// error messages name what they asked for. Matches pre-fallback
|
|
51
|
+
// semantics for HA-suffixed environments.
|
|
52
|
+
return join(process.cwd(), '.vibecarbon', `deploy_key_${environment}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build the SSH `-o` option args for host-key checking.
|
|
57
|
+
*
|
|
58
|
+
* - With `env`: StrictHostKeyChecking=yes against .vibecarbon/known_hosts_<env>
|
|
59
|
+
* (or accept-new if firstConnect=true — used during first provision).
|
|
60
|
+
*
|
|
61
|
+
* - Without `env`: bypass the user's known_hosts with UserKnownHostsFile=
|
|
62
|
+
* /dev/null and StrictHostKeyChecking=no, matching the deploy path's long-
|
|
63
|
+
* standing SSH_OPTS in compose/index.js. This keeps backup/restore/scale/
|
|
64
|
+
* failover consistent with the deploy flow and avoids the Hetzner recycled-
|
|
65
|
+
* IP footgun: destroy-then-redeploy gives you the same public IP with a new
|
|
66
|
+
* host key, and a stale TOFU entry in ~/.ssh/known_hosts would refuse the
|
|
67
|
+
* reconnect. H-1 (tracked in docs/superpowers/specs/2026-04-17-security-
|
|
68
|
+
* remediation-design.md) replaces this with env-scoped pinned known_hosts
|
|
69
|
+
* across every CLI command — callers pass `env` once the deploy path
|
|
70
|
+
* populates .vibecarbon/known_hosts_<env>.
|
|
71
|
+
*/
|
|
72
|
+
function sshHostKeyOpts(env, { firstConnect = false } = {}) {
|
|
73
|
+
const opts = [];
|
|
74
|
+
if (env) {
|
|
75
|
+
opts.push('-o', `UserKnownHostsFile=${knownHostsPath(env)}`);
|
|
76
|
+
opts.push('-o', `StrictHostKeyChecking=${firstConnect ? 'accept-new' : 'yes'}`);
|
|
77
|
+
} else {
|
|
78
|
+
opts.push('-o', 'UserKnownHostsFile=/dev/null');
|
|
79
|
+
opts.push('-o', 'StrictHostKeyChecking=no');
|
|
80
|
+
}
|
|
81
|
+
opts.push('-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10');
|
|
82
|
+
// ConnectTimeout only covers TCP connect — once the socket is open, ssh
|
|
83
|
+
// will wait indefinitely for the SSH banner / protocol traffic. RCA from
|
|
84
|
+
// iter-confirm 2026-05-02: a freshly-created Hetzner VPS accepted TCP on
|
|
85
|
+
// port 22 but never sent the SSH banner, so a scale.ha.fan SSH hung for
|
|
86
|
+
// ~600s ("Connection timed out during banner exchange") until the test
|
|
87
|
+
// runner SIGKILLed it. ServerAliveInterval+CountMax force ssh to give up
|
|
88
|
+
// after 60s of no protocol-level traffic, surfacing the failure cleanly
|
|
89
|
+
// so the existing waitForSSH/runCommandAsync retry layers can recover.
|
|
90
|
+
// Keepalives are protocol-level — long-running remote commands (tar, pg
|
|
91
|
+
// dumps) won't false-trigger because sshd acks keepalives independently
|
|
92
|
+
// of the command's stdout.
|
|
93
|
+
opts.push('-o', 'ServerAliveInterval=15', '-o', 'ServerAliveCountMax=4');
|
|
94
|
+
return opts;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Execute a command on a remote server via SSH using argv form.
|
|
99
|
+
*
|
|
100
|
+
* OpenSSH joins every post-hostname argv element with a single space before
|
|
101
|
+
* sending one command string to the remote shell — it does NOT re-quote. If
|
|
102
|
+
* we spread argv directly into ssh's invocation, any token containing a space,
|
|
103
|
+
* a pipe, parentheses, or another shell metacharacter would be word-split by
|
|
104
|
+
* the remote shell and parsed as separate tokens. For `sh -c 'gunzip | psql'`
|
|
105
|
+
* that means remote sh gets `-c gunzip` (script = "gunzip") and the pipe runs
|
|
106
|
+
* in the outer shell — silently wrong.
|
|
107
|
+
*
|
|
108
|
+
* We POSIX-quote each argv element and join them into a single command string
|
|
109
|
+
* here. The remote shell word-splits that string once, sees the quotes, and
|
|
110
|
+
* reconstructs exactly the argv we intended. The local side still uses
|
|
111
|
+
* spawn/argv (no local shell), so nothing on our end interprets the contents.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} ip
|
|
114
|
+
* @param {string} sshKeyPath
|
|
115
|
+
* @param {string[]} argv - remote command + args (e.g. ['docker', 'ps'])
|
|
116
|
+
* @param {object} [options]
|
|
117
|
+
* @param {string} [options.env] - project env for host-key pinning
|
|
118
|
+
* @param {boolean} [options.firstConnect=false]
|
|
119
|
+
* @param {number} [options.timeout=120000]
|
|
120
|
+
* @param {boolean} [options.silent=true]
|
|
121
|
+
* @returns {string} - trimmed stdout
|
|
122
|
+
*/
|
|
123
|
+
export function sshRun(ip, sshKeyPath, argv, options = {}) {
|
|
124
|
+
const { env, firstConnect = false, timeout = 120_000, silent = true } = options;
|
|
125
|
+
if (!Array.isArray(argv) || argv.length === 0) {
|
|
126
|
+
throw new Error('sshRun requires a non-empty argv array');
|
|
127
|
+
}
|
|
128
|
+
const remoteCmd = argv.map(shEscape).join(' ');
|
|
129
|
+
const cmd = [
|
|
130
|
+
'ssh',
|
|
131
|
+
'-i',
|
|
132
|
+
sshKeyPath,
|
|
133
|
+
...sshHostKeyOpts(env, { firstConnect }),
|
|
134
|
+
'--',
|
|
135
|
+
`root@${ip}`,
|
|
136
|
+
remoteCmd,
|
|
137
|
+
];
|
|
138
|
+
const out = runCommand(cmd, { silent, timeout, returnOutput: true });
|
|
139
|
+
return typeof out === 'string' ? out.trim() : '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Run a multi-line bash script on a remote server.
|
|
144
|
+
* The script is SCP'd to /tmp/vb-script-<uuid>.sh (mode 0700), executed
|
|
145
|
+
* via bash, then removed. Use for pipelines that genuinely need shell
|
|
146
|
+
* features; never interpolate untrusted values into the script string.
|
|
147
|
+
*/
|
|
148
|
+
export function sshRunScript(ip, sshKeyPath, bashScript, options = {}) {
|
|
149
|
+
const localTmp = mkdtempSync(join(tmpdir(), 'vb-sshscript-'));
|
|
150
|
+
const localPath = join(localTmp, 'script.sh');
|
|
151
|
+
const remotePath = `/tmp/vb-script-${Date.now()}-${Math.random().toString(36).slice(2, 10)}.sh`;
|
|
152
|
+
try {
|
|
153
|
+
writeFileSync(localPath, bashScript, { mode: 0o700 });
|
|
154
|
+
scpUpload(ip, sshKeyPath, localPath, remotePath, options);
|
|
155
|
+
return sshRun(ip, sshKeyPath, ['bash', remotePath], options);
|
|
156
|
+
} finally {
|
|
157
|
+
try {
|
|
158
|
+
sshRun(ip, sshKeyPath, ['rm', '-f', remotePath], {
|
|
159
|
+
...options,
|
|
160
|
+
silent: true,
|
|
161
|
+
timeout: 15_000,
|
|
162
|
+
});
|
|
163
|
+
} catch {
|
|
164
|
+
// best-effort cleanup
|
|
165
|
+
}
|
|
166
|
+
rmSync(localTmp, { recursive: true, force: true });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Download a file from a remote server via SCP. Argv form.
|
|
172
|
+
*/
|
|
173
|
+
export function scpDownload(ip, sshKeyPath, remotePath, localPath, options = {}) {
|
|
174
|
+
const { env, firstConnect = false, timeout = 300_000 } = options;
|
|
175
|
+
const cmd = [
|
|
176
|
+
'scp',
|
|
177
|
+
'-i',
|
|
178
|
+
sshKeyPath,
|
|
179
|
+
...sshHostKeyOpts(env, { firstConnect }),
|
|
180
|
+
'--',
|
|
181
|
+
`root@${ip}:${remotePath}`,
|
|
182
|
+
localPath,
|
|
183
|
+
];
|
|
184
|
+
runCommand(cmd, { silent: true, timeout });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Upload a file to a remote server via SCP. Argv form.
|
|
189
|
+
*/
|
|
190
|
+
export function scpUpload(ip, sshKeyPath, localPath, remotePath, options = {}) {
|
|
191
|
+
const { env, firstConnect = false, timeout = 300_000 } = options;
|
|
192
|
+
const cmd = [
|
|
193
|
+
'scp',
|
|
194
|
+
'-i',
|
|
195
|
+
sshKeyPath,
|
|
196
|
+
...sshHostKeyOpts(env, { firstConnect }),
|
|
197
|
+
'--',
|
|
198
|
+
localPath,
|
|
199
|
+
`root@${ip}:${remotePath}`,
|
|
200
|
+
];
|
|
201
|
+
runCommand(cmd, { silent: true, timeout });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Execute a kubectl command on a remote k3s server via SSH.
|
|
206
|
+
* Takes a kubectl argv array (WITHOUT the leading 'kubectl' token — sshKubectl
|
|
207
|
+
* prepends it). The KUBECONFIG env var is injected via `env VAR=VAL`.
|
|
208
|
+
*
|
|
209
|
+
* @param {string} ip
|
|
210
|
+
* @param {string} sshKeyPath
|
|
211
|
+
* @param {string[]} kubectlArgv - kubectl subcommand + args (e.g. ['get', 'pods', '-n', 'vibecarbon'])
|
|
212
|
+
* @param {object} [options]
|
|
213
|
+
* @returns {string} trimmed stdout
|
|
214
|
+
*/
|
|
215
|
+
export function sshKubectl(ip, sshKeyPath, kubectlArgv, options = {}) {
|
|
216
|
+
if (!Array.isArray(kubectlArgv) || kubectlArgv.length === 0) {
|
|
217
|
+
throw new Error('sshKubectl requires a non-empty argv array (without leading "kubectl")');
|
|
218
|
+
}
|
|
219
|
+
return sshRun(
|
|
220
|
+
ip,
|
|
221
|
+
sshKeyPath,
|
|
222
|
+
['env', `KUBECONFIG=${K3S_KUBECONFIG}`, 'kubectl', ...kubectlArgv],
|
|
223
|
+
options,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get the name of the PostgreSQL pod in the vibecarbon namespace.
|
|
229
|
+
*/
|
|
230
|
+
export function getPostgresPod(ip, sshKeyPath) {
|
|
231
|
+
return sshKubectl(ip, sshKeyPath, [
|
|
232
|
+
'get',
|
|
233
|
+
'pods',
|
|
234
|
+
'-n',
|
|
235
|
+
'vibecarbon',
|
|
236
|
+
'-l',
|
|
237
|
+
'app.kubernetes.io/name=supabase-db',
|
|
238
|
+
'-o',
|
|
239
|
+
'jsonpath={.items[0].metadata.name}',
|
|
240
|
+
]);
|
|
241
|
+
}
|