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,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project configuration management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { basename, join, resolve } from 'node:path';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sanitize a project name to be a valid hostname
|
|
11
|
+
* - Extracts base name from paths (../my-app -> my-app)
|
|
12
|
+
* - Removes invalid characters
|
|
13
|
+
* - Converts to lowercase
|
|
14
|
+
* - Ensures doesn't start/end with hyphen
|
|
15
|
+
*
|
|
16
|
+
* @param {string} name - Project name to sanitize
|
|
17
|
+
* @returns {string} - Valid hostname
|
|
18
|
+
*/
|
|
19
|
+
export function sanitizeHostname(name) {
|
|
20
|
+
if (!name) return 'project';
|
|
21
|
+
|
|
22
|
+
// Extract base name from path (handles ../my-app, ./my-app, /path/to/my-app)
|
|
23
|
+
let sanitized = basename(name);
|
|
24
|
+
|
|
25
|
+
// Remove any characters that aren't alphanumeric or hyphens
|
|
26
|
+
sanitized = sanitized.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
27
|
+
|
|
28
|
+
// Remove consecutive hyphens
|
|
29
|
+
sanitized = sanitized.replace(/-+/g, '-');
|
|
30
|
+
|
|
31
|
+
// Remove leading/trailing hyphens
|
|
32
|
+
sanitized = sanitized.replace(/^-+|-+$/g, '');
|
|
33
|
+
|
|
34
|
+
// Ensure it's not empty
|
|
35
|
+
if (!sanitized) return 'project';
|
|
36
|
+
|
|
37
|
+
// Ensure it doesn't exceed hostname limits (63 chars for labels)
|
|
38
|
+
if (sanitized.length > 63) {
|
|
39
|
+
sanitized = sanitized.substring(0, 63).replace(/-+$/, '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return sanitized;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load project configuration from multiple sources
|
|
47
|
+
* Priority: .vibecarbon.json > .env.local > package.json
|
|
48
|
+
*
|
|
49
|
+
* @param {string} [cwd] - Working directory (defaults to process.cwd())
|
|
50
|
+
* @returns {object|null} - Project config or null if not found
|
|
51
|
+
*/
|
|
52
|
+
export function loadProjectConfig(cwd = process.cwd()) {
|
|
53
|
+
// Try to load .vibecarbon.json
|
|
54
|
+
const configPath = join(cwd, '.vibecarbon.json');
|
|
55
|
+
if (existsSync(configPath)) {
|
|
56
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
57
|
+
|
|
58
|
+
// If projectName is missing or empty, try to get it from .env.local or package.json
|
|
59
|
+
if (!config.projectName) {
|
|
60
|
+
const envPath = join(cwd, '.env.local');
|
|
61
|
+
if (existsSync(envPath)) {
|
|
62
|
+
const envContent = readFileSync(envPath, 'utf-8');
|
|
63
|
+
for (const line of envContent.split('\n')) {
|
|
64
|
+
if (line.startsWith('PROJECT_NAME=')) {
|
|
65
|
+
const rawValue = line.split('=')[1].replace(/^["']|["']$/g, '');
|
|
66
|
+
config.projectName = sanitizeHostname(rawValue);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Fall back to package.json name
|
|
73
|
+
if (!config.projectName) {
|
|
74
|
+
const pkgPath = join(cwd, 'package.json');
|
|
75
|
+
if (existsSync(pkgPath)) {
|
|
76
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
77
|
+
config.projectName = sanitizeHostname(pkg.name);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Final fallback - ensure projectName is always set
|
|
83
|
+
if (!config.projectName) {
|
|
84
|
+
config.projectName = 'project';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return config;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Try to load from .env.local
|
|
91
|
+
const envPath = join(cwd, '.env.local');
|
|
92
|
+
if (existsSync(envPath)) {
|
|
93
|
+
const envContent = readFileSync(envPath, 'utf-8');
|
|
94
|
+
const config = {
|
|
95
|
+
projectName: null,
|
|
96
|
+
secrets: {},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
for (const line of envContent.split('\n')) {
|
|
100
|
+
if (line.startsWith('#') || !line.includes('=')) continue;
|
|
101
|
+
const [key, ...valueParts] = line.split('=');
|
|
102
|
+
const value = valueParts.join('=').replace(/^["']|["']$/g, '');
|
|
103
|
+
|
|
104
|
+
if (key === 'PROJECT_NAME') {
|
|
105
|
+
config.projectName = sanitizeHostname(value);
|
|
106
|
+
}
|
|
107
|
+
config.secrets[key.trim()] = value;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Try to get project name from package.json
|
|
111
|
+
const pkgPath = join(cwd, 'package.json');
|
|
112
|
+
if (!config.projectName && existsSync(pkgPath)) {
|
|
113
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
114
|
+
config.projectName = sanitizeHostname(pkg.name);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Final fallback - ensure projectName is always set
|
|
118
|
+
if (!config.projectName) {
|
|
119
|
+
config.projectName = 'project';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return config;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Try package.json as fallback
|
|
126
|
+
const pkgPath = join(cwd, 'package.json');
|
|
127
|
+
if (existsSync(pkgPath)) {
|
|
128
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
129
|
+
return { projectName: sanitizeHostname(pkg.name), secrets: {} };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Save project configuration to .vibecarbon.json
|
|
137
|
+
*
|
|
138
|
+
* @param {object} config - Configuration to save
|
|
139
|
+
* @param {string} [cwd] - Working directory (defaults to process.cwd())
|
|
140
|
+
*/
|
|
141
|
+
export function saveProjectConfig(config, cwd = process.cwd()) {
|
|
142
|
+
const configPath = join(cwd, '.vibecarbon.json');
|
|
143
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Load S3 configuration for a specific environment
|
|
148
|
+
*
|
|
149
|
+
* @param {string} envName - Environment name (e.g., 'prod', 'staging')
|
|
150
|
+
* @param {string} [cwd] - Working directory (defaults to process.cwd())
|
|
151
|
+
* @returns {object|null} - S3 config or null if not configured
|
|
152
|
+
*/
|
|
153
|
+
export function loadS3Config(envName, cwd = process.cwd()) {
|
|
154
|
+
const config = loadProjectConfig(cwd);
|
|
155
|
+
return config?.environments?.[envName]?.s3 || null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Load backup configuration for a specific environment
|
|
160
|
+
*
|
|
161
|
+
* @param {string} envName - Environment name (e.g., 'prod', 'staging')
|
|
162
|
+
* @param {string} [cwd] - Working directory (defaults to process.cwd())
|
|
163
|
+
* @returns {object|null} - Backup config or null if not configured
|
|
164
|
+
*/
|
|
165
|
+
export function loadBackupConfig(envName, cwd = process.cwd()) {
|
|
166
|
+
const config = loadProjectConfig(cwd);
|
|
167
|
+
return config?.environments?.[envName]?.backup || null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Load backup S3 configuration for a specific environment.
|
|
172
|
+
* This is the dedicated backup bucket (separate from storage bucket).
|
|
173
|
+
*
|
|
174
|
+
* @param {string} envName - Environment name (e.g., 'prod', 'staging')
|
|
175
|
+
* @param {string} [cwd] - Working directory (defaults to process.cwd())
|
|
176
|
+
* @returns {object|null} - Backup S3 config or null if not configured
|
|
177
|
+
*/
|
|
178
|
+
export function loadBackupS3Config(envName, cwd = process.cwd()) {
|
|
179
|
+
const config = loadProjectConfig(cwd);
|
|
180
|
+
return config?.environments?.[envName]?.backupS3 || null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// GLOBAL PROJECT REGISTRY (~/.vibecarbon/projects.json)
|
|
185
|
+
// ============================================================================
|
|
186
|
+
|
|
187
|
+
const DEFAULT_CONFIG_DIR = join(homedir(), '.vibecarbon');
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Ensure the global config directory exists with secure permissions
|
|
191
|
+
*
|
|
192
|
+
* @param {string} [configDir] - Config directory (default: ~/.vibecarbon/)
|
|
193
|
+
*/
|
|
194
|
+
function ensureConfigDir(configDir = DEFAULT_CONFIG_DIR) {
|
|
195
|
+
if (!existsSync(configDir)) {
|
|
196
|
+
mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ============================================================================
|
|
201
|
+
// GLOBAL CREDENTIAL STORAGE (~/.vibecarbon/credentials.json)
|
|
202
|
+
// ============================================================================
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Load saved credentials from ~/.vibecarbon/credentials.json
|
|
206
|
+
*
|
|
207
|
+
* @param {string} [configDir] - Config directory for testability
|
|
208
|
+
* @returns {{ hetzner?: { apiToken: string }, s3?: { accessKey: string, secretKey: string }, cloudflare?: { apiToken: string } }}
|
|
209
|
+
*/
|
|
210
|
+
export function loadCredentials(configDir = DEFAULT_CONFIG_DIR) {
|
|
211
|
+
const credPath = join(configDir, 'credentials.json');
|
|
212
|
+
if (!existsSync(credPath)) {
|
|
213
|
+
return {};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
return JSON.parse(readFileSync(credPath, 'utf-8'));
|
|
218
|
+
} catch {
|
|
219
|
+
return {};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Save credentials to ~/.vibecarbon/credentials.json (merges with existing)
|
|
225
|
+
* File is written with mode 0o600 (owner read/write only).
|
|
226
|
+
*
|
|
227
|
+
* @param {object} credentials - Credential object to merge (e.g. { hetzner: { apiToken } })
|
|
228
|
+
* @param {string} [configDir] - Config directory for testability
|
|
229
|
+
*/
|
|
230
|
+
export function saveCredentials(credentials, configDir = DEFAULT_CONFIG_DIR) {
|
|
231
|
+
ensureConfigDir(configDir);
|
|
232
|
+
const credPath = join(configDir, 'credentials.json');
|
|
233
|
+
const existing = loadCredentials(configDir);
|
|
234
|
+
const merged = { ...existing, ...credentials };
|
|
235
|
+
writeFileSync(credPath, `${JSON.stringify(merged, null, 2)}\n`, { mode: 0o600 });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Delete the credentials file
|
|
240
|
+
*
|
|
241
|
+
* @param {string} [configDir] - Config directory for testability
|
|
242
|
+
*/
|
|
243
|
+
export function clearCredentials(configDir = DEFAULT_CONFIG_DIR) {
|
|
244
|
+
const credPath = join(configDir, 'credentials.json');
|
|
245
|
+
if (existsSync(credPath)) {
|
|
246
|
+
unlinkSync(credPath);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Load the global project registry
|
|
252
|
+
*
|
|
253
|
+
* @param {string} [configDir] - Config directory for testability
|
|
254
|
+
* @returns {{ projects: Array<{ name: string, path: string, createdAt: string, updatedAt: string }> }}
|
|
255
|
+
*/
|
|
256
|
+
export function loadGlobalRegistry(configDir = DEFAULT_CONFIG_DIR) {
|
|
257
|
+
const registryPath = join(configDir, 'projects.json');
|
|
258
|
+
if (!existsSync(registryPath)) {
|
|
259
|
+
return { projects: [] };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const content = readFileSync(registryPath, 'utf-8');
|
|
264
|
+
const data = JSON.parse(content);
|
|
265
|
+
if (!Array.isArray(data.projects)) {
|
|
266
|
+
return { projects: [] };
|
|
267
|
+
}
|
|
268
|
+
return data;
|
|
269
|
+
} catch {
|
|
270
|
+
return { projects: [] };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Register (or update) a project in the global registry.
|
|
276
|
+
* Upserts by resolved absolute path.
|
|
277
|
+
*
|
|
278
|
+
* @param {string} name - Project name
|
|
279
|
+
* @param {string} projectPath - Project directory path
|
|
280
|
+
* @param {string} [configDir] - Config directory for testability
|
|
281
|
+
*/
|
|
282
|
+
export function registerProject(name, projectPath, configDir = DEFAULT_CONFIG_DIR) {
|
|
283
|
+
ensureConfigDir(configDir);
|
|
284
|
+
const absPath = resolve(projectPath);
|
|
285
|
+
const registry = loadGlobalRegistry(configDir);
|
|
286
|
+
|
|
287
|
+
const existing = registry.projects.find((p) => p.path === absPath);
|
|
288
|
+
const now = new Date().toISOString();
|
|
289
|
+
|
|
290
|
+
if (existing) {
|
|
291
|
+
existing.name = name;
|
|
292
|
+
existing.updatedAt = now;
|
|
293
|
+
} else {
|
|
294
|
+
registry.projects.push({ name, path: absPath, createdAt: now, updatedAt: now });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const registryPath = join(configDir, 'projects.json');
|
|
298
|
+
writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`, { mode: 0o600 });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Remove a project from the global registry by path.
|
|
303
|
+
*
|
|
304
|
+
* @param {string} projectPath - Project directory path
|
|
305
|
+
* @param {string} [configDir] - Config directory for testability
|
|
306
|
+
*/
|
|
307
|
+
export function unregisterProject(projectPath, configDir = DEFAULT_CONFIG_DIR) {
|
|
308
|
+
const absPath = resolve(projectPath);
|
|
309
|
+
const registry = loadGlobalRegistry(configDir);
|
|
310
|
+
registry.projects = registry.projects.filter((p) => p.path !== absPath);
|
|
311
|
+
|
|
312
|
+
const registryPath = join(configDir, 'projects.json');
|
|
313
|
+
if (existsSync(join(configDir))) {
|
|
314
|
+
writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`, { mode: 0o600 });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Remove registry entries where the directory no longer exists.
|
|
320
|
+
*
|
|
321
|
+
* @param {string} [configDir] - Config directory for testability
|
|
322
|
+
*/
|
|
323
|
+
export function cleanStaleProjects(configDir = DEFAULT_CONFIG_DIR) {
|
|
324
|
+
const registry = loadGlobalRegistry(configDir);
|
|
325
|
+
const before = registry.projects.length;
|
|
326
|
+
registry.projects = registry.projects.filter((p) => existsSync(p.path));
|
|
327
|
+
|
|
328
|
+
if (registry.projects.length !== before) {
|
|
329
|
+
const registryPath = join(configDir, 'projects.json');
|
|
330
|
+
writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`, { mode: 0o600 });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ============================================================================
|
|
335
|
+
// PROJECT DETECTION UTILITIES
|
|
336
|
+
// ============================================================================
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Check if docker-compose.yml exists in the given directory
|
|
340
|
+
*
|
|
341
|
+
* @param {string} [cwd] - Working directory (defaults to process.cwd())
|
|
342
|
+
* @returns {boolean}
|
|
343
|
+
*/
|
|
344
|
+
export function hasDockerCompose(cwd = process.cwd()) {
|
|
345
|
+
return existsSync(join(cwd, 'docker-compose.yml'));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Check if K8s HA overlay directories exist
|
|
350
|
+
*
|
|
351
|
+
* @param {string} [cwd] - Working directory (defaults to process.cwd())
|
|
352
|
+
* @returns {boolean}
|
|
353
|
+
*/
|
|
354
|
+
export function isHAConfigured(cwd = process.cwd()) {
|
|
355
|
+
return (
|
|
356
|
+
existsSync(join(cwd, 'k8s/overlays/production-hel1')) ||
|
|
357
|
+
existsSync(join(cwd, 'k8s/overlays/production-nbg1'))
|
|
358
|
+
);
|
|
359
|
+
}
|
package/src/lib/cost.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared cost breakdown utilities for deploy and scale commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { c } from './colors.js';
|
|
6
|
+
import { HetznerProvider } from './providers/hetzner.js';
|
|
7
|
+
import { parsePrice } from './server-types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build an itemized cost breakdown for the deployment.
|
|
11
|
+
* Returns { lines: [{label, cost}], total }.
|
|
12
|
+
*/
|
|
13
|
+
export function buildCostBreakdown({
|
|
14
|
+
isComposeDeploy,
|
|
15
|
+
ha,
|
|
16
|
+
serverType,
|
|
17
|
+
masterServerType,
|
|
18
|
+
supabaseServerType,
|
|
19
|
+
workerServerType,
|
|
20
|
+
region,
|
|
21
|
+
services,
|
|
22
|
+
}) {
|
|
23
|
+
const { INFRA_COSTS, VOLUME_SIZES } = HetznerProvider;
|
|
24
|
+
const lines = [];
|
|
25
|
+
let total = 0;
|
|
26
|
+
|
|
27
|
+
if (isComposeDeploy) {
|
|
28
|
+
const price = parsePrice(HetznerProvider.getPrice(serverType, region));
|
|
29
|
+
if (ha) {
|
|
30
|
+
const cost = price * 2;
|
|
31
|
+
lines.push({ label: 'Server:', value: `${serverType} ×2`, cost });
|
|
32
|
+
total += cost;
|
|
33
|
+
} else {
|
|
34
|
+
lines.push({ label: 'Server:', value: serverType, cost: price });
|
|
35
|
+
total += price;
|
|
36
|
+
}
|
|
37
|
+
} else {
|
|
38
|
+
const regionMultiplier = ha ? 2 : 1;
|
|
39
|
+
const suffix = ha ? ' ×2' : '';
|
|
40
|
+
|
|
41
|
+
const masterPrice = parsePrice(HetznerProvider.getPrice(masterServerType, region));
|
|
42
|
+
const supabasePrice = parsePrice(HetznerProvider.getPrice(supabaseServerType, region));
|
|
43
|
+
const workerPrice = parsePrice(HetznerProvider.getPrice(workerServerType, region));
|
|
44
|
+
|
|
45
|
+
const masterCost = masterPrice * regionMultiplier;
|
|
46
|
+
const supabaseCost = supabasePrice * regionMultiplier;
|
|
47
|
+
const workerCost = workerPrice * regionMultiplier;
|
|
48
|
+
lines.push({ label: 'Master:', value: `${masterServerType}${suffix}`, cost: masterCost });
|
|
49
|
+
lines.push({ label: 'Supabase:', value: `${supabaseServerType}${suffix}`, cost: supabaseCost });
|
|
50
|
+
lines.push({ label: 'Worker:', value: `${workerServerType}${suffix}`, cost: workerCost });
|
|
51
|
+
total += masterCost + supabaseCost + workerCost;
|
|
52
|
+
|
|
53
|
+
// Floating IP
|
|
54
|
+
const fipCost = INFRA_COSTS.floatingIp * regionMultiplier;
|
|
55
|
+
lines.push({ label: `Floating IP${suffix}`, cost: fipCost });
|
|
56
|
+
total += fipCost;
|
|
57
|
+
|
|
58
|
+
// CSI Volumes
|
|
59
|
+
let volumeGb = VOLUME_SIZES.base;
|
|
60
|
+
if (services.observability) volumeGb += VOLUME_SIZES.observability;
|
|
61
|
+
if (services.n8n) volumeGb += VOLUME_SIZES.n8n;
|
|
62
|
+
if (services.metabase) volumeGb += VOLUME_SIZES.metabase;
|
|
63
|
+
if (services.redis) volumeGb += VOLUME_SIZES.redis;
|
|
64
|
+
const volumeCost = volumeGb * INFRA_COSTS.volumePerGb * regionMultiplier;
|
|
65
|
+
lines.push({ label: 'Volumes:', value: `${volumeGb}GB${suffix}`, cost: volumeCost });
|
|
66
|
+
total += volumeCost;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// S3 Object Storage (shared, not multiplied by region)
|
|
70
|
+
lines.push({ label: 'S3 Backups', cost: INFRA_COSTS.s3MinBucket });
|
|
71
|
+
total += INFRA_COSTS.s3MinBucket;
|
|
72
|
+
|
|
73
|
+
return { lines, total };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Format a cost breakdown into a colored, grid-aligned display string.
|
|
78
|
+
*/
|
|
79
|
+
export function formatCostLines({ lines, total }) {
|
|
80
|
+
const labelCol = 14;
|
|
81
|
+
const colWidth = 30;
|
|
82
|
+
const header = `\n${c.bold('Estimated Monthly Cost')}`;
|
|
83
|
+
|
|
84
|
+
const priceWidth = 10;
|
|
85
|
+
|
|
86
|
+
const rows = lines.map((item) => {
|
|
87
|
+
const priceStr = `€${item.cost.toFixed(2)}`.padStart(priceWidth);
|
|
88
|
+
if (item.value) {
|
|
89
|
+
const paddedLabel = item.label.padEnd(labelCol);
|
|
90
|
+
const fullLabel = `${paddedLabel}${c.bold(item.value)}`;
|
|
91
|
+
// Pad the visible width (without ANSI codes) to colWidth
|
|
92
|
+
const visibleLen = paddedLabel.length + item.value.length;
|
|
93
|
+
const gap = Math.max(0, colWidth - visibleLen);
|
|
94
|
+
return `${fullLabel}${' '.repeat(gap)}${c.bold(priceStr)}`;
|
|
95
|
+
}
|
|
96
|
+
return `${item.label.padEnd(colWidth)}${c.bold(priceStr)}`;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const separator = c.dim('─'.repeat(colWidth + priceWidth));
|
|
100
|
+
const totalLine = `${'Est. Total'.padEnd(colWidth)}${c.boldYellow(`€${total.toFixed(2)}/mo`.padStart(priceWidth))}`;
|
|
101
|
+
|
|
102
|
+
return [header, ...rows, separator, totalLine].join('\n');
|
|
103
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-First Bundle Rendering
|
|
3
|
+
* Stages all deployment artifacts (Compose files, .env, scripts) into a local temp directory
|
|
4
|
+
* before any infrastructure is provisioned or network calls are made.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { escapeDotenv } from '../shell.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Render the reconcile.sh script
|
|
14
|
+
*/
|
|
15
|
+
function renderReconcileScript(projectName, composeFlags, isFast = false) {
|
|
16
|
+
const pullStep = isFast
|
|
17
|
+
? ''
|
|
18
|
+
: `
|
|
19
|
+
# 1. Pull latest images if needed
|
|
20
|
+
echo "Pulling images..."
|
|
21
|
+
docker compose ${composeFlags} pull --quiet --policy missing --ignore-buildable || true
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
return `#!/bin/bash
|
|
25
|
+
set -e
|
|
26
|
+
|
|
27
|
+
# Declarative Compose Reconciler
|
|
28
|
+
# Generated by Vibecarbon
|
|
29
|
+
|
|
30
|
+
PROJECT_DIR="/opt/${projectName}"
|
|
31
|
+
cd "$PROJECT_DIR"
|
|
32
|
+
|
|
33
|
+
echo "Reconciling project: ${projectName}..."
|
|
34
|
+
|
|
35
|
+
echo "File layout in $(pwd):"
|
|
36
|
+
ls -R | grep ":$" | sed 's/:$//' | xargs -I {} sh -c "echo '{}:' && ls {} | head -n 5"
|
|
37
|
+
|
|
38
|
+
if [ -f ".env" ]; then
|
|
39
|
+
echo "Found .env file"
|
|
40
|
+
# Don't print secrets, but check for existence of keys
|
|
41
|
+
grep -E "^(S3_BUCKET|PROJECT_NAME|APP_IMAGE)=" .env | sed 's/=.*$/=***/'
|
|
42
|
+
else
|
|
43
|
+
echo "WARNING: .env file NOT FOUND in $(pwd)"
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
${pullStep}
|
|
47
|
+
# 2. Re-create containers if state differs
|
|
48
|
+
echo "Updating containers..."
|
|
49
|
+
docker compose ${composeFlags} up -d --remove-orphans
|
|
50
|
+
|
|
51
|
+
# 3. Ensure systemd service is active (if applicable)
|
|
52
|
+
if [ -f "/etc/systemd/system/${projectName}.service" ]; then
|
|
53
|
+
echo "Ensuring systemd service is active..."
|
|
54
|
+
systemctl daemon-reload
|
|
55
|
+
systemctl enable "${projectName}.service"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
echo "Reconciliation complete."
|
|
59
|
+
`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Render the systemd service unit
|
|
64
|
+
*/
|
|
65
|
+
function renderSystemdUnit(projectName, composeFlags, _isFast = false) {
|
|
66
|
+
return `[Unit]
|
|
67
|
+
Description=Vibecarbon Project: ${projectName}
|
|
68
|
+
After=docker.service
|
|
69
|
+
Requires=docker.service
|
|
70
|
+
|
|
71
|
+
[Service]
|
|
72
|
+
Type=oneshot
|
|
73
|
+
RemainAfterExit=yes
|
|
74
|
+
WorkingDirectory=/opt/${projectName}
|
|
75
|
+
ExecStart=/usr/bin/docker compose ${composeFlags} up -d --remove-orphans
|
|
76
|
+
ExecStop=/usr/bin/docker compose ${composeFlags} down
|
|
77
|
+
Restart=on-failure
|
|
78
|
+
|
|
79
|
+
[Install]
|
|
80
|
+
WantedBy=multi-user.target
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build the compose files flags string
|
|
86
|
+
*/
|
|
87
|
+
function composeFileFlags(options = {}) {
|
|
88
|
+
const files = ['-f', 'docker-compose.yml', '-f', 'docker-compose.prod.yml'];
|
|
89
|
+
if (options.observability) {
|
|
90
|
+
files.push('-f', 'docker-compose.observability.yml');
|
|
91
|
+
files.push('-f', 'docker-compose.observability.prod.yml');
|
|
92
|
+
}
|
|
93
|
+
if (options.n8n) {
|
|
94
|
+
files.push('-f', 'docker-compose.n8n.yml', '-f', 'docker-compose.n8n.prod.yml');
|
|
95
|
+
}
|
|
96
|
+
if (options.metabase) {
|
|
97
|
+
files.push('-f', 'docker-compose.metabase.yml', '-f', 'docker-compose.metabase.prod.yml');
|
|
98
|
+
}
|
|
99
|
+
if (options.redis) files.push('-f', 'docker-compose.redis.yml');
|
|
100
|
+
return files.join(' ');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Render the deployment bundle to a local temp directory
|
|
105
|
+
* @param {string} projectName
|
|
106
|
+
* @param {object} options
|
|
107
|
+
* @returns {string} The path to the staged bundle directory
|
|
108
|
+
*/
|
|
109
|
+
export function renderBundle(projectName, options = {}) {
|
|
110
|
+
const cwd = process.cwd();
|
|
111
|
+
const stageDir = mkdtempSync(join(tmpdir(), 'vc-deploy-bundle-'));
|
|
112
|
+
|
|
113
|
+
// Core Docker files
|
|
114
|
+
const rootFiles = [
|
|
115
|
+
'docker-compose.yml',
|
|
116
|
+
'docker-compose.prod.yml',
|
|
117
|
+
'Dockerfile',
|
|
118
|
+
'.dockerignore',
|
|
119
|
+
];
|
|
120
|
+
if (options.observability) {
|
|
121
|
+
rootFiles.push('docker-compose.observability.yml', 'docker-compose.observability.prod.yml');
|
|
122
|
+
}
|
|
123
|
+
if (options.n8n) {
|
|
124
|
+
rootFiles.push('docker-compose.n8n.yml', 'docker-compose.n8n.prod.yml');
|
|
125
|
+
}
|
|
126
|
+
if (options.metabase) {
|
|
127
|
+
rootFiles.push('docker-compose.metabase.yml', 'docker-compose.metabase.prod.yml');
|
|
128
|
+
}
|
|
129
|
+
if (options.redis) rootFiles.push('docker-compose.redis.yml');
|
|
130
|
+
|
|
131
|
+
for (const file of rootFiles) {
|
|
132
|
+
const src = join(cwd, file);
|
|
133
|
+
if (existsSync(src)) {
|
|
134
|
+
if (options.verbose) console.log(`[bundle] Copying ${file}`);
|
|
135
|
+
cpSync(src, join(stageDir, file));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Render .env locally with overrides
|
|
140
|
+
const localEnvPath = join(cwd, '.env');
|
|
141
|
+
let envContent = '';
|
|
142
|
+
if (existsSync(localEnvPath)) {
|
|
143
|
+
envContent = readFileSync(localEnvPath, 'utf-8');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const envOverrides = { ...(options.envOverrides || {}) };
|
|
147
|
+
if (options.domain) {
|
|
148
|
+
envOverrides.DOMAIN = options.domain;
|
|
149
|
+
envOverrides.ACME_EMAIL = `admin@${options.domain}`;
|
|
150
|
+
envOverrides.SITE_URL = `https://api.${options.domain}`;
|
|
151
|
+
envOverrides.SUPABASE_URL = 'http://kong:8000';
|
|
152
|
+
}
|
|
153
|
+
if (options.image) {
|
|
154
|
+
envOverrides.APP_IMAGE = options.image;
|
|
155
|
+
}
|
|
156
|
+
if (options.s3) {
|
|
157
|
+
envOverrides.S3_ACCESS_KEY = options.s3.accessKey;
|
|
158
|
+
envOverrides.S3_SECRET_KEY = options.s3.secretKey;
|
|
159
|
+
envOverrides.S3_REGION = options.s3.region;
|
|
160
|
+
envOverrides.S3_ENDPOINT = options.s3.endpoint;
|
|
161
|
+
envOverrides.S3_BUCKET = options.s3.bucket;
|
|
162
|
+
}
|
|
163
|
+
if (options.backupS3) {
|
|
164
|
+
envOverrides.S3_BACKUP_BUCKET = options.backupS3.bucket;
|
|
165
|
+
}
|
|
166
|
+
if (projectName) {
|
|
167
|
+
envOverrides.PROJECT_NAME = projectName;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Merge overrides into local copy
|
|
171
|
+
const lines = envContent.split('\n');
|
|
172
|
+
const seen = new Set();
|
|
173
|
+
const merged = lines.map((line) => {
|
|
174
|
+
const m = line.match(/^([A-Z_][A-Z0-9_]*)=/);
|
|
175
|
+
if (m && envOverrides[m[1]] !== undefined) {
|
|
176
|
+
seen.add(m[1]);
|
|
177
|
+
return `${m[1]}=${escapeDotenv(envOverrides[m[1]])}`;
|
|
178
|
+
}
|
|
179
|
+
return line;
|
|
180
|
+
});
|
|
181
|
+
for (const [k, v] of Object.entries(envOverrides)) {
|
|
182
|
+
if (!seen.has(k)) merged.push(`${k}=${escapeDotenv(v)}`);
|
|
183
|
+
}
|
|
184
|
+
writeFileSync(join(stageDir, '.env'), merged.join('\n'));
|
|
185
|
+
|
|
186
|
+
const isFast = Boolean(options.image?.endsWith(':local'));
|
|
187
|
+
const flags = composeFileFlags(options);
|
|
188
|
+
writeFileSync(join(stageDir, 'reconcile.sh'), renderReconcileScript(projectName, flags, isFast), {
|
|
189
|
+
mode: 0o755,
|
|
190
|
+
});
|
|
191
|
+
writeFileSync(
|
|
192
|
+
join(stageDir, `${projectName}.service`),
|
|
193
|
+
renderSystemdUnit(projectName, flags, isFast),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const copyDirIfExist = (srcDir, destDir) => {
|
|
197
|
+
const src = join(cwd, srcDir);
|
|
198
|
+
if (existsSync(src)) {
|
|
199
|
+
if (options.verbose) console.log(`[bundle] Copying ${srcDir} to ${destDir}`);
|
|
200
|
+
cpSync(src, join(stageDir, destDir), { recursive: true });
|
|
201
|
+
} else {
|
|
202
|
+
if (options.verbose) console.log(`[bundle] Skipping ${srcDir} (not found)`);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
copyDirIfExist('db', 'db');
|
|
207
|
+
copyDirIfExist('volumes/db', 'volumes/db');
|
|
208
|
+
copyDirIfExist('supabase/migrations', 'supabase/migrations');
|
|
209
|
+
copyDirIfExist('volumes/kong', 'volumes/kong');
|
|
210
|
+
|
|
211
|
+
// volumes/traefik/middlewares.yml
|
|
212
|
+
const traefikMiddlewares = join(cwd, 'volumes', 'traefik', 'middlewares.yml');
|
|
213
|
+
if (existsSync(traefikMiddlewares)) {
|
|
214
|
+
mkdirSync(join(stageDir, 'volumes', 'traefik'), { recursive: true });
|
|
215
|
+
cpSync(traefikMiddlewares, join(stageDir, 'volumes', 'traefik', 'middlewares.yml'));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
copyDirIfExist('backup', 'backup');
|
|
219
|
+
if (options.n8n) copyDirIfExist('volumes/n8n', 'volumes/n8n');
|
|
220
|
+
if (options.metabase) copyDirIfExist('volumes/metabase', 'volumes/metabase');
|
|
221
|
+
|
|
222
|
+
if (options.observability) {
|
|
223
|
+
copyDirIfExist('prometheus', 'prometheus');
|
|
224
|
+
copyDirIfExist('grafana', 'grafana');
|
|
225
|
+
for (const subdir of ['prometheus', 'grafana', 'loki', 'promtail']) {
|
|
226
|
+
copyDirIfExist(`volumes/${subdir}`, `volumes/${subdir}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return stageDir;
|
|
231
|
+
}
|