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/add.js
ADDED
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vibecarbon Add Command
|
|
3
|
+
*
|
|
4
|
+
* Adds optional services to an existing Vibecarbon project. Services
|
|
5
|
+
* are fetched from GitHub at runtime so users always get the latest
|
|
6
|
+
* templates regardless of their CLI version. -offline falls back to
|
|
7
|
+
* bundled templates for air-gapped environments.
|
|
8
|
+
*
|
|
9
|
+
* Interactive-by-default: bare `vibecarbon add` prompts for a feature
|
|
10
|
+
* to add. Positional features (`vibecarbon add observability redis`)
|
|
11
|
+
* skip the prompt and queue multiple installs.
|
|
12
|
+
*
|
|
13
|
+
* Form rule: vibecarbon uses single-dash flags only — see
|
|
14
|
+
* memory:feedback_cli_single_dash_flags. -l is gone (the bare-form
|
|
15
|
+
* help body already lists features).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
chmodSync,
|
|
20
|
+
existsSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
readdirSync,
|
|
23
|
+
readFileSync,
|
|
24
|
+
writeFileSync,
|
|
25
|
+
} from 'node:fs';
|
|
26
|
+
import { dirname, join } from 'node:path';
|
|
27
|
+
import { fileURLToPath } from 'node:url';
|
|
28
|
+
import * as p from '@clack/prompts';
|
|
29
|
+
|
|
30
|
+
import { renderHelp } from './lib/cli/help.js';
|
|
31
|
+
import { parseFlags } from './lib/cli/parse-flags.js';
|
|
32
|
+
import { requireTTYOrFlags } from './lib/cli/tty-guard.js';
|
|
33
|
+
import { c, printBanner } from './lib/colors.js';
|
|
34
|
+
import { isHAConfigured, registerProject } from './lib/config.js';
|
|
35
|
+
import { fetchWithRetry } from './lib/fetch-retry.js';
|
|
36
|
+
import {
|
|
37
|
+
appendToEnv,
|
|
38
|
+
loadEnvVariables,
|
|
39
|
+
loadManifest,
|
|
40
|
+
saveManifest,
|
|
41
|
+
setEnvVar,
|
|
42
|
+
} from './lib/project.js';
|
|
43
|
+
import { assertInProjectDir } from './lib/project-guard.js';
|
|
44
|
+
import { generatePassword } from './lib/secrets.js';
|
|
45
|
+
import { VERSION } from './lib/version.js';
|
|
46
|
+
|
|
47
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
48
|
+
const __dirname = dirname(__filename);
|
|
49
|
+
|
|
50
|
+
// GitHub repository for fetching service bundles
|
|
51
|
+
const GITHUB_REPO = 'hyperformant/vibecarbon';
|
|
52
|
+
const GITHUB_BRANCH = 'main';
|
|
53
|
+
const GITHUB_RAW_BASE = `https://raw.githubusercontent.com/${GITHUB_REPO}/${GITHUB_BRANCH}`;
|
|
54
|
+
const SERVICES_PATH = 'services';
|
|
55
|
+
|
|
56
|
+
// Local services directory (for --offline mode and development)
|
|
57
|
+
const LOCAL_SERVICES_DIR = join(__dirname, '..', SERVICES_PATH);
|
|
58
|
+
|
|
59
|
+
// Template variable placeholders
|
|
60
|
+
const PLACEHOLDERS = {
|
|
61
|
+
PROJECT_NAME: '{{PROJECT_NAME}}',
|
|
62
|
+
DB_PASSWORD: '{{DB_PASSWORD}}',
|
|
63
|
+
ANON_KEY: '{{ANON_KEY}}',
|
|
64
|
+
SERVICE_ROLE_KEY: '{{SERVICE_ROLE_KEY}}',
|
|
65
|
+
N8N_PASSWORD: '{{N8N_PASSWORD}}',
|
|
66
|
+
DOMAIN: '{{DOMAIN}}',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Map service names to their env flags for dashboard visibility
|
|
70
|
+
// client: VITE_* flags for frontend (sidebar links, UI)
|
|
71
|
+
// server: flags for backend (services status monitoring)
|
|
72
|
+
const SERVICE_ENV_FLAGS = {
|
|
73
|
+
n8n: { client: 'VITE_N8N_ENABLED', server: 'N8N_ENABLED' },
|
|
74
|
+
metabase: { client: 'VITE_METABASE_ENABLED', server: 'METABASE_ENABLED' },
|
|
75
|
+
observability: { client: 'VITE_OBSERVABILITY_ENABLED', server: 'OBSERVABILITY_ENABLED' },
|
|
76
|
+
redis: { client: 'VITE_REDIS_ENABLED', server: 'REDIS_ENABLED' },
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// COMMAND SPEC — single source of truth for argv parsing AND help output.
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The features list is referenced by both the SPEC (so help body
|
|
85
|
+
* shows what's available) and `addService` (the dispatcher). Defined
|
|
86
|
+
* later in the file as `LOCAL_FEATURES`; lazy access via a getter so
|
|
87
|
+
* this module can resolve hoisting cleanly.
|
|
88
|
+
*/
|
|
89
|
+
function buildSpec() {
|
|
90
|
+
return /** @type {import('./lib/cli/parse-flags.js').CommandSpec & { summary?: string, description?: string, examples?: Array<{ command: string, description?: string }> }} */ ({
|
|
91
|
+
name: 'add',
|
|
92
|
+
summary: 'Add a feature to a Vibecarbon project',
|
|
93
|
+
description: [
|
|
94
|
+
'Available features:',
|
|
95
|
+
...LOCAL_FEATURES.map((f) => ` ${f.name.padEnd(16)} ${f.description}`),
|
|
96
|
+
'',
|
|
97
|
+
'External services (CI/CD, Stripe, OAuth, etc.) live in `vibecarbon configure`.',
|
|
98
|
+
].join('\n'),
|
|
99
|
+
positional: [
|
|
100
|
+
{
|
|
101
|
+
name: 'features',
|
|
102
|
+
variadic: true,
|
|
103
|
+
optional: true,
|
|
104
|
+
description: 'One or more features to add (skips the feature prompt)',
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
flags: [
|
|
108
|
+
{ name: 'h', boolean: true, description: 'Show this help' },
|
|
109
|
+
{ name: 'v', boolean: true, description: 'Show version' },
|
|
110
|
+
{ name: 'y', boolean: true, description: 'Skip confirmation prompts' },
|
|
111
|
+
{
|
|
112
|
+
name: 'offline',
|
|
113
|
+
boolean: true,
|
|
114
|
+
description: 'Use bundled templates (for air-gapped environments)',
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
examples: [
|
|
118
|
+
{ command: 'vibecarbon add', description: 'prompts for a feature to add' },
|
|
119
|
+
{ command: 'vibecarbon add observability', description: 'add a specific feature' },
|
|
120
|
+
{ command: 'vibecarbon add observability redis', description: 'add multiple features' },
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// GITHUB FETCHING
|
|
127
|
+
// ============================================================================
|
|
128
|
+
|
|
129
|
+
async function fetchFromGitHub(path) {
|
|
130
|
+
const url = `${GITHUB_RAW_BASE}/${path}`;
|
|
131
|
+
try {
|
|
132
|
+
const response = await fetchWithRetry(url);
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw new Error(`HTTP ${response.status}`);
|
|
135
|
+
}
|
|
136
|
+
return await response.text();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
throw new Error(`Failed to fetch ${url}: ${error.message}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function fetchServiceManifest(serviceName, offline = false) {
|
|
143
|
+
if (offline) {
|
|
144
|
+
const manifestPath = join(LOCAL_SERVICES_DIR, serviceName, 'manifest.json');
|
|
145
|
+
if (!existsSync(manifestPath)) {
|
|
146
|
+
throw new Error(`Service "${serviceName}" not found in bundled services`);
|
|
147
|
+
}
|
|
148
|
+
return JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const content = await fetchFromGitHub(`${SERVICES_PATH}/${serviceName}/manifest.json`);
|
|
153
|
+
return JSON.parse(content);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw new Error(`Service "${serviceName}" not found: ${error.message}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function fetchServiceFile(serviceName, filePath, offline = false) {
|
|
160
|
+
if (offline) {
|
|
161
|
+
const localPath = join(LOCAL_SERVICES_DIR, serviceName, filePath);
|
|
162
|
+
if (!existsSync(localPath)) {
|
|
163
|
+
throw new Error(`File not found: ${localPath}`);
|
|
164
|
+
}
|
|
165
|
+
return readFileSync(localPath, 'utf-8');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return await fetchFromGitHub(`${SERVICES_PATH}/${serviceName}/${filePath}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// FILE OPERATIONS
|
|
173
|
+
// ============================================================================
|
|
174
|
+
|
|
175
|
+
function applyVariables(content, variables) {
|
|
176
|
+
let result = content;
|
|
177
|
+
// Replace predefined placeholders
|
|
178
|
+
for (const [key, placeholder] of Object.entries(PLACEHOLDERS)) {
|
|
179
|
+
if (variables[key] !== undefined) {
|
|
180
|
+
result = result.replaceAll(placeholder, variables[key]);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Also replace any {{VARIABLE_NAME}} patterns from dynamic variables
|
|
184
|
+
// This handles service-specific vars like METABASE_PASSWORD, REDIS_PASSWORD, etc.
|
|
185
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
186
|
+
if (value !== undefined) {
|
|
187
|
+
result = result.replaceAll(`{{${key}}}`, value);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function installServiceFile(serviceName, srcPath, destPath, variables, offline) {
|
|
194
|
+
const content = await fetchServiceFile(serviceName, srcPath, offline);
|
|
195
|
+
const processedContent = applyVariables(content, variables);
|
|
196
|
+
|
|
197
|
+
const fullDestPath = join(process.cwd(), destPath);
|
|
198
|
+
mkdirSync(dirname(fullDestPath), { recursive: true });
|
|
199
|
+
writeFileSync(fullDestPath, processedContent);
|
|
200
|
+
|
|
201
|
+
// Make shell scripts executable
|
|
202
|
+
if (destPath.endsWith('.sh')) {
|
|
203
|
+
chmodSync(fullDestPath, 0o755);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function installServiceDirectory(serviceName, srcDir, destDir, variables, offline) {
|
|
208
|
+
// For directories, we need to list files
|
|
209
|
+
// In offline mode, we can read the directory
|
|
210
|
+
// In online mode, we'd need the manifest to list files
|
|
211
|
+
|
|
212
|
+
if (offline) {
|
|
213
|
+
const localDir = join(LOCAL_SERVICES_DIR, serviceName, srcDir);
|
|
214
|
+
if (!existsSync(localDir)) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const entries = readdirSync(localDir, { withFileTypes: true });
|
|
219
|
+
for (const entry of entries) {
|
|
220
|
+
const srcPath = join(srcDir, entry.name);
|
|
221
|
+
const destPath = join(destDir, entry.name);
|
|
222
|
+
|
|
223
|
+
if (entry.isDirectory()) {
|
|
224
|
+
await installServiceDirectory(serviceName, srcPath, destPath, variables, offline);
|
|
225
|
+
} else {
|
|
226
|
+
await installServiceFile(serviceName, srcPath, destPath, variables, offline);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
// For online mode, the manifest should list all files
|
|
231
|
+
// We'll handle this in the main install function
|
|
232
|
+
throw new Error('Directory installation requires file listing in manifest');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ============================================================================
|
|
237
|
+
// KUBERNETES CONFIGURATION
|
|
238
|
+
// ============================================================================
|
|
239
|
+
|
|
240
|
+
function updateBaseKustomization(kustomizationEntry) {
|
|
241
|
+
if (!kustomizationEntry) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const kustomizationPath = join(process.cwd(), 'k8s', 'base', 'kustomization.yaml');
|
|
246
|
+
|
|
247
|
+
if (!existsSync(kustomizationPath)) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let content = readFileSync(kustomizationPath, 'utf-8');
|
|
252
|
+
const entry = ` - ${kustomizationEntry}`;
|
|
253
|
+
|
|
254
|
+
if (content.includes(entry)) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Insert before labels/commonLabels section so entry stays in resources block
|
|
259
|
+
const labelsIndex = content.indexOf('labels:');
|
|
260
|
+
const commonLabelsIndex = content.indexOf('commonLabels:');
|
|
261
|
+
const insertBefore = labelsIndex !== -1 ? labelsIndex : commonLabelsIndex;
|
|
262
|
+
|
|
263
|
+
if (insertBefore !== -1) {
|
|
264
|
+
content = `${content.slice(0, insertBefore).trimEnd()}\n${entry}\n\n${content.slice(insertBefore)}`;
|
|
265
|
+
} else {
|
|
266
|
+
content = `${content.trimEnd()}\n${entry}\n`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
writeFileSync(kustomizationPath, content);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function removeFromBaseKustomization(kustomizationEntry) {
|
|
273
|
+
if (!kustomizationEntry) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const kustomizationPath = join(process.cwd(), 'k8s', 'base', 'kustomization.yaml');
|
|
278
|
+
|
|
279
|
+
if (!existsSync(kustomizationPath)) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let content = readFileSync(kustomizationPath, 'utf-8');
|
|
284
|
+
const entry = ` - ${kustomizationEntry}`;
|
|
285
|
+
|
|
286
|
+
content = content.replace(`${entry}\n\n`, '');
|
|
287
|
+
content = content.replace(`${entry}\n`, '');
|
|
288
|
+
content = content.replace(entry, '');
|
|
289
|
+
|
|
290
|
+
writeFileSync(kustomizationPath, content);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ============================================================================
|
|
294
|
+
// SPECIALIZED FEATURE HANDLERS
|
|
295
|
+
// ============================================================================
|
|
296
|
+
|
|
297
|
+
// Features with specialized handlers (not manifest-based). CI/CD used to be
|
|
298
|
+
// here too; it moved to `vibecarbon configure` (CI/CD is an external service,
|
|
299
|
+
// not a local container, so it lives with the other configure features).
|
|
300
|
+
const SPECIALIZED_FEATURES = ['observability'];
|
|
301
|
+
|
|
302
|
+
async function handleSpecializedFeature(featureName, options) {
|
|
303
|
+
const { project } = options;
|
|
304
|
+
|
|
305
|
+
switch (featureName) {
|
|
306
|
+
case 'observability': {
|
|
307
|
+
return handleObservabilityFeature(project);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
default:
|
|
311
|
+
throw new Error(`Unknown specialized feature: ${featureName}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Enable observability stack (Prometheus, Grafana, Loki)
|
|
317
|
+
* Files are already included in the project - this just enables them
|
|
318
|
+
*/
|
|
319
|
+
async function handleObservabilityFeature(_project) {
|
|
320
|
+
const manifest = loadManifest();
|
|
321
|
+
|
|
322
|
+
// Check if already enabled
|
|
323
|
+
if (manifest.services?.observability) {
|
|
324
|
+
p.log.info('Observability is already enabled.');
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Check that the observability compose file exists
|
|
329
|
+
const observabilityCompose = join(process.cwd(), 'docker-compose.observability.yml');
|
|
330
|
+
if (!existsSync(observabilityCompose)) {
|
|
331
|
+
p.log.error('Observability files not found. Please ensure your project is up to date.');
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const s = p.spinner();
|
|
336
|
+
s.start('Enabling observability stack');
|
|
337
|
+
|
|
338
|
+
// Set env flags for dashboard visibility (client + server)
|
|
339
|
+
setEnvVar('VITE_OBSERVABILITY_ENABLED', 'true');
|
|
340
|
+
setEnvVar('OBSERVABILITY_ENABLED', 'true');
|
|
341
|
+
|
|
342
|
+
// Update manifest
|
|
343
|
+
if (!manifest.services) manifest.services = {};
|
|
344
|
+
manifest.services.observability = { addedAt: new Date().toISOString() };
|
|
345
|
+
saveManifest(manifest);
|
|
346
|
+
|
|
347
|
+
s.stop('Observability enabled');
|
|
348
|
+
|
|
349
|
+
p.note(
|
|
350
|
+
[
|
|
351
|
+
'Observability stack enabled:',
|
|
352
|
+
` ${c.info('Prometheus')} http://prometheus.localhost`,
|
|
353
|
+
` ${c.info('Grafana')} http://grafana.localhost`,
|
|
354
|
+
` ${c.info('Loki')} http://loki.localhost`,
|
|
355
|
+
].join('\n'),
|
|
356
|
+
'Observability',
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ============================================================================
|
|
363
|
+
// ADD SERVICE
|
|
364
|
+
// ============================================================================
|
|
365
|
+
|
|
366
|
+
async function addService(serviceName, options) {
|
|
367
|
+
const { project } = options;
|
|
368
|
+
let { offline } = options;
|
|
369
|
+
|
|
370
|
+
// Check if this is a specialized feature with a custom handler
|
|
371
|
+
if (SPECIALIZED_FEATURES.includes(serviceName)) {
|
|
372
|
+
return handleSpecializedFeature(serviceName, options);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
let manifest;
|
|
376
|
+
try {
|
|
377
|
+
manifest = await fetchServiceManifest(serviceName, offline);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
// If online fetch failed, automatically try offline mode
|
|
380
|
+
if (!offline) {
|
|
381
|
+
try {
|
|
382
|
+
manifest = await fetchServiceManifest(serviceName, true);
|
|
383
|
+
offline = true; // Switch to offline for file fetching
|
|
384
|
+
} catch {
|
|
385
|
+
const suggestion = findClosestFeature(serviceName);
|
|
386
|
+
if (suggestion) {
|
|
387
|
+
p.log.error(`Service "${serviceName}" not found. Did you mean "${suggestion}"?`);
|
|
388
|
+
} else {
|
|
389
|
+
p.log.error(`Service "${serviceName}" not found.`);
|
|
390
|
+
}
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
} else {
|
|
394
|
+
const suggestion = findClosestFeature(serviceName);
|
|
395
|
+
if (suggestion) {
|
|
396
|
+
p.log.error(`${error.message} Did you mean "${suggestion}"?`);
|
|
397
|
+
} else {
|
|
398
|
+
p.log.error(error.message);
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const projectManifest = loadManifest();
|
|
405
|
+
|
|
406
|
+
// Check if already added
|
|
407
|
+
if (projectManifest.services?.[serviceName]) {
|
|
408
|
+
p.log.warn(`Service "${serviceName}" is already added to this project.`);
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// n8n requires Redis for queue mode — add it automatically if not already present
|
|
413
|
+
if (serviceName === 'n8n' && !projectManifest.services?.redis) {
|
|
414
|
+
p.log.info('n8n requires Redis for queue mode — adding Redis automatically');
|
|
415
|
+
const redisAdded = await addService('redis', options);
|
|
416
|
+
if (!redisAdded) {
|
|
417
|
+
p.log.error('Failed to add Redis (required by n8n)');
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Load existing env vars
|
|
423
|
+
const envVars = loadEnvVariables();
|
|
424
|
+
|
|
425
|
+
// Generate variables for template substitution
|
|
426
|
+
const variables = {
|
|
427
|
+
PROJECT_NAME: project.projectName,
|
|
428
|
+
DB_PASSWORD: envVars.POSTGRES_PASSWORD || envVars.DB_PASSWORD || '',
|
|
429
|
+
ANON_KEY: envVars.SUPABASE_ANON_KEY || '',
|
|
430
|
+
SERVICE_ROLE_KEY: envVars.SUPABASE_SERVICE_ROLE_KEY || '',
|
|
431
|
+
DOMAIN: envVars.DOMAIN || 'localhost',
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
// Generate service-specific env vars
|
|
435
|
+
const serviceEnvVars = {};
|
|
436
|
+
for (const [key, config] of Object.entries(manifest.envVars || {})) {
|
|
437
|
+
if (config.generate === 'password') {
|
|
438
|
+
serviceEnvVars[key] = generatePassword(config.length || 24);
|
|
439
|
+
variables[key] = serviceEnvVars[key];
|
|
440
|
+
} else if (config.from && envVars[config.from]) {
|
|
441
|
+
// Inherit from existing env var (e.g., ADMIN_EMAIL)
|
|
442
|
+
serviceEnvVars[key] = envVars[config.from];
|
|
443
|
+
variables[key] = envVars[config.from];
|
|
444
|
+
} else if (config.value !== undefined) {
|
|
445
|
+
serviceEnvVars[key] = config.value;
|
|
446
|
+
variables[key] = config.value;
|
|
447
|
+
} else if (config.default !== undefined) {
|
|
448
|
+
serviceEnvVars[key] = config.default;
|
|
449
|
+
variables[key] = config.default;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const s = p.spinner();
|
|
454
|
+
s.start(`Adding ${manifest.name}${offline ? ' (offline)' : ''}...`);
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
// Install files based on manifest mappings
|
|
458
|
+
for (const [src, dest] of Object.entries(manifest.files || {})) {
|
|
459
|
+
if (src.endsWith('/')) {
|
|
460
|
+
// Directory mapping
|
|
461
|
+
await installServiceDirectory(serviceName, src, dest, variables, offline);
|
|
462
|
+
} else {
|
|
463
|
+
// File mapping
|
|
464
|
+
await installServiceFile(serviceName, src, dest, variables, offline);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Update kustomization.yaml if needed
|
|
469
|
+
if (manifest.kustomization?.entry) {
|
|
470
|
+
updateBaseKustomization(manifest.kustomization.entry);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Update .env.local with service env vars
|
|
474
|
+
if (Object.keys(serviceEnvVars).length > 0) {
|
|
475
|
+
appendToEnv(serviceName, serviceEnvVars);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Patch k8s secrets manifest with service-specific env vars
|
|
479
|
+
const k8sSecretsPath = join(process.cwd(), 'k8s', 'base', 'secrets', 'secrets.yaml');
|
|
480
|
+
if (existsSync(k8sSecretsPath) && Object.keys(serviceEnvVars).length > 0) {
|
|
481
|
+
let secretsContent = readFileSync(k8sSecretsPath, 'utf-8');
|
|
482
|
+
for (const [key, value] of Object.entries(serviceEnvVars)) {
|
|
483
|
+
if (!secretsContent.includes(` ${key}:`)) {
|
|
484
|
+
// Append under stringData section
|
|
485
|
+
secretsContent = `${secretsContent.trimEnd()}\n ${key}: "${value}"\n`;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
writeFileSync(k8sSecretsPath, secretsContent);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Set env flags for dashboard visibility (client + server)
|
|
492
|
+
const envFlags = SERVICE_ENV_FLAGS[serviceName];
|
|
493
|
+
if (envFlags) {
|
|
494
|
+
if (envFlags.client) setEnvVar(envFlags.client, 'true');
|
|
495
|
+
if (envFlags.server) setEnvVar(envFlags.server, 'true');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Update project manifest
|
|
499
|
+
if (!projectManifest.services) {
|
|
500
|
+
projectManifest.services = {};
|
|
501
|
+
}
|
|
502
|
+
projectManifest.services[serviceName] = {
|
|
503
|
+
addedAt: new Date().toISOString(),
|
|
504
|
+
version: manifest.version,
|
|
505
|
+
};
|
|
506
|
+
saveManifest(projectManifest);
|
|
507
|
+
|
|
508
|
+
s.stop(`${manifest.name} added successfully`);
|
|
509
|
+
|
|
510
|
+
// Show credentials if defined
|
|
511
|
+
if (manifest.credentials) {
|
|
512
|
+
const credentialLines = manifest.credentials.fields.map((field) => {
|
|
513
|
+
const value = field.envVar ? serviceEnvVars[field.envVar] : field.value;
|
|
514
|
+
return `${field.label}: ${value}`;
|
|
515
|
+
});
|
|
516
|
+
p.note(credentialLines.join('\n'), manifest.credentials.title);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return true;
|
|
520
|
+
} catch (error) {
|
|
521
|
+
s.stop(`Failed to add ${manifest.name}`);
|
|
522
|
+
p.log.error(error.message);
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ============================================================================
|
|
528
|
+
// LIST SERVICES
|
|
529
|
+
// ============================================================================
|
|
530
|
+
|
|
531
|
+
// Features that work locally without external dependencies
|
|
532
|
+
const LOCAL_FEATURES = [
|
|
533
|
+
{ name: 'observability', description: 'Prometheus, Grafana, Loki monitoring stack' },
|
|
534
|
+
{ name: 'n8n', description: 'Workflow automation platform' },
|
|
535
|
+
{ name: 'metabase', description: 'Business intelligence and analytics' },
|
|
536
|
+
{ name: 'redis', description: 'In-memory cache and session store' },
|
|
537
|
+
];
|
|
538
|
+
|
|
539
|
+
// External services were previously listed here (CI/CD via GitHub Actions);
|
|
540
|
+
// they all moved to `vibecarbon configure` since they wire up off-stack
|
|
541
|
+
// providers rather than installing local containers.
|
|
542
|
+
|
|
543
|
+
const ALL_FEATURES = [...LOCAL_FEATURES];
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Calculate Levenshtein distance between two strings
|
|
547
|
+
*/
|
|
548
|
+
function levenshteinDistance(a, b) {
|
|
549
|
+
const matrix = Array.from({ length: a.length + 1 }, (_, i) =>
|
|
550
|
+
Array.from({ length: b.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
for (let i = 1; i <= a.length; i++) {
|
|
554
|
+
for (let j = 1; j <= b.length; j++) {
|
|
555
|
+
matrix[i][j] =
|
|
556
|
+
a[i - 1] === b[j - 1]
|
|
557
|
+
? matrix[i - 1][j - 1]
|
|
558
|
+
: Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + 1);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return matrix[a.length][b.length];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Find the closest matching feature name for typo correction
|
|
566
|
+
*/
|
|
567
|
+
function findClosestFeature(input) {
|
|
568
|
+
const featureNames = ALL_FEATURES.map((f) => f.name);
|
|
569
|
+
let closest = null;
|
|
570
|
+
let minDistance = Infinity;
|
|
571
|
+
|
|
572
|
+
for (const name of featureNames) {
|
|
573
|
+
const distance = levenshteinDistance(input.toLowerCase(), name.toLowerCase());
|
|
574
|
+
// Only suggest if the distance is reasonable (less than half the word length)
|
|
575
|
+
if (distance < minDistance && distance <= Math.ceil(name.length / 2)) {
|
|
576
|
+
minDistance = distance;
|
|
577
|
+
closest = name;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return closest;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ============================================================================
|
|
584
|
+
// MAIN
|
|
585
|
+
// ============================================================================
|
|
586
|
+
|
|
587
|
+
async function main(cliArgs) {
|
|
588
|
+
const SPEC = buildSpec();
|
|
589
|
+
const { values, positional, errors } = parseFlags(cliArgs, SPEC);
|
|
590
|
+
|
|
591
|
+
if (errors.length > 0) {
|
|
592
|
+
for (const e of errors) {
|
|
593
|
+
process.stderr.write(`${c.error('✗')} ${e}\n`);
|
|
594
|
+
}
|
|
595
|
+
process.stderr.write(`Run ${c.info('vibecarbon add -h')} for usage.\n`);
|
|
596
|
+
process.exit(1);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (values.h) {
|
|
600
|
+
process.stdout.write(renderHelp(SPEC));
|
|
601
|
+
process.exit(0);
|
|
602
|
+
}
|
|
603
|
+
if (values.v) {
|
|
604
|
+
console.log(`vibecarbon add v${VERSION}`);
|
|
605
|
+
process.exit(0);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Detect project FIRST, before the secret scan. Otherwise an accidental
|
|
609
|
+
// invocation from a parent directory (e.g. ~/repos) would walk every
|
|
610
|
+
// sibling repo and dump real tokens to the operator's scrollback before
|
|
611
|
+
// the friendly "not a project" message ever appears.
|
|
612
|
+
const projectConfig = assertInProjectDir();
|
|
613
|
+
|
|
614
|
+
// Refuse to add if the working tree contains likely secrets — `add`
|
|
615
|
+
// mutates files (compose includes, k8s manifests) that get pushed
|
|
616
|
+
// alongside whatever else is in the tree. A leak through `add`
|
|
617
|
+
// looks the same as a leak through `deploy`.
|
|
618
|
+
await refuseIfSecretsPresent();
|
|
619
|
+
|
|
620
|
+
const project = {
|
|
621
|
+
projectName: projectConfig.projectName,
|
|
622
|
+
haConfigured: isHAConfigured(),
|
|
623
|
+
provider: projectConfig.provider || 'hetzner',
|
|
624
|
+
services: projectConfig.services || {},
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
// Resolve which services to add. Positional list wins; otherwise
|
|
628
|
+
// prompt on TTY, fail off-TTY with the canonical message naming the
|
|
629
|
+
// missing positional.
|
|
630
|
+
/** @type {string[]} */
|
|
631
|
+
const seedServices = /** @type {string[]|undefined} */ (positional.features || []).map((s) =>
|
|
632
|
+
s.toLowerCase(),
|
|
633
|
+
);
|
|
634
|
+
|
|
635
|
+
if (seedServices.length === 0) {
|
|
636
|
+
requireTTYOrFlags({
|
|
637
|
+
requirements: [
|
|
638
|
+
{
|
|
639
|
+
flag: 'features',
|
|
640
|
+
description: 'name a feature to add (e.g. observability, redis, n8n)',
|
|
641
|
+
satisfied: false,
|
|
642
|
+
},
|
|
643
|
+
],
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
printBanner();
|
|
648
|
+
p.intro(`${c.bold('vibecarbon add')} ${c.dim(`v${VERSION}`)}`);
|
|
649
|
+
|
|
650
|
+
/** @type {string[]} */
|
|
651
|
+
let services = seedServices;
|
|
652
|
+
if (services.length === 0) {
|
|
653
|
+
const choice = await p.select({
|
|
654
|
+
message: 'Which feature do you want to add?',
|
|
655
|
+
options: LOCAL_FEATURES.map((f) => ({
|
|
656
|
+
value: f.name,
|
|
657
|
+
label: f.name,
|
|
658
|
+
hint: f.description,
|
|
659
|
+
})),
|
|
660
|
+
});
|
|
661
|
+
if (p.isCancel(choice)) {
|
|
662
|
+
p.cancel('Operation cancelled.');
|
|
663
|
+
process.exit(0);
|
|
664
|
+
}
|
|
665
|
+
services = [/** @type {string} */ (choice)];
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
let success = true;
|
|
669
|
+
for (const serviceName of services) {
|
|
670
|
+
const result = await addService(serviceName, {
|
|
671
|
+
offline: !!values.offline,
|
|
672
|
+
project,
|
|
673
|
+
cliArgs: cliArgs, // Pass raw CLI args for infrastructure services
|
|
674
|
+
});
|
|
675
|
+
if (!result) {
|
|
676
|
+
success = false;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (success) {
|
|
681
|
+
registerProject(project.projectName, process.cwd());
|
|
682
|
+
p.note(
|
|
683
|
+
['# Restart services to apply changes:', 'vibecarbon down && vibecarbon up'].join('\n'),
|
|
684
|
+
'Next steps',
|
|
685
|
+
);
|
|
686
|
+
p.outro('Services added successfully!');
|
|
687
|
+
} else {
|
|
688
|
+
p.outro('Some services could not be added.');
|
|
689
|
+
process.exit(1);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export async function run(args) {
|
|
694
|
+
await main(args);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async function refuseIfSecretsPresent() {
|
|
698
|
+
const { scanTree, formatFindings } = await import('./lib/secret-scan.js');
|
|
699
|
+
const findings = scanTree(process.cwd());
|
|
700
|
+
if (findings.length === 0) return;
|
|
701
|
+
process.stderr.write(`\n${c.error('✗')} Refusing to add: secrets detected.\n\n`);
|
|
702
|
+
process.stderr.write(formatFindings(findings));
|
|
703
|
+
process.exit(1);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// ============================================================================
|
|
707
|
+
// EXPORTS FOR TESTING
|
|
708
|
+
// ============================================================================
|
|
709
|
+
|
|
710
|
+
export {
|
|
711
|
+
addService,
|
|
712
|
+
buildSpec,
|
|
713
|
+
fetchServiceManifest,
|
|
714
|
+
loadManifest,
|
|
715
|
+
removeFromBaseKustomization,
|
|
716
|
+
saveManifest,
|
|
717
|
+
updateBaseKustomization,
|
|
718
|
+
};
|