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,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task-Level Deployment State Tracker
|
|
3
|
+
* Manages persistence and hashing of deployment steps to enable granular resume.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
|
|
10
|
+
export class StateTracker {
|
|
11
|
+
constructor(projectName, environment) {
|
|
12
|
+
this.projectName = projectName;
|
|
13
|
+
this.environment = environment;
|
|
14
|
+
this.stateDir = join(process.cwd(), '.vibecarbon');
|
|
15
|
+
this.stateFile = join(this.stateDir, `deploy-state-${environment}.json`);
|
|
16
|
+
this.state = this.load();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Load state from disk
|
|
21
|
+
*/
|
|
22
|
+
load() {
|
|
23
|
+
if (existsSync(this.stateFile)) {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(readFileSync(this.stateFile, 'utf-8'));
|
|
26
|
+
} catch {
|
|
27
|
+
return { steps: {} };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { steps: {} };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Save state to disk
|
|
35
|
+
*/
|
|
36
|
+
save() {
|
|
37
|
+
if (!existsSync(this.stateDir)) {
|
|
38
|
+
mkdirSync(this.stateDir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
writeFileSync(this.stateFile, JSON.stringify(this.state, null, 2));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Calculate a hash for a step's inputs
|
|
45
|
+
* @param {object} inputs
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
calculateHash(inputs) {
|
|
49
|
+
const data = JSON.stringify(inputs);
|
|
50
|
+
return createHash('sha256').update(data).digest('hex');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if a step should be skipped based on its input hash
|
|
55
|
+
* @param {string} stepId
|
|
56
|
+
* @param {object} inputs
|
|
57
|
+
* @returns {boolean}
|
|
58
|
+
*/
|
|
59
|
+
shouldSkip(stepId, inputs) {
|
|
60
|
+
const currentHash = this.calculateHash(inputs);
|
|
61
|
+
const stepState = this.state.steps[stepId];
|
|
62
|
+
|
|
63
|
+
if (stepState && stepState.status === 'completed' && stepState.hash === currentHash) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Record a step as started
|
|
71
|
+
* @param {string} stepId
|
|
72
|
+
* @param {object} inputs
|
|
73
|
+
*/
|
|
74
|
+
startStep(stepId, inputs) {
|
|
75
|
+
const hash = this.calculateHash(inputs);
|
|
76
|
+
this.state.steps[stepId] = {
|
|
77
|
+
status: 'started',
|
|
78
|
+
hash,
|
|
79
|
+
startedAt: new Date().toISOString(),
|
|
80
|
+
};
|
|
81
|
+
this.save();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Record a step as completed
|
|
86
|
+
* @param {string} stepId
|
|
87
|
+
* @param {object} result
|
|
88
|
+
*/
|
|
89
|
+
completeStep(stepId, result = {}) {
|
|
90
|
+
if (!this.state.steps[stepId]) {
|
|
91
|
+
throw new Error(`Step ${stepId} was not started`);
|
|
92
|
+
}
|
|
93
|
+
this.state.steps[stepId].status = 'completed';
|
|
94
|
+
this.state.steps[stepId].completedAt = new Date().toISOString();
|
|
95
|
+
this.state.steps[stepId].result = result;
|
|
96
|
+
this.save();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get the result of a completed step
|
|
101
|
+
* @param {string} stepId
|
|
102
|
+
* @returns {object|null}
|
|
103
|
+
*/
|
|
104
|
+
getStepResult(stepId) {
|
|
105
|
+
const stepState = this.state.steps[stepId];
|
|
106
|
+
return stepState && stepState.status === 'completed' ? stepState.result : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Clear state for a new deployment
|
|
111
|
+
*/
|
|
112
|
+
clear() {
|
|
113
|
+
this.state = { steps: {} };
|
|
114
|
+
if (existsSync(this.stateFile)) {
|
|
115
|
+
try {
|
|
116
|
+
writeFileSync(this.stateFile, JSON.stringify(this.state, null, 2));
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment Utilities
|
|
3
|
+
* Shared helper functions for all deployment modes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
chmodSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
mkdtempSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
rmSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from 'node:fs';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import { runCommand } from '../command.js';
|
|
19
|
+
import { getProviderClass } from '../providers/index.js';
|
|
20
|
+
import { escapeDotenv } from '../shell.js';
|
|
21
|
+
|
|
22
|
+
// Deployment Defaults
|
|
23
|
+
// Cluster-autoscaler is enabled by default with a static floor of 1 and a
|
|
24
|
+
// bounded ceiling of 3 (CA pool spawns 0..(max-min) workers on top of the
|
|
25
|
+
// static floor). See specs/2026-04-25-k3s-pivot-design.md for the design
|
|
26
|
+
// decision: autoscaling on by default, bounded ceiling to cap blast radius.
|
|
27
|
+
export const DEFAULT_WORKER_MIN = 1;
|
|
28
|
+
export const DEFAULT_WORKER_MAX = 3;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get provider configuration from registry
|
|
32
|
+
* @param {string} providerName
|
|
33
|
+
* @returns {object}
|
|
34
|
+
*/
|
|
35
|
+
export function getProviderConfig(providerName) {
|
|
36
|
+
const ProviderClass = getProviderClass(providerName);
|
|
37
|
+
return {
|
|
38
|
+
name: ProviderClass.NAME,
|
|
39
|
+
regions: ProviderClass.REGIONS,
|
|
40
|
+
serverTypes: ProviderClass.SERVER_TYPES,
|
|
41
|
+
defaultType: ProviderClass.DEFAULT_TYPE,
|
|
42
|
+
haRegions: ProviderClass.HA_REGIONS,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Normalize environment name: lowercase, and treat "production" as "prod"
|
|
48
|
+
* @param {string} envName
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
export function normalizeEnvName(envName) {
|
|
52
|
+
const normalized = envName.toLowerCase();
|
|
53
|
+
return normalized === 'production' ? 'prod' : normalized;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Branch naming: environment name = branch name, except prod uses main
|
|
58
|
+
* @param {string} envName
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
export function getBranchName(envName) {
|
|
62
|
+
return envName === 'prod' ? 'main' : envName;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Merge env overrides into the remote .env file by pulling it down,
|
|
67
|
+
* updating in-memory, and SCPing it back.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} host - remote host IP
|
|
70
|
+
* @param {string} sshOpts - space-separated SSH options string
|
|
71
|
+
* @param {string} remoteDir - absolute path to the remote deployment directory
|
|
72
|
+
* @param {Record<string, string>} updates - key/value pairs to set in .env
|
|
73
|
+
*/
|
|
74
|
+
export function mergeRemoteDotenv(host, sshOpts, remoteDir, updates) {
|
|
75
|
+
const dir = mkdtempSync(join(tmpdir(), 'vibecarbon-envmerge-'));
|
|
76
|
+
const local = join(dir, '.env');
|
|
77
|
+
try {
|
|
78
|
+
// Pull remote .env
|
|
79
|
+
try {
|
|
80
|
+
runCommand(['scp', ...sshOpts.split(' '), `root@${host}:${remoteDir}/.env`, local], {
|
|
81
|
+
silent: true,
|
|
82
|
+
});
|
|
83
|
+
} catch (e) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Failed to pull ${remoteDir}/.env from ${host} — is the remote file present and SSH reachable? Underlying error: ${e?.message ?? e}`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const existing = readFileSync(local, 'utf-8').split('\n');
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
const merged = existing.map((line) => {
|
|
91
|
+
const m = line.match(/^([A-Z_][A-Z0-9_]*)=/);
|
|
92
|
+
if (m && updates[m[1]] !== undefined) {
|
|
93
|
+
seen.add(m[1]);
|
|
94
|
+
return `${m[1]}=${escapeDotenv(updates[m[1]])}`;
|
|
95
|
+
}
|
|
96
|
+
return line;
|
|
97
|
+
});
|
|
98
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
99
|
+
if (!seen.has(k)) merged.push(`${k}=${escapeDotenv(v)}`);
|
|
100
|
+
}
|
|
101
|
+
writeFileSync(local, merged.join('\n'));
|
|
102
|
+
// Push merged .env back
|
|
103
|
+
try {
|
|
104
|
+
runCommand(['scp', ...sshOpts.split(' '), local, `root@${host}:${remoteDir}/.env`], {
|
|
105
|
+
silent: true,
|
|
106
|
+
});
|
|
107
|
+
} catch (e) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Failed to push merged .env back to ${host}:${remoteDir}/.env — remote still has the previous contents. Underlying error: ${e?.message ?? e}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
} finally {
|
|
113
|
+
rmSync(dir, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Generate SSH key pair for deployment
|
|
119
|
+
* @param {string} keyPath
|
|
120
|
+
* @returns {string} public key content
|
|
121
|
+
*/
|
|
122
|
+
export function generateSSHKeyPair(keyPath) {
|
|
123
|
+
if (!existsSync(keyPath)) {
|
|
124
|
+
// Ensure parent directory exists
|
|
125
|
+
const keyDir = dirname(keyPath);
|
|
126
|
+
if (!existsSync(keyDir)) {
|
|
127
|
+
mkdirSync(keyDir, { recursive: true });
|
|
128
|
+
}
|
|
129
|
+
runCommand(
|
|
130
|
+
['ssh-keygen', '-t', 'ed25519', '-f', keyPath, '-N', '', '-C', 'vibecarbon-deploy'],
|
|
131
|
+
{
|
|
132
|
+
silent: true,
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
// Ensure correct permissions for SSH
|
|
137
|
+
chmodSync(keyPath, 0o600);
|
|
138
|
+
chmodSync(`${keyPath}.pub`, 0o644);
|
|
139
|
+
return readFileSync(`${keyPath}.pub`, 'utf-8').trim();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Wait for SSH to become available on a host
|
|
144
|
+
* @param {string} host
|
|
145
|
+
* @param {string} sshKeyPath
|
|
146
|
+
* @param {number} maxAttempts
|
|
147
|
+
* @returns {Promise<boolean>}
|
|
148
|
+
*/
|
|
149
|
+
export async function waitForSSH(host, sshKeyPath, maxAttempts = 30) {
|
|
150
|
+
const sshOpts = `-i "${sshKeyPath}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes`;
|
|
151
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
152
|
+
try {
|
|
153
|
+
runCommand(
|
|
154
|
+
['ssh', ...sshOpts.split(' '), '-o', 'ConnectTimeout=5', `root@${host}`, 'echo', 'ok'],
|
|
155
|
+
{
|
|
156
|
+
stdio: 'pipe',
|
|
157
|
+
timeout: 10000,
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
return true;
|
|
161
|
+
} catch {
|
|
162
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Initial server setup - copy project files needed for docker compose
|
|
170
|
+
* @param {string} host - remote host IP
|
|
171
|
+
* @param {string} sshKeyPath - path to the SSH private key
|
|
172
|
+
* @param {string} projectName - name of the project
|
|
173
|
+
* @param {object} options - setup options (observability, n8n, etc.)
|
|
174
|
+
*/
|
|
175
|
+
export async function setupServerFiles(host, sshKeyPath, projectName, options = {}) {
|
|
176
|
+
const cwd = process.cwd();
|
|
177
|
+
const remoteDir = `/opt/${projectName}`;
|
|
178
|
+
const sshOpts = `-i "${sshKeyPath}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes`;
|
|
179
|
+
|
|
180
|
+
// Create remote directory and required subdirectories
|
|
181
|
+
runCommand(
|
|
182
|
+
[
|
|
183
|
+
'ssh',
|
|
184
|
+
...sshOpts.split(' '),
|
|
185
|
+
`root@${host}`,
|
|
186
|
+
'mkdir',
|
|
187
|
+
'-p',
|
|
188
|
+
`${remoteDir}/volumes/db`,
|
|
189
|
+
`${remoteDir}/volumes/kong`,
|
|
190
|
+
`${remoteDir}/supabase/migrations`,
|
|
191
|
+
],
|
|
192
|
+
{ stdio: 'pipe' },
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// List of files to copy
|
|
196
|
+
const filesToCopy = [
|
|
197
|
+
'docker-compose.yml',
|
|
198
|
+
'docker-compose.prod.yml',
|
|
199
|
+
'Dockerfile',
|
|
200
|
+
'.dockerignore',
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
// Add optional service compose files if enabled
|
|
204
|
+
if (options.observability) {
|
|
205
|
+
filesToCopy.push('docker-compose.observability.yml', 'docker-compose.observability.prod.yml');
|
|
206
|
+
}
|
|
207
|
+
if (options.n8n) {
|
|
208
|
+
filesToCopy.push('docker-compose.n8n.yml', 'docker-compose.n8n.prod.yml');
|
|
209
|
+
}
|
|
210
|
+
if (options.metabase) {
|
|
211
|
+
filesToCopy.push('docker-compose.metabase.yml', 'docker-compose.metabase.prod.yml');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Copy each file that exists
|
|
215
|
+
for (const file of filesToCopy) {
|
|
216
|
+
const localPath = join(cwd, file);
|
|
217
|
+
if (existsSync(localPath)) {
|
|
218
|
+
runCommand(['scp', ...sshOpts.split(' '), localPath, `root@${host}:${remoteDir}/`], {
|
|
219
|
+
stdio: 'pipe',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Copy all volumes/db init files (SQL + shell scripts)
|
|
225
|
+
const volumesDbDir = join(cwd, 'volumes', 'db');
|
|
226
|
+
if (existsSync(volumesDbDir)) {
|
|
227
|
+
for (const file of readdirSync(volumesDbDir)) {
|
|
228
|
+
const filePath = join(volumesDbDir, file);
|
|
229
|
+
if (existsSync(filePath)) {
|
|
230
|
+
runCommand(
|
|
231
|
+
['scp', ...sshOpts.split(' '), filePath, `root@${host}:${remoteDir}/volumes/db/`],
|
|
232
|
+
{
|
|
233
|
+
stdio: 'pipe',
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Copy supabase migrations directory
|
|
241
|
+
const migrationsDir = join(cwd, 'supabase', 'migrations');
|
|
242
|
+
if (existsSync(migrationsDir)) {
|
|
243
|
+
runCommand(
|
|
244
|
+
[
|
|
245
|
+
'scp',
|
|
246
|
+
...sshOpts.split(' '),
|
|
247
|
+
'-r',
|
|
248
|
+
`${migrationsDir}/*`,
|
|
249
|
+
`root@${host}:${remoteDir}/supabase/migrations/`,
|
|
250
|
+
],
|
|
251
|
+
{
|
|
252
|
+
stdio: 'pipe',
|
|
253
|
+
ignoreError: true, // handle the 2>/dev/null || true semantics
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Copy kong configuration
|
|
259
|
+
const volumesKongDir = join(cwd, 'volumes', 'kong');
|
|
260
|
+
if (existsSync(volumesKongDir)) {
|
|
261
|
+
for (const file of readdirSync(volumesKongDir)) {
|
|
262
|
+
const filePath = join(volumesKongDir, file);
|
|
263
|
+
if (existsSync(filePath)) {
|
|
264
|
+
runCommand(
|
|
265
|
+
['scp', ...sshOpts.split(' '), filePath, `root@${host}:${remoteDir}/volumes/kong/`],
|
|
266
|
+
{
|
|
267
|
+
stdio: 'pipe',
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Copy local .env file if it exists
|
|
275
|
+
const localEnvPath = join(cwd, '.env');
|
|
276
|
+
if (existsSync(localEnvPath)) {
|
|
277
|
+
runCommand(['scp', ...sshOpts.split(' '), localEnvPath, `root@${host}:${remoteDir}/`], {
|
|
278
|
+
stdio: 'pipe',
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Override/append production env vars
|
|
283
|
+
const envOverrides = {};
|
|
284
|
+
if (options.githubRepo && options.envName) {
|
|
285
|
+
envOverrides.APP_IMAGE = `ghcr.io/${options.githubRepo}:${options.envName}`;
|
|
286
|
+
}
|
|
287
|
+
if (options.domain) {
|
|
288
|
+
envOverrides.DOMAIN = options.domain;
|
|
289
|
+
envOverrides.ACME_EMAIL = `admin@${options.domain}`;
|
|
290
|
+
envOverrides.SITE_URL = `https://api.${options.domain}`;
|
|
291
|
+
envOverrides.SUPABASE_URL = 'http://kong:8000';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (Object.keys(envOverrides).length > 0) {
|
|
295
|
+
mergeRemoteDotenv(host, sshOpts, remoteDir, envOverrides);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Copy prometheus config
|
|
299
|
+
const prometheusDir = join(cwd, 'prometheus');
|
|
300
|
+
if (options.observability && existsSync(prometheusDir)) {
|
|
301
|
+
runCommand(
|
|
302
|
+
['ssh', ...sshOpts.split(' '), `root@${host}`, 'mkdir', '-p', `${remoteDir}/prometheus`],
|
|
303
|
+
{ stdio: 'pipe' },
|
|
304
|
+
);
|
|
305
|
+
runCommand(
|
|
306
|
+
[
|
|
307
|
+
'scp',
|
|
308
|
+
...sshOpts.split(' '),
|
|
309
|
+
'-r',
|
|
310
|
+
`${prometheusDir}/*`,
|
|
311
|
+
`root@${host}:${remoteDir}/prometheus/`,
|
|
312
|
+
],
|
|
313
|
+
{
|
|
314
|
+
stdio: 'pipe',
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Copy grafana provisioning
|
|
320
|
+
const grafanaDir = join(cwd, 'grafana');
|
|
321
|
+
if (options.observability && existsSync(grafanaDir)) {
|
|
322
|
+
runCommand(['scp', ...sshOpts.split(' '), '-r', grafanaDir, `root@${host}:${remoteDir}/`], {
|
|
323
|
+
stdio: 'pipe',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent deploy logger.
|
|
3
|
+
*
|
|
4
|
+
* Tees every byte written to process.stdout / process.stderr during a
|
|
5
|
+
* deploy invocation into ~/.vibecarbon/logs/<env>-<ts>.log. Survives
|
|
6
|
+
* cluster teardown and process exit so a failed deploy can be reviewed
|
|
7
|
+
* after the fact — including the long-running Pulumi/k3s/Flux output
|
|
8
|
+
* that's otherwise lost when a session ends.
|
|
9
|
+
*
|
|
10
|
+
* ANSI escape sequences (color, cursor moves, spinner frames) are
|
|
11
|
+
* stripped from the file so it's grep-able and reads cleanly in
|
|
12
|
+
* `less`/editors. The terminal still gets the original colored output.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createWriteStream, existsSync, mkdirSync } from 'node:fs';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
// Strip ANSI CSI sequences (ESC + `[` + params + final byte) and bare CRs.
|
|
20
|
+
// Built from String.fromCharCode to avoid biome's no-control-characters lint
|
|
21
|
+
// rejecting a literal escape in the regex; equivalent to
|
|
22
|
+
// `/\x1b\[[0-9;?]*[a-zA-Z]|\r/g`.
|
|
23
|
+
const ANSI_RE = new RegExp(`${String.fromCharCode(0x1b)}\\[[0-9;?]*[a-zA-Z]|\\r`, 'g');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Wrap an async function so all stdout/stderr during its run is also
|
|
27
|
+
* tee'd to ~/.vibecarbon/logs/<envName>-<timestamp>.log. Returns
|
|
28
|
+
* whatever the wrapped function returns; rethrows after restoring.
|
|
29
|
+
*
|
|
30
|
+
* The returned promise resolves only after the underlying file stream
|
|
31
|
+
* has flushed and closed, so the caller can safely read the log file
|
|
32
|
+
* back as soon as the call settles.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} envName Environment name for the log filename
|
|
35
|
+
* (defaults to "deploy" if missing).
|
|
36
|
+
* @param {() => Promise<any>} fn Async work to run.
|
|
37
|
+
* @returns {Promise<{result: any, logPath: string}>}
|
|
38
|
+
*/
|
|
39
|
+
export async function withDeployLog(envName, fn) {
|
|
40
|
+
const logDir = join(homedir(), '.vibecarbon', 'logs');
|
|
41
|
+
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
42
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
43
|
+
const safeEnv = (envName || 'deploy').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
44
|
+
const logPath = join(logDir, `${safeEnv}-${ts}.log`);
|
|
45
|
+
const stream = createWriteStream(logPath, { flags: 'a' });
|
|
46
|
+
|
|
47
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
48
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
49
|
+
|
|
50
|
+
const tee = (orig) => (chunk, encoding, cb) => {
|
|
51
|
+
try {
|
|
52
|
+
const s = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString();
|
|
53
|
+
stream.write(s.replace(ANSI_RE, ''));
|
|
54
|
+
} catch {
|
|
55
|
+
// best-effort — never break terminal output for the sake of logging
|
|
56
|
+
}
|
|
57
|
+
return orig(chunk, encoding, cb);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
process.stdout.write = tee(originalStdoutWrite);
|
|
61
|
+
process.stderr.write = tee(originalStderrWrite);
|
|
62
|
+
|
|
63
|
+
// Header so the file is self-describing.
|
|
64
|
+
stream.write(
|
|
65
|
+
`# vibecarbon deploy log\n# env: ${envName ?? '(unset)'}\n# started: ${new Date().toISOString()}\n# argv: ${process.argv.join(' ')}\n\n`,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Wait for the underlying fd to actually close before resolving so
|
|
69
|
+
// callers can read the file immediately after the await.
|
|
70
|
+
const closeStream = () =>
|
|
71
|
+
new Promise((resolve) => {
|
|
72
|
+
stream.on('close', resolve);
|
|
73
|
+
stream.end();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const restore = async () => {
|
|
77
|
+
process.stdout.write = originalStdoutWrite;
|
|
78
|
+
process.stderr.write = originalStderrWrite;
|
|
79
|
+
await closeStream();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const result = await fn();
|
|
84
|
+
await restore();
|
|
85
|
+
return { result, logPath };
|
|
86
|
+
} catch (err) {
|
|
87
|
+
try {
|
|
88
|
+
stream.write(`\n# deploy threw: ${err instanceof Error ? err.stack || err.message : err}\n`);
|
|
89
|
+
} catch {}
|
|
90
|
+
await restore();
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wait for a DNS A record to propagate to public resolvers.
|
|
3
|
+
*
|
|
4
|
+
* Polls Cloudflare (1.1.1.1) and Google (8.8.8.8) resolvers until at least
|
|
5
|
+
* one returns the expected IP, or the timeout elapses. Returns true on
|
|
6
|
+
* success, false if the budget runs out (caller decides whether to proceed
|
|
7
|
+
* or hard-fail).
|
|
8
|
+
*
|
|
9
|
+
* Why this matters for compose deploys: Traefik attempts ACME HTTP-01
|
|
10
|
+
* challenges immediately on container start and bursts ~7 attempts in
|
|
11
|
+
* ~30s. Let's Encrypt rejects challenges with "no valid A records found"
|
|
12
|
+
* if its own resolver sees stale or absent records (the 0.0.0.0 warm-up
|
|
13
|
+
* placeholder also fails this check — LE treats 0.0.0.0 as not-a-valid-
|
|
14
|
+
* A-record). Once Traefik gives up, acme.json has zero certs and the
|
|
15
|
+
* browser sees the TRAEFIK DEFAULT CERT, which is self-signed and shows
|
|
16
|
+
* NET::ERR_CERT_AUTHORITY_INVALID. Cold-deploy 2026-05-19 RCA against
|
|
17
|
+
* vibecarbon.com hit exactly this.
|
|
18
|
+
*
|
|
19
|
+
* The fix: gate compose-up on DNS propagation so Traefik's first ACME
|
|
20
|
+
* attempt sees real records. Polling public resolvers is the closest
|
|
21
|
+
* approximation we have to "LE will see this" without actually calling
|
|
22
|
+
* LE's resolver directly.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} domain - The hostname to check (e.g. "vibecarbon.com")
|
|
25
|
+
* @param {string} expectedIp - The IP we expect the record to point to
|
|
26
|
+
* @param {number} [timeoutMs=120_000] - Total budget in ms
|
|
27
|
+
* @returns {Promise<boolean>} true if propagated, false if timed out
|
|
28
|
+
*/
|
|
29
|
+
export async function waitForDNSPropagation(domain, expectedIp, timeoutMs = 120_000) {
|
|
30
|
+
const dns = await import('node:dns');
|
|
31
|
+
const resolver = new dns.promises.Resolver();
|
|
32
|
+
resolver.setServers(['1.1.1.1', '8.8.8.8']);
|
|
33
|
+
|
|
34
|
+
const deadline = Date.now() + timeoutMs;
|
|
35
|
+
while (Date.now() < deadline) {
|
|
36
|
+
try {
|
|
37
|
+
const addrs = await resolver.resolve4(domain);
|
|
38
|
+
if (addrs.includes(expectedIp)) return true;
|
|
39
|
+
} catch {
|
|
40
|
+
// NXDOMAIN / SERVFAIL / timeout — record may not exist yet or
|
|
41
|
+
// hasn't propagated to this resolver. Retry; cheap to fail.
|
|
42
|
+
}
|
|
43
|
+
const remaining = deadline - Date.now();
|
|
44
|
+
if (remaining <= 0) break;
|
|
45
|
+
await new Promise((r) => setTimeout(r, Math.min(5_000, remaining)));
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared environment resolution
|
|
3
|
+
* Used by backup.js, restore.js, and failover.js
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as p from '@clack/prompts';
|
|
7
|
+
import { c } from './colors.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve which environment the user wants to operate on.
|
|
11
|
+
* - If `args.env` is set, validates it against config
|
|
12
|
+
* - If only one environment exists, selects it automatically
|
|
13
|
+
* - Otherwise, prompts the user to pick one
|
|
14
|
+
*
|
|
15
|
+
* @param {{ env: string | null }} args - Parsed CLI arguments
|
|
16
|
+
* @param {object} projectConfig - Project configuration with environments
|
|
17
|
+
* @returns {Promise<{ envName: string, envConfig: object }>}
|
|
18
|
+
*/
|
|
19
|
+
export async function resolveEnvironment(args, projectConfig) {
|
|
20
|
+
const envNames = Object.keys(projectConfig.environments || {});
|
|
21
|
+
|
|
22
|
+
if (envNames.length === 0) {
|
|
23
|
+
p.log.error('No deployed environments found');
|
|
24
|
+
p.log.info(`Run ${c.info('vibecarbon deploy')} first`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let envName = args.env;
|
|
29
|
+
|
|
30
|
+
if (!envName) {
|
|
31
|
+
if (envNames.length === 1) {
|
|
32
|
+
envName = envNames[0];
|
|
33
|
+
} else {
|
|
34
|
+
envName = await p.select({
|
|
35
|
+
message: 'Which environment?',
|
|
36
|
+
options: envNames.map((name) => ({
|
|
37
|
+
value: name,
|
|
38
|
+
label: name,
|
|
39
|
+
hint: projectConfig.environments[name].servers?.map((s) => s.ip).join(', '),
|
|
40
|
+
})),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (p.isCancel(envName)) {
|
|
46
|
+
p.cancel('Operation cancelled.');
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const envConfig = projectConfig.environments[envName];
|
|
51
|
+
if (!envConfig) {
|
|
52
|
+
p.log.error(`Environment '${envName}' not found`);
|
|
53
|
+
p.log.info(`Available environments: ${envNames.join(', ')}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { envName, envConfig };
|
|
58
|
+
}
|