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,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud Provider Registry
|
|
3
|
+
*
|
|
4
|
+
* Provides a unified interface for accessing cloud providers.
|
|
5
|
+
* Currently supports Hetzner Cloud, with abstraction for future providers.
|
|
6
|
+
*
|
|
7
|
+
* To add a new provider:
|
|
8
|
+
* 1. Create a new provider class extending BaseProvider
|
|
9
|
+
* 2. Import and register it in PROVIDERS below
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { BaseProvider } from './base.js';
|
|
13
|
+
import { HetznerProvider } from './hetzner.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Registry of available cloud providers
|
|
17
|
+
* @type {Object<string, typeof BaseProvider>}
|
|
18
|
+
*/
|
|
19
|
+
export const PROVIDERS = {
|
|
20
|
+
hetzner: HetznerProvider,
|
|
21
|
+
// Future providers:
|
|
22
|
+
// digitalocean: DigitalOceanProvider,
|
|
23
|
+
// vultr: VultrProvider,
|
|
24
|
+
// linode: LinodeProvider,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get a provider instance
|
|
29
|
+
* @param {string} providerName - Provider name (e.g., 'hetzner')
|
|
30
|
+
* @param {string} apiToken - API token for authentication
|
|
31
|
+
* @returns {BaseProvider} Provider instance
|
|
32
|
+
* @throws {Error} If provider not found
|
|
33
|
+
*/
|
|
34
|
+
export function getProvider(providerName, apiToken) {
|
|
35
|
+
const Provider = PROVIDERS[providerName.toLowerCase()];
|
|
36
|
+
|
|
37
|
+
if (!Provider) {
|
|
38
|
+
const available = Object.keys(PROVIDERS).join(', ');
|
|
39
|
+
throw new Error(`Unknown provider: ${providerName}. Available providers: ${available}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new Provider(apiToken);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get a provider class (not instantiated)
|
|
47
|
+
* @param {string} providerName - Provider name
|
|
48
|
+
* @returns {typeof BaseProvider} Provider class
|
|
49
|
+
* @throws {Error} If provider not found
|
|
50
|
+
*/
|
|
51
|
+
export function getProviderClass(providerName) {
|
|
52
|
+
const Provider = PROVIDERS[providerName.toLowerCase()];
|
|
53
|
+
|
|
54
|
+
if (!Provider) {
|
|
55
|
+
const available = Object.keys(PROVIDERS).join(', ');
|
|
56
|
+
throw new Error(`Unknown provider: ${providerName}. Available providers: ${available}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return Provider;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* List all available providers with their details
|
|
64
|
+
* @returns {Array<{id: string, name: string, regions: object, serverTypes: object}>}
|
|
65
|
+
*/
|
|
66
|
+
export function listProviders() {
|
|
67
|
+
return Object.entries(PROVIDERS).map(([id, Provider]) => ({
|
|
68
|
+
id,
|
|
69
|
+
name: Provider.NAME,
|
|
70
|
+
regions: Provider.REGIONS,
|
|
71
|
+
serverTypes: Provider.SERVER_TYPES,
|
|
72
|
+
defaultType: Provider.DEFAULT_TYPE,
|
|
73
|
+
haRegions: Provider.HA_REGIONS,
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get list of provider IDs
|
|
79
|
+
* @returns {string[]}
|
|
80
|
+
*/
|
|
81
|
+
export function getProviderIds() {
|
|
82
|
+
return Object.keys(PROVIDERS);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if a provider exists
|
|
87
|
+
* @param {string} providerName - Provider name to check
|
|
88
|
+
* @returns {boolean}
|
|
89
|
+
*/
|
|
90
|
+
export function hasProvider(providerName) {
|
|
91
|
+
return providerName.toLowerCase() in PROVIDERS;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get the default provider ID
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
export function getDefaultProvider() {
|
|
99
|
+
return 'hetzner';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validate provider configuration
|
|
104
|
+
* @param {string} providerName - Provider name
|
|
105
|
+
* @param {object} config - Configuration to validate
|
|
106
|
+
* @param {string} [config.region] - Region to validate
|
|
107
|
+
* @param {string} [config.serverType] - Server type to validate
|
|
108
|
+
* @returns {{valid: boolean, errors: string[]}}
|
|
109
|
+
*/
|
|
110
|
+
export function validateProviderConfig(providerName, config) {
|
|
111
|
+
const errors = [];
|
|
112
|
+
|
|
113
|
+
const Provider = PROVIDERS[providerName.toLowerCase()];
|
|
114
|
+
if (!Provider) {
|
|
115
|
+
return { valid: false, errors: [`Unknown provider: ${providerName}`] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (config.region && !(config.region in Provider.REGIONS)) {
|
|
119
|
+
const available = Object.keys(Provider.REGIONS).join(', ');
|
|
120
|
+
errors.push(`Invalid region "${config.region}". Available regions: ${available}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (config.serverType && !(config.serverType in Provider.SERVER_TYPES)) {
|
|
124
|
+
const available = Object.keys(Provider.SERVER_TYPES).join(', ');
|
|
125
|
+
errors.push(`Invalid server type "${config.serverType}". Available types: ${available}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { valid: errors.length === 0, errors };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Format provider regions for display (e.g., in CLI prompts)
|
|
133
|
+
* @param {string} providerName - Provider name
|
|
134
|
+
* @returns {Array<{value: string, label: string}>}
|
|
135
|
+
*/
|
|
136
|
+
export function formatRegionsForSelect(providerName) {
|
|
137
|
+
const Provider = PROVIDERS[providerName.toLowerCase()];
|
|
138
|
+
if (!Provider) return [];
|
|
139
|
+
|
|
140
|
+
return Object.entries(Provider.REGIONS).map(([id, name]) => ({
|
|
141
|
+
value: id,
|
|
142
|
+
label: `${id} - ${name}`,
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Format provider server types for display
|
|
148
|
+
* @param {string} providerName - Provider name
|
|
149
|
+
* @returns {Array<{value: string, label: string, hint: string}>}
|
|
150
|
+
*/
|
|
151
|
+
export function formatServerTypesForSelect(providerName) {
|
|
152
|
+
const Provider = PROVIDERS[providerName.toLowerCase()];
|
|
153
|
+
if (!Provider) return [];
|
|
154
|
+
|
|
155
|
+
return Object.entries(Provider.SERVER_TYPES).map(([id, spec]) => ({
|
|
156
|
+
value: id,
|
|
157
|
+
label: id,
|
|
158
|
+
hint: `${spec.vcpu} vCPU, ${spec.ram}GB RAM, ${spec.disk}GB SSD - ${spec.price}`,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Re-export base class and providers for direct access
|
|
163
|
+
export { BaseProvider } from './base.js';
|
|
164
|
+
export { HetznerProvider } from './hetzner.js';
|
package/src/lib/s3.js
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vibecarbon Add S3 Command
|
|
3
|
+
* Adds Hetzner Object Storage (S3) to Supabase Storage backend
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* vibecarbon add s3 # Interactive setup
|
|
7
|
+
* vibecarbon add s3 --help # Show help
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { dirname, join } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import * as p from '@clack/prompts';
|
|
14
|
+
|
|
15
|
+
import { c, printBanner } from './colors.js';
|
|
16
|
+
import { hasDockerCompose, loadProjectConfig } from './config.js';
|
|
17
|
+
import { loadManifest, saveManifest } from './project.js';
|
|
18
|
+
import { HetznerS3Provider, sanitizeBucketName } from './providers/hetzner-s3.js';
|
|
19
|
+
import { VERSION } from './version.js';
|
|
20
|
+
|
|
21
|
+
// Supported S3 providers (will expand when we add more cloud providers)
|
|
22
|
+
const S3_PROVIDERS = {
|
|
23
|
+
hetzner: {
|
|
24
|
+
name: 'Hetzner Object Storage',
|
|
25
|
+
providerClass: HetznerS3Provider,
|
|
26
|
+
},
|
|
27
|
+
// Future: digitalocean, vultr, etc.
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
31
|
+
const __dirname = dirname(__filename);
|
|
32
|
+
|
|
33
|
+
// Local services directory
|
|
34
|
+
const SERVICES_DIR = join(__dirname, '..', '..', 'services');
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// CLI ARGUMENT PARSING
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
function parseArgs(args) {
|
|
41
|
+
const parsed = {
|
|
42
|
+
help: false,
|
|
43
|
+
region: null,
|
|
44
|
+
ha: false,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < args.length; i++) {
|
|
48
|
+
const arg = args[i];
|
|
49
|
+
const nextArg = args[i + 1];
|
|
50
|
+
|
|
51
|
+
if (arg === '--help' || arg === '-h') {
|
|
52
|
+
parsed.help = true;
|
|
53
|
+
} else if (arg === '--region' || arg === '-r') {
|
|
54
|
+
parsed.region = nextArg;
|
|
55
|
+
i++;
|
|
56
|
+
} else if (arg === '--ha') {
|
|
57
|
+
parsed.ha = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return parsed;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function showHelp() {
|
|
65
|
+
console.log(`
|
|
66
|
+
${c.bold('Vibecarbon Add S3')} - Add Hetzner Object Storage
|
|
67
|
+
|
|
68
|
+
${c.bold('USAGE')}
|
|
69
|
+
vibecarbon add s3 [options]
|
|
70
|
+
|
|
71
|
+
${c.bold('OPTIONS')}
|
|
72
|
+
-r, --region <region> S3 region (fsn1, nbg1, hel1)
|
|
73
|
+
--ha Enable HA mode with cross-region replication
|
|
74
|
+
-h, --help Show this help message
|
|
75
|
+
|
|
76
|
+
${c.bold('EXAMPLES')}
|
|
77
|
+
${c.dim('# Interactive setup')}
|
|
78
|
+
vibecarbon add s3
|
|
79
|
+
|
|
80
|
+
${c.dim('# Specify region')}
|
|
81
|
+
vibecarbon add s3 --region fsn1
|
|
82
|
+
|
|
83
|
+
${c.dim('# HA setup with cross-region sync')}
|
|
84
|
+
vibecarbon add s3 --ha
|
|
85
|
+
|
|
86
|
+
${c.bold('NOTES')}
|
|
87
|
+
- S3 credentials must be created in Hetzner Console first
|
|
88
|
+
- Buckets are created automatically based on project name
|
|
89
|
+
- For HA, buckets are created in two regions with rclone sync
|
|
90
|
+
`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// ENVIRONMENT UPDATES
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
function appendS3ToEnv(s3Config, isHA = false) {
|
|
98
|
+
const envFiles = ['.env.local', '.env'];
|
|
99
|
+
|
|
100
|
+
for (const filename of envFiles) {
|
|
101
|
+
const envPath = join(process.cwd(), filename);
|
|
102
|
+
if (!existsSync(envPath)) continue;
|
|
103
|
+
|
|
104
|
+
const content = readFileSync(envPath, 'utf-8');
|
|
105
|
+
|
|
106
|
+
// Check if S3 section already exists
|
|
107
|
+
if (content.includes('# S3 STORAGE')) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let s3Section;
|
|
112
|
+
if (isHA) {
|
|
113
|
+
s3Section = `
|
|
114
|
+
|
|
115
|
+
# =============================================================================
|
|
116
|
+
# S3 STORAGE (Hetzner Object Storage - HA)
|
|
117
|
+
# =============================================================================
|
|
118
|
+
|
|
119
|
+
S3_ACCESS_KEY="${s3Config.accessKey}"
|
|
120
|
+
S3_SECRET_KEY="${s3Config.secretKey}"
|
|
121
|
+
S3_PRIMARY_BUCKET="${s3Config.primaryBucket}"
|
|
122
|
+
S3_STANDBY_BUCKET="${s3Config.standbyBucket}"
|
|
123
|
+
S3_PRIMARY_ENDPOINT="${s3Config.primaryEndpoint}"
|
|
124
|
+
S3_STANDBY_ENDPOINT="${s3Config.standbyEndpoint}"
|
|
125
|
+
S3_PRIMARY_REGION="${s3Config.primaryRegion}"
|
|
126
|
+
S3_STANDBY_REGION="${s3Config.standbyRegion}"
|
|
127
|
+
S3_SYNC_INTERVAL="${s3Config.syncInterval || '5'}"
|
|
128
|
+
`;
|
|
129
|
+
} else {
|
|
130
|
+
s3Section = `
|
|
131
|
+
|
|
132
|
+
# =============================================================================
|
|
133
|
+
# S3 STORAGE (Hetzner Object Storage)
|
|
134
|
+
# =============================================================================
|
|
135
|
+
|
|
136
|
+
S3_ACCESS_KEY="${s3Config.accessKey}"
|
|
137
|
+
S3_SECRET_KEY="${s3Config.secretKey}"
|
|
138
|
+
S3_BUCKET="${s3Config.bucket}"
|
|
139
|
+
S3_ENDPOINT="${s3Config.endpoint}"
|
|
140
|
+
S3_REGION="${s3Config.region}"
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
writeFileSync(envPath, content.trimEnd() + s3Section);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============================================================================
|
|
149
|
+
// COMPOSE FILE INSTALLATION
|
|
150
|
+
// ============================================================================
|
|
151
|
+
|
|
152
|
+
function installComposeOverlay(isHA = false) {
|
|
153
|
+
const srcFile = isHA ? 'docker-compose.s3-ha.yml' : 'docker-compose.s3.yml';
|
|
154
|
+
const srcPath = join(SERVICES_DIR, 's3', 'compose', srcFile);
|
|
155
|
+
const destPath = join(process.cwd(), srcFile);
|
|
156
|
+
|
|
157
|
+
if (!existsSync(srcPath)) {
|
|
158
|
+
throw new Error(`Service file not found: ${srcPath}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
copyFileSync(srcPath, destPath);
|
|
162
|
+
return destPath;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// PACKAGE.JSON SCRIPTS
|
|
167
|
+
// ============================================================================
|
|
168
|
+
|
|
169
|
+
function updatePackageJsonScripts(isHA = false) {
|
|
170
|
+
const packageJsonPath = join(process.cwd(), 'package.json');
|
|
171
|
+
if (!existsSync(packageJsonPath)) return;
|
|
172
|
+
|
|
173
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
174
|
+
|
|
175
|
+
if (!packageJson.scripts) {
|
|
176
|
+
packageJson.scripts = {};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Detect package manager from packageManager field for cross-script references
|
|
180
|
+
const pm = packageJson.packageManager?.split('@')[0] || 'pnpm';
|
|
181
|
+
const run = pm === 'npm' ? 'npm run' : pm;
|
|
182
|
+
|
|
183
|
+
if (isHA) {
|
|
184
|
+
packageJson.scripts['docker:up:s3'] =
|
|
185
|
+
'docker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.s3-ha.yml up -d';
|
|
186
|
+
packageJson.scripts['dev:start:s3'] = `${run} docker:up:s3 && ${run} db:migrate && ${run} dev`;
|
|
187
|
+
} else {
|
|
188
|
+
packageJson.scripts['docker:up:s3'] =
|
|
189
|
+
'docker compose -f docker-compose.yml -f docker-compose.s3.yml up -d';
|
|
190
|
+
packageJson.scripts['dev:start:s3'] = `${run} docker:up:s3 && ${run} db:migrate && ${run} dev`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// INSTRUCTIONS
|
|
198
|
+
// ============================================================================
|
|
199
|
+
|
|
200
|
+
function showS3Instructions() {
|
|
201
|
+
const instructions = `
|
|
202
|
+
${c.bold('To use S3 storage, create credentials in Hetzner Console:')}
|
|
203
|
+
|
|
204
|
+
1. Go to ${c.info('https://console.hetzner.cloud/')}
|
|
205
|
+
2. Select your project (or create one)
|
|
206
|
+
3. Click ${c.bold('Object Storage')} in the sidebar
|
|
207
|
+
4. Click ${c.bold('Create Credentials')}
|
|
208
|
+
5. Copy the ${c.bold('Access Key')} and ${c.bold('Secret Key')} (secret shown only once!)
|
|
209
|
+
`;
|
|
210
|
+
|
|
211
|
+
p.note(instructions.trim(), 'Hetzner Object Storage Setup');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================================================
|
|
215
|
+
// MAIN FLOW
|
|
216
|
+
// ============================================================================
|
|
217
|
+
|
|
218
|
+
async function addS3(options) {
|
|
219
|
+
const { project, args } = options;
|
|
220
|
+
const isHA = args.ha || project.haConfigured;
|
|
221
|
+
const cloudProvider = project.provider || 'hetzner';
|
|
222
|
+
|
|
223
|
+
// Check if the provider's S3 service is supported
|
|
224
|
+
const s3Provider = S3_PROVIDERS[cloudProvider];
|
|
225
|
+
if (!s3Provider) {
|
|
226
|
+
p.log.error(`S3 storage is not yet supported for provider: ${cloudProvider}`);
|
|
227
|
+
p.log.info('Currently supported: Hetzner Object Storage');
|
|
228
|
+
p.log.info('Support for DigitalOcean Spaces, Vultr Object Storage coming soon.');
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const S3ProviderClass = s3Provider.providerClass;
|
|
233
|
+
const s3ProviderName = s3Provider.name;
|
|
234
|
+
|
|
235
|
+
// Check if S3 is already configured
|
|
236
|
+
const manifest = loadManifest();
|
|
237
|
+
if (manifest.services?.s3) {
|
|
238
|
+
p.log.warn('S3 storage is already configured for this project.');
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Show provider-specific instructions
|
|
243
|
+
p.log.info(`Using ${c.bold(s3ProviderName)} (detected from deployment config)`);
|
|
244
|
+
showS3Instructions();
|
|
245
|
+
|
|
246
|
+
// Prompt for credentials
|
|
247
|
+
const accessKey = await p.text({
|
|
248
|
+
message: `Enter your ${s3ProviderName} Access Key`,
|
|
249
|
+
validate: (v) => (v.length < 10 ? 'Access Key is required' : undefined),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (p.isCancel(accessKey)) {
|
|
253
|
+
p.cancel('Operation cancelled.');
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const secretKey = await p.password({
|
|
258
|
+
message: `Enter your ${s3ProviderName} Secret Key`,
|
|
259
|
+
validate: (v) => (v.length < 10 ? 'Secret Key is required' : undefined),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (p.isCancel(secretKey)) {
|
|
263
|
+
p.cancel('Operation cancelled.');
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Region selection
|
|
268
|
+
const regionOptions = Object.entries(S3ProviderClass.REGIONS).map(([id, name]) => ({
|
|
269
|
+
value: id,
|
|
270
|
+
label: `${id} - ${name}`,
|
|
271
|
+
}));
|
|
272
|
+
|
|
273
|
+
let primaryRegion, standbyRegion;
|
|
274
|
+
|
|
275
|
+
if (isHA) {
|
|
276
|
+
primaryRegion = await p.select({
|
|
277
|
+
message: 'Select PRIMARY region for S3 bucket',
|
|
278
|
+
options: regionOptions,
|
|
279
|
+
initialValue: args.region || 'fsn1',
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
if (p.isCancel(primaryRegion)) {
|
|
283
|
+
p.cancel('Operation cancelled.');
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Filter out primary region for standby selection
|
|
288
|
+
const standbyOptions = regionOptions.filter((r) => r.value !== primaryRegion);
|
|
289
|
+
standbyRegion = await p.select({
|
|
290
|
+
message: 'Select STANDBY region for S3 bucket',
|
|
291
|
+
options: standbyOptions,
|
|
292
|
+
initialValue: primaryRegion === 'fsn1' ? 'hel1' : 'fsn1',
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
if (p.isCancel(standbyRegion)) {
|
|
296
|
+
p.cancel('Operation cancelled.');
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
primaryRegion = await p.select({
|
|
301
|
+
message: 'Select region for S3 bucket',
|
|
302
|
+
options: regionOptions,
|
|
303
|
+
initialValue: args.region || 'fsn1',
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (p.isCancel(primaryRegion)) {
|
|
307
|
+
p.cancel('Operation cancelled.');
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Validate credentials
|
|
313
|
+
const s = p.spinner();
|
|
314
|
+
s.start('Verifying S3 credentials...');
|
|
315
|
+
|
|
316
|
+
const provider = new S3ProviderClass(accessKey, secretKey, primaryRegion);
|
|
317
|
+
const validation = await provider.validateCredentials();
|
|
318
|
+
|
|
319
|
+
if (!validation.valid) {
|
|
320
|
+
s.stop(`Credential validation failed: ${validation.error}`);
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
s.stop('Credentials verified');
|
|
325
|
+
|
|
326
|
+
// Create bucket(s)
|
|
327
|
+
const baseBucketName = sanitizeBucketName(project.projectName);
|
|
328
|
+
|
|
329
|
+
let s3Config;
|
|
330
|
+
|
|
331
|
+
if (isHA) {
|
|
332
|
+
// Create primary bucket
|
|
333
|
+
s.start(`Creating primary bucket: ${baseBucketName}-primary`);
|
|
334
|
+
const primaryBucket = await provider.createBucket(`${baseBucketName}-primary`);
|
|
335
|
+
|
|
336
|
+
if (primaryBucket.created) {
|
|
337
|
+
s.stop(`Primary bucket created: ${primaryBucket.name}`);
|
|
338
|
+
} else {
|
|
339
|
+
s.stop(`Primary bucket exists: ${primaryBucket.name}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Configure CORS for primary
|
|
343
|
+
s.start('Configuring CORS for primary bucket...');
|
|
344
|
+
await provider.configureCORS(`${baseBucketName}-primary`);
|
|
345
|
+
s.stop('CORS configured');
|
|
346
|
+
|
|
347
|
+
// Create standby bucket in different region
|
|
348
|
+
const standbyProvider = new S3ProviderClass(accessKey, secretKey, standbyRegion);
|
|
349
|
+
s.start(`Creating standby bucket: ${baseBucketName}-standby`);
|
|
350
|
+
const standbyBucket = await standbyProvider.createBucket(`${baseBucketName}-standby`);
|
|
351
|
+
|
|
352
|
+
if (standbyBucket.created) {
|
|
353
|
+
s.stop(`Standby bucket created: ${standbyBucket.name}`);
|
|
354
|
+
} else {
|
|
355
|
+
s.stop(`Standby bucket exists: ${standbyBucket.name}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Configure CORS for standby
|
|
359
|
+
s.start('Configuring CORS for standby bucket...');
|
|
360
|
+
await standbyProvider.configureCORS(`${baseBucketName}-standby`);
|
|
361
|
+
s.stop('CORS configured');
|
|
362
|
+
|
|
363
|
+
s3Config = {
|
|
364
|
+
accessKey,
|
|
365
|
+
secretKey,
|
|
366
|
+
primaryBucket: `${baseBucketName}-primary`,
|
|
367
|
+
standbyBucket: `${baseBucketName}-standby`,
|
|
368
|
+
primaryEndpoint: S3ProviderClass.getEndpointForRegion(primaryRegion),
|
|
369
|
+
standbyEndpoint: S3ProviderClass.getEndpointForRegion(standbyRegion),
|
|
370
|
+
primaryRegion,
|
|
371
|
+
standbyRegion,
|
|
372
|
+
syncInterval: '5',
|
|
373
|
+
};
|
|
374
|
+
} else {
|
|
375
|
+
// Single bucket setup
|
|
376
|
+
s.start(`Creating bucket: ${baseBucketName}`);
|
|
377
|
+
const bucket = await provider.createBucket(baseBucketName);
|
|
378
|
+
|
|
379
|
+
if (bucket.created) {
|
|
380
|
+
s.stop(`Bucket created: ${bucket.name}`);
|
|
381
|
+
} else {
|
|
382
|
+
s.stop(`Bucket exists: ${bucket.name}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Configure CORS
|
|
386
|
+
s.start('Configuring CORS...');
|
|
387
|
+
await provider.configureCORS(baseBucketName);
|
|
388
|
+
s.stop('CORS configured');
|
|
389
|
+
|
|
390
|
+
s3Config = {
|
|
391
|
+
accessKey,
|
|
392
|
+
secretKey,
|
|
393
|
+
bucket: baseBucketName,
|
|
394
|
+
endpoint: provider.getEndpoint(),
|
|
395
|
+
region: primaryRegion,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Install compose overlay
|
|
400
|
+
s.start('Installing docker-compose overlay...');
|
|
401
|
+
const composeFile = installComposeOverlay(isHA);
|
|
402
|
+
s.stop(`Created: ${composeFile.replace(`${process.cwd()}/`, '')}`);
|
|
403
|
+
|
|
404
|
+
// Update .env files
|
|
405
|
+
s.start('Updating environment files...');
|
|
406
|
+
appendS3ToEnv(s3Config, isHA);
|
|
407
|
+
s.stop('Environment files updated');
|
|
408
|
+
|
|
409
|
+
// Update package.json scripts
|
|
410
|
+
s.start('Adding npm scripts...');
|
|
411
|
+
updatePackageJsonScripts(isHA);
|
|
412
|
+
s.stop('Scripts added');
|
|
413
|
+
|
|
414
|
+
// Update project manifest
|
|
415
|
+
manifest.services = manifest.services || {};
|
|
416
|
+
manifest.services.s3 = {
|
|
417
|
+
addedAt: new Date().toISOString(),
|
|
418
|
+
version: '1.0.0',
|
|
419
|
+
provider: cloudProvider, // Cloud provider (hetzner, digitalocean, etc.)
|
|
420
|
+
s3Service: s3ProviderName, // S3 service name (Hetzner Object Storage, etc.)
|
|
421
|
+
ha: isHA,
|
|
422
|
+
region: primaryRegion,
|
|
423
|
+
...(isHA ? { standbyRegion } : {}),
|
|
424
|
+
};
|
|
425
|
+
saveManifest(manifest);
|
|
426
|
+
|
|
427
|
+
// Show summary
|
|
428
|
+
if (isHA) {
|
|
429
|
+
p.note(
|
|
430
|
+
[
|
|
431
|
+
`Primary: ${s3Config.primaryBucket} (${primaryRegion})`,
|
|
432
|
+
`Standby: ${s3Config.standbyBucket} (${standbyRegion})`,
|
|
433
|
+
`Sync: Every ${s3Config.syncInterval} minutes via rclone`,
|
|
434
|
+
'',
|
|
435
|
+
`Endpoint: ${s3Config.primaryEndpoint}`,
|
|
436
|
+
].join('\n'),
|
|
437
|
+
'S3 HA Storage Configured',
|
|
438
|
+
);
|
|
439
|
+
} else {
|
|
440
|
+
p.note(
|
|
441
|
+
[
|
|
442
|
+
`Bucket: ${s3Config.bucket}`,
|
|
443
|
+
`Region: ${primaryRegion}`,
|
|
444
|
+
`Endpoint: ${s3Config.endpoint}`,
|
|
445
|
+
].join('\n'),
|
|
446
|
+
'S3 Storage Configured',
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ============================================================================
|
|
454
|
+
// MAIN
|
|
455
|
+
// ============================================================================
|
|
456
|
+
|
|
457
|
+
async function main(cliArgs) {
|
|
458
|
+
const args = parseArgs(cliArgs);
|
|
459
|
+
|
|
460
|
+
if (args.help) {
|
|
461
|
+
showHelp();
|
|
462
|
+
process.exit(0);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Detect project
|
|
466
|
+
const projectConfig = loadProjectConfig();
|
|
467
|
+
if (!projectConfig || !hasDockerCompose()) {
|
|
468
|
+
p.log.error('Not in a Vibecarbon project directory.');
|
|
469
|
+
p.log.info('Run this command from within a project created with `vibecarbon create`.');
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
const project = { projectName: projectConfig.projectName };
|
|
473
|
+
|
|
474
|
+
printBanner();
|
|
475
|
+
p.intro(`${c.bold('vibecarbon add s3')} ${c.dim(`v${VERSION}`)}`);
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
const success = await addS3({ project, args });
|
|
479
|
+
|
|
480
|
+
if (success) {
|
|
481
|
+
p.note(
|
|
482
|
+
['# Restart services to apply changes:', 'vibecarbon down && vibecarbon up'].join('\n'),
|
|
483
|
+
'Next steps',
|
|
484
|
+
);
|
|
485
|
+
p.outro('S3 storage added successfully!');
|
|
486
|
+
} else {
|
|
487
|
+
p.outro('S3 setup was not completed.');
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
} catch (error) {
|
|
491
|
+
p.log.error(`Failed to add S3: ${error.message}`);
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export async function run(args) {
|
|
497
|
+
await main(args);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ============================================================================
|
|
501
|
+
// EXPORTS FOR TESTING
|
|
502
|
+
// ============================================================================
|
|
503
|
+
|
|
504
|
+
export {
|
|
505
|
+
appendS3ToEnv,
|
|
506
|
+
installComposeOverlay,
|
|
507
|
+
parseArgs,
|
|
508
|
+
sanitizeBucketName,
|
|
509
|
+
updatePackageJsonScripts,
|
|
510
|
+
};
|