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,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Prompts
|
|
3
|
+
* Interactive CLI flow for gathering deployment configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as p from '@clack/prompts';
|
|
7
|
+
import { getZones as cloudflareGetZones } from '../cloudflare.js';
|
|
8
|
+
import { c, printBanner } from '../colors.js';
|
|
9
|
+
import { loadProjectConfig } from '../config.js';
|
|
10
|
+
import { getZones as hetznerDnsGetZones } from '../hetzner-dns.js';
|
|
11
|
+
import { getApiToken, getS3Credentials } from '../hetzner-guided-setup.js';
|
|
12
|
+
import { HetznerS3Provider, sanitizeBucketName } from '../providers/hetzner-s3.js';
|
|
13
|
+
import { HetznerProvider } from '../providers/index.js';
|
|
14
|
+
import { validateDomain } from '../validators.js';
|
|
15
|
+
import { VERSION } from '../version.js';
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_WORKER_MAX,
|
|
18
|
+
DEFAULT_WORKER_MIN,
|
|
19
|
+
getBranchName,
|
|
20
|
+
getProviderConfig,
|
|
21
|
+
normalizeEnvName,
|
|
22
|
+
} from './utils.js';
|
|
23
|
+
|
|
24
|
+
// Re-export constants
|
|
25
|
+
export { DEFAULT_WORKER_MAX, DEFAULT_WORKER_MIN };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve deploy mode (compose vs kubernetes) based on args and existing config.
|
|
29
|
+
*/
|
|
30
|
+
export async function resolveDeployMode(args, envConfig) {
|
|
31
|
+
// Respect existing environment's deploy mode + HA setting
|
|
32
|
+
if (envConfig.deployMode) {
|
|
33
|
+
return { deployMode: envConfig.deployMode, ha: envConfig.ha?.enabled || false };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Diamond or Scale tier features: resolve from CLI flags
|
|
37
|
+
if (args.compose && args.ha) return { deployMode: 'compose-ha', ha: true };
|
|
38
|
+
if (args.compose) return { deployMode: 'compose', ha: false };
|
|
39
|
+
if (args.k8s && args.ha) return { deployMode: 'kubernetes', ha: true };
|
|
40
|
+
if (args.k8s) return { deployMode: 'kubernetes', ha: false };
|
|
41
|
+
if (args.ha) return { deployMode: 'kubernetes', ha: true };
|
|
42
|
+
|
|
43
|
+
// Non-interactive: default to compose when --yes and no mode flags
|
|
44
|
+
if (args.yes) {
|
|
45
|
+
return { deployMode: 'compose', ha: false };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Interactive: Simple choices optimized for robustness
|
|
49
|
+
const mode = await p.select({
|
|
50
|
+
message: 'Deployment architecture:',
|
|
51
|
+
options: [
|
|
52
|
+
{
|
|
53
|
+
value: 'compose',
|
|
54
|
+
label: 'Docker Compose (Fast)',
|
|
55
|
+
hint: '1 VPS - Best for startups and internal tools',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
value: 'compose-ha',
|
|
59
|
+
label: 'Docker Compose HA (Auto Failover)',
|
|
60
|
+
hint: '2 VPS - Simple failover without K8s complexity',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
value: 'kubernetes',
|
|
64
|
+
label: 'Kubernetes (Auto Scaling)',
|
|
65
|
+
hint: 'k3s + Autoscaling - Best for high-traffic apps',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
value: 'kubernetes-ha',
|
|
69
|
+
label: 'Kubernetes HA (Auto Scaling + Failover)',
|
|
70
|
+
hint: 'Multi-region cluster - Maximum availability',
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (p.isCancel(mode)) {
|
|
76
|
+
p.cancel('Operation cancelled.');
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (mode === 'kubernetes-ha') return { deployMode: 'kubernetes', ha: true };
|
|
81
|
+
if (mode === 'compose-ha') return { deployMode: 'compose-ha', ha: true };
|
|
82
|
+
return { deployMode: mode, ha: false };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Helper: fetch DNS zones with retry on transient network errors
|
|
87
|
+
*/
|
|
88
|
+
async function fetchZonesWithRetry(fetchFn, providerLabel) {
|
|
89
|
+
const s = p.spinner();
|
|
90
|
+
let lastError;
|
|
91
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
92
|
+
s.start(
|
|
93
|
+
attempt === 1
|
|
94
|
+
? `Fetching your ${providerLabel} zones`
|
|
95
|
+
: `Retrying zone fetch (attempt ${attempt}/3)`,
|
|
96
|
+
);
|
|
97
|
+
try {
|
|
98
|
+
const zones = await fetchFn();
|
|
99
|
+
s.stop('Zones retrieved');
|
|
100
|
+
return zones;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
lastError = error;
|
|
103
|
+
const isTransient =
|
|
104
|
+
error.message?.includes('fetch failed') ||
|
|
105
|
+
error.message?.includes('ECONNRESET') ||
|
|
106
|
+
error.message?.includes('ETIMEDOUT');
|
|
107
|
+
if (!isTransient || attempt === 3) {
|
|
108
|
+
s.stop(`Failed to fetch ${providerLabel} zones: ${error.message}`);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
s.stop(`Zone fetch failed (${error.message}), retrying...`);
|
|
112
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
throw lastError;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Helper: select a zone and prompt for domain
|
|
120
|
+
*/
|
|
121
|
+
async function selectZoneAndDomain(zones, providerLabel, existingDomain) {
|
|
122
|
+
let selectedZone;
|
|
123
|
+
|
|
124
|
+
if (existingDomain) {
|
|
125
|
+
const domainParts = existingDomain.split('.');
|
|
126
|
+
for (let i = 0; i < domainParts.length - 1; i++) {
|
|
127
|
+
const possibleZone = domainParts.slice(i).join('.');
|
|
128
|
+
selectedZone = zones.find((z) => z.name === possibleZone);
|
|
129
|
+
if (selectedZone) break;
|
|
130
|
+
}
|
|
131
|
+
if (selectedZone) {
|
|
132
|
+
p.log.info(`Auto-detected zone: ${c.bold(selectedZone.name)}`);
|
|
133
|
+
return { zone: selectedZone, domain: existingDomain };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (zones.length === 1) {
|
|
138
|
+
selectedZone = zones[0];
|
|
139
|
+
p.log.info(`Using zone: ${c.bold(selectedZone.name)}`);
|
|
140
|
+
} else {
|
|
141
|
+
const zoneOptions = zones.map((z) => ({ value: z, label: z.name }));
|
|
142
|
+
selectedZone = await p.select({
|
|
143
|
+
message: `Select your ${providerLabel} DNS zone`,
|
|
144
|
+
options: zoneOptions,
|
|
145
|
+
});
|
|
146
|
+
if (p.isCancel(selectedZone)) {
|
|
147
|
+
p.cancel('Operation cancelled.');
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const suggestedDomain = `app.${selectedZone.name}`;
|
|
153
|
+
const chosenDomain = await p.text({
|
|
154
|
+
message: `Domain name for ${selectedZone.name}`,
|
|
155
|
+
placeholder: suggestedDomain,
|
|
156
|
+
defaultValue: suggestedDomain,
|
|
157
|
+
initialValue: existingDomain || undefined,
|
|
158
|
+
validate: (v) => {
|
|
159
|
+
if (!v) return undefined;
|
|
160
|
+
const domainErr = validateDomain(v);
|
|
161
|
+
if (domainErr) return domainErr;
|
|
162
|
+
if (!v.endsWith(selectedZone.name) && v !== selectedZone.name) {
|
|
163
|
+
return `Domain must be under ${selectedZone.name} (e.g., app.${selectedZone.name})`;
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
if (p.isCancel(chosenDomain)) {
|
|
169
|
+
p.cancel('Operation cancelled.');
|
|
170
|
+
process.exit(0);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { zone: selectedZone, domain: chosenDomain || null };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Gather full deployment configuration through interactive prompts and CLI args
|
|
178
|
+
*/
|
|
179
|
+
export async function gatherDeploymentConfig(args) {
|
|
180
|
+
console.clear();
|
|
181
|
+
printBanner();
|
|
182
|
+
p.intro(`${c.bold('vibecarbon deploy')} ${c.dim(`v${VERSION}`)}`);
|
|
183
|
+
|
|
184
|
+
const projectConfig = loadProjectConfig();
|
|
185
|
+
if (!projectConfig) {
|
|
186
|
+
p.log.error('No Vibecarbon project found. Run this command from your project directory.');
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const services = projectConfig.services || {};
|
|
191
|
+
const environment = normalizeEnvName(args.env || 'prod');
|
|
192
|
+
const envConfig = projectConfig.environments?.[environment] || {};
|
|
193
|
+
const resuming = envConfig.status === 'deploying';
|
|
194
|
+
|
|
195
|
+
if (resuming) {
|
|
196
|
+
p.log.info(`Resuming previous deployment to ${c.bold(environment)}...`);
|
|
197
|
+
} else {
|
|
198
|
+
p.log.info(`Deploying to environment: ${c.bold(environment)}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const { deployMode, ha } = await resolveDeployMode(args, envConfig);
|
|
202
|
+
const isComposeDeploy = deployMode === 'compose' || deployMode === 'compose-ha';
|
|
203
|
+
const config = {
|
|
204
|
+
environment,
|
|
205
|
+
provider: 'hetzner',
|
|
206
|
+
ha,
|
|
207
|
+
observability: services.observability || false,
|
|
208
|
+
deployMode,
|
|
209
|
+
};
|
|
210
|
+
const providerConfig = getProviderConfig(config.provider);
|
|
211
|
+
|
|
212
|
+
// Direct vs Push prompt (compose only, when CI/CD is configured).
|
|
213
|
+
//
|
|
214
|
+
// K8s is local-first via deployK3s — no prompt, no choice. Compose with
|
|
215
|
+
// CI/CD offers both paths: direct is faster (~30s warm) but bypasses
|
|
216
|
+
// the CI review workflow; push is slower (~3 min) but goes through GHA
|
|
217
|
+
// and deploys via Flux.
|
|
218
|
+
//
|
|
219
|
+
// Skipped when:
|
|
220
|
+
// - -y / --yes — non-interactive auto-detects (push if CI configured,
|
|
221
|
+
// direct otherwise)
|
|
222
|
+
// - CI/CD not configured — direct is the only option anyway
|
|
223
|
+
//
|
|
224
|
+
// Mutates args.direct / args.push so the downstream resolveBuildMode call
|
|
225
|
+
// in orchestrator picks up the selection. Pre-PR-5 the operator could
|
|
226
|
+
// also set these via CLI flags; PR 5 dropped that surface — this prompt
|
|
227
|
+
// is now the only path to the override.
|
|
228
|
+
if (isComposeDeploy && !args.yes && !resuming) {
|
|
229
|
+
const { ciAvailable } = await import('../ci-setup.js');
|
|
230
|
+
if (ciAvailable()) {
|
|
231
|
+
const choice = await p.select({
|
|
232
|
+
message: 'How would you like to deploy?',
|
|
233
|
+
options: [
|
|
234
|
+
{
|
|
235
|
+
value: 'direct',
|
|
236
|
+
label: 'Direct',
|
|
237
|
+
hint: 'Build here and push to the server (~30s warm, no CI)',
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
value: 'push',
|
|
241
|
+
label: 'Push',
|
|
242
|
+
hint: 'Commit + GitHub Actions build & deploy (~3 min)',
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
initialValue: 'direct',
|
|
246
|
+
});
|
|
247
|
+
if (p.isCancel(choice)) {
|
|
248
|
+
p.cancel('Operation cancelled.');
|
|
249
|
+
process.exit(0);
|
|
250
|
+
}
|
|
251
|
+
if (choice === 'direct') args.direct = true;
|
|
252
|
+
else if (choice === 'push') args.push = true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Region Selection (Aggressive Default)
|
|
257
|
+
let region = args.region || envConfig.region || 'nbg1'; // Nuremberg as default robust region
|
|
258
|
+
if (!args.region && !envConfig.region && !args.yes) {
|
|
259
|
+
const regionOptions = Object.entries(providerConfig.regions).map(([id, name]) => ({
|
|
260
|
+
value: id,
|
|
261
|
+
label: `${id} - ${name}`,
|
|
262
|
+
}));
|
|
263
|
+
region = await p.select({
|
|
264
|
+
message: 'Region:',
|
|
265
|
+
options: regionOptions,
|
|
266
|
+
initialValue: region,
|
|
267
|
+
});
|
|
268
|
+
if (p.isCancel(region)) {
|
|
269
|
+
p.cancel('Operation cancelled.');
|
|
270
|
+
process.exit(0);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let secondaryRegion = null;
|
|
275
|
+
if (ha) {
|
|
276
|
+
secondaryRegion =
|
|
277
|
+
args.secondaryRegion || envConfig.secondaryRegion || (region === 'nbg1' ? 'fsn1' : 'nbg1');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const apiToken = await getApiToken(projectConfig.projectName);
|
|
281
|
+
if (!apiToken) {
|
|
282
|
+
p.log.error('API token required');
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
await HetznerProvider.fetchServerTypes(apiToken);
|
|
287
|
+
const regionDefaults = HetznerProvider.getRegionDefaults(region);
|
|
288
|
+
|
|
289
|
+
// Server Type Selection (Streamlined)
|
|
290
|
+
let serverType, masterServerType, workerServerType, supabaseServerType;
|
|
291
|
+
|
|
292
|
+
if (isComposeDeploy) {
|
|
293
|
+
// Compose = single-host Supabase stack, so use the medium-tier default
|
|
294
|
+
// (2+ vCPU, 4+ GB) for the whole box. regionDefaults.supabaseType is
|
|
295
|
+
// live-catalog-derived so it avoids retired SKUs like cpx21 in EU.
|
|
296
|
+
serverType = args.serverType || envConfig.serverType || regionDefaults.supabaseType;
|
|
297
|
+
masterServerType = workerServerType = supabaseServerType = serverType;
|
|
298
|
+
} else {
|
|
299
|
+
// `--type <x>` (args.serverType) acts as a blanket fallback for all three
|
|
300
|
+
// node roles when a role-specific flag isn't set. Matches the compose
|
|
301
|
+
// case's simpler mental model and lets e2e pin the whole cluster
|
|
302
|
+
// to a single known-good SKU via `--type cx23`.
|
|
303
|
+
const blanket = args.serverType || envConfig.serverType;
|
|
304
|
+
masterServerType =
|
|
305
|
+
args.masterServerType || envConfig.masterServerType || blanket || regionDefaults.masterType;
|
|
306
|
+
supabaseServerType =
|
|
307
|
+
args.supabaseServerType ||
|
|
308
|
+
envConfig.supabaseServerType ||
|
|
309
|
+
blanket ||
|
|
310
|
+
regionDefaults.supabaseType;
|
|
311
|
+
workerServerType =
|
|
312
|
+
args.workerServerType || envConfig.workerServerType || blanket || regionDefaults.workerType;
|
|
313
|
+
serverType = workerServerType;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// DNS provider selection.
|
|
317
|
+
//
|
|
318
|
+
// Default is Hetzner: the same Cloud API token (`apiToken` resolved above)
|
|
319
|
+
// already administers Hetzner DNS zones, so there's zero extra setup for
|
|
320
|
+
// operators who keep DNS in Hetzner. Cloudflare is offered as an opt-in;
|
|
321
|
+
// if selected without a saved token, we prompt for one (with an offer to
|
|
322
|
+
// save). Manual skips DNS automation entirely.
|
|
323
|
+
//
|
|
324
|
+
// (cert-manager DNS-01 for Hetzner reads the Cloud API token from Secret
|
|
325
|
+
// `hetzner` / key `token`; the legacy separate DNS Console token was
|
|
326
|
+
// retired in May 2026 when Hetzner folded zone management into the main
|
|
327
|
+
// Cloud Console.)
|
|
328
|
+
let dnsProvider = args.dnsProvider || envConfig.dnsProvider;
|
|
329
|
+
let domain = args.domain || envConfig.domain || null;
|
|
330
|
+
let cloudflareZoneId = envConfig.dns?.cloudflareZoneId || null;
|
|
331
|
+
let hetznerDnsZoneId = envConfig.dns?.hetznerZoneId || null;
|
|
332
|
+
|
|
333
|
+
const { loadCredentials, saveCredentials } = await import('../config.js');
|
|
334
|
+
const savedCreds = loadCredentials();
|
|
335
|
+
let cloudflareApiToken =
|
|
336
|
+
process.env.CLOUDFLARE_API_TOKEN || savedCreds.cloudflare?.apiToken || null;
|
|
337
|
+
|
|
338
|
+
if (!dnsProvider) {
|
|
339
|
+
if (args.yes) {
|
|
340
|
+
dnsProvider = 'hetzner';
|
|
341
|
+
} else {
|
|
342
|
+
const choice = await p.select({
|
|
343
|
+
message: 'DNS provider:',
|
|
344
|
+
options: [
|
|
345
|
+
{
|
|
346
|
+
value: 'hetzner',
|
|
347
|
+
label: 'Hetzner DNS',
|
|
348
|
+
hint: 'Uses your Hetzner Cloud API token (no extra setup)',
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
value: 'cloudflare',
|
|
352
|
+
label: 'Cloudflare',
|
|
353
|
+
hint: cloudflareApiToken ? 'API token saved' : 'Will prompt for API token',
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
value: 'manual',
|
|
357
|
+
label: 'Manual',
|
|
358
|
+
hint: 'Skip DNS automation — configure records yourself',
|
|
359
|
+
},
|
|
360
|
+
],
|
|
361
|
+
initialValue: 'hetzner',
|
|
362
|
+
});
|
|
363
|
+
if (p.isCancel(choice)) {
|
|
364
|
+
p.cancel('Operation cancelled.');
|
|
365
|
+
process.exit(0);
|
|
366
|
+
}
|
|
367
|
+
dnsProvider = choice;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Cloudflare selected without a saved/env token → prompt for one (and
|
|
372
|
+
// offer to persist). Under --yes we cannot prompt; fall back to manual
|
|
373
|
+
// with a clear warning.
|
|
374
|
+
if (dnsProvider === 'cloudflare' && !cloudflareApiToken) {
|
|
375
|
+
if (args.yes) {
|
|
376
|
+
p.log.warn(
|
|
377
|
+
'Cloudflare selected but no API token found (env CLOUDFLARE_API_TOKEN or ~/.vibecarbon/credentials.json). Falling back to manual DNS.',
|
|
378
|
+
);
|
|
379
|
+
dnsProvider = 'manual';
|
|
380
|
+
} else {
|
|
381
|
+
const token = await p.password({
|
|
382
|
+
message: 'Cloudflare API token:',
|
|
383
|
+
validate: (v) => (v && v.length > 0 ? undefined : 'Token is required'),
|
|
384
|
+
});
|
|
385
|
+
if (p.isCancel(token)) {
|
|
386
|
+
p.cancel('Operation cancelled.');
|
|
387
|
+
process.exit(0);
|
|
388
|
+
}
|
|
389
|
+
cloudflareApiToken = token;
|
|
390
|
+
const persist = await p.confirm({
|
|
391
|
+
message: 'Save token to ~/.vibecarbon/credentials.json for future deploys?',
|
|
392
|
+
initialValue: true,
|
|
393
|
+
});
|
|
394
|
+
if (!p.isCancel(persist) && persist) {
|
|
395
|
+
saveCredentials({ cloudflare: { apiToken: cloudflareApiToken } });
|
|
396
|
+
p.log.success('Cloudflare API token saved');
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Auto-discover zone IDs when a domain is already configured (config-driven
|
|
402
|
+
// re-deploys land here without prompting).
|
|
403
|
+
if (domain && dnsProvider === 'cloudflare' && !cloudflareZoneId && cloudflareApiToken) {
|
|
404
|
+
try {
|
|
405
|
+
const zones = await cloudflareGetZones(cloudflareApiToken);
|
|
406
|
+
const zone = zones.find((z) => domain.endsWith(z.name));
|
|
407
|
+
if (zone) cloudflareZoneId = zone.id;
|
|
408
|
+
} catch {
|
|
409
|
+
/* ignore */
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (domain && dnsProvider === 'hetzner' && !hetznerDnsZoneId) {
|
|
413
|
+
try {
|
|
414
|
+
const zones = await hetznerDnsGetZones(apiToken);
|
|
415
|
+
const zone = zones.find((z) => domain.endsWith(z.name));
|
|
416
|
+
if (zone) hetznerDnsZoneId = zone.id;
|
|
417
|
+
} catch {
|
|
418
|
+
/* ignore */
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Interactive zone-and-domain selection when no domain is configured yet.
|
|
423
|
+
// Skipped under --yes (only the auto-discovery above runs).
|
|
424
|
+
if (!args.yes) {
|
|
425
|
+
if (dnsProvider === 'cloudflare' && !cloudflareZoneId && cloudflareApiToken) {
|
|
426
|
+
try {
|
|
427
|
+
const zones = await fetchZonesWithRetry(
|
|
428
|
+
() => cloudflareGetZones(cloudflareApiToken),
|
|
429
|
+
'Cloudflare',
|
|
430
|
+
);
|
|
431
|
+
const result = await selectZoneAndDomain(zones, 'Cloudflare', domain);
|
|
432
|
+
domain = result.domain;
|
|
433
|
+
cloudflareZoneId = result.zone.id;
|
|
434
|
+
} catch (err) {
|
|
435
|
+
p.log.warn(`Cloudflare unavailable (${err.message}). Falling back to manual DNS.`);
|
|
436
|
+
dnsProvider = 'manual';
|
|
437
|
+
}
|
|
438
|
+
} else if (dnsProvider === 'hetzner' && !hetznerDnsZoneId) {
|
|
439
|
+
try {
|
|
440
|
+
const zones = await fetchZonesWithRetry(() => hetznerDnsGetZones(apiToken), 'Hetzner');
|
|
441
|
+
if (zones.length === 0) {
|
|
442
|
+
p.log.warn('No Hetzner DNS zones found on this account. Falling back to manual DNS.');
|
|
443
|
+
dnsProvider = 'manual';
|
|
444
|
+
} else {
|
|
445
|
+
const result = await selectZoneAndDomain(zones, 'Hetzner', domain);
|
|
446
|
+
domain = result.domain;
|
|
447
|
+
hetznerDnsZoneId = result.zone.id;
|
|
448
|
+
}
|
|
449
|
+
} catch (err) {
|
|
450
|
+
p.log.warn(`Hetzner DNS unavailable (${err.message}). Falling back to manual DNS.`);
|
|
451
|
+
dnsProvider = 'manual';
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// S3 (Standardized)
|
|
457
|
+
let s3AccessKey = args.s3AccessKey || envConfig.s3?.accessKey;
|
|
458
|
+
let s3SecretKey = args.s3SecretKey || null;
|
|
459
|
+
// Env-var override (HETZNER_S3_REGION) sits between explicit CLI flag and
|
|
460
|
+
// project config so operators can route the Pulumi-state + backup buckets
|
|
461
|
+
// to a different Hetzner OS region during a regional incident without
|
|
462
|
+
// editing .vibecarbon.json. Mirrors HETZNER_API_TOKEN's role for the cloud
|
|
463
|
+
// API. Surfaced after 2026-04-29 nbg1 OS degradation killed both HA
|
|
464
|
+
// scenarios on Pulumi backend ops (lock delete 503, history save 504,
|
|
465
|
+
// checkpoint read conn-reset).
|
|
466
|
+
const s3Region =
|
|
467
|
+
args.s3Region ||
|
|
468
|
+
process.env.HETZNER_S3_REGION ||
|
|
469
|
+
envConfig.s3?.region ||
|
|
470
|
+
HetznerS3Provider.resolveS3Region(region);
|
|
471
|
+
|
|
472
|
+
if (!s3AccessKey) {
|
|
473
|
+
const credentials = await getS3Credentials(projectConfig.projectName);
|
|
474
|
+
s3AccessKey = credentials.accessKey;
|
|
475
|
+
s3SecretKey = credentials.secretKey;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const s3Provider = new HetznerS3Provider(s3AccessKey, s3SecretKey, s3Region);
|
|
479
|
+
const bucketName = sanitizeBucketName(projectConfig.projectName);
|
|
480
|
+
const backupBucketName = sanitizeBucketName(projectConfig.projectName, 'backups');
|
|
481
|
+
const backupConfig = {
|
|
482
|
+
schedule: args.backupSchedule || envConfig.backup?.schedule || '0 */6 * * *',
|
|
483
|
+
retentionDays: Number.parseInt(
|
|
484
|
+
args.backupRetentionDays || envConfig.backup?.retentionDays || '30',
|
|
485
|
+
10,
|
|
486
|
+
),
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const branchName = getBranchName(config.environment);
|
|
490
|
+
|
|
491
|
+
if (!args.yes && !resuming) {
|
|
492
|
+
p.note(
|
|
493
|
+
`
|
|
494
|
+
Env: ${c.bold(config.environment)}
|
|
495
|
+
Region: ${c.bold(region)}${ha ? ` + ${secondaryRegion}` : ''}
|
|
496
|
+
Stack: ${c.bold(isComposeDeploy ? 'Docker Compose' : 'Kubernetes (k3s)')}
|
|
497
|
+
Domain: ${c.bold(domain || 'None')}
|
|
498
|
+
`,
|
|
499
|
+
'Deployment Summary',
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
const confirmed = await p.confirm({ message: 'Proceed?', initialValue: true });
|
|
503
|
+
if (!confirmed || p.isCancel(confirmed)) {
|
|
504
|
+
p.cancel('Operation cancelled.');
|
|
505
|
+
process.exit(0);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
projectConfig,
|
|
511
|
+
envConfig,
|
|
512
|
+
environment,
|
|
513
|
+
resuming,
|
|
514
|
+
config,
|
|
515
|
+
apiToken,
|
|
516
|
+
region,
|
|
517
|
+
secondaryRegion,
|
|
518
|
+
serverType,
|
|
519
|
+
masterServerType,
|
|
520
|
+
supabaseServerType,
|
|
521
|
+
workerServerType,
|
|
522
|
+
minWorkers: args.minWorkers ?? DEFAULT_WORKER_MIN,
|
|
523
|
+
maxWorkers: args.maxWorkers ?? DEFAULT_WORKER_MAX,
|
|
524
|
+
domain,
|
|
525
|
+
dnsProvider,
|
|
526
|
+
cloudflareApiToken,
|
|
527
|
+
cloudflareZoneId,
|
|
528
|
+
hetznerDnsZoneId,
|
|
529
|
+
s3Config: {
|
|
530
|
+
accessKey: s3AccessKey,
|
|
531
|
+
secretKey: s3SecretKey,
|
|
532
|
+
bucket: bucketName,
|
|
533
|
+
region: s3Region,
|
|
534
|
+
endpoint: s3Provider.getEndpoint(),
|
|
535
|
+
},
|
|
536
|
+
backupS3Config: {
|
|
537
|
+
bucket: backupBucketName,
|
|
538
|
+
region: s3Region,
|
|
539
|
+
endpoint: s3Provider.getEndpoint(),
|
|
540
|
+
},
|
|
541
|
+
backupConfig,
|
|
542
|
+
services,
|
|
543
|
+
branchName,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote Build Logic
|
|
3
|
+
* Uses native Docker via SSH contexts to build images directly on target architecture.
|
|
4
|
+
* Bypasses local QEMU and remote registries for maximum speed.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import * as p from '@clack/prompts';
|
|
11
|
+
import { runCommand } from '../command.js';
|
|
12
|
+
import { perfTimer } from '../perf.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Execute a remote build using native Docker over SSH
|
|
16
|
+
* @param {string} ip - Remote host IP
|
|
17
|
+
* @param {string} sshKeyPath - Path to the SSH private key
|
|
18
|
+
* @param {string} imageTag - The tag to apply to the built image
|
|
19
|
+
* @param {string} cwd - The build context directory (usually project root)
|
|
20
|
+
* @param {Record<string, string>} buildArgs - Key/value pairs for --build-arg
|
|
21
|
+
* @returns {Promise<boolean>} Success
|
|
22
|
+
*/
|
|
23
|
+
export async function buildRemote(ip, sshKeyPath, imageTag, cwd, buildArgs = {}) {
|
|
24
|
+
const s = p.spinner();
|
|
25
|
+
s.start(`Building image natively on ${ip}...`);
|
|
26
|
+
|
|
27
|
+
const dockerHost = `ssh://root@${ip}`;
|
|
28
|
+
|
|
29
|
+
// Create a temporary directory for our SSH wrapper
|
|
30
|
+
const tempBinDir = mkdtempSync(join(tmpdir(), 'vc-bin-'));
|
|
31
|
+
const sshWrapperPath = join(tempBinDir, 'ssh');
|
|
32
|
+
|
|
33
|
+
// Create an SSH wrapper script that forces our options. Docker uses the
|
|
34
|
+
// system 'ssh' command found in PATH — we prepend a temp dir containing
|
|
35
|
+
// this wrapper so Docker's SSH transport picks it up instead. Pass the
|
|
36
|
+
// key path via env var rather than string-interpolating into the script
|
|
37
|
+
// to keep shell-escaping concerns out of the picture (paths with spaces
|
|
38
|
+
// or quotes would otherwise break the wrapper).
|
|
39
|
+
const sshWrapper = `#!/bin/bash
|
|
40
|
+
exec /usr/bin/ssh -i "$VIBECARBON_SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=30 "$@"
|
|
41
|
+
`;
|
|
42
|
+
writeFileSync(sshWrapperPath, sshWrapper, { mode: 0o755 });
|
|
43
|
+
|
|
44
|
+
const env = {
|
|
45
|
+
...process.env,
|
|
46
|
+
DOCKER_HOST: dockerHost,
|
|
47
|
+
DOCKER_BUILDKIT: '1',
|
|
48
|
+
PATH: `${tempBinDir}:${process.env.PATH}`, // Prepend our wrapper
|
|
49
|
+
VIBECARBON_SSH_KEY: sshKeyPath,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// 1. Ensure the host is reachable and sshd is ready. Caller has usually
|
|
54
|
+
// already gone through setupServer/waitForDockerReady, so SSH is live by
|
|
55
|
+
// attempt 0; the fast leg [1, 1, 2, 2] catches that path quickly. Tail
|
|
56
|
+
// keeps 5s pacing for the rare genuinely slow-coming-up case.
|
|
57
|
+
// Gated under perfTimer because the SSH-probe wait is invisible to the
|
|
58
|
+
// outer `deploy.image.remoteBuild` timer (which only covers the docker
|
|
59
|
+
// build subprocess) — and analysis of iter-reliab3 showed the wrapper
|
|
60
|
+
// running 71s longer than the longest child timer, suggesting this
|
|
61
|
+
// probe loop is sometimes burning serious time we couldn't attribute.
|
|
62
|
+
const sshProbeT = perfTimer('deploy.image.remoteBuild.sshProbe');
|
|
63
|
+
const SSH_DELAYS_MS = [
|
|
64
|
+
1000, 1000, 2000, 2000, 5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000, 5000, 10_000, 10_000,
|
|
65
|
+
];
|
|
66
|
+
let sshReady = false;
|
|
67
|
+
for (let i = 0; i < SSH_DELAYS_MS.length; i++) {
|
|
68
|
+
try {
|
|
69
|
+
// Bare `ssh` resolves through PATH to the wrapper script written
|
|
70
|
+
// above (sshWrapper) which adds -i + -o BatchMode=yes etc.; the
|
|
71
|
+
// wrapper is the only `ssh` on PATH for this child process.
|
|
72
|
+
// shell-safety-ignore: ssh is the PATH-prepended wrapper, not system ssh
|
|
73
|
+
runCommand(['ssh', `root@${ip}`, 'echo', 'ready'], { silent: true, env });
|
|
74
|
+
sshReady = true;
|
|
75
|
+
break;
|
|
76
|
+
} catch {
|
|
77
|
+
await new Promise((r) => setTimeout(r, SSH_DELAYS_MS[i]));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
sshProbeT.end();
|
|
81
|
+
|
|
82
|
+
if (!sshReady) {
|
|
83
|
+
s.stop('Remote SSH not reachable');
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 2. Run the build directly on the remote host via DOCKER_HOST=ssh://...
|
|
88
|
+
const args = ['docker', 'build', '-t', imageTag, '.'];
|
|
89
|
+
|
|
90
|
+
for (const [key, value] of Object.entries(buildArgs)) {
|
|
91
|
+
args.push('--build-arg', `${key}=${value}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The build itself is the dominant cost on direct-mode deploys; gate it
|
|
95
|
+
// behind a perfTimer so cold (first-build, ~2-5min) vs warm (cached
|
|
96
|
+
// BuildKit, 1-3s) is attributable in the trace.
|
|
97
|
+
//
|
|
98
|
+
// runCommand without silent:true returns `false` on non-zero exit instead
|
|
99
|
+
// of throwing (see lib/command.js:135). Without this check, a failed
|
|
100
|
+
// remote build silently proceeded to `s.stop('Image built natively')`,
|
|
101
|
+
// returning true to the HA fan; the missing image then surfaced ~3 min
|
|
102
|
+
// later as `pull access denied for <project>-app` during compose up
|
|
103
|
+
// (compose-ha 2026-05-09T00:38 e2 failure). Treat the false return as a
|
|
104
|
+
// hard failure so the error path captures + reports it.
|
|
105
|
+
const buildT = perfTimer('deploy.image.remoteBuild');
|
|
106
|
+
const buildOk = runCommand(args, {
|
|
107
|
+
cwd,
|
|
108
|
+
stdio: 'inherit',
|
|
109
|
+
env,
|
|
110
|
+
timeout: 600000,
|
|
111
|
+
});
|
|
112
|
+
buildT.end();
|
|
113
|
+
if (buildOk === false) {
|
|
114
|
+
throw new Error(`docker build failed on ${ip} for tag ${imageTag}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
s.stop(`Image built natively: ${imageTag}`);
|
|
118
|
+
return true;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
s.stop(`Remote build failed`);
|
|
121
|
+
p.log.error(error.message);
|
|
122
|
+
return false;
|
|
123
|
+
} finally {
|
|
124
|
+
try {
|
|
125
|
+
rmSync(tempBinDir, { recursive: true, force: true });
|
|
126
|
+
} catch {
|
|
127
|
+
/* ignore cleanup errors */
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|