vibecarbon 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +663 -0
- package/README.md +188 -0
- package/carbon/.claude/hooks/task-completed-gate.sh +19 -0
- package/carbon/.claude/hooks/teammate-idle-gate.sh +29 -0
- package/carbon/.claude/settings.json +29 -0
- package/carbon/.cursor/rules/vibecarbon.mdc +6 -0
- package/carbon/.dockerignore +57 -0
- package/carbon/.env.example +219 -0
- package/carbon/.github/copilot-instructions.md +3 -0
- package/carbon/.github/workflows/deploy.yml +346 -0
- package/carbon/.github/workflows/vibecarbon-build.yml +81 -0
- package/carbon/.windsurfrules +74 -0
- package/carbon/AGENTS.md +422 -0
- package/carbon/CLAUDE.md +3 -0
- package/carbon/DEVELOPMENT.md +187 -0
- package/carbon/Dockerfile +98 -0
- package/carbon/LICENSE +21 -0
- package/carbon/PRODUCTION.md +364 -0
- package/carbon/README.md +193 -0
- package/carbon/TESTING.md +138 -0
- package/carbon/backup/Dockerfile +75 -0
- package/carbon/backup/backup.sh +95 -0
- package/carbon/backup/compose-backup.sh +140 -0
- package/carbon/biome.json +83 -0
- package/carbon/cloud-init/docker-ce-setup.yaml +55 -0
- package/carbon/cloud-init/k3s/master-init.sh +215 -0
- package/carbon/cloud-init/k3s/supabase-init.sh +167 -0
- package/carbon/cloud-init/k3s/worker-init.sh +147 -0
- package/carbon/components.json +24 -0
- package/carbon/content/blog/authentication-guide.mdx +34 -0
- package/carbon/content/blog/getting-started.mdx +41 -0
- package/carbon/content/changelog/v0-1-0.mdx +40 -0
- package/carbon/content/docs/analytics.mdx +90 -0
- package/carbon/content/docs/authentication.mdx +231 -0
- package/carbon/content/docs/background-jobs.mdx +116 -0
- package/carbon/content/docs/cli.mdx +630 -0
- package/carbon/content/docs/database.mdx +236 -0
- package/carbon/content/docs/deployment.mdx +227 -0
- package/carbon/content/docs/development.mdx +238 -0
- package/carbon/content/docs/environments.mdx +84 -0
- package/carbon/content/docs/getting-started.mdx +112 -0
- package/carbon/content/docs/legal/privacy-policy.mdx +89 -0
- package/carbon/content/docs/legal/terms-of-service.mdx +99 -0
- package/carbon/content/docs/optional-services.mdx +160 -0
- package/carbon/db/Dockerfile +23 -0
- package/carbon/docker-compose.dev-init.yml +5 -0
- package/carbon/docker-compose.metabase.override.yml +14 -0
- package/carbon/docker-compose.metabase.prod.yml +28 -0
- package/carbon/docker-compose.metabase.yml +84 -0
- package/carbon/docker-compose.n8n.override.yml +14 -0
- package/carbon/docker-compose.n8n.prod.yml +31 -0
- package/carbon/docker-compose.n8n.yml +97 -0
- package/carbon/docker-compose.observability.override.yml +18 -0
- package/carbon/docker-compose.observability.prod.yml +28 -0
- package/carbon/docker-compose.observability.yml +125 -0
- package/carbon/docker-compose.override.yml +28 -0
- package/carbon/docker-compose.prod.yml +294 -0
- package/carbon/docker-compose.yml +508 -0
- package/carbon/docker-entrypoint.sh +12 -0
- package/carbon/ha/activate-standby.sh +52 -0
- package/carbon/ha/primary-init.sql +36 -0
- package/carbon/ha/standby-init.sh +74 -0
- package/carbon/k8s/LICENSE +99 -0
- package/carbon/k8s/README.md +272 -0
- package/carbon/k8s/base/app/deployment.yaml +130 -0
- package/carbon/k8s/base/app/hpa.yaml +44 -0
- package/carbon/k8s/base/app/kustomization.yaml +10 -0
- package/carbon/k8s/base/app/network-policy.yaml +124 -0
- package/carbon/k8s/base/app/pdb.yaml +14 -0
- package/carbon/k8s/base/app/rbac.yaml +36 -0
- package/carbon/k8s/base/app/service.yaml +16 -0
- package/carbon/k8s/base/backup/cronjob.yaml +120 -0
- package/carbon/k8s/base/backup/kustomization.yaml +7 -0
- package/carbon/k8s/base/backup/network-policy.yaml +31 -0
- package/carbon/k8s/base/backup/rbac.yaml +7 -0
- package/carbon/k8s/base/cluster-autoscaler/deployment.yaml +103 -0
- package/carbon/k8s/base/cluster-autoscaler/kustomization.yaml +17 -0
- package/carbon/k8s/base/cluster-autoscaler/rbac.yaml +109 -0
- package/carbon/k8s/base/config/configmap.yaml +48 -0
- package/carbon/k8s/base/config/kustomization.yaml +8 -0
- package/carbon/k8s/base/hetzner-ccm/kustomization.yaml +9 -0
- package/carbon/k8s/base/hetzner-csi/kustomization.yaml +9 -0
- package/carbon/k8s/base/kustomization.yaml +43 -0
- package/carbon/k8s/base/namespace.yaml +7 -0
- package/carbon/k8s/base/network-policies.yaml +95 -0
- package/carbon/k8s/base/registry/kustomization.yaml +5 -0
- package/carbon/k8s/base/registry/local-registry.yaml +193 -0
- package/carbon/k8s/base/traefik/certificate.yaml +21 -0
- package/carbon/k8s/base/traefik/configmap.yaml +13 -0
- package/carbon/k8s/base/traefik/deployment.yaml +165 -0
- package/carbon/k8s/base/traefik/ingressroute.yaml +141 -0
- package/carbon/k8s/base/traefik/kustomization.yaml +11 -0
- package/carbon/k8s/base/traefik/middleware.yaml +109 -0
- package/carbon/k8s/base/traefik/network-policy.yaml +92 -0
- package/carbon/k8s/base/traefik/service.yaml +38 -0
- package/carbon/k8s/flux/README.md +49 -0
- package/carbon/k8s/flux/clusters/primary/vibecarbon.yaml +128 -0
- package/carbon/k8s/flux/clusters/standby/vibecarbon.yaml +83 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-release.yaml +38 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/helm-repository.yaml +16 -0
- package/carbon/k8s/gitops/cert-manager-webhook-hetzner/kustomization.yaml +10 -0
- package/carbon/k8s/gitops/supabase/helm-release.yaml +66 -0
- package/carbon/k8s/gitops/supabase/helm-repository.yaml +18 -0
- package/carbon/k8s/gitops/supabase/kustomization.yaml +33 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-cloudflare.yaml +51 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-hetzner.yaml +59 -0
- package/carbon/k8s/infra/cert-manager-resources/cluster-issuers-manual.yaml +43 -0
- package/carbon/k8s/infra/cert-manager-resources/kustomization.yaml +15 -0
- package/carbon/k8s/infra/kustomization.yaml +21 -0
- package/carbon/k8s/infra/traefik-crds/kustomization.yaml +10 -0
- package/carbon/k8s/overlays/local/configmap.yaml +23 -0
- package/carbon/k8s/overlays/local/ingressroute-studio.yaml +104 -0
- package/carbon/k8s/overlays/local/kustomization.yaml +215 -0
- package/carbon/k8s/overlays/local/namespace.yaml +8 -0
- package/carbon/k8s/overlays/local/secrets.yaml +32 -0
- package/carbon/k8s/overlays/production/kustomization.yaml +34 -0
- package/carbon/k8s/test-local.sh +318 -0
- package/carbon/k8s/values/supabase.values.yaml +133 -0
- package/carbon/package.json +154 -0
- package/carbon/runtime.Dockerfile +17 -0
- package/carbon/scripts/_dev-jwt.mjs +45 -0
- package/carbon/scripts/check-rls.ts +53 -0
- package/carbon/scripts/dev-init.js +191 -0
- package/carbon/scripts/dev.js +191 -0
- package/carbon/scripts/docker-down.js +63 -0
- package/carbon/scripts/docker-logs.js +59 -0
- package/carbon/scripts/docker-up.js +222 -0
- package/carbon/scripts/generate-dev-configs.sh +131 -0
- package/carbon/scripts/generate-rss.ts +116 -0
- package/carbon/scripts/generate-sitemap.ts +102 -0
- package/carbon/scripts/k8s-apply.js +71 -0
- package/carbon/scripts/k8s-delete.js +75 -0
- package/carbon/scripts/lib/manifest.js +176 -0
- package/carbon/scripts/secret-scan.mjs +278 -0
- package/carbon/scripts/validate-dev-configs.sh +101 -0
- package/carbon/src/client/App.tsx +202 -0
- package/carbon/src/client/assets/hyperformant-dark.svg +29 -0
- package/carbon/src/client/assets/hyperformant-light.svg +29 -0
- package/carbon/src/client/assets/logos/aider.svg +1 -0
- package/carbon/src/client/assets/logos/antigravity.svg +1 -0
- package/carbon/src/client/assets/logos/bolt.svg +1 -0
- package/carbon/src/client/assets/logos/claude-code.svg +1 -0
- package/carbon/src/client/assets/logos/copilot.svg +1 -0
- package/carbon/src/client/assets/logos/cursor.svg +1 -0
- package/carbon/src/client/assets/logos/gemini-cli.svg +1 -0
- package/carbon/src/client/assets/logos/lovable.svg +1 -0
- package/carbon/src/client/assets/logos/openai-codex.svg +1 -0
- package/carbon/src/client/assets/logos/v0.svg +1 -0
- package/carbon/src/client/assets/logos/windsurf.svg +1 -0
- package/carbon/src/client/assets/vibecarbon-icon.svg +19 -0
- package/carbon/src/client/assets/vibecarbon-logo-dark.svg +29 -0
- package/carbon/src/client/assets/vibecarbon-logo-light.svg +29 -0
- package/carbon/src/client/components/AIIntegrationSection.tsx +120 -0
- package/carbon/src/client/components/AppSidebar.tsx +760 -0
- package/carbon/src/client/components/ArchitectureSection.tsx +46 -0
- package/carbon/src/client/components/CTAFooter.tsx +59 -0
- package/carbon/src/client/components/ComparisonSection.tsx +132 -0
- package/carbon/src/client/components/ContentPanel.tsx +66 -0
- package/carbon/src/client/components/ContentSkeleton.tsx +21 -0
- package/carbon/src/client/components/ErrorBoundary.tsx +46 -0
- package/carbon/src/client/components/FAQSection.tsx +76 -0
- package/carbon/src/client/components/FileUpload.tsx +210 -0
- package/carbon/src/client/components/HeaderActions.tsx +17 -0
- package/carbon/src/client/components/Hero.tsx +608 -0
- package/carbon/src/client/components/ImpersonationBanner.tsx +31 -0
- package/carbon/src/client/components/LanguageSwitcher.tsx +67 -0
- package/carbon/src/client/components/Logo.tsx +87 -0
- package/carbon/src/client/components/LogoStrip.tsx +76 -0
- package/carbon/src/client/components/MetricsStrip.tsx +48 -0
- package/carbon/src/client/components/Nav.tsx +195 -0
- package/carbon/src/client/components/NewsletterSignup.tsx +147 -0
- package/carbon/src/client/components/NotificationDrawer.tsx +171 -0
- package/carbon/src/client/components/PageHeader.tsx +24 -0
- package/carbon/src/client/components/PlanGate.tsx +36 -0
- package/carbon/src/client/components/PricingSection.tsx +371 -0
- package/carbon/src/client/components/SEO.tsx +34 -0
- package/carbon/src/client/components/SmoothScroll.tsx +45 -0
- package/carbon/src/client/components/TechStackSection.tsx +165 -0
- package/carbon/src/client/components/WorkflowSection.tsx +604 -0
- package/carbon/src/client/components/admin/DockerLogs.tsx +276 -0
- package/carbon/src/client/components/admin/PerformanceMetrics.tsx +230 -0
- package/carbon/src/client/components/admin/ServiceCard.tsx +45 -0
- package/carbon/src/client/components/admin/ServicesStatus.tsx +296 -0
- package/carbon/src/client/components/architecture/ArchitectureDiagram.tsx +413 -0
- package/carbon/src/client/components/auth/AuthProvider.tsx +466 -0
- package/carbon/src/client/components/effects/FilmGrainOverlay.tsx +35 -0
- package/carbon/src/client/components/effects/FresnelEdge.tsx +49 -0
- package/carbon/src/client/components/effects/GlowTracker.tsx +46 -0
- package/carbon/src/client/components/effects/SparkBurst.tsx +145 -0
- package/carbon/src/client/components/effects/VGlowEffect.tsx +83 -0
- package/carbon/src/client/components/effects/index.ts +2 -0
- package/carbon/src/client/components/layouts/SidebarLayout.tsx +20 -0
- package/carbon/src/client/components/scroll/ScrollSection.tsx +76 -0
- package/carbon/src/client/components/scroll/ScrollytellingProvider.tsx +81 -0
- package/carbon/src/client/components/scroll/index.ts +2 -0
- package/carbon/src/client/components/ui/accordion.tsx +71 -0
- package/carbon/src/client/components/ui/alert-dialog.tsx +162 -0
- package/carbon/src/client/components/ui/alert.tsx +73 -0
- package/carbon/src/client/components/ui/aspect-ratio.tsx +22 -0
- package/carbon/src/client/components/ui/avatar.tsx +91 -0
- package/carbon/src/client/components/ui/badge.tsx +50 -0
- package/carbon/src/client/components/ui/breadcrumb.tsx +100 -0
- package/carbon/src/client/components/ui/button-group.tsx +78 -0
- package/carbon/src/client/components/ui/button.tsx +120 -0
- package/carbon/src/client/components/ui/calendar.tsx +182 -0
- package/carbon/src/client/components/ui/card.tsx +85 -0
- package/carbon/src/client/components/ui/carousel.tsx +227 -0
- package/carbon/src/client/components/ui/chart.tsx +357 -0
- package/carbon/src/client/components/ui/checkbox.tsx +27 -0
- package/carbon/src/client/components/ui/collapsible.tsx +15 -0
- package/carbon/src/client/components/ui/command.tsx +178 -0
- package/carbon/src/client/components/ui/context-menu.tsx +233 -0
- package/carbon/src/client/components/ui/dialog.tsx +132 -0
- package/carbon/src/client/components/ui/drawer.tsx +118 -0
- package/carbon/src/client/components/ui/dropdown-menu.tsx +242 -0
- package/carbon/src/client/components/ui/empty.tsx +94 -0
- package/carbon/src/client/components/ui/field.tsx +226 -0
- package/carbon/src/client/components/ui/hover-card.tsx +44 -0
- package/carbon/src/client/components/ui/input-group.tsx +146 -0
- package/carbon/src/client/components/ui/input-otp.tsx +83 -0
- package/carbon/src/client/components/ui/input.tsx +20 -0
- package/carbon/src/client/components/ui/item.tsx +188 -0
- package/carbon/src/client/components/ui/kbd.tsx +26 -0
- package/carbon/src/client/components/ui/label.tsx +21 -0
- package/carbon/src/client/components/ui/menubar.tsx +250 -0
- package/carbon/src/client/components/ui/navigation-menu.tsx +155 -0
- package/carbon/src/client/components/ui/pagination.tsx +102 -0
- package/carbon/src/client/components/ui/popover.tsx +75 -0
- package/carbon/src/client/components/ui/progress.tsx +66 -0
- package/carbon/src/client/components/ui/radio-group.tsx +36 -0
- package/carbon/src/client/components/ui/resizable.tsx +46 -0
- package/carbon/src/client/components/ui/scroll-area.tsx +48 -0
- package/carbon/src/client/components/ui/select.tsx +186 -0
- package/carbon/src/client/components/ui/separator.tsx +21 -0
- package/carbon/src/client/components/ui/sheet.tsx +124 -0
- package/carbon/src/client/components/ui/sidebar.tsx +702 -0
- package/carbon/src/client/components/ui/skeleton.tsx +13 -0
- package/carbon/src/client/components/ui/slider.tsx +57 -0
- package/carbon/src/client/components/ui/sonner.tsx +45 -0
- package/carbon/src/client/components/ui/spinner.tsx +15 -0
- package/carbon/src/client/components/ui/switch.tsx +30 -0
- package/carbon/src/client/components/ui/table.tsx +89 -0
- package/carbon/src/client/components/ui/tabs.tsx +73 -0
- package/carbon/src/client/components/ui/textarea.tsx +18 -0
- package/carbon/src/client/components/ui/toggle-group.tsx +87 -0
- package/carbon/src/client/components/ui/toggle.tsx +44 -0
- package/carbon/src/client/components/ui/tooltip.tsx +56 -0
- package/carbon/src/client/hooks/api/index.ts +14 -0
- package/carbon/src/client/hooks/api/useAuthSettings.ts +80 -0
- package/carbon/src/client/hooks/api/useSubscription.ts +87 -0
- package/carbon/src/client/hooks/use-mobile.ts +21 -0
- package/carbon/src/client/hooks/useMousePosition.ts +91 -0
- package/carbon/src/client/hooks/useNotifications.tsx +124 -0
- package/carbon/src/client/hooks/useOrganizationMembers.ts +127 -0
- package/carbon/src/client/hooks/useOrganizations.tsx +230 -0
- package/carbon/src/client/hooks/usePackageManager.ts +69 -0
- package/carbon/src/client/hooks/useReducedMotion.ts +20 -0
- package/carbon/src/client/hooks/useRunningServices.ts +114 -0
- package/carbon/src/client/hooks/useScrollProgress.ts +56 -0
- package/carbon/src/client/index.css +467 -0
- package/carbon/src/client/index.html +56 -0
- package/carbon/src/client/lib/admin-services.ts +151 -0
- package/carbon/src/client/lib/api.ts +32 -0
- package/carbon/src/client/lib/blog.ts +35 -0
- package/carbon/src/client/lib/changelog.ts +33 -0
- package/carbon/src/client/lib/docs-search.ts +101 -0
- package/carbon/src/client/lib/docs.ts +37 -0
- package/carbon/src/client/lib/i18n.ts +32 -0
- package/carbon/src/client/lib/supabase.ts +72 -0
- package/carbon/src/client/lib/tailwind-colors.ts +357 -0
- package/carbon/src/client/lib/theme.ts +117 -0
- package/carbon/src/client/lib/utils.ts +22 -0
- package/carbon/src/client/locales/de.json +529 -0
- package/carbon/src/client/locales/en.json +461 -0
- package/carbon/src/client/locales/es.json +529 -0
- package/carbon/src/client/locales/fr.json +529 -0
- package/carbon/src/client/locales/pt.json +529 -0
- package/carbon/src/client/main.tsx +56 -0
- package/carbon/src/client/mdx.d.ts +13 -0
- package/carbon/src/client/pages/ApiDocs.tsx +76 -0
- package/carbon/src/client/pages/AuthCallback.tsx +34 -0
- package/carbon/src/client/pages/Blog.tsx +167 -0
- package/carbon/src/client/pages/Changelog.tsx +171 -0
- package/carbon/src/client/pages/Charts.tsx +388 -0
- package/carbon/src/client/pages/Checkout.tsx +227 -0
- package/carbon/src/client/pages/Contact.tsx +174 -0
- package/carbon/src/client/pages/Dashboard.tsx +368 -0
- package/carbon/src/client/pages/Docs.tsx +372 -0
- package/carbon/src/client/pages/ForgotPassword.tsx +111 -0
- package/carbon/src/client/pages/Home.tsx +187 -0
- package/carbon/src/client/pages/Legal.tsx +100 -0
- package/carbon/src/client/pages/Login.tsx +408 -0
- package/carbon/src/client/pages/MFAVerify.tsx +156 -0
- package/carbon/src/client/pages/NotFound.tsx +21 -0
- package/carbon/src/client/pages/Onboarding.tsx +246 -0
- package/carbon/src/client/pages/ResetPassword.tsx +200 -0
- package/carbon/src/client/pages/UIComponents.tsx +390 -0
- package/carbon/src/client/pages/admin/ContactSubmissions.tsx +220 -0
- package/carbon/src/client/pages/admin/Dashboard.tsx +24 -0
- package/carbon/src/client/pages/admin/Infrastructure.tsx +257 -0
- package/carbon/src/client/pages/admin/Jobs.tsx +225 -0
- package/carbon/src/client/pages/admin/Logs.tsx +18 -0
- package/carbon/src/client/pages/admin/Newsletter.tsx +300 -0
- package/carbon/src/client/pages/admin/Notifications.tsx +603 -0
- package/carbon/src/client/pages/admin/Organizations.tsx +306 -0
- package/carbon/src/client/pages/admin/Settings.tsx +314 -0
- package/carbon/src/client/pages/admin/Theme.tsx +465 -0
- package/carbon/src/client/pages/admin/Users.tsx +365 -0
- package/carbon/src/client/pages/api-docs.css +156 -0
- package/carbon/src/client/pages/organizations/Details.tsx +200 -0
- package/carbon/src/client/pages/organizations/Members.tsx +402 -0
- package/carbon/src/client/pages/settings/Billing.tsx +473 -0
- package/carbon/src/client/pages/settings/Profile.tsx +160 -0
- package/carbon/src/client/pages/settings/Security.tsx +341 -0
- package/carbon/src/client/public/favicon.svg +19 -0
- package/carbon/src/client/public/robots.txt +8 -0
- package/carbon/src/server/billing/index.ts +104 -0
- package/carbon/src/server/billing/provider.ts +81 -0
- package/carbon/src/server/billing/providers/paddle.ts +314 -0
- package/carbon/src/server/billing/providers/polar.ts +325 -0
- package/carbon/src/server/billing/providers/stripe.ts +233 -0
- package/carbon/src/server/emails/templates.ts +116 -0
- package/carbon/src/server/index.ts +554 -0
- package/carbon/src/server/lib/auth.ts +6 -0
- package/carbon/src/server/lib/email.ts +64 -0
- package/carbon/src/server/lib/env.ts +112 -0
- package/carbon/src/server/lib/errors.ts +21 -0
- package/carbon/src/server/lib/logger.ts +17 -0
- package/carbon/src/server/lib/rate-limiter.ts +288 -0
- package/carbon/src/server/lib/request.ts +34 -0
- package/carbon/src/server/lib/stripe.ts +42 -0
- package/carbon/src/server/lib/supabase.ts +65 -0
- package/carbon/src/server/middleware/requirePlan.ts +80 -0
- package/carbon/src/server/routes/_internal/services-status.ts +958 -0
- package/carbon/src/server/routes/_internal/verify-role.ts +185 -0
- package/carbon/src/server/routes/health.ts +48 -0
- package/carbon/src/server/routes/v1/admin/contact.ts +128 -0
- package/carbon/src/server/routes/v1/admin/jobs.ts +171 -0
- package/carbon/src/server/routes/v1/admin/newsletter.ts +237 -0
- package/carbon/src/server/routes/v1/auth.ts +390 -0
- package/carbon/src/server/routes/v1/billing.ts +718 -0
- package/carbon/src/server/routes/v1/contact.ts +93 -0
- package/carbon/src/server/routes/v1/index.ts +1333 -0
- package/carbon/src/server/routes/v1/newsletter.ts +181 -0
- package/carbon/src/server/routes/v1/performance.ts +157 -0
- package/carbon/src/server/routes/v1/stats.ts +170 -0
- package/carbon/src/server/routes/v1/theme.ts +106 -0
- package/carbon/src/server/routes/webhooks/billing.ts +376 -0
- package/carbon/src/server/routes/webhooks/stripe.ts +276 -0
- package/carbon/src/server/types.ts +11 -0
- package/carbon/src/shared/pricing.ts +155 -0
- package/carbon/src/shared/types.ts +338 -0
- package/carbon/supabase/migrations/00001_init.sql +717 -0
- package/carbon/supabase/migrations/00002_theme_settings.sql +13 -0
- package/carbon/supabase/migrations/00003_pg_cron.sql +121 -0
- package/carbon/supabase/migrations/00004_contact_newsletter.sql +81 -0
- package/carbon/supabase/migrations/00005_localization_languages.sql +22 -0
- package/carbon/supabase/seed.sql +16 -0
- package/carbon/tests/_helpers/app.ts +45 -0
- package/carbon/tests/_helpers/env.ts +37 -0
- package/carbon/tests/_helpers/factories.ts +69 -0
- package/carbon/tests/_helpers/jwt.ts +23 -0
- package/carbon/tests/_helpers/setup-integration.ts +20 -0
- package/carbon/tests/_helpers/setup-rtl.ts +12 -0
- package/carbon/tests/component/ErrorBoundary.test.tsx +53 -0
- package/carbon/tests/component/use-auth-settings.test.tsx +119 -0
- package/carbon/tests/integration/server/routes/contact.test.ts +162 -0
- package/carbon/tests/integration/server/routes/health.test.ts +61 -0
- package/carbon/tests/structural/i18n-parity.test.ts +42 -0
- package/carbon/tests/unit/client/utils.test.ts +49 -0
- package/carbon/tests/unit/shared/pricing.test.ts +93 -0
- package/carbon/tsconfig.json +27 -0
- package/carbon/tsconfig.server.json +27 -0
- package/carbon/tsconfig.test.json +9 -0
- package/carbon/vite.config.ts +110 -0
- package/carbon/vitest.config.ts +74 -0
- package/carbon/volumes/db/jwt.sql +57 -0
- package/carbon/volumes/db/metabase-init.sh +29 -0
- package/carbon/volumes/db/n8n-init.sh +25 -0
- package/carbon/volumes/db/realtime.sql +33 -0
- package/carbon/volumes/db/roles.sql +93 -0
- package/carbon/volumes/db/set-passwords.sh +12 -0
- package/carbon/volumes/db/super-admin.dev.sql +113 -0
- package/carbon/volumes/db/super-admin.generated.sql +113 -0
- package/carbon/volumes/db/super-admin.sql +114 -0
- package/carbon/volumes/grafana/dashboards/logs.json +179 -0
- package/carbon/volumes/grafana/dashboards/overview.json +523 -0
- package/carbon/volumes/grafana/dashboards/postgresql.json +337 -0
- package/carbon/volumes/grafana/dashboards.dev/logs.json +179 -0
- package/carbon/volumes/grafana/dashboards.dev/overview.json +156 -0
- package/carbon/volumes/grafana/dashboards.dev/postgresql.json +337 -0
- package/carbon/volumes/grafana/provisioning/dashboards/dashboards.dev.yml +16 -0
- package/carbon/volumes/grafana/provisioning/dashboards/dashboards.yml +16 -0
- package/carbon/volumes/grafana/provisioning/datasources/datasources.yml +43 -0
- package/carbon/volumes/kong/docker-entrypoint.sh +9 -0
- package/carbon/volumes/kong/kong.yml +208 -0
- package/carbon/volumes/loki/loki-config.yml +58 -0
- package/carbon/volumes/n8n/hooks.js +131 -0
- package/carbon/volumes/n8n/scripts/setup.sh +66 -0
- package/carbon/volumes/prometheus/prometheus.yml +43 -0
- package/carbon/volumes/promtail/promtail-config.yml +64 -0
- package/carbon/volumes/traefik/middlewares.dev.yml +83 -0
- package/carbon/volumes/traefik/middlewares.yml +95 -0
- package/carbon/volumes/traefik/vite-dev.yml +20 -0
- package/package.json +95 -0
- package/src/access.js +354 -0
- package/src/activate.js +187 -0
- package/src/add.js +718 -0
- package/src/backup.js +786 -0
- package/src/cli.js +350 -0
- package/src/configure.js +967 -0
- package/src/console.js +155 -0
- package/src/create.js +1499 -0
- package/src/deploy.js +311 -0
- package/src/destroy.js +2033 -0
- package/src/diagnose.js +735 -0
- package/src/down.js +80 -0
- package/src/failover.js +1032 -0
- package/src/lib/backup-s3.js +179 -0
- package/src/lib/build.js +33 -0
- package/src/lib/checksum.js +28 -0
- package/src/lib/ci-setup.js +666 -0
- package/src/lib/cli/help.js +129 -0
- package/src/lib/cli/parse-flags.js +160 -0
- package/src/lib/cli/select-action.js +65 -0
- package/src/lib/cli/select-environment.js +108 -0
- package/src/lib/cli/tty-guard.js +75 -0
- package/src/lib/cloudflare.js +447 -0
- package/src/lib/colors.js +52 -0
- package/src/lib/command.js +361 -0
- package/src/lib/config.js +359 -0
- package/src/lib/cost.js +103 -0
- package/src/lib/deploy/bundle.js +231 -0
- package/src/lib/deploy/compose/acme-verify.js +121 -0
- package/src/lib/deploy/compose/build-args.js +78 -0
- package/src/lib/deploy/compose/ha.js +1874 -0
- package/src/lib/deploy/compose/index.js +1294 -0
- package/src/lib/deploy/compose/reconcile.js +74 -0
- package/src/lib/deploy/github.js +382 -0
- package/src/lib/deploy/image.js +191 -0
- package/src/lib/deploy/index.js +119 -0
- package/src/lib/deploy/k8s/LICENSE +99 -0
- package/src/lib/deploy/k8s/gitops-deploy.js +298 -0
- package/src/lib/deploy/k8s/ha/index.js +1000 -0
- package/src/lib/deploy/k8s/index.js +62 -0
- package/src/lib/deploy/k8s/k3s.js +2515 -0
- package/src/lib/deploy/orchestrator.js +1401 -0
- package/src/lib/deploy/prompts.js +545 -0
- package/src/lib/deploy/remote-build.js +130 -0
- package/src/lib/deploy/state.js +120 -0
- package/src/lib/deploy/utils.js +328 -0
- package/src/lib/deploy-logger.js +93 -0
- package/src/lib/dns-propagation.js +48 -0
- package/src/lib/environment.js +58 -0
- package/src/lib/fetch-retry.js +103 -0
- package/src/lib/github-environments.js +335 -0
- package/src/lib/hetzner-dns.js +377 -0
- package/src/lib/hetzner-guided-setup.js +275 -0
- package/src/lib/host-keys.js +37 -0
- package/src/lib/iac/cloud-init.js +52 -0
- package/src/lib/iac/index.js +325 -0
- package/src/lib/iac/programs/hetzner-compose.js +130 -0
- package/src/lib/iac/programs/hetzner-k8s.js +320 -0
- package/src/lib/kubectl.js +81 -0
- package/src/lib/licensing/index.js +259 -0
- package/src/lib/licensing/tiers.js +181 -0
- package/src/lib/licensing/validator.js +171 -0
- package/src/lib/merge-package-json.js +90 -0
- package/src/lib/operator-ip.js +381 -0
- package/src/lib/package-manager.js +111 -0
- package/src/lib/perf.js +71 -0
- package/src/lib/project-guard.js +39 -0
- package/src/lib/project.js +334 -0
- package/src/lib/providers/base.js +276 -0
- package/src/lib/providers/hetzner-s3.js +656 -0
- package/src/lib/providers/hetzner.js +755 -0
- package/src/lib/providers/index.js +164 -0
- package/src/lib/s3.js +510 -0
- package/src/lib/secret-scan.js +583 -0
- package/src/lib/secrets.js +63 -0
- package/src/lib/server-types.js +195 -0
- package/src/lib/shell.js +91 -0
- package/src/lib/ssh.js +241 -0
- package/src/lib/tracker.js +242 -0
- package/src/lib/upgrade-policy.js +170 -0
- package/src/lib/validators.js +105 -0
- package/src/lib/version.js +5 -0
- package/src/remove.js +292 -0
- package/src/reset.js +97 -0
- package/src/restore.js +871 -0
- package/src/scale.js +1734 -0
- package/src/shell.js +222 -0
- package/src/status.js +981 -0
- package/src/up.js +264 -0
- package/src/upgrade.js +721 -0
package/src/configure.js
ADDED
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vibecarbon Configure Command
|
|
3
|
+
* Interactive wizard for configuring external services (billing, OAuth, SMTP, etc.)
|
|
4
|
+
*
|
|
5
|
+
* Guides the user through selecting which features to enable and entering
|
|
6
|
+
* the required credentials. Writes all values to .env.local and .env.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* vibecarbon configure # Interactive configuration wizard
|
|
10
|
+
* vibecarbon configure cicd <env> # Layer GitOps (Flux + GHA) on a deployed k8s cluster
|
|
11
|
+
* vibecarbon configure -h # Show help
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import * as p from '@clack/prompts';
|
|
17
|
+
|
|
18
|
+
import { renderHelp } from './lib/cli/help.js';
|
|
19
|
+
import { parseFlags } from './lib/cli/parse-flags.js';
|
|
20
|
+
import { c, printBanner } from './lib/colors.js';
|
|
21
|
+
import { loadCredentials, loadProjectConfig } from './lib/config.js';
|
|
22
|
+
import { requireLicense } from './lib/licensing/index.js';
|
|
23
|
+
import { buildGitAddArgv, loadEnvVariables, setEnvVar } from './lib/project.js';
|
|
24
|
+
import { assertInProjectDir } from './lib/project-guard.js';
|
|
25
|
+
import { VERSION } from './lib/version.js';
|
|
26
|
+
|
|
27
|
+
/** @type {import('./lib/cli/parse-flags.js').CommandSpec & { summary?: string, description?: string, examples?: Array<{ command: string, description?: string }> }} */
|
|
28
|
+
const SPEC = {
|
|
29
|
+
name: 'configure',
|
|
30
|
+
summary: 'Configure external services (billing, OAuth, SMTP, CI/CD)',
|
|
31
|
+
description: [
|
|
32
|
+
'Interactive wizard for configuring external services in your Vibecarbon',
|
|
33
|
+
'project. Walks you through enabling features and entering API keys,',
|
|
34
|
+
'then writes everything to .env.local and .env.',
|
|
35
|
+
'',
|
|
36
|
+
'Configured credentials are deployed automatically when you run',
|
|
37
|
+
'`vibecarbon deploy`.',
|
|
38
|
+
'',
|
|
39
|
+
'SUBCOMMAND',
|
|
40
|
+
' cicd <env> Layer Flux + GitHub Actions wiring onto a deployed',
|
|
41
|
+
' k8s/k8s-ha cluster. Diamond/Fullerene only.',
|
|
42
|
+
'',
|
|
43
|
+
'CONFIGURABLE FEATURES',
|
|
44
|
+
' CI/CD GitHub Actions build + deploy workflows',
|
|
45
|
+
' Billing Stripe, Paddle, or Polar payment processing',
|
|
46
|
+
' Google OAuth Sign in with Google',
|
|
47
|
+
' Microsoft OAuth Sign in with Microsoft',
|
|
48
|
+
' SMTP / Email Transactional emails (Resend, Mailgun, SES, etc.)',
|
|
49
|
+
' Plausible Analytics Privacy-friendly web analytics',
|
|
50
|
+
].join('\n'),
|
|
51
|
+
flags: [
|
|
52
|
+
{ name: 'h', boolean: true, description: 'Show this help' },
|
|
53
|
+
{ name: 'v', boolean: true, description: 'Show version' },
|
|
54
|
+
],
|
|
55
|
+
examples: [
|
|
56
|
+
{
|
|
57
|
+
command: 'vibecarbon configure',
|
|
58
|
+
description: 'Configure a new project after creation',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
command: 'vibecarbon configure cicd prod',
|
|
62
|
+
description: 'Layer GitOps onto a deployed k8s cluster',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// FEATURE PROMPT FUNCTIONS
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Helper: prompt for a text value, showing current value as placeholder.
|
|
73
|
+
* Returns the entered value, or the current value if user pressed Enter on a non-empty placeholder.
|
|
74
|
+
*/
|
|
75
|
+
async function promptText(message, currentValue, options = {}) {
|
|
76
|
+
const placeholder = currentValue || options.placeholder || '';
|
|
77
|
+
const defaultValue = currentValue || options.defaultValue || undefined;
|
|
78
|
+
const result = await p.text({
|
|
79
|
+
message,
|
|
80
|
+
placeholder: placeholder ? `${placeholder}` : undefined,
|
|
81
|
+
defaultValue,
|
|
82
|
+
validate: options.validate,
|
|
83
|
+
});
|
|
84
|
+
if (p.isCancel(result)) return null;
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function requireNonEmpty(label) {
|
|
89
|
+
return (value) => {
|
|
90
|
+
if (!value?.trim()) return `${label} is required`;
|
|
91
|
+
return undefined;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Helper: prompt for a secret value (masked input).
|
|
97
|
+
*/
|
|
98
|
+
async function promptSecret(message, currentValue) {
|
|
99
|
+
const result = await p.password({
|
|
100
|
+
message: currentValue ? `${message} ${c.dim('(press Enter to keep current)')}` : message,
|
|
101
|
+
validate: (v) => {
|
|
102
|
+
if (!v && !currentValue) return 'This field is required';
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
if (p.isCancel(result)) return null;
|
|
106
|
+
// If user pressed Enter with no input and there's a current value, keep it
|
|
107
|
+
return result || currentValue || '';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---- Billing ----
|
|
111
|
+
|
|
112
|
+
// Webhook endpoint that all billing providers point at. The unified handler
|
|
113
|
+
// (carbon/src/server/routes/webhooks/billing.ts) dispatches based on the
|
|
114
|
+
// active provider; Stripe also keeps /api/webhooks/stripe for back-compat.
|
|
115
|
+
const BILLING_WEBHOOK_PATH = '/api/webhooks/billing';
|
|
116
|
+
|
|
117
|
+
async function promptBilling(env) {
|
|
118
|
+
p.log.info(c.bold('Billing Configuration'));
|
|
119
|
+
p.log.info(c.dim('Choose your payment provider and enter API credentials.'));
|
|
120
|
+
|
|
121
|
+
const currentProvider = env.BILLING_PROVIDER || 'stripe';
|
|
122
|
+
const provider = await p.select({
|
|
123
|
+
message: 'Billing provider',
|
|
124
|
+
options: [
|
|
125
|
+
{ value: 'stripe', label: 'Stripe', hint: 'most popular' },
|
|
126
|
+
{ value: 'paddle', label: 'Paddle', hint: 'merchant of record' },
|
|
127
|
+
{ value: 'polar', label: 'Polar', hint: 'open-source focused' },
|
|
128
|
+
],
|
|
129
|
+
initialValue: currentProvider,
|
|
130
|
+
});
|
|
131
|
+
if (p.isCancel(provider)) return null;
|
|
132
|
+
|
|
133
|
+
const vars = { BILLING_PROVIDER: provider };
|
|
134
|
+
|
|
135
|
+
if (provider === 'stripe') {
|
|
136
|
+
p.note(
|
|
137
|
+
[
|
|
138
|
+
`${c.bold('1. Secret key')}`,
|
|
139
|
+
` ${c.info('https://dashboard.stripe.com/apikeys')}`,
|
|
140
|
+
` Use ${c.dim('sk_test_...')} for testing or ${c.dim('sk_live_...')} in production.`,
|
|
141
|
+
'',
|
|
142
|
+
`${c.bold('2. Webhook')} (Workbench → Webhooks → Create an event destination)`,
|
|
143
|
+
` ${c.info('https://dashboard.stripe.com/webhooks')}`,
|
|
144
|
+
` • Endpoint URL: ${c.dim(`https://<your-domain>${BILLING_WEBHOOK_PATH}`)}`,
|
|
145
|
+
' • Events to send:',
|
|
146
|
+
' checkout.session.completed',
|
|
147
|
+
' customer.subscription.created',
|
|
148
|
+
' customer.subscription.updated',
|
|
149
|
+
' customer.subscription.deleted',
|
|
150
|
+
' invoice.payment_failed',
|
|
151
|
+
` • After creating, click ${c.dim('Reveal')} to copy the ${c.dim('whsec_...')} signing secret.`,
|
|
152
|
+
'',
|
|
153
|
+
`${c.bold('3. Price IDs')}`,
|
|
154
|
+
` ${c.info('https://dashboard.stripe.com/products')}`,
|
|
155
|
+
' Create a Startup and Pro product, then copy each recurring',
|
|
156
|
+
` price's ${c.dim('price_...')} ID.`,
|
|
157
|
+
].join('\n'),
|
|
158
|
+
'Stripe setup',
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const secretKey = await promptSecret('Stripe secret key (sk_...)', env.STRIPE_SECRET_KEY);
|
|
162
|
+
if (secretKey === null) return null;
|
|
163
|
+
vars.STRIPE_SECRET_KEY = secretKey;
|
|
164
|
+
|
|
165
|
+
const webhookSecret = await promptSecret(
|
|
166
|
+
'Stripe webhook secret (whsec_...)',
|
|
167
|
+
env.STRIPE_WEBHOOK_SECRET,
|
|
168
|
+
);
|
|
169
|
+
if (webhookSecret === null) return null;
|
|
170
|
+
vars.STRIPE_WEBHOOK_SECRET = webhookSecret;
|
|
171
|
+
|
|
172
|
+
const priceStarter = await promptText(
|
|
173
|
+
'Stripe price ID for Startup plan (price_...)',
|
|
174
|
+
env.STRIPE_PRICE_STARTER,
|
|
175
|
+
);
|
|
176
|
+
if (priceStarter === null) return null;
|
|
177
|
+
vars.STRIPE_PRICE_STARTER = priceStarter;
|
|
178
|
+
|
|
179
|
+
const pricePro = await promptText(
|
|
180
|
+
'Stripe price ID for Pro plan (price_...)',
|
|
181
|
+
env.STRIPE_PRICE_PRO,
|
|
182
|
+
);
|
|
183
|
+
if (pricePro === null) return null;
|
|
184
|
+
vars.STRIPE_PRICE_PRO = pricePro;
|
|
185
|
+
} else if (provider === 'paddle') {
|
|
186
|
+
p.note(
|
|
187
|
+
[
|
|
188
|
+
`${c.bold('1. API key')} (Developer Tools → Authentication → API keys → New API key)`,
|
|
189
|
+
` ${c.info('https://vendors.paddle.com')}`,
|
|
190
|
+
' Generate a new key with read + write scope.',
|
|
191
|
+
'',
|
|
192
|
+
`${c.bold('2. Webhook')} (Developer Tools → Notifications → New destination)`,
|
|
193
|
+
` ${c.info('https://vendors.paddle.com')}`,
|
|
194
|
+
` • Endpoint URL: ${c.dim(`https://<your-domain>${BILLING_WEBHOOK_PATH}`)}`,
|
|
195
|
+
' • Events to send:',
|
|
196
|
+
' transaction.completed',
|
|
197
|
+
' transaction.payment_failed',
|
|
198
|
+
' subscription.created',
|
|
199
|
+
' subscription.updated',
|
|
200
|
+
' subscription.canceled',
|
|
201
|
+
' subscription.past_due',
|
|
202
|
+
' • Copy the secret key shown after creating the destination.',
|
|
203
|
+
'',
|
|
204
|
+
`${c.bold('3. Price IDs')} (Catalog → Products → pick product → Prices)`,
|
|
205
|
+
` ${c.info('https://vendors.paddle.com')}`,
|
|
206
|
+
' Create a Startup and Pro product, then copy each recurring',
|
|
207
|
+
` price's ${c.dim('pri_...')} ID.`,
|
|
208
|
+
].join('\n'),
|
|
209
|
+
'Paddle setup',
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const apiKey = await promptSecret('Paddle API key', env.PADDLE_API_KEY);
|
|
213
|
+
if (apiKey === null) return null;
|
|
214
|
+
vars.PADDLE_API_KEY = apiKey;
|
|
215
|
+
|
|
216
|
+
const webhookSecret = await promptSecret('Paddle webhook secret', env.PADDLE_WEBHOOK_SECRET);
|
|
217
|
+
if (webhookSecret === null) return null;
|
|
218
|
+
vars.PADDLE_WEBHOOK_SECRET = webhookSecret;
|
|
219
|
+
|
|
220
|
+
const environment = await p.select({
|
|
221
|
+
message: 'Paddle environment',
|
|
222
|
+
options: [
|
|
223
|
+
{ value: 'sandbox', label: 'Sandbox', hint: 'for testing' },
|
|
224
|
+
{ value: 'production', label: 'Production', hint: 'live payments' },
|
|
225
|
+
],
|
|
226
|
+
initialValue: env.PADDLE_ENVIRONMENT || 'sandbox',
|
|
227
|
+
});
|
|
228
|
+
if (p.isCancel(environment)) return null;
|
|
229
|
+
vars.PADDLE_ENVIRONMENT = environment;
|
|
230
|
+
|
|
231
|
+
const priceStarter = await promptText(
|
|
232
|
+
'Paddle price ID for Startup plan',
|
|
233
|
+
env.PADDLE_PRICE_STARTER,
|
|
234
|
+
);
|
|
235
|
+
if (priceStarter === null) return null;
|
|
236
|
+
vars.PADDLE_PRICE_STARTER = priceStarter;
|
|
237
|
+
|
|
238
|
+
const pricePro = await promptText('Paddle price ID for Pro plan', env.PADDLE_PRICE_PRO);
|
|
239
|
+
if (pricePro === null) return null;
|
|
240
|
+
vars.PADDLE_PRICE_PRO = pricePro;
|
|
241
|
+
} else if (provider === 'polar') {
|
|
242
|
+
p.note(
|
|
243
|
+
[
|
|
244
|
+
`${c.bold('1. Access token')} (Settings → Developers → New access token)`,
|
|
245
|
+
` ${c.info('https://polar.sh/dashboard/<org>/settings')}`,
|
|
246
|
+
'',
|
|
247
|
+
`${c.bold('2. Webhook')} (Settings → Webhooks → Add Endpoint)`,
|
|
248
|
+
` ${c.info('https://polar.sh/dashboard/<org>/settings/webhooks')}`,
|
|
249
|
+
` • Endpoint URL: ${c.dim(`https://<your-domain>${BILLING_WEBHOOK_PATH}`)}`,
|
|
250
|
+
' • Events to send:',
|
|
251
|
+
' checkout.created',
|
|
252
|
+
' subscription.created',
|
|
253
|
+
' subscription.updated',
|
|
254
|
+
' subscription.canceled',
|
|
255
|
+
' subscription.revoked',
|
|
256
|
+
' • Copy the secret shown after creating the endpoint.',
|
|
257
|
+
'',
|
|
258
|
+
`${c.bold('3. Organization ID')}`,
|
|
259
|
+
` Shown in the dashboard URL after ${c.dim('/dashboard/')}.`,
|
|
260
|
+
'',
|
|
261
|
+
`${c.bold('4. Price IDs')}`,
|
|
262
|
+
` ${c.info('https://polar.sh/dashboard/<org>/products')}`,
|
|
263
|
+
].join('\n'),
|
|
264
|
+
'Polar setup',
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const accessToken = await promptSecret('Polar access token', env.POLAR_ACCESS_TOKEN);
|
|
268
|
+
if (accessToken === null) return null;
|
|
269
|
+
vars.POLAR_ACCESS_TOKEN = accessToken;
|
|
270
|
+
|
|
271
|
+
const webhookSecret = await promptSecret('Polar webhook secret', env.POLAR_WEBHOOK_SECRET);
|
|
272
|
+
if (webhookSecret === null) return null;
|
|
273
|
+
vars.POLAR_WEBHOOK_SECRET = webhookSecret;
|
|
274
|
+
|
|
275
|
+
const orgId = await promptText('Polar organization ID', env.POLAR_ORGANIZATION_ID);
|
|
276
|
+
if (orgId === null) return null;
|
|
277
|
+
vars.POLAR_ORGANIZATION_ID = orgId;
|
|
278
|
+
|
|
279
|
+
const priceStarter = await promptText(
|
|
280
|
+
'Polar price ID for Startup plan',
|
|
281
|
+
env.POLAR_PRICE_STARTER,
|
|
282
|
+
);
|
|
283
|
+
if (priceStarter === null) return null;
|
|
284
|
+
vars.POLAR_PRICE_STARTER = priceStarter;
|
|
285
|
+
|
|
286
|
+
const pricePro = await promptText('Polar price ID for Pro plan', env.POLAR_PRICE_PRO);
|
|
287
|
+
if (pricePro === null) return null;
|
|
288
|
+
vars.POLAR_PRICE_PRO = pricePro;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return vars;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---- Google OAuth ----
|
|
295
|
+
|
|
296
|
+
// Supabase Auth handles the OAuth dance; providers redirect to its callback,
|
|
297
|
+
// not to your app. Show the user the exact URI to paste so they don't guess.
|
|
298
|
+
function supabaseCallbackUrl(env) {
|
|
299
|
+
const base = (env.SUPABASE_URL || '').replace(/\/$/, '');
|
|
300
|
+
if (!base) return c.dim('<SUPABASE_URL>/auth/v1/callback');
|
|
301
|
+
return c.dim(`${base}/auth/v1/callback`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function promptGoogleOAuth(env) {
|
|
305
|
+
p.log.info(c.bold('Google OAuth'));
|
|
306
|
+
|
|
307
|
+
p.note(
|
|
308
|
+
[
|
|
309
|
+
`${c.bold('1. Configure consent screen')} (one-time prerequisite)`,
|
|
310
|
+
` ${c.info('https://console.cloud.google.com/auth/branding')}`,
|
|
311
|
+
' Google Auth Platform → Branding. Add app name + support email.',
|
|
312
|
+
'',
|
|
313
|
+
`${c.bold('2. Create OAuth client')} (Clients → Create client)`,
|
|
314
|
+
` ${c.info('https://console.cloud.google.com/auth/clients')}`,
|
|
315
|
+
` Application type: ${c.dim('Web application')}.`,
|
|
316
|
+
'',
|
|
317
|
+
`${c.bold('3. Authorized redirect URI')}`,
|
|
318
|
+
` ${supabaseCallbackUrl(env)}`,
|
|
319
|
+
' (paste this exact URL — Google must match it character-for-character)',
|
|
320
|
+
'',
|
|
321
|
+
`${c.bold('4. Copy the Client ID and Client secret')}`,
|
|
322
|
+
` ${c.dim('The secret is only visible at creation — copy it immediately.')}`,
|
|
323
|
+
].join('\n'),
|
|
324
|
+
'Google OAuth setup',
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const clientId = await promptText('Google Client ID', env.GOOGLE_CLIENT_ID);
|
|
328
|
+
if (clientId === null) return null;
|
|
329
|
+
|
|
330
|
+
const clientSecret = await promptSecret('Google Client Secret', env.GOOGLE_CLIENT_SECRET);
|
|
331
|
+
if (clientSecret === null) return null;
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
GOOGLE_ENABLED: 'true',
|
|
335
|
+
GOOGLE_CLIENT_ID: clientId,
|
|
336
|
+
GOOGLE_CLIENT_SECRET: clientSecret,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ---- Microsoft OAuth ----
|
|
341
|
+
|
|
342
|
+
async function promptMicrosoftOAuth(env) {
|
|
343
|
+
p.log.info(c.bold('Microsoft OAuth'));
|
|
344
|
+
|
|
345
|
+
p.note(
|
|
346
|
+
[
|
|
347
|
+
`${c.bold('1. Register an app')} (App registrations → New registration)`,
|
|
348
|
+
` ${c.info('https://entra.microsoft.com')}`,
|
|
349
|
+
' Microsoft Entra ID → App registrations → New registration.',
|
|
350
|
+
'',
|
|
351
|
+
`${c.bold('2. Redirect URI')} (platform: Web)`,
|
|
352
|
+
` ${supabaseCallbackUrl(env)}`,
|
|
353
|
+
'',
|
|
354
|
+
`${c.bold('3. Client secret')} (Certificates & secrets → New client secret)`,
|
|
355
|
+
` ${c.dim('Copy the Value (not the Secret ID) — Entra only shows it once.')}`,
|
|
356
|
+
'',
|
|
357
|
+
`${c.bold('4. Tenant ID')}`,
|
|
358
|
+
` Use ${c.dim('common')} to allow any Microsoft account, or paste your`,
|
|
359
|
+
" directory's tenant UUID for single-tenant apps.",
|
|
360
|
+
].join('\n'),
|
|
361
|
+
'Microsoft OAuth setup',
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const clientId = await promptText('Microsoft Client ID', env.MICROSOFT_CLIENT_ID);
|
|
365
|
+
if (clientId === null) return null;
|
|
366
|
+
|
|
367
|
+
const clientSecret = await promptSecret('Microsoft Client Secret', env.MICROSOFT_CLIENT_SECRET);
|
|
368
|
+
if (clientSecret === null) return null;
|
|
369
|
+
|
|
370
|
+
const tenantId = await promptText('Microsoft Tenant ID', env.MICROSOFT_TENANT_ID, {
|
|
371
|
+
placeholder: 'common',
|
|
372
|
+
defaultValue: 'common',
|
|
373
|
+
});
|
|
374
|
+
if (tenantId === null) return null;
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
MICROSOFT_ENABLED: 'true',
|
|
378
|
+
MICROSOFT_CLIENT_ID: clientId,
|
|
379
|
+
MICROSOFT_CLIENT_SECRET: clientSecret,
|
|
380
|
+
MICROSOFT_TENANT_ID: tenantId,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ---- SMTP / Email ----
|
|
385
|
+
|
|
386
|
+
async function promptSmtp(env) {
|
|
387
|
+
p.log.info(c.bold('SMTP / Email'));
|
|
388
|
+
p.log.info(c.dim('Used for password resets, verification, and transactional emails.'));
|
|
389
|
+
|
|
390
|
+
p.note(
|
|
391
|
+
[
|
|
392
|
+
'Pick a provider, create an SMTP credential, then paste the values below.',
|
|
393
|
+
'',
|
|
394
|
+
`${c.bold('Resend')}`,
|
|
395
|
+
` ${c.info('https://resend.com/api-keys')}`,
|
|
396
|
+
` host ${c.dim('smtp.resend.com')} • port ${c.dim('587')} • user ${c.dim('resend')} • pass = API key`,
|
|
397
|
+
'',
|
|
398
|
+
`${c.bold('Mailgun')}`,
|
|
399
|
+
` ${c.info('https://app.mailgun.com/app/sending/domains')}`,
|
|
400
|
+
` host ${c.dim('smtp.mailgun.org')} • port ${c.dim('587')}`,
|
|
401
|
+
'',
|
|
402
|
+
`${c.bold('AWS SES')}`,
|
|
403
|
+
` ${c.info('https://console.aws.amazon.com/ses/home#/smtp-settings')}`,
|
|
404
|
+
` region-specific host • port ${c.dim('587')}`,
|
|
405
|
+
'',
|
|
406
|
+
`${c.bold('Postmark')}`,
|
|
407
|
+
` ${c.info('https://account.postmarkapp.com/servers')}`,
|
|
408
|
+
` host ${c.dim('smtp.postmarkapp.com')} • port ${c.dim('587')}`,
|
|
409
|
+
'',
|
|
410
|
+
`${c.bold('Verify your sender domain')} in the provider before going live,`,
|
|
411
|
+
'or production emails will land in spam (or get rejected outright).',
|
|
412
|
+
].join('\n'),
|
|
413
|
+
'SMTP setup',
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
const host = await promptText('SMTP host', env.SMTP_HOST, {
|
|
417
|
+
placeholder: 'smtp.resend.com',
|
|
418
|
+
validate: requireNonEmpty('SMTP host'),
|
|
419
|
+
});
|
|
420
|
+
if (host === null) return null;
|
|
421
|
+
|
|
422
|
+
const port = await promptText('SMTP port', env.SMTP_PORT || '587', {
|
|
423
|
+
placeholder: '587',
|
|
424
|
+
});
|
|
425
|
+
if (port === null) return null;
|
|
426
|
+
|
|
427
|
+
const user = await promptText('SMTP username', env.SMTP_USER, {
|
|
428
|
+
validate: requireNonEmpty('SMTP username'),
|
|
429
|
+
});
|
|
430
|
+
if (user === null) return null;
|
|
431
|
+
|
|
432
|
+
const pass = await promptSecret('SMTP password', env.SMTP_PASS);
|
|
433
|
+
if (pass === null) return null;
|
|
434
|
+
|
|
435
|
+
const adminEmail = await promptText('Sender email address (from)', env.SMTP_ADMIN_EMAIL, {
|
|
436
|
+
placeholder: 'noreply@yourdomain.com',
|
|
437
|
+
validate: requireNonEmpty('Sender email'),
|
|
438
|
+
});
|
|
439
|
+
if (adminEmail === null) return null;
|
|
440
|
+
|
|
441
|
+
const senderDefault = env.PROJECT_NAME || 'My App';
|
|
442
|
+
const senderName = await promptText('Sender display name', env.SMTP_SENDER_NAME, {
|
|
443
|
+
placeholder: senderDefault,
|
|
444
|
+
defaultValue: senderDefault,
|
|
445
|
+
});
|
|
446
|
+
if (senderName === null) return null;
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
SMTP_HOST: host,
|
|
450
|
+
SMTP_PORT: port,
|
|
451
|
+
SMTP_USER: user,
|
|
452
|
+
SMTP_PASS: pass,
|
|
453
|
+
SMTP_ADMIN_EMAIL: adminEmail,
|
|
454
|
+
SMTP_SENDER_NAME: senderName,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ---- Plausible Analytics ----
|
|
459
|
+
|
|
460
|
+
async function promptPlausible(env) {
|
|
461
|
+
p.log.info(c.bold('Plausible Analytics'));
|
|
462
|
+
|
|
463
|
+
p.note(
|
|
464
|
+
[
|
|
465
|
+
`${c.bold('1. Add your site')}`,
|
|
466
|
+
` ${c.info('https://plausible.io/sites/new')}`,
|
|
467
|
+
` Enter your domain ${c.dim('(without https://)')} — this becomes the value below.`,
|
|
468
|
+
'',
|
|
469
|
+
`${c.bold('2. Script URL')}`,
|
|
470
|
+
` Defaults to ${c.dim('https://plausible.io/js/script.js')}.`,
|
|
471
|
+
' Self-hosting Plausible? Point it at your own instance, e.g.',
|
|
472
|
+
` ${c.dim('https://analytics.yourdomain.com/js/script.js')}.`,
|
|
473
|
+
].join('\n'),
|
|
474
|
+
'Plausible setup',
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
const domain = await promptText('Your site domain (e.g., myapp.com)', env.VITE_PLAUSIBLE_DOMAIN);
|
|
478
|
+
if (domain === null) return null;
|
|
479
|
+
|
|
480
|
+
const scriptUrl = await promptText(
|
|
481
|
+
'Plausible script URL',
|
|
482
|
+
env.VITE_PLAUSIBLE_SCRIPT_URL || 'https://plausible.io/js/script.js',
|
|
483
|
+
);
|
|
484
|
+
if (scriptUrl === null) return null;
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
VITE_PLAUSIBLE_DOMAIN: domain,
|
|
488
|
+
VITE_PLAUSIBLE_SCRIPT_URL: scriptUrl,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// (i18n moved to admin Settings → Localization card — language list lives in
|
|
493
|
+
// app_settings.enabled_languages, not .env.local. See migration
|
|
494
|
+
// 00005_localization_languages.sql.)
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* CI/CD setup — bootstraps the project's GitHub repo (if missing), installs
|
|
498
|
+
* the canonical GHA workflows, rewrites image references with the real owner,
|
|
499
|
+
* and records the opt-in flag. Absorbs everything the legacy `vibecarbon add
|
|
500
|
+
* cicd` did; that command is gone (CI/CD is an external service, so it lives
|
|
501
|
+
* here in `configure`, not in `add` which is for local containers).
|
|
502
|
+
*
|
|
503
|
+
* Flow:
|
|
504
|
+
* 1. Verify `gh` CLI is installed + authenticated
|
|
505
|
+
* 2. Resolve owner: existing remote → use it; otherwise prompt + create repo
|
|
506
|
+
* via `gh repo create` (with `git init` + initial commit if needed)
|
|
507
|
+
* 3. Rewrite ghcr.io / github.com image refs in compose + k8s manifests
|
|
508
|
+
* 4. Install `.github/workflows/vibecarbon-build.yml` + `deploy.yml`
|
|
509
|
+
* 5. Set `cicdEnabled: true` in `vibecarbon.json`
|
|
510
|
+
*
|
|
511
|
+
* After this, `vibecarbon deploy` for compose prompts Direct/Push per-deploy
|
|
512
|
+
* (push becomes the default once CI/CD is configured). K8s and K8s-HA deploys
|
|
513
|
+
* are local-first via deployK3s and ignore CI/CD entirely — this feature is
|
|
514
|
+
* purely additive: it adds the git-push deploy path for compose, and lays
|
|
515
|
+
* down GHA workflow files in the repo for teams that want them.
|
|
516
|
+
*
|
|
517
|
+
* Org-level secret seeding (Hetzner/Cloudflare/S3 tokens) is NOT done here —
|
|
518
|
+
* the first push-mode compose deploy (or first `-mode k8s` deploy) still runs
|
|
519
|
+
* `seedOrgSecrets` via `deployK8sGitOps`. Kept that way so secrets only land
|
|
520
|
+
* in GitHub when the user actually deploys, not when they merely configure.
|
|
521
|
+
*/
|
|
522
|
+
async function promptCicd() {
|
|
523
|
+
p.log.info(c.bold('CI/CD (GitHub Actions)'));
|
|
524
|
+
p.log.info(
|
|
525
|
+
c.dim(
|
|
526
|
+
'Bootstraps the GitHub repo (if needed) and installs build + deploy workflows so this project can ship via git push. Optional in all modes — k8s/k8s-ha deploys are local-first and never need CI/CD; compose deploys use git-push when configured, otherwise direct.',
|
|
527
|
+
),
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
const {
|
|
531
|
+
detectGitHubUsername,
|
|
532
|
+
getGitHubRepo,
|
|
533
|
+
installDeployWorkflowFile,
|
|
534
|
+
installWorkflowFile,
|
|
535
|
+
updateImageReferences,
|
|
536
|
+
} = await import('./lib/ci-setup.js');
|
|
537
|
+
const { checkDependency, runCommand } = await import('./lib/command.js');
|
|
538
|
+
|
|
539
|
+
if (!checkDependency('gh', 'GitHub CLI')) {
|
|
540
|
+
p.log.error(
|
|
541
|
+
'`gh` CLI is required. Install from https://cli.github.com/, then run `gh auth login`.',
|
|
542
|
+
);
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
runCommand(['gh', 'auth', 'status'], { silent: true });
|
|
547
|
+
} catch {
|
|
548
|
+
p.log.error(
|
|
549
|
+
'`gh` is not authenticated. Run `gh auth login` and re-run `vibecarbon configure`.',
|
|
550
|
+
);
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const cwd = process.cwd();
|
|
555
|
+
const projectConfig = loadProjectConfig(cwd);
|
|
556
|
+
const projectName = projectConfig?.projectName;
|
|
557
|
+
if (!projectName) {
|
|
558
|
+
p.log.error('vibecarbon.json missing or has no projectName. Run `vibecarbon create` first.');
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Resolve owner: prefer an existing origin remote; otherwise bootstrap one.
|
|
563
|
+
let { owner: githubOwner, name: repoName } = getGitHubRepo(cwd);
|
|
564
|
+
const s = p.spinner();
|
|
565
|
+
|
|
566
|
+
if (githubOwner && repoName) {
|
|
567
|
+
p.log.info(`GitHub repo detected: ${c.bold(`${githubOwner}/${repoName}`)}`);
|
|
568
|
+
} else {
|
|
569
|
+
p.log.info('No GitHub origin remote — creating one now.');
|
|
570
|
+
const detected = detectGitHubUsername();
|
|
571
|
+
const ownerInput = await p.text({
|
|
572
|
+
message: 'GitHub username or organization:',
|
|
573
|
+
placeholder: detected || 'your-username',
|
|
574
|
+
initialValue: detected || '',
|
|
575
|
+
validate: (value) => {
|
|
576
|
+
if (!value) return 'GitHub username is required';
|
|
577
|
+
if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(value))
|
|
578
|
+
return 'Invalid GitHub username format';
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
if (p.isCancel(ownerInput)) return null;
|
|
582
|
+
githubOwner = ownerInput;
|
|
583
|
+
|
|
584
|
+
const visibility = await p.select({
|
|
585
|
+
message: 'Repository visibility',
|
|
586
|
+
options: [
|
|
587
|
+
{ value: 'private', label: 'Private', hint: 'recommended' },
|
|
588
|
+
{ value: 'public', label: 'Public' },
|
|
589
|
+
],
|
|
590
|
+
initialValue: 'private',
|
|
591
|
+
});
|
|
592
|
+
if (p.isCancel(visibility)) return null;
|
|
593
|
+
|
|
594
|
+
if (!existsSync(join(cwd, '.git'))) {
|
|
595
|
+
s.start('Initializing git repository');
|
|
596
|
+
runCommand(['git', 'init'], { silent: true, cwd });
|
|
597
|
+
s.stop('Git repository initialized');
|
|
598
|
+
}
|
|
599
|
+
const hasCommits = runCommand(['git', 'rev-parse', 'HEAD'], {
|
|
600
|
+
silent: true,
|
|
601
|
+
ignoreError: true,
|
|
602
|
+
cwd,
|
|
603
|
+
});
|
|
604
|
+
if (!hasCommits) {
|
|
605
|
+
s.start('Creating initial commit');
|
|
606
|
+
runCommand(buildGitAddArgv(), { silent: true, ignoreError: true, cwd });
|
|
607
|
+
runCommand(['git', 'commit', '-m', 'Initial commit - Vibecarbon project'], {
|
|
608
|
+
silent: true,
|
|
609
|
+
cwd,
|
|
610
|
+
});
|
|
611
|
+
s.stop('Initial commit created');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
s.start(`Creating GitHub repository: ${githubOwner}/${projectName}`);
|
|
615
|
+
try {
|
|
616
|
+
// projectName is basename-validated at create time; visibility is a literal.
|
|
617
|
+
runCommand(
|
|
618
|
+
['gh', 'repo', 'create', projectName, `--${visibility}`, '--source=.', '--remote=origin'],
|
|
619
|
+
{ silent: true, timeout: 60000, cwd },
|
|
620
|
+
);
|
|
621
|
+
s.stop('GitHub repository created');
|
|
622
|
+
} catch (err) {
|
|
623
|
+
s.stop(`Failed to create GitHub repository: ${err.message}`);
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
repoName = projectName;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Rewrite ghcr.io / github.com refs in compose + k8s manifests so they
|
|
630
|
+
// point at the real owner. Idempotent — re-running with the same owner
|
|
631
|
+
// is a no-op.
|
|
632
|
+
s.start('Updating Docker / Flux image references');
|
|
633
|
+
updateImageReferences(projectName, githubOwner, cwd);
|
|
634
|
+
s.stop('Image references updated');
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
s.start('Installing .github/workflows/vibecarbon-build.yml');
|
|
638
|
+
const installedBuild = installWorkflowFile(cwd);
|
|
639
|
+
s.stop(installedBuild ? 'Build workflow installed' : 'Build workflow already present');
|
|
640
|
+
|
|
641
|
+
s.start('Installing .github/workflows/deploy.yml');
|
|
642
|
+
const installedDeploy = installDeployWorkflowFile(cwd);
|
|
643
|
+
s.stop(installedDeploy ? 'Deploy workflow installed' : 'Deploy workflow already present');
|
|
644
|
+
} catch (err) {
|
|
645
|
+
s.stop(`Failed to install workflow files: ${err.message}`);
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Record opt-in on the project so `ciAvailable()` detects CI/CD without
|
|
650
|
+
// shelling out to `gh` on every deploy.
|
|
651
|
+
const { saveProjectConfig: savePc } = await import('./lib/config.js');
|
|
652
|
+
if (projectConfig) {
|
|
653
|
+
savePc({ ...projectConfig, cicdEnabled: true }, cwd);
|
|
654
|
+
p.log.success('Marked CI/CD as enabled in vibecarbon.json');
|
|
655
|
+
} else {
|
|
656
|
+
p.log.warn(
|
|
657
|
+
'vibecarbon.json not found — workflows installed but the CI/CD flag could not be persisted. Subsequent deploys may require `--push` explicitly.',
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
p.log.info(
|
|
662
|
+
c.dim(
|
|
663
|
+
'Next: `git add .github/workflows && git commit && git push` to register the workflows with GitHub.',
|
|
664
|
+
),
|
|
665
|
+
);
|
|
666
|
+
|
|
667
|
+
// No env vars to merge — side-effects only.
|
|
668
|
+
return {};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ============================================================================
|
|
672
|
+
// FEATURE DEFINITIONS
|
|
673
|
+
// ============================================================================
|
|
674
|
+
|
|
675
|
+
const FEATURES = [
|
|
676
|
+
{
|
|
677
|
+
value: 'cicd',
|
|
678
|
+
label: 'CI/CD',
|
|
679
|
+
hint: 'GitHub Actions build + deploy workflows (optional; enables git-push deploys)',
|
|
680
|
+
promptFn: promptCicd,
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
value: 'billing',
|
|
684
|
+
label: 'Billing',
|
|
685
|
+
hint: 'Stripe, Paddle, or Polar',
|
|
686
|
+
promptFn: promptBilling,
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
value: 'google-oauth',
|
|
690
|
+
label: 'Google OAuth',
|
|
691
|
+
hint: 'Sign in with Google',
|
|
692
|
+
promptFn: promptGoogleOAuth,
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
value: 'microsoft-oauth',
|
|
696
|
+
label: 'Microsoft OAuth',
|
|
697
|
+
hint: 'Sign in with Microsoft',
|
|
698
|
+
promptFn: promptMicrosoftOAuth,
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
value: 'smtp',
|
|
702
|
+
label: 'SMTP / Email',
|
|
703
|
+
hint: 'password resets, verification, transactional emails',
|
|
704
|
+
promptFn: promptSmtp,
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
value: 'plausible',
|
|
708
|
+
label: 'Plausible Analytics',
|
|
709
|
+
hint: 'privacy-friendly web analytics',
|
|
710
|
+
promptFn: promptPlausible,
|
|
711
|
+
},
|
|
712
|
+
];
|
|
713
|
+
|
|
714
|
+
// ============================================================================
|
|
715
|
+
// MAIN
|
|
716
|
+
// ============================================================================
|
|
717
|
+
|
|
718
|
+
async function main(cliArgs) {
|
|
719
|
+
const { values, errors } = parseFlags(cliArgs, SPEC);
|
|
720
|
+
|
|
721
|
+
if (errors.length > 0) {
|
|
722
|
+
for (const e of errors) {
|
|
723
|
+
process.stderr.write(`${c.error('✗')} ${e}\n`);
|
|
724
|
+
}
|
|
725
|
+
process.stderr.write(`Run ${c.info('vibecarbon configure -h')} for usage.\n`);
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (values.h) {
|
|
730
|
+
process.stdout.write(renderHelp(SPEC));
|
|
731
|
+
process.exit(0);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (values.v) {
|
|
735
|
+
console.log(`vibecarbon v${VERSION}`);
|
|
736
|
+
process.exit(0);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Detect vibecarbon project
|
|
740
|
+
const cwd = process.cwd();
|
|
741
|
+
assertInProjectDir(cwd);
|
|
742
|
+
|
|
743
|
+
printBanner();
|
|
744
|
+
p.intro(`${c.bold('vibecarbon configure')} ${c.dim(`v${VERSION}`)}`);
|
|
745
|
+
|
|
746
|
+
// Load current env values
|
|
747
|
+
const env = loadEnvVariables(cwd);
|
|
748
|
+
|
|
749
|
+
// Feature selection — opt-in: nothing selected by default. Most users only
|
|
750
|
+
// want to configure one or two features per run; default-on forced them to
|
|
751
|
+
// uncheck the rest, which was tedious and hid the intent of the prompt.
|
|
752
|
+
const selected = await p.multiselect({
|
|
753
|
+
message: 'Which features would you like to configure?',
|
|
754
|
+
options: FEATURES.map((f) => ({
|
|
755
|
+
value: f.value,
|
|
756
|
+
label: f.label,
|
|
757
|
+
hint: f.hint,
|
|
758
|
+
})),
|
|
759
|
+
required: false,
|
|
760
|
+
initialValues: [],
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
if (p.isCancel(selected)) {
|
|
764
|
+
p.cancel('Operation cancelled.');
|
|
765
|
+
process.exit(0);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (selected.length === 0) {
|
|
769
|
+
p.log.warn('No features selected.');
|
|
770
|
+
p.outro('Nothing to configure.');
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// Collect credentials for each selected feature
|
|
775
|
+
const allVars = {};
|
|
776
|
+
const configuredFeatures = [];
|
|
777
|
+
|
|
778
|
+
for (const featureValue of selected) {
|
|
779
|
+
const feature = FEATURES.find((f) => f.value === featureValue);
|
|
780
|
+
const result = await feature.promptFn(env);
|
|
781
|
+
|
|
782
|
+
if (result === null) {
|
|
783
|
+
p.log.warn(`Skipped ${feature.label} (cancelled)`);
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
Object.assign(allVars, result);
|
|
788
|
+
configuredFeatures.push(feature.label);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (configuredFeatures.length === 0) {
|
|
792
|
+
p.outro('No features configured.');
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Show summary
|
|
797
|
+
const summaryLines = configuredFeatures.map((f) => ` ${c.success('+')} ${f}`).join('\n');
|
|
798
|
+
p.note(summaryLines, 'Features to configure');
|
|
799
|
+
|
|
800
|
+
// Write all values
|
|
801
|
+
const s = p.spinner();
|
|
802
|
+
s.start('Writing configuration');
|
|
803
|
+
|
|
804
|
+
for (const [key, value] of Object.entries(allVars)) {
|
|
805
|
+
setEnvVar(key, value, cwd);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
s.stop('Configuration saved to .env.local and .env');
|
|
809
|
+
|
|
810
|
+
p.outro(
|
|
811
|
+
`Configured ${configuredFeatures.length} feature${configuredFeatures.length === 1 ? '' : 's'}. Run ${c.info('vibecarbon deploy')} to push to production.`,
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ============================================================================
|
|
816
|
+
// `vibecarbon configure cicd <env>` subcommand
|
|
817
|
+
// ============================================================================
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Layer GitOps (Flux + GitHub Actions wiring) onto a deployed k8s cluster.
|
|
821
|
+
*
|
|
822
|
+
* Pre-PR-5 this was the body of the `--gitops` deploy flag (see
|
|
823
|
+
* orchestrator.js's now-deleted handover block). PR 7 lifted it into a
|
|
824
|
+
* standalone subcommand so:
|
|
825
|
+
* - The deploy path stays local-first by default (no Flux unless the
|
|
826
|
+
* operator explicitly opts in).
|
|
827
|
+
* - Operators can layer GitOps onto an existing cluster without
|
|
828
|
+
* re-deploying.
|
|
829
|
+
* - Tier gating happens at the entry point (Diamond/Fullerene only)
|
|
830
|
+
* instead of being a flag-on-deploy concern.
|
|
831
|
+
*
|
|
832
|
+
* Pre-conditions:
|
|
833
|
+
* - Project is initialized (`.vibecarbon.json` exists).
|
|
834
|
+
* - The named environment was deployed via `vibecarbon deploy -mode k8s`
|
|
835
|
+
* (the kubeconfig at `.vibecarbon/kubeconfig-<env>` must exist).
|
|
836
|
+
* - `gh` CLI is authenticated and the project has a GitHub origin.
|
|
837
|
+
* - License is Diamond or higher.
|
|
838
|
+
*/
|
|
839
|
+
export async function runConfigureCicd(envName) {
|
|
840
|
+
// Project guard runs before banner so an accidental `vibecarbon
|
|
841
|
+
// configure cicd` from a parent directory emits the canonical message.
|
|
842
|
+
const cwd = process.cwd();
|
|
843
|
+
const projectConfig = assertInProjectDir(cwd);
|
|
844
|
+
|
|
845
|
+
requireLicense('configure cicd');
|
|
846
|
+
printBanner();
|
|
847
|
+
p.intro(`${c.bold('vibecarbon configure cicd')} ${c.dim(`v${VERSION}`)}`);
|
|
848
|
+
|
|
849
|
+
if (!envName) {
|
|
850
|
+
p.log.error('Missing environment argument. Usage: vibecarbon configure cicd <env>');
|
|
851
|
+
process.exit(1);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const envConfig = projectConfig.environments?.[envName];
|
|
855
|
+
if (!envConfig) {
|
|
856
|
+
p.log.error(
|
|
857
|
+
`Environment "${envName}" not found in vibecarbon.json. Deploy it first with \`vibecarbon deploy ${envName} -mode k8s\`.`,
|
|
858
|
+
);
|
|
859
|
+
process.exit(1);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// GitOps only makes sense for k8s clusters. Compose deploys go through
|
|
863
|
+
// the build-workflow path that promptCicd already handles via the
|
|
864
|
+
// interactive wizard's "CI/CD" feature.
|
|
865
|
+
const isK8s =
|
|
866
|
+
envConfig.deployMode === 'kubernetes' ||
|
|
867
|
+
envConfig.deployMode === 'kubernetes-ha' ||
|
|
868
|
+
envConfig.deployMode === 'k8s' ||
|
|
869
|
+
envConfig.deployMode === 'k8s-ha';
|
|
870
|
+
if (!isK8s) {
|
|
871
|
+
p.log.error(
|
|
872
|
+
`Environment "${envName}" is ${envConfig.deployMode || 'unknown'} mode. configure cicd only applies to k8s / k8s-ha deploys.`,
|
|
873
|
+
);
|
|
874
|
+
p.log.info(
|
|
875
|
+
'For compose CI/CD, run `vibecarbon configure` (no subcommand) and pick CI/CD from the menu.',
|
|
876
|
+
);
|
|
877
|
+
process.exit(1);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const kubeconfigPath = join(cwd, '.vibecarbon', `kubeconfig-${envName}`);
|
|
881
|
+
if (!existsSync(kubeconfigPath)) {
|
|
882
|
+
p.log.error(
|
|
883
|
+
`Kubeconfig not found at ${kubeconfigPath}. Deploy the cluster first: vibecarbon deploy ${envName} -mode k8s`,
|
|
884
|
+
);
|
|
885
|
+
process.exit(1);
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Stage 1: install build + deploy workflow files locally + flip cicdEnabled.
|
|
889
|
+
// Reuses the same code path that the interactive wizard invokes for the
|
|
890
|
+
// CI/CD feature so behavior stays consistent across entry points.
|
|
891
|
+
p.log.step('Stage 1: install GitHub Actions workflows + mark cicdEnabled');
|
|
892
|
+
const projectSide = await promptCicd();
|
|
893
|
+
if (projectSide === null) {
|
|
894
|
+
p.log.error('Workflow installation aborted. Fix the gh-CLI / origin issues above and re-run.');
|
|
895
|
+
process.exit(1);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Stage 2: layer Flux + GH Environment + workflow trigger onto the live
|
|
899
|
+
// cluster. This is the body that orchestrator.js used to run inline when
|
|
900
|
+
// --gitops was passed (deleted in PR 5).
|
|
901
|
+
p.log.step('Stage 2: install Flux + seed GitHub Environment + trigger first reconcile');
|
|
902
|
+
const { deployK8sGitOps } = await import('./lib/deploy/k8s/gitops-deploy.js');
|
|
903
|
+
const { getImageTag } = await import('./lib/ci-setup.js');
|
|
904
|
+
const creds = loadCredentials();
|
|
905
|
+
const imageTag = getImageTag(cwd);
|
|
906
|
+
|
|
907
|
+
// HA clusters split into <env>-primary and <env>-standby kubeconfigs;
|
|
908
|
+
// each needs its own GitOps handover. The orchestrator used to iterate
|
|
909
|
+
// over `[{suffix:'primary'}, {suffix:'standby'}]` in HA, single in non-HA.
|
|
910
|
+
const isHA = envConfig.deployMode === 'kubernetes-ha' || envConfig.deployMode === 'k8s-ha';
|
|
911
|
+
const targets = isHA
|
|
912
|
+
? [
|
|
913
|
+
{ suffix: 'primary', envName: `${envName}-primary` },
|
|
914
|
+
{ suffix: 'standby', envName: `${envName}-standby` },
|
|
915
|
+
]
|
|
916
|
+
: [{ suffix: null, envName }];
|
|
917
|
+
|
|
918
|
+
for (const target of targets) {
|
|
919
|
+
const targetKubeconfig = join(cwd, '.vibecarbon', `kubeconfig-${target.envName}`);
|
|
920
|
+
if (!existsSync(targetKubeconfig)) {
|
|
921
|
+
p.log.warn(
|
|
922
|
+
`Kubeconfig for ${target.envName} not found at ${targetKubeconfig} — skipping. Re-deploy that cluster with vibecarbon deploy first.`,
|
|
923
|
+
);
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
const subEnvConfig = projectConfig.environments?.[target.envName] ?? envConfig;
|
|
927
|
+
p.log.info(`Configuring GitOps for ${c.bold(target.envName)}`);
|
|
928
|
+
await deployK8sGitOps({
|
|
929
|
+
projectDir: cwd,
|
|
930
|
+
environment: target.envName,
|
|
931
|
+
domain: subEnvConfig.domain || envConfig.domain,
|
|
932
|
+
projectName: projectConfig.projectName,
|
|
933
|
+
kubeconfigPath: targetKubeconfig,
|
|
934
|
+
networkId: subEnvConfig.networkId || envConfig.networkId,
|
|
935
|
+
dnsProvider: subEnvConfig.dnsProvider || envConfig.dnsProvider,
|
|
936
|
+
creds,
|
|
937
|
+
imageTag,
|
|
938
|
+
s3BackupBucket: subEnvConfig.backupBucket || envConfig.backupBucket,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
p.outro(c.success(`GitOps layered onto ${envName}. Future commits to main reconcile via Flux.`));
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// ============================================================================
|
|
946
|
+
// RUN FUNCTION (called by CLI entry point)
|
|
947
|
+
// ============================================================================
|
|
948
|
+
|
|
949
|
+
export async function run(args) {
|
|
950
|
+
// Subcommand routing: `vibecarbon configure cicd <env>` skips the
|
|
951
|
+
// interactive feature menu and runs the GitOps add-on flow directly.
|
|
952
|
+
// Any other first-positional arg falls through to the wizard's parser
|
|
953
|
+
// (which currently only handles -h / -v).
|
|
954
|
+
if (args[0] === 'cicd') {
|
|
955
|
+
return runConfigureCicd(args[1]);
|
|
956
|
+
}
|
|
957
|
+
await main(args);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// ============================================================================
|
|
961
|
+
// TEST EXPORTS
|
|
962
|
+
// ============================================================================
|
|
963
|
+
|
|
964
|
+
export const __test__ = {
|
|
965
|
+
FEATURES,
|
|
966
|
+
SPEC,
|
|
967
|
+
};
|