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,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for loading + rendering the k3s cloud-init scripts in
|
|
3
|
+
* `carbon/cloud-init/k3s/` (master-init.sh / supabase-init.sh /
|
|
4
|
+
* worker-init.sh).
|
|
5
|
+
*
|
|
6
|
+
* Used by:
|
|
7
|
+
* - `src/lib/iac/programs/hetzner-k8s.js` to render user_data baked
|
|
8
|
+
* into the master/supabase/static-worker servers at Pulumi up.
|
|
9
|
+
* - `src/lib/deploy/k8s/k3s.js` (`renderCAClusterConfig`) to render
|
|
10
|
+
* the worker-init.sh body that cluster-autoscaler stuffs into its
|
|
11
|
+
* `cluster-config.json` Secret. CA spawns workers on demand and
|
|
12
|
+
* base64-encodes this body into user_data on each `ServerCreate`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = dirname(__filename);
|
|
21
|
+
|
|
22
|
+
// Cloud-init scripts live in carbon/cloud-init/k3s/.
|
|
23
|
+
// Relative to this file: src/lib/iac/ → ../../../carbon/cloud-init/k3s
|
|
24
|
+
const CLOUD_INIT_DIR = join(__dirname, '../../../carbon/cloud-init/k3s');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read a cloud-init script template by basename (e.g. 'master-init.sh').
|
|
28
|
+
* Returns the raw template — `${var}` placeholders are NOT yet
|
|
29
|
+
* substituted; pass to `renderScript()` to substitute.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} name Basename inside carbon/cloud-init/k3s/.
|
|
32
|
+
* @returns {string} UTF-8 contents of the script.
|
|
33
|
+
*/
|
|
34
|
+
export function loadCloudInit(name) {
|
|
35
|
+
return readFileSync(join(CLOUD_INIT_DIR, name), 'utf-8');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Substitute ${var} placeholders in a cloud-init script template with
|
|
40
|
+
* values from a JS object. Used to render the master/supabase/worker
|
|
41
|
+
* cloud-init scripts at deploy time.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} tmpl
|
|
44
|
+
* @param {Record<string, unknown>} vars
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
export function renderScript(tmpl, vars) {
|
|
48
|
+
return Object.entries(vars).reduce(
|
|
49
|
+
(acc, [k, v]) => acc.replace(new RegExp(`\\$\\{${k}\\}`, 'g'), String(v ?? '')),
|
|
50
|
+
tmpl,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulumi-based Infrastructure-as-Code runtime for Vibecarbon.
|
|
3
|
+
*
|
|
4
|
+
* Entry point for Pulumi's Automation API. Programs live under
|
|
5
|
+
* `src/lib/iac/programs/` and are invoked from here. State is stored in
|
|
6
|
+
* the user's Hetzner S3 bucket by default; we fall back to a local file
|
|
7
|
+
* backend when no S3 is configured.
|
|
8
|
+
*
|
|
9
|
+
* Design notes:
|
|
10
|
+
* - We use inline programs (Automation API, no Pulumi.yaml files) so there's
|
|
11
|
+
* nothing for users to manage on disk. Stack state lives in the backend;
|
|
12
|
+
* the program code lives in this npm package.
|
|
13
|
+
* - One project name ("vibecarbon"), many stacks (one per environment like
|
|
14
|
+
* "prod", "staging"). HA deploys use two stacks: "<env>-primary" and
|
|
15
|
+
* "<env>-standby".
|
|
16
|
+
* - Pulumi's own secrets provider isn't used — secrets stay in
|
|
17
|
+
* ~/.vibecarbon/credentials.json; we pass them to programs as plain config.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { mkdirSync } from 'node:fs';
|
|
21
|
+
import { homedir } from 'node:os';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { LocalWorkspace } from '@pulumi/pulumi/automation/index.js';
|
|
24
|
+
|
|
25
|
+
const PULUMI_PROJECT = 'vibecarbon';
|
|
26
|
+
const LOCAL_STATE_DIR = join(homedir(), '.vibecarbon', 'pulumi-state');
|
|
27
|
+
|
|
28
|
+
// Pulumi's file:// backend requires the target directory to exist before
|
|
29
|
+
// `stack select` runs, otherwise the first scenario to race for a stack
|
|
30
|
+
// fails with `error: unable to open bucket file:///... no such file or
|
|
31
|
+
// directory`. Create it at module load so every call path sees it.
|
|
32
|
+
mkdirSync(LOCAL_STATE_DIR, { recursive: true });
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Compute the Pulumi backend URL. Prefers Hetzner S3 when s3Config is
|
|
36
|
+
* provided; otherwise falls back to a local file:// backend under
|
|
37
|
+
* ~/.vibecarbon/pulumi-state. Pulumi Cloud is opt-in via
|
|
38
|
+
* PULUMI_BACKEND_URL env var before invoking vibecarbon.
|
|
39
|
+
*
|
|
40
|
+
* Hetzner S3 uses path-style URLs via the Hetzner-provided endpoint, so we
|
|
41
|
+
* pass both `endpoint` and `s3ForcePathStyle=true` in the query string.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveBackendUrl(s3Config) {
|
|
44
|
+
if (process.env.PULUMI_BACKEND_URL) {
|
|
45
|
+
return process.env.PULUMI_BACKEND_URL;
|
|
46
|
+
}
|
|
47
|
+
if (s3Config?.bucket && s3Config?.endpoint) {
|
|
48
|
+
const endpointHost = s3Config.endpoint.replace(/^https?:\/\//, '');
|
|
49
|
+
const region = s3Config.region || 'nbg1';
|
|
50
|
+
return `s3://${s3Config.bucket}?endpoint=${endpointHost}®ion=${region}&s3ForcePathStyle=true`;
|
|
51
|
+
}
|
|
52
|
+
return `file://${LOCAL_STATE_DIR}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build the env var bag Pulumi needs. Hetzner provider reads HCLOUD_TOKEN;
|
|
57
|
+
* S3 backend reads AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY; our programs
|
|
58
|
+
* read any additional config we pass through `stack.setConfig(...)`.
|
|
59
|
+
*/
|
|
60
|
+
function buildEnv(options) {
|
|
61
|
+
const env = { ...process.env };
|
|
62
|
+
if (options.hcloudToken) env.HCLOUD_TOKEN = options.hcloudToken;
|
|
63
|
+
if (options.s3Config?.accessKey) env.AWS_ACCESS_KEY_ID = options.s3Config.accessKey;
|
|
64
|
+
if (options.s3Config?.secretKey) env.AWS_SECRET_ACCESS_KEY = options.s3Config.secretKey;
|
|
65
|
+
// Disable Pulumi's telemetry for solo-hosted users. No data leaves the box.
|
|
66
|
+
env.PULUMI_SKIP_UPDATE_CHECK = 'true';
|
|
67
|
+
// Inline programs don't need a passphrase — but Pulumi insists on one for
|
|
68
|
+
// the secrets provider. A deterministic empty value keeps the automation
|
|
69
|
+
// API happy without us caring about secrets encryption at the state level
|
|
70
|
+
// (we never store secrets in Pulumi state; they flow in at program time).
|
|
71
|
+
env.PULUMI_CONFIG_PASSPHRASE = options.configPassphrase || '';
|
|
72
|
+
return env;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create or select a stack with the given name + inline program. Returns
|
|
77
|
+
* the Stack instance (caller drives up/destroy/refresh).
|
|
78
|
+
*/
|
|
79
|
+
export async function getOrCreateStack(stackName, program, options = {}) {
|
|
80
|
+
const backendUrl = resolveBackendUrl(options.s3Config);
|
|
81
|
+
const envVars = buildEnv(options);
|
|
82
|
+
|
|
83
|
+
const stack = await LocalWorkspace.createOrSelectStack(
|
|
84
|
+
{
|
|
85
|
+
stackName,
|
|
86
|
+
projectName: PULUMI_PROJECT,
|
|
87
|
+
program,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
projectSettings: {
|
|
91
|
+
name: PULUMI_PROJECT,
|
|
92
|
+
runtime: 'nodejs',
|
|
93
|
+
backend: { url: backendUrl },
|
|
94
|
+
},
|
|
95
|
+
envVars,
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return stack;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Pre-initialize a Pulumi S3 backend by creating a no-op stack and removing
|
|
104
|
+
* it. The first `pulumi stack select` against a freshly-created S3 bucket
|
|
105
|
+
* fails with `could not list bucket: blob (code=NotFound): NoSuchKey`
|
|
106
|
+
* because the backend hasn't laid down its `.pulumi/meta.yaml` yet. When
|
|
107
|
+
* `deployK8sHA` launches primary + standby `deployK3s` in parallel against
|
|
108
|
+
* the SAME bucket, both threads race the bucket-init and both see the
|
|
109
|
+
* NoSuchKey error, killing the deploy in <20s. Calling this once
|
|
110
|
+
* sequentially before the parallel block writes the backend metadata so
|
|
111
|
+
* subsequent stack ops succeed.
|
|
112
|
+
*
|
|
113
|
+
* Idempotent: if the bucket is already initialized, the no-op stack is
|
|
114
|
+
* created and removed cleanly. If it fails (e.g., bucket access issue),
|
|
115
|
+
* we log and proceed — the parallel deploys will surface the underlying
|
|
116
|
+
* problem with a clearer error.
|
|
117
|
+
*
|
|
118
|
+
* @param {object} options - Same shape as getOrCreateStack options
|
|
119
|
+
* (s3Config, hcloudToken, configPassphrase). The stack name we use is
|
|
120
|
+
* internal (`__init`); callers don't see it.
|
|
121
|
+
*/
|
|
122
|
+
export async function initBackend(options = {}) {
|
|
123
|
+
const initStackName = '__init';
|
|
124
|
+
// No-op program — returns nothing. We don't run `up` so this never
|
|
125
|
+
// executes Hetzner API calls. The act of `createStack` writing the
|
|
126
|
+
// stack's checkpoint file to S3 forces the backend to lay down its
|
|
127
|
+
// metadata.
|
|
128
|
+
const noopProgram = async () => ({});
|
|
129
|
+
try {
|
|
130
|
+
const stack = await getOrCreateStack(initStackName, noopProgram, options);
|
|
131
|
+
// Remove it so it doesn't leak into stack listings.
|
|
132
|
+
await stack.workspace.removeStack(initStackName).catch(() => {});
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error(`[pulumi] backend pre-init warning: ${err.message?.split('\n')[0] || err}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Run `pulumi up` and return the raw outputs. Streams Pulumi logs to an
|
|
140
|
+
* optional `onOutput` sink so callers can forward progress to a spinner.
|
|
141
|
+
*/
|
|
142
|
+
export async function upStack(stackName, program, options = {}) {
|
|
143
|
+
const stack = await getOrCreateStack(stackName, program, options);
|
|
144
|
+
// Clear any stale update lock before `up` — when a prior run aborted
|
|
145
|
+
// mid-flight (Ctrl-C, scenario timeout, background-task stop), Pulumi's
|
|
146
|
+
// file backend leaves a `.lock.*` in the bucket and subsequent `up`
|
|
147
|
+
// calls fail with:
|
|
148
|
+
//
|
|
149
|
+
// error: the stack is currently locked by 1 lock(s). Either wait for
|
|
150
|
+
// the other process(es) to end or delete the lock file with
|
|
151
|
+
// `pulumi cancel`.
|
|
152
|
+
//
|
|
153
|
+
// Since e2e runs one scenario per stack at a time, it's always
|
|
154
|
+
// safe to clear. `stack.cancel()` is idempotent (no-op if no lock).
|
|
155
|
+
try {
|
|
156
|
+
await stack.cancel();
|
|
157
|
+
} catch {
|
|
158
|
+
/* no lock — ignore */
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Refresh state before `up` to purge pending operations from a crashed
|
|
162
|
+
// prior run. Without this, Pulumi's `up` with "N pending operations
|
|
163
|
+
// from previous deployment" warnings can trigger a delete-before-replace
|
|
164
|
+
// on resources that don't actually exist in the cloud, hitting provider
|
|
165
|
+
// panics (e.g., hcloud@1.32.1 nil-deref on Firewall Delete). `refresh`
|
|
166
|
+
// reconciles state with live cloud resources and clears the pending-op
|
|
167
|
+
// markers. Idempotent — no-op when state already matches reality.
|
|
168
|
+
//
|
|
169
|
+
// Skip on truly-empty stacks: a freshly-created stack has only the
|
|
170
|
+
// bookkeeping `pulumi:pulumi:Stack` resource (no real cloud resources
|
|
171
|
+
// to reconcile). Refresh still pays plugin spawn + cloud API listing,
|
|
172
|
+
// ~3-8s on cold compose deploys for nothing. exportStack() failure
|
|
173
|
+
// falls through to the safer always-refresh path.
|
|
174
|
+
let needsRefresh = true;
|
|
175
|
+
try {
|
|
176
|
+
const state = await stack.exportStack();
|
|
177
|
+
const resources = state?.deployment?.resources ?? [];
|
|
178
|
+
needsRefresh = resources.some((r) => r.type !== 'pulumi:pulumi:Stack');
|
|
179
|
+
} catch {
|
|
180
|
+
/* exportStack failed — fall through and refresh as before */
|
|
181
|
+
}
|
|
182
|
+
if (needsRefresh) {
|
|
183
|
+
try {
|
|
184
|
+
await stack.refresh({ onOutput: options.onOutput, color: 'never' });
|
|
185
|
+
} catch (err) {
|
|
186
|
+
// refresh can fail if the state file is completely corrupt or a
|
|
187
|
+
// resource is unreachable. Log and proceed — `up` may still succeed
|
|
188
|
+
// or surface a cleaner error.
|
|
189
|
+
console.error(`[pulumi] pre-up refresh warning: ${err.message?.split('\n')[0] || err}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const result = await stack.up({
|
|
194
|
+
onOutput: options.onOutput,
|
|
195
|
+
color: 'never',
|
|
196
|
+
});
|
|
197
|
+
// Flatten outputs: { foo: { value: 'bar', secret: false } } → { foo: 'bar' }
|
|
198
|
+
const outputs = {};
|
|
199
|
+
for (const [key, { value }] of Object.entries(result.outputs || {})) {
|
|
200
|
+
outputs[key] = value;
|
|
201
|
+
}
|
|
202
|
+
return { outputs, summary: result.summary };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Run `pulumi destroy` and remove the stack. Idempotent — if the stack
|
|
207
|
+
* doesn't exist the function resolves without error.
|
|
208
|
+
*/
|
|
209
|
+
export async function destroyStack(stackName, program, options = {}) {
|
|
210
|
+
let stack;
|
|
211
|
+
try {
|
|
212
|
+
stack = await getOrCreateStack(stackName, program, options);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
// Stack doesn't exist yet — treat as a no-op so destroy is idempotent.
|
|
215
|
+
if (/no stack named/i.test(err.message || '')) return { destroyed: false };
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
218
|
+
// Refresh first so destroy tolerates resources that were already reaped
|
|
219
|
+
// out-of-band (e.g. Hetzner server deleted by a prior partial destroy or
|
|
220
|
+
// by direct API cleanup). Without this, hcloud returns 404 and pulumi
|
|
221
|
+
// treats it as a hard error — surfaced in orphan-stack cleanup as
|
|
222
|
+
// "server not found (not_found, ...)".
|
|
223
|
+
try {
|
|
224
|
+
await stack.refresh({ onOutput: options.onOutput, color: 'never' });
|
|
225
|
+
} catch (err) {
|
|
226
|
+
console.error(`[pulumi] pre-destroy refresh warning: ${err.message?.split('\n')[0] || err}`);
|
|
227
|
+
}
|
|
228
|
+
await stack.destroy({ onOutput: options.onOutput, color: 'never' });
|
|
229
|
+
await stack.workspace.removeStack(stackName);
|
|
230
|
+
return { destroyed: true };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Read the current outputs of a stack without running an update. Used by
|
|
235
|
+
* commands like `status`, `scale`, `failover` that need infra identifiers
|
|
236
|
+
* (IPs, IDs) but shouldn't drift the state.
|
|
237
|
+
*/
|
|
238
|
+
export async function getStackOutputs(stackName, program, options = {}) {
|
|
239
|
+
const stack = await getOrCreateStack(stackName, program, options);
|
|
240
|
+
const raw = await stack.outputs();
|
|
241
|
+
const outputs = {};
|
|
242
|
+
for (const [key, { value }] of Object.entries(raw)) {
|
|
243
|
+
outputs[key] = value;
|
|
244
|
+
}
|
|
245
|
+
return outputs;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Classify a prior-outputs probe for the k3sToken probe-and-replay path.
|
|
250
|
+
*
|
|
251
|
+
* `scale.js` reads the prior stack outputs before constructing the real
|
|
252
|
+
* Pulumi program so it can replay the deploy-time `k3sToken` and avoid
|
|
253
|
+
* destructive userData drift (see the master-replace defense). The probe
|
|
254
|
+
* has three observable outcomes:
|
|
255
|
+
*
|
|
256
|
+
* - `recovered`: outputs has a non-empty k3sToken — replay it
|
|
257
|
+
* - `empty`: outputs object exists but k3sToken is missing or empty —
|
|
258
|
+
* legitimate when the stack is fresh (no prior `up`), but
|
|
259
|
+
* also what we'd see if the deploy somehow skipped the
|
|
260
|
+
* output declaration. Caller must not silently mint a
|
|
261
|
+
* fresh token; the master-replace defense catches the
|
|
262
|
+
* destructive case.
|
|
263
|
+
* - `errored`: the probe call itself threw — backend unreachable,
|
|
264
|
+
* expired creds, etc. Caller should surface loudly.
|
|
265
|
+
*
|
|
266
|
+
* Pure function; the Pulumi state read happens upstream.
|
|
267
|
+
*
|
|
268
|
+
* @param {{ outputs?: Record<string, unknown> | null, error?: Error | null }} probe
|
|
269
|
+
* @returns {{ status: 'recovered' | 'empty' | 'errored', priorK3sToken?: string, reason: string }}
|
|
270
|
+
*/
|
|
271
|
+
export function classifyK3sTokenProbe({ outputs, error } = {}) {
|
|
272
|
+
if (error) {
|
|
273
|
+
const msg = error.message?.split('\n')[0] || String(error);
|
|
274
|
+
return { status: 'errored', reason: `probe threw: ${msg}` };
|
|
275
|
+
}
|
|
276
|
+
if (!outputs || typeof outputs !== 'object') {
|
|
277
|
+
return { status: 'empty', reason: 'no outputs returned' };
|
|
278
|
+
}
|
|
279
|
+
const token = outputs.k3sToken;
|
|
280
|
+
if (typeof token === 'string' && token.length > 0) {
|
|
281
|
+
return { status: 'recovered', priorK3sToken: token, reason: 'k3sToken present' };
|
|
282
|
+
}
|
|
283
|
+
const keys = Object.keys(outputs);
|
|
284
|
+
return {
|
|
285
|
+
status: 'empty',
|
|
286
|
+
reason:
|
|
287
|
+
keys.length === 0
|
|
288
|
+
? 'outputs object empty (fresh stack)'
|
|
289
|
+
: `outputs missing k3sToken (keys=${keys.join(',')})`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Refresh the stack's view of reality without applying program changes.
|
|
295
|
+
* Useful before a partial scale operation.
|
|
296
|
+
*/
|
|
297
|
+
export async function refreshStack(stackName, program, options = {}) {
|
|
298
|
+
const stack = await getOrCreateStack(stackName, program, options);
|
|
299
|
+
await stack.refresh({ onOutput: options.onOutput, color: 'never' });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* List stack names that exist in the configured backend for this project.
|
|
304
|
+
* Used by `destroy` to surface orphans (stacks without a matching envConfig).
|
|
305
|
+
* Returns an empty list if no backend is reachable yet.
|
|
306
|
+
*/
|
|
307
|
+
export async function listStacks(options = {}) {
|
|
308
|
+
const backendUrl = resolveBackendUrl(options.s3Config);
|
|
309
|
+
const envVars = buildEnv(options);
|
|
310
|
+
try {
|
|
311
|
+
const workspace = await LocalWorkspace.create({
|
|
312
|
+
projectSettings: {
|
|
313
|
+
name: PULUMI_PROJECT,
|
|
314
|
+
runtime: 'nodejs',
|
|
315
|
+
backend: { url: backendUrl },
|
|
316
|
+
},
|
|
317
|
+
envVars,
|
|
318
|
+
});
|
|
319
|
+
const stacks = await workspace.listStacks();
|
|
320
|
+
return stacks.map((s) => s.name);
|
|
321
|
+
} catch {
|
|
322
|
+
// No backend yet (fresh install) — no orphans possible.
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
325
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulumi program for a single Hetzner Docker Compose VPS.
|
|
3
|
+
*
|
|
4
|
+
* Much simpler than the k8s program — one server, one firewall, optional
|
|
5
|
+
* floating IP. Cloud-init installs Docker via the shipped docker-ce-setup.yaml
|
|
6
|
+
* (carbon/cloud-init/), which is reused as-is (no templating needed).
|
|
7
|
+
*
|
|
8
|
+
* Outputs: serverIp, serverId, firewallId, sshKeyId.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import * as hcloud from '@pulumi/hcloud';
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = dirname(__filename);
|
|
18
|
+
|
|
19
|
+
const CLOUD_INIT_PATH = join(__dirname, '../../../../carbon/cloud-init/docker-ce-setup.yaml');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} ComposeStackConfig
|
|
23
|
+
* @property {string} projectName
|
|
24
|
+
* @property {string} environment
|
|
25
|
+
* @property {string} sshPublicKey
|
|
26
|
+
* @property {string} [existingSshKeyId]
|
|
27
|
+
* @property {string} location
|
|
28
|
+
* @property {string} serverType
|
|
29
|
+
* @property {Record<string,string>} [labels]
|
|
30
|
+
* @property {string[]} [allowedSshIps]
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {ComposeStackConfig} config
|
|
35
|
+
* @returns {() => Promise<Record<string, unknown>>}
|
|
36
|
+
*/
|
|
37
|
+
export function buildHetznerComposeProgram(config) {
|
|
38
|
+
const name = `${config.projectName}-${config.environment}`;
|
|
39
|
+
const labels = {
|
|
40
|
+
project: config.projectName,
|
|
41
|
+
environment: config.environment,
|
|
42
|
+
...(config.labels ?? {}),
|
|
43
|
+
};
|
|
44
|
+
// Operator-IP firewall lock (H-2). The deploy flow populates this from
|
|
45
|
+
// projectConfig.operatorCidrs; an empty list = nothing can SSH in. We
|
|
46
|
+
// refuse to build a program that would silently apply an open default.
|
|
47
|
+
if (!config.allowedSshIps?.length) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
'allowedSshIps required (no default; pass operatorCidrs from .vibecarbon.json or set ALLOWED_SSH_IPS=...)',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const allowedSshIps = config.allowedSshIps;
|
|
53
|
+
const userData = readFileSync(CLOUD_INIT_PATH, 'utf-8');
|
|
54
|
+
|
|
55
|
+
return async () => {
|
|
56
|
+
let sshKeyId;
|
|
57
|
+
if (config.existingSshKeyId) {
|
|
58
|
+
sshKeyId = config.existingSshKeyId;
|
|
59
|
+
} else {
|
|
60
|
+
// Name includes region so parallel or re-deploys across regions don't
|
|
61
|
+
// collide in Hetzner's per-project SSH-key namespace.
|
|
62
|
+
const sshKey = new hcloud.SshKey('ssh-key', {
|
|
63
|
+
name: `${name}-${config.location}-key`,
|
|
64
|
+
publicKey: config.sshPublicKey,
|
|
65
|
+
labels,
|
|
66
|
+
});
|
|
67
|
+
sshKeyId = sshKey.id;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const firewall = new hcloud.Firewall(
|
|
71
|
+
'firewall',
|
|
72
|
+
{
|
|
73
|
+
name: `${name}-firewall`,
|
|
74
|
+
labels,
|
|
75
|
+
rules: [
|
|
76
|
+
{
|
|
77
|
+
direction: 'in',
|
|
78
|
+
protocol: 'tcp',
|
|
79
|
+
port: '22',
|
|
80
|
+
sourceIps: allowedSshIps,
|
|
81
|
+
description: 'SSH',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
direction: 'in',
|
|
85
|
+
protocol: 'tcp',
|
|
86
|
+
port: '80',
|
|
87
|
+
sourceIps: ['0.0.0.0/0', '::/0'],
|
|
88
|
+
description: 'HTTP',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
direction: 'in',
|
|
92
|
+
protocol: 'tcp',
|
|
93
|
+
port: '443',
|
|
94
|
+
sourceIps: ['0.0.0.0/0', '::/0'],
|
|
95
|
+
description: 'HTTPS',
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
// Upstream hcloud-go@1.32.1 bug: the Firewall `Update` code path
|
|
100
|
+
// nil-pointer-panics when rules change on a live firewall. Forcing
|
|
101
|
+
// full replacement avoids the Update call site entirely. Recreate
|
|
102
|
+
// is cheap (~2s).
|
|
103
|
+
{ replaceOnChanges: ['*'], deleteBeforeReplace: true },
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const server = new hcloud.Server('server', {
|
|
107
|
+
name,
|
|
108
|
+
serverType: config.serverType,
|
|
109
|
+
// Hetzner-maintained app image: Ubuntu LTS with docker + the
|
|
110
|
+
// docker-compose plugin pre-installed. Matches the contract in
|
|
111
|
+
// carbon/cloud-init/docker-ce-setup.yaml ("VPS boot time in parallel
|
|
112
|
+
// with Hetzner's default docker-ce app"). Plain `ubuntu-24.04` leaves
|
|
113
|
+
// the VM without docker and the deploy hits `docker: command not
|
|
114
|
+
// found` on the first `docker compose up` call.
|
|
115
|
+
image: 'docker-ce',
|
|
116
|
+
location: config.location,
|
|
117
|
+
sshKeys: [sshKeyId],
|
|
118
|
+
firewallIds: [firewall.id.apply((id) => Number.parseInt(id, 10))],
|
|
119
|
+
labels: { ...labels, role: 'compose' },
|
|
120
|
+
userData,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
serverIp: server.ipv4Address,
|
|
125
|
+
serverId: server.id.apply((id) => String(id)),
|
|
126
|
+
firewallId: firewall.id.apply((id) => String(id)),
|
|
127
|
+
sshKeyId: typeof sshKeyId === 'string' ? sshKeyId : sshKeyId.apply((id) => String(id)),
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
}
|