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
package/src/restore.js
ADDED
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vibecarbon Restore Command
|
|
3
|
+
*
|
|
4
|
+
* Interactive-by-default. Bare `vibecarbon restore` walks the operator
|
|
5
|
+
* through env selection → source selection → confirm → restore.
|
|
6
|
+
* Positional and flag seeds skip the corresponding prompt. `-l` lists
|
|
7
|
+
* available backups for an env without restoring (the read-only path
|
|
8
|
+
* that was missing from the old CLI).
|
|
9
|
+
*
|
|
10
|
+
* `-source <name-or-path>` subsumes the old `--file` / `--from-s3` split:
|
|
11
|
+
* if the value resolves to an existing local file, treat it as a local
|
|
12
|
+
* upload-and-restore; otherwise validate as an S3 backup name and
|
|
13
|
+
* restore from S3.
|
|
14
|
+
*
|
|
15
|
+
* Form rule: vibecarbon uses single-dash flags only — see
|
|
16
|
+
* memory:feedback_cli_single_dash_flags.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, unlinkSync } from 'node:fs';
|
|
20
|
+
import { basename, join } from 'node:path';
|
|
21
|
+
import * as p from '@clack/prompts';
|
|
22
|
+
import { downloadS3Backup, listS3Backups } from './lib/backup-s3.js';
|
|
23
|
+
import { renderHelp } from './lib/cli/help.js';
|
|
24
|
+
import { parseFlags } from './lib/cli/parse-flags.js';
|
|
25
|
+
import { selectEnvironment } from './lib/cli/select-environment.js';
|
|
26
|
+
import { requireTTYOrFlags } from './lib/cli/tty-guard.js';
|
|
27
|
+
import { c, printBanner } from './lib/colors.js';
|
|
28
|
+
import {
|
|
29
|
+
loadBackupS3Config,
|
|
30
|
+
loadCredentials,
|
|
31
|
+
loadProjectConfig,
|
|
32
|
+
loadS3Config,
|
|
33
|
+
} from './lib/config.js';
|
|
34
|
+
import { getS3Credentials } from './lib/hetzner-guided-setup.js';
|
|
35
|
+
import { requireLicense } from './lib/licensing/index.js';
|
|
36
|
+
import { ensureOperatorIpAccess } from './lib/operator-ip.js';
|
|
37
|
+
import { perfAsync } from './lib/perf.js';
|
|
38
|
+
import { assertInProjectDir } from './lib/project-guard.js';
|
|
39
|
+
import {
|
|
40
|
+
getPostgresPod,
|
|
41
|
+
getSSHKeyPath,
|
|
42
|
+
scpUpload,
|
|
43
|
+
sshKubectl,
|
|
44
|
+
sshRun,
|
|
45
|
+
sshRunScript,
|
|
46
|
+
} from './lib/ssh.js';
|
|
47
|
+
import { createTracker } from './lib/tracker.js';
|
|
48
|
+
import { validateBackupFilename } from './lib/validators.js';
|
|
49
|
+
import { VERSION } from './lib/version.js';
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// COMMAND SPEC — single source of truth for argv parsing AND help output.
|
|
53
|
+
// ============================================================================
|
|
54
|
+
|
|
55
|
+
/** @type {import('./lib/cli/parse-flags.js').CommandSpec & { summary?: string, examples?: Array<{ command: string, description?: string }> }} */
|
|
56
|
+
const SPEC = {
|
|
57
|
+
name: 'restore',
|
|
58
|
+
summary: 'Restore a database from a backup',
|
|
59
|
+
positional: [
|
|
60
|
+
{
|
|
61
|
+
name: 'env',
|
|
62
|
+
optional: true,
|
|
63
|
+
description: 'Environment to restore (skips the env prompt)',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
flags: [
|
|
67
|
+
{ name: 'h', boolean: true, description: 'Show this help' },
|
|
68
|
+
{ name: 'v', boolean: true, description: 'Show version' },
|
|
69
|
+
{ name: 'y', boolean: true, description: 'Skip confirmation prompts' },
|
|
70
|
+
{
|
|
71
|
+
name: 'l',
|
|
72
|
+
boolean: true,
|
|
73
|
+
description: 'List available backups (read-only; does not restore)',
|
|
74
|
+
},
|
|
75
|
+
{ name: 'env', value: '<name>', description: 'Environment seed (alternative to positional)' },
|
|
76
|
+
{
|
|
77
|
+
name: 'source',
|
|
78
|
+
value: '<file-or-name-or-latest>',
|
|
79
|
+
description: 'Backup source: local file path, S3 backup name, or `latest`',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
examples: [
|
|
83
|
+
{ command: 'vibecarbon restore', description: 'prompts for env and backup' },
|
|
84
|
+
{ command: 'vibecarbon restore prod -l', description: 'list available backups for prod' },
|
|
85
|
+
{
|
|
86
|
+
command: 'vibecarbon restore prod -source myapp_20260507.tar.gz',
|
|
87
|
+
description: 'restore a specific backup from S3',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
command: 'vibecarbon restore prod -source ./backup.tar.gz -y',
|
|
91
|
+
description: 'restore from a local file, skipping confirmation',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
command: 'vibecarbon restore prod -y -source latest',
|
|
95
|
+
description: 'restore the most-recent S3 backup, non-interactively',
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// KUBECTL HELPERS (k8s mode)
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
function listRemoteBackups(ip, sshKeyPath) {
|
|
105
|
+
const pod = getPostgresPod(ip, sshKeyPath);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const output = sshKubectl(ip, sshKeyPath, [
|
|
109
|
+
'exec',
|
|
110
|
+
'-n',
|
|
111
|
+
'vibecarbon',
|
|
112
|
+
pod,
|
|
113
|
+
'--',
|
|
114
|
+
'sh',
|
|
115
|
+
'-c',
|
|
116
|
+
'ls -lt /backups/*_full.tar.gz /backups/*.sql.gz 2>/dev/null || echo NO_BACKUPS',
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
if (output === 'NO_BACKUPS' || !output) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return output
|
|
124
|
+
.split('\n')
|
|
125
|
+
.filter((line) => line.includes('.tar.gz') || line.includes('.sql.gz'))
|
|
126
|
+
.map((line) => {
|
|
127
|
+
const parts = line.split(/\s+/);
|
|
128
|
+
return {
|
|
129
|
+
size: parts[4],
|
|
130
|
+
date: `${parts[5]} ${parts[6]} ${parts[7]}`,
|
|
131
|
+
name: parts[parts.length - 1].replace('/backups/', ''),
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
} catch {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// RESTORE OPERATIONS (k8s mode)
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
function scaleDownApp(ip, sshKeyPath) {
|
|
144
|
+
sshKubectl(ip, sshKeyPath, ['scale', 'deployment', 'app', '-n', 'vibecarbon', '--replicas=0']);
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
sshKubectl(ip, sshKeyPath, [
|
|
148
|
+
'rollout',
|
|
149
|
+
'status',
|
|
150
|
+
'deployment/app',
|
|
151
|
+
'-n',
|
|
152
|
+
'vibecarbon',
|
|
153
|
+
'--timeout=60s',
|
|
154
|
+
]);
|
|
155
|
+
} catch {
|
|
156
|
+
// Continue even if timeout
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function scaleUpApp(ip, sshKeyPath) {
|
|
161
|
+
sshKubectl(ip, sshKeyPath, ['scale', 'deployment', 'app', '-n', 'vibecarbon', '--replicas=2']);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function verifyPostgres(ip, sshKeyPath) {
|
|
165
|
+
const pod = getPostgresPod(ip, sshKeyPath);
|
|
166
|
+
sshKubectl(ip, sshKeyPath, [
|
|
167
|
+
'exec',
|
|
168
|
+
'-n',
|
|
169
|
+
'vibecarbon',
|
|
170
|
+
pod,
|
|
171
|
+
'--',
|
|
172
|
+
'pg_isready',
|
|
173
|
+
'-U',
|
|
174
|
+
'postgres',
|
|
175
|
+
]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Restore from a full archive (.tar.gz containing postgres.sql.gz + optional storage.tar.gz)
|
|
180
|
+
*/
|
|
181
|
+
function restoreFromArchive(ip, sshKeyPath, remotePath) {
|
|
182
|
+
const pod = getPostgresPod(ip, sshKeyPath);
|
|
183
|
+
|
|
184
|
+
// Extract the archive on the server — argv form; remotePath is never shell-interpolated.
|
|
185
|
+
sshRun(ip, sshKeyPath, ['mkdir', '-p', '/tmp/restore']);
|
|
186
|
+
// GNU tar strips leading `/` by default (no `-P` / `--absolute-names`),
|
|
187
|
+
// so absolute paths in a hostile archive never escape /tmp/restore.
|
|
188
|
+
sshRun(ip, sshKeyPath, [
|
|
189
|
+
'tar',
|
|
190
|
+
'--no-xattrs',
|
|
191
|
+
'--no-same-owner',
|
|
192
|
+
'-xzf',
|
|
193
|
+
remotePath,
|
|
194
|
+
'-C',
|
|
195
|
+
'/tmp/restore',
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
// Check archive contents
|
|
199
|
+
const contents = sshRun(ip, sshKeyPath, ['ls', '/tmp/restore/']);
|
|
200
|
+
|
|
201
|
+
// Restore database
|
|
202
|
+
if (contents.includes('postgres.sql.gz')) {
|
|
203
|
+
sshKubectl(ip, sshKeyPath, [
|
|
204
|
+
'cp',
|
|
205
|
+
'/tmp/restore/postgres.sql.gz',
|
|
206
|
+
`vibecarbon/${pod}:/tmp/postgres.sql.gz`,
|
|
207
|
+
]);
|
|
208
|
+
sshKubectl(ip, sshKeyPath, [
|
|
209
|
+
'exec',
|
|
210
|
+
'-n',
|
|
211
|
+
'vibecarbon',
|
|
212
|
+
pod,
|
|
213
|
+
'--',
|
|
214
|
+
'sh',
|
|
215
|
+
'-c',
|
|
216
|
+
'gunzip -c /tmp/postgres.sql.gz | psql -U supabase_admin postgres',
|
|
217
|
+
]);
|
|
218
|
+
sshKubectl(ip, sshKeyPath, [
|
|
219
|
+
'exec',
|
|
220
|
+
'-n',
|
|
221
|
+
'vibecarbon',
|
|
222
|
+
pod,
|
|
223
|
+
'--',
|
|
224
|
+
'rm',
|
|
225
|
+
'-f',
|
|
226
|
+
'/tmp/postgres.sql.gz',
|
|
227
|
+
]);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Restore storage files if present and using file backend
|
|
231
|
+
if (contents.includes('storage.tar.gz')) {
|
|
232
|
+
// kubectl get returns empty string if STORAGE_BACKEND env var is unset;
|
|
233
|
+
// default to 'file' so the legacy single-backend path still works.
|
|
234
|
+
let storageBackend;
|
|
235
|
+
try {
|
|
236
|
+
storageBackend = sshKubectl(ip, sshKeyPath, [
|
|
237
|
+
'get',
|
|
238
|
+
'deployment',
|
|
239
|
+
'storage',
|
|
240
|
+
'-n',
|
|
241
|
+
'vibecarbon',
|
|
242
|
+
'-o',
|
|
243
|
+
'jsonpath={.spec.template.spec.containers[0].env[?(@.name=="STORAGE_BACKEND")].value}',
|
|
244
|
+
]);
|
|
245
|
+
} catch {
|
|
246
|
+
storageBackend = 'file';
|
|
247
|
+
}
|
|
248
|
+
if (!storageBackend) storageBackend = 'file';
|
|
249
|
+
|
|
250
|
+
if (storageBackend === 'file') {
|
|
251
|
+
const storagePod = sshKubectl(ip, sshKeyPath, [
|
|
252
|
+
'get',
|
|
253
|
+
'pods',
|
|
254
|
+
'-n',
|
|
255
|
+
'vibecarbon',
|
|
256
|
+
'-l',
|
|
257
|
+
'app.kubernetes.io/name=supabase-storage',
|
|
258
|
+
'-o',
|
|
259
|
+
'jsonpath={.items[0].metadata.name}',
|
|
260
|
+
]);
|
|
261
|
+
|
|
262
|
+
sshKubectl(ip, sshKeyPath, [
|
|
263
|
+
'cp',
|
|
264
|
+
'/tmp/restore/storage.tar.gz',
|
|
265
|
+
`vibecarbon/${storagePod}:/tmp/storage.tar.gz`,
|
|
266
|
+
]);
|
|
267
|
+
sshKubectl(ip, sshKeyPath, [
|
|
268
|
+
'exec',
|
|
269
|
+
'-n',
|
|
270
|
+
'vibecarbon',
|
|
271
|
+
storagePod,
|
|
272
|
+
'--',
|
|
273
|
+
'sh',
|
|
274
|
+
'-c',
|
|
275
|
+
'tar --no-xattrs --no-same-owner -xzf /tmp/storage.tar.gz -C / && rm -f /tmp/storage.tar.gz',
|
|
276
|
+
]);
|
|
277
|
+
} else {
|
|
278
|
+
p.log.info('Storage backend is S3 — skipping file restore (files already in S3)');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Cleanup
|
|
283
|
+
sshRun(ip, sshKeyPath, ['rm', '-rf', '/tmp/restore']);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Restore from a legacy .sql.gz file (database only)
|
|
288
|
+
*/
|
|
289
|
+
function restoreFromSqlGz(ip, sshKeyPath, backupName) {
|
|
290
|
+
const pod = getPostgresPod(ip, sshKeyPath);
|
|
291
|
+
|
|
292
|
+
// backupName is validated via validateBackupFilename at entry; still pass it
|
|
293
|
+
// as a positional sh arg so the remote shell never expands it as code.
|
|
294
|
+
sshKubectl(ip, sshKeyPath, [
|
|
295
|
+
'exec',
|
|
296
|
+
'-n',
|
|
297
|
+
'vibecarbon',
|
|
298
|
+
pod,
|
|
299
|
+
'--',
|
|
300
|
+
'sh',
|
|
301
|
+
'-c',
|
|
302
|
+
'gunzip -c "/backups/$1" | psql -U supabase_admin postgres',
|
|
303
|
+
'_',
|
|
304
|
+
backupName,
|
|
305
|
+
]);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function restoreFromRemote(ip, sshKeyPath, backupName) {
|
|
309
|
+
scaleDownApp(ip, sshKeyPath);
|
|
310
|
+
|
|
311
|
+
if (backupName.endsWith('_full.tar.gz')) {
|
|
312
|
+
restoreFromArchive(ip, sshKeyPath, `/tmp/${backupName}`);
|
|
313
|
+
} else {
|
|
314
|
+
restoreFromSqlGz(ip, sshKeyPath, backupName);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
scaleUpApp(ip, sshKeyPath);
|
|
318
|
+
verifyPostgres(ip, sshKeyPath);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function uploadAndRestore(ip, sshKeyPath, localFile) {
|
|
322
|
+
const filename = basename(localFile);
|
|
323
|
+
|
|
324
|
+
// Upload local file to server
|
|
325
|
+
scpUpload(ip, sshKeyPath, localFile, `/tmp/${filename}`);
|
|
326
|
+
|
|
327
|
+
scaleDownApp(ip, sshKeyPath);
|
|
328
|
+
|
|
329
|
+
if (filename.endsWith('_full.tar.gz')) {
|
|
330
|
+
restoreFromArchive(ip, sshKeyPath, `/tmp/${filename}`);
|
|
331
|
+
} else if (filename.endsWith('.sql.gz')) {
|
|
332
|
+
const pod = getPostgresPod(ip, sshKeyPath);
|
|
333
|
+
sshKubectl(ip, sshKeyPath, [
|
|
334
|
+
'cp',
|
|
335
|
+
`/tmp/${filename}`,
|
|
336
|
+
`vibecarbon/${pod}:/backups/${filename}`,
|
|
337
|
+
]);
|
|
338
|
+
restoreFromSqlGz(ip, sshKeyPath, filename);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Clean up temp file on host
|
|
342
|
+
sshRun(ip, sshKeyPath, ['rm', '-f', `/tmp/${filename}`]);
|
|
343
|
+
|
|
344
|
+
scaleUpApp(ip, sshKeyPath);
|
|
345
|
+
verifyPostgres(ip, sshKeyPath);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ============================================================================
|
|
349
|
+
// SOURCE INTERPRETATION
|
|
350
|
+
// ============================================================================
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The `-source` flag accepts either a local file path or an S3 backup
|
|
354
|
+
* name. Existence on disk is the signal: if the value resolves to a
|
|
355
|
+
* file we treat it as upload-and-restore; otherwise we validate as an
|
|
356
|
+
* S3 backup name and restore from S3.
|
|
357
|
+
*
|
|
358
|
+
* @param {string} source
|
|
359
|
+
* @returns {{ kind: 'local', path: string, name: string } | { kind: 's3', name: string }}
|
|
360
|
+
*/
|
|
361
|
+
function classifySource(source) {
|
|
362
|
+
if (existsSync(source)) {
|
|
363
|
+
return { kind: 'local', path: source, name: basename(source) };
|
|
364
|
+
}
|
|
365
|
+
// Not a local path — assume S3 backup name. Validate format up front
|
|
366
|
+
// so downstream code can trust it.
|
|
367
|
+
const err = validateBackupFilename(source);
|
|
368
|
+
if (err) {
|
|
369
|
+
p.log.error(`Invalid -source: ${err}`);
|
|
370
|
+
p.log.info(
|
|
371
|
+
`Expected a local file path or S3 backup name (e.g. myapp_20260507_120000_full.tar.gz)`,
|
|
372
|
+
);
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
return { kind: 's3', name: source };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ============================================================================
|
|
379
|
+
// MAIN
|
|
380
|
+
// ============================================================================
|
|
381
|
+
|
|
382
|
+
export async function run(args) {
|
|
383
|
+
const { values, positional, errors } = parseFlags(args, SPEC);
|
|
384
|
+
|
|
385
|
+
if (errors.length > 0) {
|
|
386
|
+
for (const e of errors) {
|
|
387
|
+
process.stderr.write(`${c.error('✗')} ${e}\n`);
|
|
388
|
+
}
|
|
389
|
+
process.stderr.write(`Run ${c.info('vibecarbon restore -h')} for usage.\n`);
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (values.h) {
|
|
394
|
+
process.stdout.write(renderHelp(SPEC));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (values.v) {
|
|
398
|
+
console.log(`vibecarbon v${VERSION}`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Project guard runs before banner so an accidental `vibecarbon
|
|
403
|
+
// restore` from a parent directory emits the canonical message.
|
|
404
|
+
assertInProjectDir();
|
|
405
|
+
|
|
406
|
+
requireLicense('restore');
|
|
407
|
+
printBanner();
|
|
408
|
+
p.intro(`${c.bold('vibecarbon restore')} ${c.dim(`v${VERSION}`)}`);
|
|
409
|
+
|
|
410
|
+
const projectConfig = loadProjectConfig();
|
|
411
|
+
const envCount = Object.keys(projectConfig.environments || {}).length;
|
|
412
|
+
// Positional wins over flag when both are supplied — bare-form is the
|
|
413
|
+
// canonical surface, the flag is the scripting alternative.
|
|
414
|
+
const envSeed =
|
|
415
|
+
/** @type {string|undefined} */ (positional.env) ||
|
|
416
|
+
/** @type {string|null} */ (values.env) ||
|
|
417
|
+
null;
|
|
418
|
+
|
|
419
|
+
// Top-level TTY guard: env prompt fires if multi-env and no seed.
|
|
420
|
+
// Source-prompt requirements gate after the env is resolved (since
|
|
421
|
+
// the picker depends on which backups exist for the env).
|
|
422
|
+
requireTTYOrFlags({
|
|
423
|
+
requirements: [
|
|
424
|
+
{
|
|
425
|
+
flag: 'env',
|
|
426
|
+
description: 'name an environment to restore',
|
|
427
|
+
satisfied: !!envSeed || envCount <= 1,
|
|
428
|
+
},
|
|
429
|
+
],
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const { envName, envConfig } = await selectEnvironment(projectConfig, {
|
|
433
|
+
actionVerb: 'restore',
|
|
434
|
+
seed: envSeed,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const serverIp = envConfig.servers?.[0]?.ip;
|
|
438
|
+
if (!serverIp) {
|
|
439
|
+
p.log.error(`No server IP found for environment '${envName}'.`);
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Source TTY guard fires *before* infrastructure checks so the
|
|
444
|
+
// off-TTY error message is "needs -source" rather than "SSH key
|
|
445
|
+
// not found" — the latter would correctly diagnose a different
|
|
446
|
+
// problem but obscures what the operator actually needs to fix
|
|
447
|
+
// for a scripted invocation.
|
|
448
|
+
if (!values.l && !values.source) {
|
|
449
|
+
requireTTYOrFlags({
|
|
450
|
+
requirements: [
|
|
451
|
+
{
|
|
452
|
+
flag: 'source',
|
|
453
|
+
description: 'name the backup to restore (S3 name or local file path)',
|
|
454
|
+
satisfied: false,
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Operator-IP firewall refresh — the listing path needs SSH/k8s
|
|
461
|
+
// reachability too (we may fall back to a pod-list when S3 isn't
|
|
462
|
+
// configured), so we run this regardless of -l.
|
|
463
|
+
const apiToken = loadCredentials()?.hetzner?.apiToken;
|
|
464
|
+
if (apiToken) {
|
|
465
|
+
try {
|
|
466
|
+
const isHA = envConfig.deployMode === 'compose-ha' || !!envConfig.ha?.enabled;
|
|
467
|
+
const result = await ensureOperatorIpAccess({
|
|
468
|
+
projectConfig,
|
|
469
|
+
environment: envName,
|
|
470
|
+
isHA,
|
|
471
|
+
apiToken,
|
|
472
|
+
yes: !!values.y,
|
|
473
|
+
onMessage: (msg) => p.log.info(msg),
|
|
474
|
+
});
|
|
475
|
+
if (result.added) {
|
|
476
|
+
p.log.success(`Firewall updated: SSH/k8s API now allow ${result.cidr}`);
|
|
477
|
+
}
|
|
478
|
+
} catch (err) {
|
|
479
|
+
p.log.error(`Operator-IP check failed: ${err.message}`);
|
|
480
|
+
process.exit(1);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// SSH key is required for everything restore touches.
|
|
485
|
+
const sshKeyPath = getSSHKeyPath(envName);
|
|
486
|
+
if (!existsSync(sshKeyPath)) {
|
|
487
|
+
p.log.error(`SSH key not found: ${sshKeyPath}`);
|
|
488
|
+
p.log.info(
|
|
489
|
+
'The deployment key is created during deploy and is required for restore operations.',
|
|
490
|
+
);
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const projectName = projectConfig.projectName || 'project';
|
|
495
|
+
const isCompose = envConfig.deployMode === 'compose' || envConfig.deployMode === 'compose-ha';
|
|
496
|
+
|
|
497
|
+
// -l: list-only path. Read-only; print and exit.
|
|
498
|
+
if (values.l) {
|
|
499
|
+
await runList({ isCompose, envName, projectName, serverIp, sshKeyPath });
|
|
500
|
+
p.outro('');
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Resolve mode-specific S3 config + the chosen backup source.
|
|
505
|
+
const s3 = await resolveS3({ isCompose, envName, projectName });
|
|
506
|
+
const useS3 = Boolean(s3?.secretKey);
|
|
507
|
+
|
|
508
|
+
// `-source latest` is a non-interactive sentinel that routes through the
|
|
509
|
+
// same auto-pick path the interactive prompt uses under `-y` — the newest
|
|
510
|
+
// S3 backup wins. Lets scripted callers (e2e, post-incident
|
|
511
|
+
// restores) ask for "the most recent" without first listing + parsing
|
|
512
|
+
// names from `vibecarbon backup -l`.
|
|
513
|
+
const wantsLatest = values.source === 'latest';
|
|
514
|
+
const chosenSource =
|
|
515
|
+
!wantsLatest && values.source
|
|
516
|
+
? classifySource(/** @type {string} */ (values.source))
|
|
517
|
+
: await pickInteractiveSource({
|
|
518
|
+
isCompose,
|
|
519
|
+
envName,
|
|
520
|
+
projectName,
|
|
521
|
+
serverIp,
|
|
522
|
+
sshKeyPath,
|
|
523
|
+
s3,
|
|
524
|
+
useS3,
|
|
525
|
+
yes: wantsLatest || !!values.y,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
if (!chosenSource) {
|
|
529
|
+
p.log.error('No backup selected.');
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Confirmation gates
|
|
534
|
+
p.log.warn('This will overwrite the current database!');
|
|
535
|
+
p.log.message(` ${c.dim('Environment'.padEnd(14))} ${c.bold(envName)}`);
|
|
536
|
+
p.log.message(` ${c.dim('Server'.padEnd(14))} ${c.bold(serverIp)}`);
|
|
537
|
+
p.log.message(` ${c.dim('Backup'.padEnd(14))} ${c.bold(chosenSource.name)}`);
|
|
538
|
+
|
|
539
|
+
if (!values.y) {
|
|
540
|
+
const confirm = await p.confirm({
|
|
541
|
+
message: `Restore ${c.bold(chosenSource.name)} to ${c.bold(envName)}?`,
|
|
542
|
+
});
|
|
543
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
544
|
+
p.cancel('Operation cancelled.');
|
|
545
|
+
process.exit(0);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (envName === 'prod' || envName === 'production') {
|
|
549
|
+
const doubleConfirm = await p.text({
|
|
550
|
+
message: `Type "${envName}" to confirm restoration of production database:`,
|
|
551
|
+
validate: (v) => (v !== envName ? `Please type "${envName}" to confirm` : undefined),
|
|
552
|
+
});
|
|
553
|
+
if (p.isCancel(doubleConfirm)) {
|
|
554
|
+
p.cancel('Operation cancelled.');
|
|
555
|
+
process.exit(0);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Dispatch by mode
|
|
561
|
+
const tracker = createTracker('restore', { environment: envName });
|
|
562
|
+
const s = tracker.spinner();
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
if (isCompose) {
|
|
566
|
+
await runComposeRestore({
|
|
567
|
+
s,
|
|
568
|
+
chosenSource,
|
|
569
|
+
envName,
|
|
570
|
+
projectName,
|
|
571
|
+
serverIp,
|
|
572
|
+
sshKeyPath,
|
|
573
|
+
s3WithCreds: s3,
|
|
574
|
+
});
|
|
575
|
+
} else {
|
|
576
|
+
await runK8sRestore({
|
|
577
|
+
s,
|
|
578
|
+
chosenSource,
|
|
579
|
+
envName,
|
|
580
|
+
serverIp,
|
|
581
|
+
sshKeyPath,
|
|
582
|
+
s3,
|
|
583
|
+
useS3,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
} catch (error) {
|
|
587
|
+
s.stop('Restore failed');
|
|
588
|
+
p.log.error(error.message);
|
|
589
|
+
if (!isCompose) {
|
|
590
|
+
p.log.warn('The app deployment may need to be manually scaled back up:');
|
|
591
|
+
p.log.info(
|
|
592
|
+
` ssh root@${serverIp} "kubectl scale deployment app -n vibecarbon --replicas=2"`,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
tracker.finish();
|
|
599
|
+
p.outro(c.success('Done'));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ============================================================================
|
|
603
|
+
// ORCHESTRATION HELPERS
|
|
604
|
+
// ============================================================================
|
|
605
|
+
|
|
606
|
+
async function resolveS3({ isCompose, envName, projectName }) {
|
|
607
|
+
if (isCompose) {
|
|
608
|
+
const backupS3 = loadBackupS3Config(envName);
|
|
609
|
+
if (!backupS3) return null;
|
|
610
|
+
const creds = await getS3Credentials(projectName, { save: false });
|
|
611
|
+
if (!creds) return null;
|
|
612
|
+
return { ...backupS3, accessKey: creds.accessKey, secretKey: creds.secretKey };
|
|
613
|
+
}
|
|
614
|
+
const backupS3 = loadBackupS3Config(envName);
|
|
615
|
+
const storageS3 = loadS3Config(envName);
|
|
616
|
+
let s3 = backupS3 || storageS3;
|
|
617
|
+
if (s3 && !s3.secretKey) {
|
|
618
|
+
const creds = await getS3Credentials(projectName, { save: false });
|
|
619
|
+
if (creds) {
|
|
620
|
+
s3 = { ...s3, accessKey: creds.accessKey, secretKey: creds.secretKey };
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return s3;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
async function runList({ isCompose, envName, projectName, serverIp, sshKeyPath }) {
|
|
627
|
+
const s3 = await resolveS3({ isCompose, envName, projectName });
|
|
628
|
+
const useS3 = Boolean(s3?.secretKey);
|
|
629
|
+
|
|
630
|
+
if (useS3) {
|
|
631
|
+
try {
|
|
632
|
+
const backups = await listS3Backups(s3);
|
|
633
|
+
printBackupList(backups, envName);
|
|
634
|
+
} catch (error) {
|
|
635
|
+
p.log.error(`Failed to list S3 backups: ${error.message}`);
|
|
636
|
+
process.exit(1);
|
|
637
|
+
}
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (isCompose) {
|
|
642
|
+
try {
|
|
643
|
+
const output = sshRunScript(
|
|
644
|
+
serverIp,
|
|
645
|
+
sshKeyPath,
|
|
646
|
+
`ls -lt /opt/${projectName}/backups/*_full.tar.gz 2>/dev/null || echo NO_BACKUPS`,
|
|
647
|
+
);
|
|
648
|
+
if (output === 'NO_BACKUPS' || !output.trim()) {
|
|
649
|
+
p.log.info('No backups found.');
|
|
650
|
+
p.log.info(`Create one with: ${c.info(`vibecarbon backup ${envName}`)}`);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const lines = output.trim().split('\n').filter(Boolean);
|
|
654
|
+
p.log.info(`${c.bold(`Backups for ${envName}`)} (${lines.length} found):`);
|
|
655
|
+
for (const line of lines) {
|
|
656
|
+
p.log.message(` ${line}`);
|
|
657
|
+
}
|
|
658
|
+
} catch (error) {
|
|
659
|
+
p.log.error(`Failed to list backups: ${error.message}`);
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// K8s pod fallback
|
|
665
|
+
const backups = listRemoteBackups(serverIp, sshKeyPath);
|
|
666
|
+
printBackupList(backups, envName);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function printBackupList(backups, envName) {
|
|
670
|
+
if (!backups || backups.length === 0) {
|
|
671
|
+
p.log.info('No backups found.');
|
|
672
|
+
p.log.info(`Create one with: ${c.info(`vibecarbon backup ${envName}`)}`);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
p.log.info(`${c.bold(`Backups for ${envName}`)} (${backups.length} found):`);
|
|
676
|
+
for (const backup of backups) {
|
|
677
|
+
p.log.message(` ${c.dim(backup.date.padEnd(22))} ${backup.size.padStart(10)} ${backup.name}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
async function pickInteractiveSource({
|
|
682
|
+
isCompose,
|
|
683
|
+
envName,
|
|
684
|
+
projectName,
|
|
685
|
+
serverIp,
|
|
686
|
+
sshKeyPath,
|
|
687
|
+
s3,
|
|
688
|
+
useS3,
|
|
689
|
+
yes,
|
|
690
|
+
}) {
|
|
691
|
+
const tracker = createTracker('restore.pickSource', { environment: envName });
|
|
692
|
+
const s = tracker.spinner();
|
|
693
|
+
|
|
694
|
+
if (useS3) {
|
|
695
|
+
s.start('Fetching available backups from S3');
|
|
696
|
+
const backups = await listS3Backups(s3);
|
|
697
|
+
s.stop('Backups retrieved');
|
|
698
|
+
if (backups.length === 0) {
|
|
699
|
+
p.log.error('No backups found in S3.');
|
|
700
|
+
p.log.info(`Create one with: ${c.info(`vibecarbon backup ${envName}`)}`);
|
|
701
|
+
process.exit(1);
|
|
702
|
+
}
|
|
703
|
+
if (yes) {
|
|
704
|
+
const latest = backups[0].name;
|
|
705
|
+
p.log.info(`Auto-selected latest backup: ${c.bold(latest)}`);
|
|
706
|
+
return { kind: 's3', name: latest };
|
|
707
|
+
}
|
|
708
|
+
const selected = await p.select({
|
|
709
|
+
message: 'Select backup to restore:',
|
|
710
|
+
options: backups.map((b) => ({
|
|
711
|
+
value: b.name,
|
|
712
|
+
label: b.name,
|
|
713
|
+
hint: `${b.size} — ${b.date}`,
|
|
714
|
+
})),
|
|
715
|
+
});
|
|
716
|
+
if (p.isCancel(selected)) {
|
|
717
|
+
p.cancel('Operation cancelled.');
|
|
718
|
+
process.exit(0);
|
|
719
|
+
}
|
|
720
|
+
return { kind: 's3', name: /** @type {string} */ (selected) };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Non-S3 fallback: list from server filesystem (compose) or pod (k8s).
|
|
724
|
+
if (isCompose) {
|
|
725
|
+
s.start('Listing available backups');
|
|
726
|
+
const output = sshRunScript(
|
|
727
|
+
serverIp,
|
|
728
|
+
sshKeyPath,
|
|
729
|
+
`ls -t /opt/${projectName}/backups/*_full.tar.gz 2>/dev/null || echo NO_BACKUPS`,
|
|
730
|
+
);
|
|
731
|
+
s.stop('');
|
|
732
|
+
if (output === 'NO_BACKUPS' || !output.trim()) {
|
|
733
|
+
p.log.error('No backups found on the server.');
|
|
734
|
+
p.log.info(`Create one with: ${c.info(`vibecarbon backup ${envName}`)}`);
|
|
735
|
+
process.exit(1);
|
|
736
|
+
}
|
|
737
|
+
const names = output
|
|
738
|
+
.trim()
|
|
739
|
+
.split('\n')
|
|
740
|
+
.filter(Boolean)
|
|
741
|
+
.map((f) => basename(f));
|
|
742
|
+
if (yes) {
|
|
743
|
+
const latest = names[0];
|
|
744
|
+
p.log.info(`Auto-selected latest backup: ${c.bold(latest)}`);
|
|
745
|
+
return { kind: 's3', name: latest };
|
|
746
|
+
}
|
|
747
|
+
const selected = await p.select({
|
|
748
|
+
message: 'Select backup to restore:',
|
|
749
|
+
options: names.map((b) => ({ value: b, label: b })),
|
|
750
|
+
});
|
|
751
|
+
if (p.isCancel(selected)) {
|
|
752
|
+
p.cancel('Operation cancelled.');
|
|
753
|
+
process.exit(0);
|
|
754
|
+
}
|
|
755
|
+
return { kind: 's3', name: /** @type {string} */ (selected) };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// K8s pod fallback
|
|
759
|
+
s.start('Fetching available backups');
|
|
760
|
+
const backups = listRemoteBackups(serverIp, sshKeyPath);
|
|
761
|
+
s.stop('Backups retrieved');
|
|
762
|
+
if (backups.length === 0) {
|
|
763
|
+
p.log.error('No backups found on the server.');
|
|
764
|
+
p.log.info(`Create one with: ${c.info(`vibecarbon backup ${envName}`)}`);
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|
|
767
|
+
if (yes) {
|
|
768
|
+
const latest = backups[0].name;
|
|
769
|
+
p.log.info(`Auto-selected latest backup: ${c.bold(latest)}`);
|
|
770
|
+
return { kind: 's3', name: latest };
|
|
771
|
+
}
|
|
772
|
+
const selected = await p.select({
|
|
773
|
+
message: 'Select backup to restore:',
|
|
774
|
+
options: backups.map((b) => ({ value: b.name, label: b.name, hint: `${b.size} — ${b.date}` })),
|
|
775
|
+
});
|
|
776
|
+
if (p.isCancel(selected)) {
|
|
777
|
+
p.cancel('Operation cancelled.');
|
|
778
|
+
process.exit(0);
|
|
779
|
+
}
|
|
780
|
+
return { kind: 's3', name: /** @type {string} */ (selected) };
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async function runComposeRestore({
|
|
784
|
+
s,
|
|
785
|
+
chosenSource,
|
|
786
|
+
envName,
|
|
787
|
+
projectName,
|
|
788
|
+
serverIp,
|
|
789
|
+
sshKeyPath,
|
|
790
|
+
s3WithCreds,
|
|
791
|
+
}) {
|
|
792
|
+
const backupFile = chosenSource.name;
|
|
793
|
+
|
|
794
|
+
if (chosenSource.kind === 'local') {
|
|
795
|
+
p.log.info(`Uploading local file: ${c.bold(chosenSource.path)}`);
|
|
796
|
+
scpUpload(serverIp, sshKeyPath, chosenSource.path, `/opt/${projectName}/backups/${backupFile}`);
|
|
797
|
+
} else if (s3WithCreds?.secretKey) {
|
|
798
|
+
s.start(`Downloading ${backupFile} from S3 to server`);
|
|
799
|
+
const tmpLocal = join(process.cwd(), `.restore-tmp-${backupFile}`);
|
|
800
|
+
try {
|
|
801
|
+
await downloadS3Backup(s3WithCreds, `backups/${backupFile}`, tmpLocal);
|
|
802
|
+
scpUpload(serverIp, sshKeyPath, tmpLocal, `/opt/${projectName}/backups/${backupFile}`);
|
|
803
|
+
try {
|
|
804
|
+
unlinkSync(tmpLocal);
|
|
805
|
+
} catch {
|
|
806
|
+
// ignore cleanup errors
|
|
807
|
+
}
|
|
808
|
+
s.stop('Backup transferred to server');
|
|
809
|
+
} catch (error) {
|
|
810
|
+
s.stop(`S3 download failed: ${error.message}`);
|
|
811
|
+
throw error;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
s.start('Restoring database...');
|
|
816
|
+
const { restoreCompose } = await import('./lib/deploy/compose/index.js');
|
|
817
|
+
restoreCompose(serverIp, sshKeyPath, projectName, backupFile);
|
|
818
|
+
s.stop('Database restored');
|
|
819
|
+
p.log.success('Restore completed successfully');
|
|
820
|
+
// env name is unused by restoreCompose but kept in the closure for
|
|
821
|
+
// diagnostic logging consistency.
|
|
822
|
+
void envName;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
async function runK8sRestore({ s, chosenSource, envName, serverIp, sshKeyPath, s3, useS3 }) {
|
|
826
|
+
s.start('Restoring database (app will be temporarily unavailable)');
|
|
827
|
+
|
|
828
|
+
if (chosenSource.kind === 'local') {
|
|
829
|
+
if (!chosenSource.path.endsWith('.sql.gz') && !chosenSource.path.endsWith('.tar.gz')) {
|
|
830
|
+
throw new Error('Backup file must be a .sql.gz or .tar.gz file');
|
|
831
|
+
}
|
|
832
|
+
await perfAsync('restore.uploadAndRestore', () =>
|
|
833
|
+
uploadAndRestore(serverIp, sshKeyPath, chosenSource.path),
|
|
834
|
+
);
|
|
835
|
+
s.stop('Database restored');
|
|
836
|
+
} else if (useS3) {
|
|
837
|
+
s.message('Downloading backup from S3');
|
|
838
|
+
const tempPath = join(process.cwd(), `.restore-tmp-${chosenSource.name}`);
|
|
839
|
+
await perfAsync('restore.downloadS3', () =>
|
|
840
|
+
downloadS3Backup(s3, `backups/${chosenSource.name}`, tempPath),
|
|
841
|
+
);
|
|
842
|
+
s.message('Uploading backup to server');
|
|
843
|
+
await perfAsync('restore.scpUpload', async () =>
|
|
844
|
+
scpUpload(serverIp, sshKeyPath, tempPath, `/tmp/${chosenSource.name}`),
|
|
845
|
+
);
|
|
846
|
+
try {
|
|
847
|
+
unlinkSync(tempPath);
|
|
848
|
+
} catch {
|
|
849
|
+
// ignore cleanup errors
|
|
850
|
+
}
|
|
851
|
+
s.message('Restoring database');
|
|
852
|
+
await perfAsync('restore.restoreFromRemote', () =>
|
|
853
|
+
restoreFromRemote(serverIp, sshKeyPath, chosenSource.name),
|
|
854
|
+
);
|
|
855
|
+
s.stop('Database restored');
|
|
856
|
+
} else {
|
|
857
|
+
// Pod-only fallback: backup name refers to a file already in the pod.
|
|
858
|
+
await restoreFromRemote(serverIp, sshKeyPath, chosenSource.name);
|
|
859
|
+
s.stop('Database restored');
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
p.log.success('Restore completed successfully');
|
|
863
|
+
p.log.info('The application has been scaled back up.');
|
|
864
|
+
p.log.info(`Verify with: ${c.info(`vibecarbon status ${envName}`)}`);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// ============================================================================
|
|
868
|
+
// EXPORTS FOR TESTING
|
|
869
|
+
// ============================================================================
|
|
870
|
+
|
|
871
|
+
export { SPEC };
|