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/create.js
ADDED
|
@@ -0,0 +1,1499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vibecarbon Create Command
|
|
3
|
+
* Scaffolds a complete Hono + Vite + React 19 + Supabase self-hosted stack
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* vibecarbon create my-app # Create new project
|
|
7
|
+
* vibecarbon create my-app -y # Skip prompts, use defaults
|
|
8
|
+
* vibecarbon create my-app --use-npm # Use npm instead of pnpm
|
|
9
|
+
* vibecarbon create --help # Show help
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execFileSync } from 'node:child_process';
|
|
13
|
+
import {
|
|
14
|
+
chmodSync,
|
|
15
|
+
copyFileSync,
|
|
16
|
+
existsSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
readdirSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
writeFileSync,
|
|
21
|
+
} from 'node:fs';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import * as p from '@clack/prompts';
|
|
25
|
+
import { hashContent } from './lib/checksum.js';
|
|
26
|
+
import { renderHelp } from './lib/cli/help.js';
|
|
27
|
+
import { parseFlags } from './lib/cli/parse-flags.js';
|
|
28
|
+
import { c, printBanner } from './lib/colors.js';
|
|
29
|
+
import { runCommand } from './lib/command.js';
|
|
30
|
+
import { registerProject } from './lib/config.js';
|
|
31
|
+
import {
|
|
32
|
+
adaptDockerfileForPackageManager,
|
|
33
|
+
getPackageManagerVersion,
|
|
34
|
+
} from './lib/package-manager.js';
|
|
35
|
+
import { saveManifest, validateGitignore } from './lib/project.js';
|
|
36
|
+
import {
|
|
37
|
+
generateJWT,
|
|
38
|
+
generatePassword,
|
|
39
|
+
generateReplPassword,
|
|
40
|
+
hashPassword,
|
|
41
|
+
} from './lib/secrets.js';
|
|
42
|
+
import { escapeDotenv } from './lib/shell.js';
|
|
43
|
+
import { createTracker } from './lib/tracker.js';
|
|
44
|
+
import { getUpgradeableFiles } from './lib/upgrade-policy.js';
|
|
45
|
+
import {
|
|
46
|
+
validateAdminEmail,
|
|
47
|
+
validateAdminPassword,
|
|
48
|
+
validateProjectName,
|
|
49
|
+
} from './lib/validators.js';
|
|
50
|
+
import { VERSION } from './lib/version.js';
|
|
51
|
+
|
|
52
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
53
|
+
const __dirname = dirname(__filename);
|
|
54
|
+
const TEMPLATE_DIR = join(__dirname, '..', 'carbon');
|
|
55
|
+
|
|
56
|
+
// Launch decision (2026-05-06): hide the interactive package-manager
|
|
57
|
+
// prompt and default to pnpm. The bun + npm code paths work — adapter
|
|
58
|
+
// rewrites the Dockerfile correctly, lockfile gets generated at create
|
|
59
|
+
// time — but pnpm has by far the most e2e-matrix coverage. Power
|
|
60
|
+
// users can still pick npm or bun via `--use-npm` / `--use-bun`. Flip
|
|
61
|
+
// this to `true` to bring the prompt back once bun/npm have matrix
|
|
62
|
+
// coverage parity.
|
|
63
|
+
const SHOW_PACKAGE_MANAGER_PROMPT = false;
|
|
64
|
+
|
|
65
|
+
// Template variable placeholders
|
|
66
|
+
const PLACEHOLDERS = {
|
|
67
|
+
PROJECT_NAME: '{{PROJECT_NAME}}',
|
|
68
|
+
GITHUB_OWNER: '{{GITHUB_OWNER}}',
|
|
69
|
+
DB_PASSWORD: '{{DB_PASSWORD}}',
|
|
70
|
+
JWT_SECRET: '{{JWT_SECRET}}',
|
|
71
|
+
ANON_KEY: '{{ANON_KEY}}',
|
|
72
|
+
SERVICE_ROLE_KEY: '{{SERVICE_ROLE_KEY}}',
|
|
73
|
+
REALTIME_SECRET: '{{REALTIME_SECRET}}',
|
|
74
|
+
LOGFLARE_API_KEY: '{{LOGFLARE_API_KEY}}',
|
|
75
|
+
VAULT_ENC_KEY: '{{VAULT_ENC_KEY}}',
|
|
76
|
+
PG_META_CRYPTO_KEY: '{{PG_META_CRYPTO_KEY}}',
|
|
77
|
+
DB_ENC_KEY: '{{DB_ENC_KEY}}',
|
|
78
|
+
REPL_PASSWORD: '{{REPL_PASSWORD}}',
|
|
79
|
+
SITE_URL: '{{SITE_URL}}',
|
|
80
|
+
ADMIN_EMAIL: '{{ADMIN_EMAIL}}',
|
|
81
|
+
ADMIN_PASSWORD: '{{ADMIN_PASSWORD}}',
|
|
82
|
+
ADMIN_PASSWORD_HASH: '{{ADMIN_PASSWORD_HASH}}',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Placeholders that are NEVER substituted into checked-in template files at
|
|
86
|
+
// create time. Their values land in `.env.local` (gitignored) via direct
|
|
87
|
+
// dotenv writes; the deploy code (src/lib/deploy/**) patches them into k8s
|
|
88
|
+
// manifests at deploy time. Substituting them here would bake a high-entropy
|
|
89
|
+
// secret into a file that lands in `git ls-files` and trips the preflight
|
|
90
|
+
// secret-scan during the next `vibecarbon deploy` — the exact regression
|
|
91
|
+
// fixed by this set (k8s/values/supabase.values.yaml ADMIN_PASSWORD).
|
|
92
|
+
//
|
|
93
|
+
// ADMIN_PASSWORD_HASH is intentionally NOT in this set: volumes/db/super-admin.sql
|
|
94
|
+
// is a Postgres init script with no deploy-time substitution layer, so its
|
|
95
|
+
// bcrypt hash must be substituted at create time for first-boot admin user
|
|
96
|
+
// creation to work. The scanner does not match `password_hash :=` (its regex
|
|
97
|
+
// requires `password\s*[:=]`, not `password_hash\s*:=`), so a hash leak there
|
|
98
|
+
// is benign for the scanner. If a future template adds a hash to another
|
|
99
|
+
// file, treat it as a separate fix.
|
|
100
|
+
const SECRET_PLACEHOLDERS = new Set([
|
|
101
|
+
'DB_PASSWORD',
|
|
102
|
+
'JWT_SECRET',
|
|
103
|
+
'ANON_KEY',
|
|
104
|
+
'SERVICE_ROLE_KEY',
|
|
105
|
+
'REALTIME_SECRET',
|
|
106
|
+
'LOGFLARE_API_KEY',
|
|
107
|
+
'VAULT_ENC_KEY',
|
|
108
|
+
'PG_META_CRYPTO_KEY',
|
|
109
|
+
'DB_ENC_KEY',
|
|
110
|
+
'REPL_PASSWORD',
|
|
111
|
+
'ADMIN_PASSWORD',
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// CLI ARGUMENT PARSING
|
|
116
|
+
// ============================================================================
|
|
117
|
+
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// COMMAND SPEC — single source of truth for argv parsing AND help output.
|
|
120
|
+
// ============================================================================
|
|
121
|
+
|
|
122
|
+
/** @type {import('./lib/cli/parse-flags.js').CommandSpec & { summary?: string, description?: string, examples?: Array<{ command: string, description?: string }> }} */
|
|
123
|
+
const SPEC = {
|
|
124
|
+
name: 'create',
|
|
125
|
+
summary: 'Scaffold a new Vibecarbon project',
|
|
126
|
+
description: [
|
|
127
|
+
"What's included:",
|
|
128
|
+
' • Hono API server with TypeScript',
|
|
129
|
+
' • Vite + React 19 SPA',
|
|
130
|
+
' • Supabase self-hosted (Auth, Database, Storage, Realtime)',
|
|
131
|
+
' • PostgreSQL with Row Level Security',
|
|
132
|
+
' • Kong API Gateway + Supabase Studio',
|
|
133
|
+
' • Docker Compose for local development',
|
|
134
|
+
' • Kubernetes for production with autoscaling',
|
|
135
|
+
' • Traefik reverse proxy with automatic HTTPS',
|
|
136
|
+
' • Shadcn UI component library',
|
|
137
|
+
' • GitHub Actions CI/CD',
|
|
138
|
+
' • Automated backups',
|
|
139
|
+
].join('\n'),
|
|
140
|
+
positional: [
|
|
141
|
+
{
|
|
142
|
+
name: 'projectName',
|
|
143
|
+
optional: true,
|
|
144
|
+
description: 'Project directory name (skips the name prompt)',
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
flags: [
|
|
148
|
+
{ name: 'h', boolean: true, description: 'Show this help' },
|
|
149
|
+
{ name: 'v', boolean: true, description: 'Show version' },
|
|
150
|
+
{
|
|
151
|
+
name: 'y',
|
|
152
|
+
boolean: true,
|
|
153
|
+
description: 'Skip prompts (requires -admin-email and -admin-password)',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'git',
|
|
157
|
+
boolean: true,
|
|
158
|
+
description: 'Initialize a git repository (default: off; opt-in)',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
name: 'install',
|
|
162
|
+
boolean: true,
|
|
163
|
+
description: "Run the package manager's install during create (default: off)",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: 'skip-lockfile',
|
|
167
|
+
boolean: true,
|
|
168
|
+
description:
|
|
169
|
+
'Skip lockfile generation (faster; deploy will need `vibecarbon up` first to install)',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
name: 'pm',
|
|
173
|
+
value: '<name>',
|
|
174
|
+
enum: ['npm', 'pnpm', 'bun'],
|
|
175
|
+
description: 'Package manager to use (default: pnpm)',
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'admin-email',
|
|
179
|
+
value: '<email>',
|
|
180
|
+
description: 'Admin email for dashboard access (required with -y)',
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'admin-password',
|
|
184
|
+
value: '<pw>',
|
|
185
|
+
description: 'Admin password for dashboard access (required with -y)',
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
examples: [
|
|
189
|
+
{ command: 'vibecarbon create my-saas', description: 'create a project interactively' },
|
|
190
|
+
{
|
|
191
|
+
command:
|
|
192
|
+
'vibecarbon create my-saas -y -admin-email admin@example.com -admin-password secret123',
|
|
193
|
+
description: 'non-interactive (CI/CD)',
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
command: 'vibecarbon create my-saas -git -install',
|
|
197
|
+
description: 'create + initialize git + run pm install',
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
// ============================================================================
|
|
203
|
+
// UTILITY FUNCTIONS
|
|
204
|
+
// ============================================================================
|
|
205
|
+
|
|
206
|
+
function isCI() {
|
|
207
|
+
return (
|
|
208
|
+
process.env.CI === 'true' ||
|
|
209
|
+
process.env.CONTINUOUS_INTEGRATION === 'true' ||
|
|
210
|
+
process.env.GITHUB_ACTIONS === 'true' ||
|
|
211
|
+
process.env.GITLAB_CI === 'true' ||
|
|
212
|
+
process.env.CIRCLECI === 'true' ||
|
|
213
|
+
process.env.JENKINS_URL !== undefined
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function detectPackageManager() {
|
|
218
|
+
// Check user agent first (set by npm/pnpm/bun when running npx/pnpx/etc)
|
|
219
|
+
// Only detect pnpm/bun from user agent - if someone runs via npx, we still
|
|
220
|
+
// want to default to pnpm rather than assuming they want npm for their project
|
|
221
|
+
const ua = process.env.npm_config_user_agent || '';
|
|
222
|
+
if (ua.startsWith('pnpm')) return 'pnpm';
|
|
223
|
+
if (ua.startsWith('bun')) return 'bun';
|
|
224
|
+
|
|
225
|
+
// Check for lockfiles in cwd
|
|
226
|
+
if (existsSync('pnpm-lock.yaml')) return 'pnpm';
|
|
227
|
+
if (existsSync('bun.lock') || existsSync('bun.lockb')) return 'bun';
|
|
228
|
+
if (existsSync('package-lock.json')) return 'npm';
|
|
229
|
+
|
|
230
|
+
return 'pnpm'; // default
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function getInstallCommand(pm) {
|
|
234
|
+
switch (pm) {
|
|
235
|
+
case 'npm':
|
|
236
|
+
return 'npm install';
|
|
237
|
+
case 'bun':
|
|
238
|
+
return 'bun install';
|
|
239
|
+
default:
|
|
240
|
+
// Use --no-frozen-lockfile for new projects (no lockfile exists yet)
|
|
241
|
+
// This is needed in CI environments where pnpm defaults to frozen-lockfile
|
|
242
|
+
return 'pnpm install --no-frozen-lockfile';
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Recommended minimum versions for best experience
|
|
247
|
+
const RECOMMENDED_VERSIONS = {
|
|
248
|
+
npm: { min: 10, message: 'npm 10+ recommended for better performance and security' },
|
|
249
|
+
pnpm: { min: 9, message: 'pnpm 9+ recommended for better performance' },
|
|
250
|
+
bun: { min: 1.2, message: 'bun 1.2+ recommended (uses text-based bun.lock)' },
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
function checkPackageManagerVersion(pm) {
|
|
254
|
+
try {
|
|
255
|
+
const version = runCommand([pm, '--version'], {
|
|
256
|
+
encoding: 'utf-8',
|
|
257
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
258
|
+
}).trim();
|
|
259
|
+
const parts = version.split('.');
|
|
260
|
+
const major = parseInt(parts[0], 10);
|
|
261
|
+
const minor = parseInt(parts[1] || '0', 10);
|
|
262
|
+
const recommended = RECOMMENDED_VERSIONS[pm];
|
|
263
|
+
|
|
264
|
+
if (recommended) {
|
|
265
|
+
const reqMajor = Math.floor(recommended.min);
|
|
266
|
+
const reqMinor = Math.round((recommended.min - reqMajor) * 10);
|
|
267
|
+
if (major < reqMajor || (major === reqMajor && minor < reqMinor)) {
|
|
268
|
+
return {
|
|
269
|
+
current: version,
|
|
270
|
+
isOutdated: true,
|
|
271
|
+
message: recommended.message,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return { current: version, isOutdated: false };
|
|
276
|
+
} catch {
|
|
277
|
+
return { current: null, isOutdated: false };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const INSTALL_INSTRUCTIONS = {
|
|
282
|
+
pnpm: 'npm install -g pnpm',
|
|
283
|
+
bun: 'curl -fsSL https://bun.sh/install | bash',
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
function isPackageManagerInstalled(pm) {
|
|
287
|
+
try {
|
|
288
|
+
execFileSync(pm, ['--version'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
289
|
+
return true;
|
|
290
|
+
} catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function ensurePackageManagerInstalled(pm, skipPrompts = false) {
|
|
296
|
+
if (isPackageManagerInstalled(pm)) return pm;
|
|
297
|
+
|
|
298
|
+
const installCmd = INSTALL_INSTRUCTIONS[pm];
|
|
299
|
+
if (installCmd) {
|
|
300
|
+
p.log.warn(`${c.bold(pm)} is not installed. To install it, run:\n\n ${c.info(installCmd)}\n`);
|
|
301
|
+
} else {
|
|
302
|
+
p.log.warn(`${c.bold(pm)} is not installed.`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (skipPrompts) {
|
|
306
|
+
p.log.info(`Falling back to ${c.bold('npm')}.`);
|
|
307
|
+
return 'npm';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const fallback = await p.select({
|
|
311
|
+
message: 'Choose a package manager',
|
|
312
|
+
options: [
|
|
313
|
+
{ value: 'npm', label: 'npm', hint: 'already installed with Node.js' },
|
|
314
|
+
{ value: 'pnpm', label: 'pnpm' },
|
|
315
|
+
{ value: 'bun', label: 'bun' },
|
|
316
|
+
].filter((opt) => opt.value !== pm),
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
if (p.isCancel(fallback)) {
|
|
320
|
+
p.cancel('Operation cancelled.');
|
|
321
|
+
process.exit(0);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Recurse in case the fallback isn't installed either
|
|
325
|
+
return ensurePackageManagerInstalled(fallback);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function copyTemplate(templatePath, destPath, variables) {
|
|
329
|
+
const fullTemplatePath = join(TEMPLATE_DIR, templatePath);
|
|
330
|
+
|
|
331
|
+
if (!existsSync(fullTemplatePath)) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let content = readFileSync(fullTemplatePath, 'utf-8');
|
|
336
|
+
|
|
337
|
+
for (const [key, placeholder] of Object.entries(PLACEHOLDERS)) {
|
|
338
|
+
if (SECRET_PLACEHOLDERS.has(key)) continue;
|
|
339
|
+
if (variables[key] !== undefined) {
|
|
340
|
+
content = content.replaceAll(placeholder, variables[key]);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
345
|
+
writeFileSync(destPath, content);
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function copyTemplateDir(templateSubdir, destDir, variables, exclude = [], rawCopy = []) {
|
|
350
|
+
const fullTemplatePath = join(TEMPLATE_DIR, templateSubdir);
|
|
351
|
+
|
|
352
|
+
if (!existsSync(fullTemplatePath)) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const entries = readdirSync(fullTemplatePath, { withFileTypes: true });
|
|
357
|
+
|
|
358
|
+
for (const entry of entries) {
|
|
359
|
+
const destPath = join(destDir, entry.name);
|
|
360
|
+
|
|
361
|
+
if (exclude.includes(entry.name)) continue;
|
|
362
|
+
|
|
363
|
+
if (entry.isDirectory()) {
|
|
364
|
+
mkdirSync(destPath, { recursive: true });
|
|
365
|
+
copyTemplateDir(join(templateSubdir, entry.name), destPath, variables, exclude, rawCopy);
|
|
366
|
+
} else if (rawCopy.includes(entry.name)) {
|
|
367
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
368
|
+
copyFileSync(join(fullTemplatePath, entry.name), destPath);
|
|
369
|
+
} else {
|
|
370
|
+
copyTemplate(join(templateSubdir, entry.name), destPath, variables);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Validate that no unreplaced {{PLACEHOLDER}} patterns remain in generated files.
|
|
377
|
+
* Only checks for placeholders that should have been replaced during creation
|
|
378
|
+
* (those in PLACEHOLDERS minus SECRET_PLACEHOLDERS — the latter are intentionally
|
|
379
|
+
* left as placeholders for deploy-time substitution). Deploy-only placeholders
|
|
380
|
+
* like {{DOMAIN}}, {{ACME_EMAIL}} etc. are also expected to remain.
|
|
381
|
+
*
|
|
382
|
+
* Returns an array of { file, placeholders } objects for files with issues.
|
|
383
|
+
*/
|
|
384
|
+
function validatePlaceholders(projectDir, exclude = []) {
|
|
385
|
+
const issues = [];
|
|
386
|
+
|
|
387
|
+
// Build pattern to match only creation-time placeholders (secrets are
|
|
388
|
+
// deploy-time substituted, so they're expected to remain unreplaced).
|
|
389
|
+
const creationPlaceholders = Object.keys(PLACEHOLDERS).filter(
|
|
390
|
+
(key) => !SECRET_PLACEHOLDERS.has(key),
|
|
391
|
+
);
|
|
392
|
+
const placeholderPattern = new RegExp(`\\{\\{(${creationPlaceholders.join('|')})\\}\\}`, 'g');
|
|
393
|
+
|
|
394
|
+
// Files/directories to skip (binary files, lockfiles, etc.)
|
|
395
|
+
const skipPatterns = [
|
|
396
|
+
'node_modules',
|
|
397
|
+
'.git',
|
|
398
|
+
'pnpm-lock.yaml',
|
|
399
|
+
'package-lock.json',
|
|
400
|
+
|
|
401
|
+
'bun.lock',
|
|
402
|
+
'bun.lockb',
|
|
403
|
+
'.png',
|
|
404
|
+
'.jpg',
|
|
405
|
+
'.jpeg',
|
|
406
|
+
'.gif',
|
|
407
|
+
'.ico',
|
|
408
|
+
'.woff',
|
|
409
|
+
'.woff2',
|
|
410
|
+
'.ttf',
|
|
411
|
+
'.eot',
|
|
412
|
+
];
|
|
413
|
+
|
|
414
|
+
function scanDir(dir) {
|
|
415
|
+
if (!existsSync(dir)) return;
|
|
416
|
+
|
|
417
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
418
|
+
|
|
419
|
+
for (const entry of entries) {
|
|
420
|
+
const fullPath = join(dir, entry.name);
|
|
421
|
+
const relativePath = fullPath.replace(`${projectDir}/`, '');
|
|
422
|
+
|
|
423
|
+
// Skip excluded paths
|
|
424
|
+
if (exclude.includes(entry.name)) continue;
|
|
425
|
+
if (skipPatterns.some((p) => entry.name.includes(p) || entry.name.endsWith(p))) continue;
|
|
426
|
+
|
|
427
|
+
if (entry.isDirectory()) {
|
|
428
|
+
scanDir(fullPath);
|
|
429
|
+
} else {
|
|
430
|
+
try {
|
|
431
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
432
|
+
const matches = content.match(placeholderPattern);
|
|
433
|
+
if (matches) {
|
|
434
|
+
// Get unique placeholders
|
|
435
|
+
const uniquePlaceholders = [...new Set(matches)];
|
|
436
|
+
issues.push({ file: relativePath, placeholders: uniquePlaceholders });
|
|
437
|
+
}
|
|
438
|
+
} catch {
|
|
439
|
+
// Skip files that can't be read as text (binary files)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
scanDir(projectDir);
|
|
446
|
+
return issues;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Helper to allow UI updates between operations
|
|
450
|
+
function tick() {
|
|
451
|
+
return new Promise((resolve) => setTimeout(resolve, 400));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ============================================================================
|
|
455
|
+
// MAIN BOOTSTRAP FUNCTION
|
|
456
|
+
// ============================================================================
|
|
457
|
+
|
|
458
|
+
async function bootstrap(cliArgs) {
|
|
459
|
+
const { values, positional, errors } = parseFlags(cliArgs, SPEC);
|
|
460
|
+
|
|
461
|
+
if (errors.length > 0) {
|
|
462
|
+
for (const e of errors) {
|
|
463
|
+
process.stderr.write(`${c.error('✗')} ${e}\n`);
|
|
464
|
+
}
|
|
465
|
+
process.stderr.write(`Run ${c.info('vibecarbon create -h')} for usage.\n`);
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (values.h) {
|
|
470
|
+
process.stdout.write(renderHelp(SPEC));
|
|
471
|
+
process.exit(0);
|
|
472
|
+
}
|
|
473
|
+
if (values.v) {
|
|
474
|
+
console.log(`create-vibecarbon v${VERSION}`);
|
|
475
|
+
process.exit(0);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Build the legacy `args` shape that the orchestration code reads.
|
|
479
|
+
// -git is opt-in (default off), so noGit (the legacy field) is the
|
|
480
|
+
// INVERSE of values.git.
|
|
481
|
+
const args = {
|
|
482
|
+
projectName: positional.projectName || null,
|
|
483
|
+
yes: !!values.y,
|
|
484
|
+
install: !!values.install,
|
|
485
|
+
skipLockfile: !!values['skip-lockfile'],
|
|
486
|
+
noGit: !values.git,
|
|
487
|
+
packageManager: /** @type {string|null} */ (values.pm),
|
|
488
|
+
adminEmail: /** @type {string|null} */ (values['admin-email']),
|
|
489
|
+
adminPassword: /** @type {string|null} */ (values['admin-password']),
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const skipPrompts = args.yes || isCI();
|
|
493
|
+
|
|
494
|
+
// Start interactive UI
|
|
495
|
+
printBanner();
|
|
496
|
+
p.intro(`${c.bold('vibecarbon create')} ${c.dim(`v${VERSION}`)}`);
|
|
497
|
+
|
|
498
|
+
// Gather configuration.
|
|
499
|
+
// projectName is the basename; it's also the directory name created in cwd.
|
|
500
|
+
// Legacy behavior allowed paths like '../my-app'; we now require a bare
|
|
501
|
+
// basename so the generated project always lands in a predictable place
|
|
502
|
+
// and can't escape cwd (CVE-class path-traversal).
|
|
503
|
+
let projectName = args.projectName;
|
|
504
|
+
let packageManager = args.packageManager || detectPackageManager();
|
|
505
|
+
let initGit = !args.noGit;
|
|
506
|
+
let adminEmail = args.adminEmail;
|
|
507
|
+
let adminPassword = args.adminPassword;
|
|
508
|
+
|
|
509
|
+
if (!skipPrompts) {
|
|
510
|
+
// Interactive prompts
|
|
511
|
+
const project = await p.group(
|
|
512
|
+
{
|
|
513
|
+
name: () => {
|
|
514
|
+
if (projectName) return Promise.resolve(projectName);
|
|
515
|
+
return p.text({
|
|
516
|
+
message: 'What is your project name?',
|
|
517
|
+
placeholder: 'my-saas',
|
|
518
|
+
validate: (value) => {
|
|
519
|
+
const err = validateProjectName(value);
|
|
520
|
+
if (err) return err;
|
|
521
|
+
if (existsSync(join(process.cwd(), value))) {
|
|
522
|
+
return `Directory "${value}" already exists`;
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
},
|
|
527
|
+
adminEmail: () => {
|
|
528
|
+
if (adminEmail) return Promise.resolve(adminEmail);
|
|
529
|
+
return p.text({
|
|
530
|
+
message: 'Admin email (for all dashboard access):',
|
|
531
|
+
placeholder: 'admin@example.com',
|
|
532
|
+
validate: (value) => {
|
|
533
|
+
const err = validateAdminEmail(value);
|
|
534
|
+
if (err) return err;
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
},
|
|
538
|
+
adminPassword: () => {
|
|
539
|
+
if (adminPassword) return Promise.resolve(adminPassword);
|
|
540
|
+
return p.password({
|
|
541
|
+
message: 'Admin password (min 8 characters):',
|
|
542
|
+
validate: (value) => {
|
|
543
|
+
const err = validateAdminPassword(value);
|
|
544
|
+
if (err) return err;
|
|
545
|
+
},
|
|
546
|
+
});
|
|
547
|
+
},
|
|
548
|
+
packageManager: () => {
|
|
549
|
+
if (args.packageManager) return Promise.resolve(args.packageManager);
|
|
550
|
+
// Launch decision (2026-05-06): hide the interactive package-
|
|
551
|
+
// manager prompt and default to pnpm. The bun + npm code paths
|
|
552
|
+
// work (vibecarbon create --use-bun and --use-npm both produce
|
|
553
|
+
// green builds end-to-end), but pnpm has the most e2e-
|
|
554
|
+
// matrix coverage by far. Power users can still pick npm/bun
|
|
555
|
+
// via the CLI flags. Re-enable the prompt by flipping
|
|
556
|
+
// SHOW_PACKAGE_MANAGER_PROMPT back to true once bun/npm have
|
|
557
|
+
// matrix coverage parity.
|
|
558
|
+
if (!SHOW_PACKAGE_MANAGER_PROMPT) return Promise.resolve('pnpm');
|
|
559
|
+
return p.select({
|
|
560
|
+
message: 'Which package manager?',
|
|
561
|
+
options: [
|
|
562
|
+
{ value: 'pnpm', label: 'pnpm', hint: 'recommended' },
|
|
563
|
+
{ value: 'npm', label: 'npm' },
|
|
564
|
+
{ value: 'bun', label: 'bun' },
|
|
565
|
+
],
|
|
566
|
+
initialValue: packageManager,
|
|
567
|
+
});
|
|
568
|
+
},
|
|
569
|
+
git: () => {
|
|
570
|
+
if (args.noGit) return Promise.resolve(false);
|
|
571
|
+
return p.confirm({
|
|
572
|
+
message: 'Initialize a git repository for version control?',
|
|
573
|
+
initialValue: true,
|
|
574
|
+
});
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
onCancel: () => {
|
|
579
|
+
p.cancel('Operation cancelled.');
|
|
580
|
+
process.exit(0);
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
projectName = project.name;
|
|
586
|
+
adminEmail = project.adminEmail;
|
|
587
|
+
adminPassword = project.adminPassword;
|
|
588
|
+
packageManager = project.packageManager;
|
|
589
|
+
initGit = project.git;
|
|
590
|
+
} else {
|
|
591
|
+
// Non-interactive validation
|
|
592
|
+
if (!projectName) {
|
|
593
|
+
p.log.error('Project name required. Usage: npx create-vibecarbon <project-name> [options]');
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
596
|
+
const nameErr = validateProjectName(projectName);
|
|
597
|
+
if (nameErr) {
|
|
598
|
+
p.log.error(`Invalid project name "${projectName}": ${nameErr}`);
|
|
599
|
+
process.exit(1);
|
|
600
|
+
}
|
|
601
|
+
if (existsSync(join(process.cwd(), projectName))) {
|
|
602
|
+
p.log.error(`Directory "${projectName}" already exists.`);
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
// Require admin credentials in non-interactive mode
|
|
606
|
+
if (!adminEmail) {
|
|
607
|
+
p.log.error('Admin email required. Use --admin-email=you@example.com');
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
if (!adminPassword) {
|
|
611
|
+
p.log.error('Admin password required. Use --admin-password=yourpassword');
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
const emailErr = validateAdminEmail(adminEmail);
|
|
615
|
+
if (emailErr) {
|
|
616
|
+
p.log.error(`Invalid --admin-email "${adminEmail}": ${emailErr}`);
|
|
617
|
+
process.exit(1);
|
|
618
|
+
}
|
|
619
|
+
const pwErr = validateAdminPassword(adminPassword);
|
|
620
|
+
if (pwErr) {
|
|
621
|
+
p.log.error(`Invalid --admin-password: ${pwErr}`);
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// projectDir is the full path where files are created.
|
|
627
|
+
// projectName is also the directory name since we require basenames only.
|
|
628
|
+
const projectDir = join(process.cwd(), projectName);
|
|
629
|
+
|
|
630
|
+
// Show configuration
|
|
631
|
+
p.note(
|
|
632
|
+
[
|
|
633
|
+
`Project: ${projectName}`,
|
|
634
|
+
`Admin: ${adminEmail}`,
|
|
635
|
+
`Package manager: ${packageManager}`,
|
|
636
|
+
`Git: ${initGit ? 'yes' : 'no'}`,
|
|
637
|
+
].join('\n'),
|
|
638
|
+
'Configuration',
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
// Verify package manager is installed
|
|
642
|
+
packageManager = await ensurePackageManagerInstalled(packageManager, skipPrompts);
|
|
643
|
+
|
|
644
|
+
// Check for outdated package manager and warn
|
|
645
|
+
const versionCheck = checkPackageManagerVersion(packageManager);
|
|
646
|
+
if (versionCheck.isOutdated) {
|
|
647
|
+
p.log.warn(
|
|
648
|
+
`${c.dim(`${packageManager} ${versionCheck.current} detected.`)} ${versionCheck.message}`,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Generate secure secrets
|
|
653
|
+
const dbPassword = generatePassword();
|
|
654
|
+
const jwtSecret = generatePassword(64);
|
|
655
|
+
const realtimeSecret = generatePassword(64); // SECRET_KEY_BASE (min 64 chars)
|
|
656
|
+
const logflareApiKey = generatePassword(32);
|
|
657
|
+
const vaultEncKey = generatePassword(16); // VAULT_ENC_KEY (exactly 32 hex chars = 16 bytes)
|
|
658
|
+
const pgMetaCryptoKey = generatePassword(32); // PG_META_CRYPTO_KEY (min 32 chars)
|
|
659
|
+
const dbEncKey = generatePassword(16); // DB_ENC_KEY for Realtime aes_128_ecb (must be exactly 16 bytes)
|
|
660
|
+
const replPassword = generateReplPassword();
|
|
661
|
+
const anonKey = generateJWT(jwtSecret, {
|
|
662
|
+
role: 'anon',
|
|
663
|
+
iss: 'supabase',
|
|
664
|
+
ref: projectName,
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
const serviceRoleKey = generateJWT(jwtSecret, {
|
|
668
|
+
role: 'service_role',
|
|
669
|
+
iss: 'supabase',
|
|
670
|
+
ref: projectName,
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
const variables = {
|
|
674
|
+
PROJECT_NAME: projectName,
|
|
675
|
+
GITHUB_OWNER: projectName, // Default to project name; rewritten by `vibecarbon configure` → CI/CD (or by deploy's rewriteOwnerAndRepo fallback)
|
|
676
|
+
DB_PASSWORD: dbPassword,
|
|
677
|
+
JWT_SECRET: jwtSecret,
|
|
678
|
+
ANON_KEY: anonKey,
|
|
679
|
+
SERVICE_ROLE_KEY: serviceRoleKey,
|
|
680
|
+
REALTIME_SECRET: realtimeSecret,
|
|
681
|
+
LOGFLARE_API_KEY: logflareApiKey,
|
|
682
|
+
VAULT_ENC_KEY: vaultEncKey,
|
|
683
|
+
PG_META_CRYPTO_KEY: pgMetaCryptoKey,
|
|
684
|
+
DB_ENC_KEY: dbEncKey,
|
|
685
|
+
REPL_PASSWORD: replPassword,
|
|
686
|
+
SITE_URL: 'http://localhost:5173',
|
|
687
|
+
ADMIN_EMAIL: adminEmail,
|
|
688
|
+
ADMIN_PASSWORD: adminPassword || '',
|
|
689
|
+
ADMIN_PASSWORD_HASH: adminPassword ? hashPassword(adminPassword) : '',
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
// Create project
|
|
693
|
+
const tracker = createTracker('create', { project: projectName });
|
|
694
|
+
const s = tracker.spinner();
|
|
695
|
+
|
|
696
|
+
s.start('Creating project structure');
|
|
697
|
+
mkdirSync(projectDir, { recursive: true });
|
|
698
|
+
|
|
699
|
+
const dirs = [
|
|
700
|
+
'src/server/routes/api',
|
|
701
|
+
'src/server/routes/_internal',
|
|
702
|
+
'src/server/routes/v1',
|
|
703
|
+
'src/server/lib',
|
|
704
|
+
'src/client/components/auth',
|
|
705
|
+
'src/client/components/ui',
|
|
706
|
+
'src/client/pages',
|
|
707
|
+
'src/client/lib',
|
|
708
|
+
'src/shared',
|
|
709
|
+
'supabase/migrations',
|
|
710
|
+
'db',
|
|
711
|
+
'public',
|
|
712
|
+
'backup',
|
|
713
|
+
'volumes/kong',
|
|
714
|
+
'volumes/db',
|
|
715
|
+
'volumes/traefik',
|
|
716
|
+
'volumes/prometheus',
|
|
717
|
+
'volumes/loki',
|
|
718
|
+
'volumes/promtail',
|
|
719
|
+
'volumes/grafana/provisioning/dashboards',
|
|
720
|
+
'volumes/grafana/provisioning/datasources',
|
|
721
|
+
'volumes/grafana/dashboards',
|
|
722
|
+
'volumes/grafana/dashboards.dev',
|
|
723
|
+
'.github/workflows',
|
|
724
|
+
'k8s/base/app',
|
|
725
|
+
'k8s/base/backup',
|
|
726
|
+
'k8s/base/config',
|
|
727
|
+
'k8s/base/hetzner-ccm',
|
|
728
|
+
'k8s/base/hetzner-csi',
|
|
729
|
+
'k8s/base/traefik',
|
|
730
|
+
'k8s/infra/cert-manager-resources',
|
|
731
|
+
'k8s/infra/traefik-crds',
|
|
732
|
+
'k8s/overlays/production',
|
|
733
|
+
'k8s/overlays/local',
|
|
734
|
+
'k8s/flux/clusters/primary',
|
|
735
|
+
'k8s/flux/clusters/standby',
|
|
736
|
+
'k8s/gitops/supabase',
|
|
737
|
+
'k8s/gitops/cert-manager-webhook-hetzner',
|
|
738
|
+
'k8s/values',
|
|
739
|
+
];
|
|
740
|
+
for (const dir of dirs) {
|
|
741
|
+
mkdirSync(join(projectDir, dir), { recursive: true });
|
|
742
|
+
}
|
|
743
|
+
await tick();
|
|
744
|
+
|
|
745
|
+
s.message('Generating configuration files');
|
|
746
|
+
copyTemplate('package.json', join(projectDir, 'package.json'), variables);
|
|
747
|
+
// Lock files are generated by the package manager during install — don't copy stale ones
|
|
748
|
+
copyTemplate('tsconfig.json', join(projectDir, 'tsconfig.json'), variables);
|
|
749
|
+
copyTemplate('tsconfig.server.json', join(projectDir, 'tsconfig.server.json'), variables);
|
|
750
|
+
copyTemplate('vite.config.ts', join(projectDir, 'vite.config.ts'), variables);
|
|
751
|
+
copyTemplate('biome.json', join(projectDir, 'biome.json'), variables);
|
|
752
|
+
copyTemplate('components.json', join(projectDir, 'components.json'), variables);
|
|
753
|
+
|
|
754
|
+
// Test scaffolding — three-tier vitest setup + helpers + golden seed tests.
|
|
755
|
+
// See carbon/TESTING.md for the tier conventions; the test-maintainer
|
|
756
|
+
// subagent extends from this seed when writing new tests.
|
|
757
|
+
copyTemplate('vitest.config.ts', join(projectDir, 'vitest.config.ts'), variables);
|
|
758
|
+
copyTemplate('tsconfig.test.json', join(projectDir, 'tsconfig.test.json'), variables);
|
|
759
|
+
copyTemplate('TESTING.md', join(projectDir, 'TESTING.md'), variables);
|
|
760
|
+
copyTemplateDir('tests', join(projectDir, 'tests'), variables);
|
|
761
|
+
// Copy .env.example WITHOUT replacing placeholders (it's committed to git as documentation)
|
|
762
|
+
const envExampleContent = readFileSync(join(TEMPLATE_DIR, '.env.example'), 'utf-8');
|
|
763
|
+
writeFileSync(join(projectDir, '.env.example'), envExampleContent);
|
|
764
|
+
const envLocal = generateEnvLocal(projectName, variables);
|
|
765
|
+
writeFileSync(join(projectDir, '.env.local'), envLocal, { mode: 0o600 });
|
|
766
|
+
// Also create .env for docker-compose (reads .env by default)
|
|
767
|
+
writeFileSync(join(projectDir, '.env'), envLocal, { mode: 0o600 });
|
|
768
|
+
await tick();
|
|
769
|
+
|
|
770
|
+
s.message('Generating Docker configuration');
|
|
771
|
+
copyTemplate('Dockerfile', join(projectDir, 'Dockerfile'), variables);
|
|
772
|
+
adaptDockerfileForPackageManager(projectDir, packageManager);
|
|
773
|
+
copyTemplate('docker-entrypoint.sh', join(projectDir, 'docker-entrypoint.sh'), variables);
|
|
774
|
+
copyTemplate('.dockerignore', join(projectDir, '.dockerignore'), variables);
|
|
775
|
+
copyTemplate('docker-compose.yml', join(projectDir, 'docker-compose.yml'), variables);
|
|
776
|
+
copyTemplate('docker-compose.prod.yml', join(projectDir, 'docker-compose.prod.yml'), variables);
|
|
777
|
+
copyTemplate(
|
|
778
|
+
'docker-compose.observability.yml',
|
|
779
|
+
join(projectDir, 'docker-compose.observability.yml'),
|
|
780
|
+
variables,
|
|
781
|
+
);
|
|
782
|
+
copyTemplate(
|
|
783
|
+
'docker-compose.override.yml',
|
|
784
|
+
join(projectDir, 'docker-compose.override.yml'),
|
|
785
|
+
variables,
|
|
786
|
+
);
|
|
787
|
+
copyTemplate(
|
|
788
|
+
'docker-compose.observability.override.yml',
|
|
789
|
+
join(projectDir, 'docker-compose.observability.override.yml'),
|
|
790
|
+
variables,
|
|
791
|
+
);
|
|
792
|
+
copyTemplate('PRODUCTION.md', join(projectDir, 'PRODUCTION.md'), variables);
|
|
793
|
+
copyTemplate('DEVELOPMENT.md', join(projectDir, 'DEVELOPMENT.md'), variables);
|
|
794
|
+
copyTemplate(
|
|
795
|
+
'docker-compose.dev-init.yml',
|
|
796
|
+
join(projectDir, 'docker-compose.dev-init.yml'),
|
|
797
|
+
variables,
|
|
798
|
+
);
|
|
799
|
+
copyTemplate(
|
|
800
|
+
'docker-compose.observability.prod.yml',
|
|
801
|
+
join(projectDir, 'docker-compose.observability.prod.yml'),
|
|
802
|
+
variables,
|
|
803
|
+
);
|
|
804
|
+
chmodSync(join(projectDir, 'docker-entrypoint.sh'), 0o755);
|
|
805
|
+
|
|
806
|
+
await tick();
|
|
807
|
+
|
|
808
|
+
s.message('Generating backend files');
|
|
809
|
+
copyTemplate('volumes/kong/kong.yml', join(projectDir, 'volumes/kong/kong.yml'), variables);
|
|
810
|
+
copyTemplate(
|
|
811
|
+
'volumes/kong/docker-entrypoint.sh',
|
|
812
|
+
join(projectDir, 'volumes/kong/docker-entrypoint.sh'),
|
|
813
|
+
variables,
|
|
814
|
+
);
|
|
815
|
+
copyTemplate('volumes/db/roles.sql', join(projectDir, 'volumes/db/roles.sql'), variables);
|
|
816
|
+
copyTemplate(
|
|
817
|
+
'volumes/db/set-passwords.sh',
|
|
818
|
+
join(projectDir, 'volumes/db/set-passwords.sh'),
|
|
819
|
+
variables,
|
|
820
|
+
);
|
|
821
|
+
copyTemplate('volumes/db/jwt.sql', join(projectDir, 'volumes/db/jwt.sql'), variables);
|
|
822
|
+
copyTemplate('volumes/db/realtime.sql', join(projectDir, 'volumes/db/realtime.sql'), variables);
|
|
823
|
+
copyTemplate(
|
|
824
|
+
'volumes/db/super-admin.sql',
|
|
825
|
+
join(projectDir, 'volumes/db/super-admin.sql'),
|
|
826
|
+
variables,
|
|
827
|
+
);
|
|
828
|
+
copyTemplate(
|
|
829
|
+
'volumes/db/super-admin.dev.sql',
|
|
830
|
+
join(projectDir, 'volumes/db/super-admin.dev.sql'),
|
|
831
|
+
variables,
|
|
832
|
+
);
|
|
833
|
+
copyTemplate(
|
|
834
|
+
'volumes/traefik/middlewares.yml',
|
|
835
|
+
join(projectDir, 'volumes/traefik/middlewares.yml'),
|
|
836
|
+
variables,
|
|
837
|
+
);
|
|
838
|
+
copyTemplate(
|
|
839
|
+
'volumes/traefik/middlewares.dev.yml',
|
|
840
|
+
join(projectDir, 'volumes/traefik/middlewares.dev.yml'),
|
|
841
|
+
variables,
|
|
842
|
+
);
|
|
843
|
+
copyTemplate(
|
|
844
|
+
'volumes/traefik/vite-dev.yml',
|
|
845
|
+
join(projectDir, 'volumes/traefik/vite-dev.yml'),
|
|
846
|
+
variables,
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
// Observability config files (used by deploy --observability)
|
|
850
|
+
copyTemplate(
|
|
851
|
+
'volumes/prometheus/prometheus.yml',
|
|
852
|
+
join(projectDir, 'volumes/prometheus/prometheus.yml'),
|
|
853
|
+
variables,
|
|
854
|
+
);
|
|
855
|
+
copyTemplate(
|
|
856
|
+
'volumes/loki/loki-config.yml',
|
|
857
|
+
join(projectDir, 'volumes/loki/loki-config.yml'),
|
|
858
|
+
variables,
|
|
859
|
+
);
|
|
860
|
+
copyTemplate(
|
|
861
|
+
'volumes/promtail/promtail-config.yml',
|
|
862
|
+
join(projectDir, 'volumes/promtail/promtail-config.yml'),
|
|
863
|
+
variables,
|
|
864
|
+
);
|
|
865
|
+
copyTemplate(
|
|
866
|
+
'volumes/grafana/provisioning/dashboards/dashboards.yml',
|
|
867
|
+
join(projectDir, 'volumes/grafana/provisioning/dashboards/dashboards.yml'),
|
|
868
|
+
variables,
|
|
869
|
+
);
|
|
870
|
+
copyTemplate(
|
|
871
|
+
'volumes/grafana/provisioning/dashboards/dashboards.dev.yml',
|
|
872
|
+
join(projectDir, 'volumes/grafana/provisioning/dashboards/dashboards.dev.yml'),
|
|
873
|
+
variables,
|
|
874
|
+
);
|
|
875
|
+
copyTemplate(
|
|
876
|
+
'volumes/grafana/provisioning/datasources/datasources.yml',
|
|
877
|
+
join(projectDir, 'volumes/grafana/provisioning/datasources/datasources.yml'),
|
|
878
|
+
variables,
|
|
879
|
+
);
|
|
880
|
+
copyTemplate(
|
|
881
|
+
'volumes/grafana/dashboards/logs.json',
|
|
882
|
+
join(projectDir, 'volumes/grafana/dashboards/logs.json'),
|
|
883
|
+
variables,
|
|
884
|
+
);
|
|
885
|
+
copyTemplate(
|
|
886
|
+
'volumes/grafana/dashboards/overview.json',
|
|
887
|
+
join(projectDir, 'volumes/grafana/dashboards/overview.json'),
|
|
888
|
+
variables,
|
|
889
|
+
);
|
|
890
|
+
copyTemplate(
|
|
891
|
+
'volumes/grafana/dashboards/postgresql.json',
|
|
892
|
+
join(projectDir, 'volumes/grafana/dashboards/postgresql.json'),
|
|
893
|
+
variables,
|
|
894
|
+
);
|
|
895
|
+
copyTemplate(
|
|
896
|
+
'volumes/grafana/dashboards.dev/logs.json',
|
|
897
|
+
join(projectDir, 'volumes/grafana/dashboards.dev/logs.json'),
|
|
898
|
+
variables,
|
|
899
|
+
);
|
|
900
|
+
copyTemplate(
|
|
901
|
+
'volumes/grafana/dashboards.dev/overview.json',
|
|
902
|
+
join(projectDir, 'volumes/grafana/dashboards.dev/overview.json'),
|
|
903
|
+
variables,
|
|
904
|
+
);
|
|
905
|
+
copyTemplate(
|
|
906
|
+
'volumes/grafana/dashboards.dev/postgresql.json',
|
|
907
|
+
join(projectDir, 'volumes/grafana/dashboards.dev/postgresql.json'),
|
|
908
|
+
variables,
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
copyTemplate('backup/backup.sh', join(projectDir, 'backup/backup.sh'), variables);
|
|
912
|
+
copyTemplate('backup/compose-backup.sh', join(projectDir, 'backup/compose-backup.sh'), variables);
|
|
913
|
+
copyTemplate('backup/Dockerfile', join(projectDir, 'backup/Dockerfile'), variables);
|
|
914
|
+
chmodSync(join(projectDir, 'backup/backup.sh'), 0o755);
|
|
915
|
+
chmodSync(join(projectDir, 'backup/compose-backup.sh'), 0o755);
|
|
916
|
+
// CI/CD workflow files are NOT shipped at create time — `vibecarbon
|
|
917
|
+
// configure` → CI/CD installs them from src/lib/ci-setup.js when the user
|
|
918
|
+
// explicitly opts in. Direct deploy is the default; shipping the workflow
|
|
919
|
+
// files in fresh projects would silently flip ciAvailable() and force
|
|
920
|
+
// every deploy through GitHub Actions.
|
|
921
|
+
copyTemplate(
|
|
922
|
+
'supabase/migrations/00001_init.sql',
|
|
923
|
+
join(projectDir, 'supabase/migrations/00001_init.sql'),
|
|
924
|
+
variables,
|
|
925
|
+
);
|
|
926
|
+
copyTemplate('supabase/seed.sql', join(projectDir, 'supabase/seed.sql'), variables);
|
|
927
|
+
// Copy entire src directory (server, client, shared)
|
|
928
|
+
copyTemplateDir('src', join(projectDir, 'src'), variables);
|
|
929
|
+
|
|
930
|
+
// Blog content
|
|
931
|
+
copyTemplateDir('content', join(projectDir, 'content'), variables);
|
|
932
|
+
|
|
933
|
+
// Custom database files (WAL-G Dockerfile)
|
|
934
|
+
copyTemplateDir('db', join(projectDir, 'db'), variables);
|
|
935
|
+
|
|
936
|
+
// Kubernetes manifests (exclude optional features like n8n - they're added via `vibecarbon add`)
|
|
937
|
+
copyTemplateDir('k8s', join(projectDir, 'k8s'), variables, ['n8n']);
|
|
938
|
+
|
|
939
|
+
// Scripts (manifest-based docker/k8s commands)
|
|
940
|
+
// generate-dev-configs.sh contains {{PLACEHOLDER}} tokens as its own substitution keys,
|
|
941
|
+
// so it must be copied verbatim to avoid leaking real secrets into the script.
|
|
942
|
+
copyTemplateDir(
|
|
943
|
+
'scripts',
|
|
944
|
+
join(projectDir, 'scripts'),
|
|
945
|
+
variables,
|
|
946
|
+
[],
|
|
947
|
+
['generate-dev-configs.sh'],
|
|
948
|
+
);
|
|
949
|
+
|
|
950
|
+
// Make shell scripts executable
|
|
951
|
+
const shellScripts = [
|
|
952
|
+
'k8s/test-local.sh',
|
|
953
|
+
'backup/backup.sh',
|
|
954
|
+
'docker-entrypoint.sh',
|
|
955
|
+
'volumes/kong/docker-entrypoint.sh',
|
|
956
|
+
'volumes/db/set-passwords.sh',
|
|
957
|
+
];
|
|
958
|
+
for (const script of shellScripts) {
|
|
959
|
+
const scriptPath = join(projectDir, script);
|
|
960
|
+
if (existsSync(scriptPath)) {
|
|
961
|
+
chmodSync(scriptPath, 0o755);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
await tick();
|
|
965
|
+
|
|
966
|
+
// Validate no unreplaced placeholders remain
|
|
967
|
+
s.message('Validating generated files');
|
|
968
|
+
const placeholderIssues = validatePlaceholders(projectDir, ['generate-dev-configs.sh', 'agents']);
|
|
969
|
+
if (placeholderIssues.length > 0) {
|
|
970
|
+
s.stop('Validation warning');
|
|
971
|
+
p.log.warn('Some files contain unreplaced placeholders:');
|
|
972
|
+
for (const issue of placeholderIssues.slice(0, 5)) {
|
|
973
|
+
p.log.warn(` ${c.dim(issue.file)}: ${issue.placeholders.join(', ')}`);
|
|
974
|
+
}
|
|
975
|
+
if (placeholderIssues.length > 5) {
|
|
976
|
+
p.log.warn(` ... and ${placeholderIssues.length - 5} more files`);
|
|
977
|
+
}
|
|
978
|
+
p.log.warn('This may indicate a bug in the template. Please report this issue.');
|
|
979
|
+
}
|
|
980
|
+
await tick();
|
|
981
|
+
|
|
982
|
+
s.message('Generating documentation and AI rules');
|
|
983
|
+
const readme = generateReadme(projectName, packageManager);
|
|
984
|
+
writeFileSync(join(projectDir, 'README.md'), readme);
|
|
985
|
+
|
|
986
|
+
// AI development experience files
|
|
987
|
+
copyTemplate('AGENTS.md', join(projectDir, 'AGENTS.md'), variables);
|
|
988
|
+
copyTemplate('CLAUDE.md', join(projectDir, 'CLAUDE.md'), variables);
|
|
989
|
+
copyTemplate('.windsurfrules', join(projectDir, '.windsurfrules'), variables);
|
|
990
|
+
copyTemplate(
|
|
991
|
+
'.github/copilot-instructions.md',
|
|
992
|
+
join(projectDir, '.github/copilot-instructions.md'),
|
|
993
|
+
variables,
|
|
994
|
+
);
|
|
995
|
+
mkdirSync(join(projectDir, '.cursor', 'rules'), { recursive: true });
|
|
996
|
+
copyTemplate(
|
|
997
|
+
'.cursor/rules/vibecarbon.mdc',
|
|
998
|
+
join(projectDir, '.cursor/rules/vibecarbon.mdc'),
|
|
999
|
+
variables,
|
|
1000
|
+
);
|
|
1001
|
+
|
|
1002
|
+
// Claude Code subagents — copy from repo root, stripping auto-generated memory footer
|
|
1003
|
+
const agentsSourceDir = join(__dirname, '..', '.claude', 'agents');
|
|
1004
|
+
if (existsSync(agentsSourceDir)) {
|
|
1005
|
+
const agentsDestDir = join(projectDir, '.claude', 'agents');
|
|
1006
|
+
mkdirSync(agentsDestDir, { recursive: true });
|
|
1007
|
+
for (const file of readdirSync(agentsSourceDir)) {
|
|
1008
|
+
if (!file.endsWith('.md')) continue;
|
|
1009
|
+
let content = readFileSync(join(agentsSourceDir, file), 'utf-8');
|
|
1010
|
+
// Strip the auto-generated "Persistent Agent Memory" section — Claude Code
|
|
1011
|
+
// re-injects it at runtime based on the `memory:` frontmatter field
|
|
1012
|
+
content = content.replace(/\n# Persistent Agent Memory[\s\S]*$/, '\n');
|
|
1013
|
+
writeFileSync(join(agentsDestDir, file), content);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Copy .claude/settings.json (agent team config + hooks)
|
|
1018
|
+
const settingsSource = join(TEMPLATE_DIR, '.claude', 'settings.json');
|
|
1019
|
+
if (existsSync(settingsSource)) {
|
|
1020
|
+
const settingsDest = join(projectDir, '.claude', 'settings.json');
|
|
1021
|
+
mkdirSync(dirname(settingsDest), { recursive: true });
|
|
1022
|
+
copyTemplate(join('.claude', 'settings.json'), settingsDest, variables);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// Copy .claude/hooks/
|
|
1026
|
+
const hooksSourceDir = join(TEMPLATE_DIR, '.claude', 'hooks');
|
|
1027
|
+
if (existsSync(hooksSourceDir)) {
|
|
1028
|
+
const hooksDestDir = join(projectDir, '.claude', 'hooks');
|
|
1029
|
+
mkdirSync(hooksDestDir, { recursive: true });
|
|
1030
|
+
for (const file of readdirSync(hooksSourceDir)) {
|
|
1031
|
+
copyTemplate(join('.claude', 'hooks', file), join(hooksDestDir, file), variables);
|
|
1032
|
+
}
|
|
1033
|
+
for (const file of readdirSync(hooksDestDir)) {
|
|
1034
|
+
if (file.endsWith('.sh')) chmodSync(join(hooksDestDir, file), 0o755);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Create agent memory directories
|
|
1039
|
+
for (const agent of [
|
|
1040
|
+
'backend-engineer',
|
|
1041
|
+
'frontend-engineer',
|
|
1042
|
+
'security-reviewer',
|
|
1043
|
+
'test-maintainer',
|
|
1044
|
+
'lead-coordinator',
|
|
1045
|
+
]) {
|
|
1046
|
+
mkdirSync(join(projectDir, '.claude', 'agent-memory', agent), { recursive: true });
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Update package.json with project name and package manager
|
|
1050
|
+
const packageJson = JSON.parse(readFileSync(join(projectDir, 'package.json'), 'utf-8'));
|
|
1051
|
+
packageJson.packageManager = getPackageManagerVersion(packageManager);
|
|
1052
|
+
packageJson.name = projectName;
|
|
1053
|
+
|
|
1054
|
+
// Replace pnpm references in scripts with the selected package manager
|
|
1055
|
+
if (packageManager !== 'pnpm' && packageJson.scripts) {
|
|
1056
|
+
const run = packageManager === 'npm' ? 'npm run' : packageManager;
|
|
1057
|
+
for (const [key, value] of Object.entries(packageJson.scripts)) {
|
|
1058
|
+
if (typeof value === 'string' && value.includes('pnpm ')) {
|
|
1059
|
+
packageJson.scripts[key] = value.replaceAll('pnpm ', `${run} `);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
writeFileSync(join(projectDir, 'package.json'), `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
1065
|
+
|
|
1066
|
+
// Deps are deferred to `vibecarbon up` — it auto-installs on first run if
|
|
1067
|
+
// node_modules is missing. Keeps `create` fast (e2e saw ~35s shaved
|
|
1068
|
+
// when running many `create` calls in parallel) and avoids running the
|
|
1069
|
+
// wrong PM against an empty tree. Opt-in via --install if you want the old
|
|
1070
|
+
// behavior.
|
|
1071
|
+
if (args.install) {
|
|
1072
|
+
s.message(`Installing dependencies with ${packageManager}`);
|
|
1073
|
+
await tick();
|
|
1074
|
+
const installCmd = getInstallCommand(packageManager);
|
|
1075
|
+
if (!runCommand(installCmd, { cwd: projectDir, cleanEnv: true })) {
|
|
1076
|
+
s.stop('Failed to install dependencies');
|
|
1077
|
+
p.log.error('Installation failed. You can try running the install command manually.');
|
|
1078
|
+
process.exit(1);
|
|
1079
|
+
}
|
|
1080
|
+
await tick();
|
|
1081
|
+
} else if (args.skipLockfile) {
|
|
1082
|
+
// Caller opted out of lockfile gen — they'll run `vibecarbon up` (or
|
|
1083
|
+
// the install manually) before `deploy`. Used by tests that only
|
|
1084
|
+
// verify scaffolding, and by scripted CI flows that install later.
|
|
1085
|
+
} else {
|
|
1086
|
+
// Generate the lockfile so the Dockerfile's `COPY package.json
|
|
1087
|
+
// <lockfile> ./` step has something to copy. Without this, going from
|
|
1088
|
+
// `vibecarbon create` straight to `vibecarbon deploy` (skipping `up`)
|
|
1089
|
+
// fails the docker build at the COPY step. `vibecarbon up` would
|
|
1090
|
+
// generate the lockfile too, but we shouldn't make `up` a hard
|
|
1091
|
+
// prerequisite for `deploy`.
|
|
1092
|
+
//
|
|
1093
|
+
// pnpm/npm have lockfile-only modes that skip node_modules (fast,
|
|
1094
|
+
// ~2s). Bun has no equivalent — `bun install` always touches
|
|
1095
|
+
// node_modules — but bun installs fast enough that the cost is
|
|
1096
|
+
// acceptable for create. Either way, `vibecarbon up` later sees
|
|
1097
|
+
// node_modules present and skips re-install.
|
|
1098
|
+
let lockfileCmd = null;
|
|
1099
|
+
let lockfileMsg = null;
|
|
1100
|
+
if (packageManager === 'pnpm') {
|
|
1101
|
+
lockfileCmd = ['pnpm', 'install', '--lockfile-only'];
|
|
1102
|
+
lockfileMsg = 'Generating pnpm-lock.yaml (lockfile only)';
|
|
1103
|
+
} else if (packageManager === 'npm') {
|
|
1104
|
+
lockfileCmd = ['npm', 'install', '--package-lock-only'];
|
|
1105
|
+
lockfileMsg = 'Generating package-lock.json (lockfile only)';
|
|
1106
|
+
} else if (packageManager === 'bun') {
|
|
1107
|
+
lockfileCmd = ['bun', 'install'];
|
|
1108
|
+
lockfileMsg = 'Generating bun.lock (full install — bun has no lockfile-only mode)';
|
|
1109
|
+
}
|
|
1110
|
+
if (lockfileCmd) {
|
|
1111
|
+
s.message(lockfileMsg);
|
|
1112
|
+
await tick();
|
|
1113
|
+
// Non-fatal if this fails — CI will regenerate or the user can install.
|
|
1114
|
+
runCommand(lockfileCmd, {
|
|
1115
|
+
cwd: projectDir,
|
|
1116
|
+
cleanEnv: true,
|
|
1117
|
+
ignoreError: true,
|
|
1118
|
+
});
|
|
1119
|
+
await tick();
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Initialize git
|
|
1124
|
+
if (initGit) {
|
|
1125
|
+
s.message('Initializing git repository');
|
|
1126
|
+
copyTemplate('.gitignore', join(projectDir, '.gitignore'), variables);
|
|
1127
|
+
|
|
1128
|
+
const missingPatterns = validateGitignore(join(projectDir, '.gitignore'));
|
|
1129
|
+
if (missingPatterns.length > 0) {
|
|
1130
|
+
p.log.error(
|
|
1131
|
+
`Generated .gitignore is missing required patterns: ${missingPatterns.join(', ')}`,
|
|
1132
|
+
);
|
|
1133
|
+
process.exit(1);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
runCommand('git init', { cwd: projectDir, cleanEnv: true });
|
|
1137
|
+
|
|
1138
|
+
// Install pre-commit hook. Two responsibilities:
|
|
1139
|
+
// 1. Lint staged code (catches biome/format issues early).
|
|
1140
|
+
// 2. Run vibecarbon's secret scanner against staged files. The
|
|
1141
|
+
// scanner script lives at scripts/secret-scan.mjs (copied from
|
|
1142
|
+
// the template) so this hook works without the vibecarbon CLI
|
|
1143
|
+
// installed globally — every clone of the project gets the
|
|
1144
|
+
// same protection on a fresh `git init`.
|
|
1145
|
+
//
|
|
1146
|
+
// We don't install lefthook here because most projects already have
|
|
1147
|
+
// a pre-commit setup of their own; a plain shell hook is simpler
|
|
1148
|
+
// and harder to break. If a contributor wants a richer hook
|
|
1149
|
+
// manager, lefthook.yml in the template gives them a starting
|
|
1150
|
+
// point — `npx lefthook install` will replace this with that
|
|
1151
|
+
// config.
|
|
1152
|
+
const hooksDir = join(projectDir, '.git', 'hooks');
|
|
1153
|
+
const runCmd =
|
|
1154
|
+
packageManager === 'npm' ? 'npm run' : packageManager === 'bun' ? 'bun run' : 'pnpm';
|
|
1155
|
+
const preCommitHook = `#!/bin/sh
|
|
1156
|
+
# Pre-commit hook installed by vibecarbon create.
|
|
1157
|
+
# Edit freely — but please keep the secret-scan step; it's the first
|
|
1158
|
+
# layer of defense against committing API keys, JWTs, private keys,
|
|
1159
|
+
# etc. \`vibecarbon deploy\` will refuse to push if it finds them
|
|
1160
|
+
# anyway, so removing this just delays the failure.
|
|
1161
|
+
|
|
1162
|
+
set -e
|
|
1163
|
+
|
|
1164
|
+
# 1. Lint
|
|
1165
|
+
${runCmd} lint
|
|
1166
|
+
|
|
1167
|
+
# 2. Secret scan over staged files. Skip if the script is missing
|
|
1168
|
+
# (e.g. the user wiped scripts/ during a refactor); the deploy gate
|
|
1169
|
+
# will still catch it.
|
|
1170
|
+
if [ -f scripts/secret-scan.mjs ]; then
|
|
1171
|
+
staged=$(git diff --cached --name-only --diff-filter=ACMR)
|
|
1172
|
+
if [ -n "$staged" ]; then
|
|
1173
|
+
echo "$staged" | node scripts/secret-scan.mjs --stdin
|
|
1174
|
+
fi
|
|
1175
|
+
fi
|
|
1176
|
+
`;
|
|
1177
|
+
writeFileSync(join(hooksDir, 'pre-commit'), preCommitHook);
|
|
1178
|
+
chmodSync(join(hooksDir, 'pre-commit'), 0o755);
|
|
1179
|
+
|
|
1180
|
+
// Install pre-push hook. Runs the project's `test:prepush` script —
|
|
1181
|
+
// lint + unit + component + integration. Integration mocks externals,
|
|
1182
|
+
// so this works without Docker and is fast enough for every push.
|
|
1183
|
+
// Bypass via `git push --no-verify` if you understand why your tests
|
|
1184
|
+
// are red. The test-maintainer subagent shipped with the project
|
|
1185
|
+
// keeps this suite green as code changes.
|
|
1186
|
+
const prePushHook = `#!/bin/sh
|
|
1187
|
+
# Pre-push hook installed by vibecarbon create.
|
|
1188
|
+
# Runs lint + unit + component + integration tests.
|
|
1189
|
+
#
|
|
1190
|
+
# No-ops gracefully if node_modules isn't present yet — a fresh
|
|
1191
|
+
# \`vibecarbon create -git\` lands you on a commit that's pushable
|
|
1192
|
+
# before you've run \`pnpm install\`. Once deps land the hook gates
|
|
1193
|
+
# every subsequent push. Bypass at will with \`git push --no-verify\`.
|
|
1194
|
+
|
|
1195
|
+
set -e
|
|
1196
|
+
|
|
1197
|
+
if [ ! -d node_modules ]; then
|
|
1198
|
+
echo "pre-push: node_modules not present — skipping tests."
|
|
1199
|
+
echo " Run '${runCmd} install' to enable the test gate."
|
|
1200
|
+
exit 0
|
|
1201
|
+
fi
|
|
1202
|
+
|
|
1203
|
+
${runCmd} test:prepush
|
|
1204
|
+
`;
|
|
1205
|
+
writeFileSync(join(hooksDir, 'pre-push'), prePushHook);
|
|
1206
|
+
chmodSync(join(hooksDir, 'pre-push'), 0o755);
|
|
1207
|
+
await tick();
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Store template version and file checksums for `vibecarbon upgrade`
|
|
1211
|
+
s.message('Recording file checksums');
|
|
1212
|
+
const upgradeableFiles = getUpgradeableFiles(TEMPLATE_DIR);
|
|
1213
|
+
const fileChecksums = {};
|
|
1214
|
+
for (const relPath of upgradeableFiles) {
|
|
1215
|
+
const filePath = join(projectDir, relPath);
|
|
1216
|
+
if (existsSync(filePath)) {
|
|
1217
|
+
try {
|
|
1218
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
1219
|
+
fileChecksums[relPath] = hashContent(content);
|
|
1220
|
+
} catch {
|
|
1221
|
+
// Skip files that can't be read as text (binary)
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
saveManifest(
|
|
1226
|
+
{
|
|
1227
|
+
version: '1',
|
|
1228
|
+
templateVersion: VERSION,
|
|
1229
|
+
fileChecksums,
|
|
1230
|
+
services: {},
|
|
1231
|
+
},
|
|
1232
|
+
projectDir,
|
|
1233
|
+
);
|
|
1234
|
+
await tick();
|
|
1235
|
+
|
|
1236
|
+
s.stop('Project created successfully');
|
|
1237
|
+
registerProject(projectName, projectDir);
|
|
1238
|
+
tracker.finish();
|
|
1239
|
+
await tick();
|
|
1240
|
+
|
|
1241
|
+
// Next steps
|
|
1242
|
+
const nextSteps = [
|
|
1243
|
+
'# Change directory:',
|
|
1244
|
+
`cd ${projectName}`,
|
|
1245
|
+
'',
|
|
1246
|
+
'# Run locally:',
|
|
1247
|
+
'vibecarbon up',
|
|
1248
|
+
'',
|
|
1249
|
+
'# Deploy to cloud:',
|
|
1250
|
+
'vibecarbon deploy',
|
|
1251
|
+
];
|
|
1252
|
+
|
|
1253
|
+
p.note(nextSteps.join('\n'), 'Next steps');
|
|
1254
|
+
await tick();
|
|
1255
|
+
|
|
1256
|
+
p.outro(`Happy building! Admin: ${adminEmail}`);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// ============================================================================
|
|
1260
|
+
// GENERATOR FUNCTIONS
|
|
1261
|
+
// ============================================================================
|
|
1262
|
+
|
|
1263
|
+
function generateEnvLocal(projectName, variables) {
|
|
1264
|
+
// Quote-style policy:
|
|
1265
|
+
// - Machine-generated secrets (JWT_SECRET, ANON_KEY, etc) use double-quoted
|
|
1266
|
+
// form because they are drawn from the base64/base64url alphabet and can
|
|
1267
|
+
// never contain a shell metacharacter.
|
|
1268
|
+
// - User-supplied secrets (ADMIN_PASSWORD, REPL_PASSWORD) use escapeDotenv
|
|
1269
|
+
// (single-quoted, POSIX) so any hostile character round-trips safely.
|
|
1270
|
+
return `# =============================================================================
|
|
1271
|
+
# SUPABASE CONFIGURATION
|
|
1272
|
+
# Generated by create-vibecarbon - DO NOT COMMIT THIS FILE
|
|
1273
|
+
# =============================================================================
|
|
1274
|
+
|
|
1275
|
+
# Project name (used by docker-compose for container naming and Vite HTML title)
|
|
1276
|
+
PROJECT_NAME="${projectName}"
|
|
1277
|
+
VITE_PROJECT_NAME="${projectName}"
|
|
1278
|
+
|
|
1279
|
+
# Supabase URLs
|
|
1280
|
+
SUPABASE_URL="http://localhost:8000"
|
|
1281
|
+
VITE_SUPABASE_URL="http://localhost:8000"
|
|
1282
|
+
|
|
1283
|
+
# Supabase Keys
|
|
1284
|
+
SUPABASE_ANON_KEY="${variables.ANON_KEY}"
|
|
1285
|
+
VITE_SUPABASE_ANON_KEY="${variables.ANON_KEY}"
|
|
1286
|
+
SUPABASE_SERVICE_ROLE_KEY="${variables.SERVICE_ROLE_KEY}"
|
|
1287
|
+
|
|
1288
|
+
# JWT Secret (for token generation)
|
|
1289
|
+
JWT_SECRET="${variables.JWT_SECRET}"
|
|
1290
|
+
|
|
1291
|
+
# Database
|
|
1292
|
+
DB_PASSWORD="${variables.DB_PASSWORD}"
|
|
1293
|
+
POSTGRES_PASSWORD="${variables.DB_PASSWORD}"
|
|
1294
|
+
|
|
1295
|
+
# Encryption Keys
|
|
1296
|
+
REALTIME_SECRET="${variables.REALTIME_SECRET}"
|
|
1297
|
+
VAULT_ENC_KEY="${variables.VAULT_ENC_KEY}"
|
|
1298
|
+
PG_META_CRYPTO_KEY="${variables.PG_META_CRYPTO_KEY}"
|
|
1299
|
+
DB_ENC_KEY="${variables.DB_ENC_KEY}"
|
|
1300
|
+
LOGFLARE_API_KEY="${variables.LOGFLARE_API_KEY}"
|
|
1301
|
+
|
|
1302
|
+
# HA PostgreSQL replication (used by vibecarbon deploy --ha)
|
|
1303
|
+
REPL_PASSWORD=${escapeDotenv(variables.REPL_PASSWORD)}
|
|
1304
|
+
|
|
1305
|
+
# =============================================================================
|
|
1306
|
+
# ADMIN CREDENTIALS
|
|
1307
|
+
# =============================================================================
|
|
1308
|
+
|
|
1309
|
+
# Admin user with dashboard access (created during project setup)
|
|
1310
|
+
# Note: Password stored here for your reference only. Database stores bcrypt hash.
|
|
1311
|
+
ADMIN_EMAIL="${variables.ADMIN_EMAIL}"
|
|
1312
|
+
ADMIN_PASSWORD=${escapeDotenv(variables.ADMIN_PASSWORD)}
|
|
1313
|
+
|
|
1314
|
+
# Site URL
|
|
1315
|
+
SITE_URL="http://localhost:5173"
|
|
1316
|
+
|
|
1317
|
+
# =============================================================================
|
|
1318
|
+
# OAUTH PROVIDERS (optional)
|
|
1319
|
+
# =============================================================================
|
|
1320
|
+
|
|
1321
|
+
GOOGLE_ENABLED="false"
|
|
1322
|
+
GOOGLE_CLIENT_ID=""
|
|
1323
|
+
GOOGLE_CLIENT_SECRET=""
|
|
1324
|
+
|
|
1325
|
+
MICROSOFT_ENABLED="false"
|
|
1326
|
+
MICROSOFT_CLIENT_ID=""
|
|
1327
|
+
MICROSOFT_CLIENT_SECRET=""
|
|
1328
|
+
MICROSOFT_TENANT_ID=""
|
|
1329
|
+
|
|
1330
|
+
# =============================================================================
|
|
1331
|
+
# EXTERNAL SERVICES (optional)
|
|
1332
|
+
# =============================================================================
|
|
1333
|
+
|
|
1334
|
+
# Stripe
|
|
1335
|
+
STRIPE_SECRET_KEY=""
|
|
1336
|
+
STRIPE_WEBHOOK_SECRET=""
|
|
1337
|
+
|
|
1338
|
+
# SMTP Email (shared by Supabase Auth and app transactional emails)
|
|
1339
|
+
SMTP_HOST=""
|
|
1340
|
+
SMTP_PORT=465
|
|
1341
|
+
SMTP_USER=""
|
|
1342
|
+
SMTP_PASS=""
|
|
1343
|
+
SMTP_ADMIN_EMAIL="noreply@example.com"
|
|
1344
|
+
SMTP_SENDER_NAME="${variables.PROJECT_NAME}"
|
|
1345
|
+
|
|
1346
|
+
# =============================================================================
|
|
1347
|
+
# OPTIONAL SERVICES (for admin dashboard visibility)
|
|
1348
|
+
# =============================================================================
|
|
1349
|
+
# These control which services appear in the admin dashboard
|
|
1350
|
+
# Set automatically when using 'vibecarbon add' or manual docker-compose files
|
|
1351
|
+
|
|
1352
|
+
VITE_N8N_ENABLED="false"
|
|
1353
|
+
VITE_OBSERVABILITY_ENABLED="false"
|
|
1354
|
+
|
|
1355
|
+
# =============================================================================
|
|
1356
|
+
# PORT CONFIGURATION (for running multiple projects)
|
|
1357
|
+
# =============================================================================
|
|
1358
|
+
# Use DEV_PORT_OFFSET to shift all ports when running multiple projects.
|
|
1359
|
+
# Example: DEV_PORT_OFFSET=100 shifts Vite to 5273, API to 3100, etc.
|
|
1360
|
+
|
|
1361
|
+
DEV_PORT_OFFSET="0"
|
|
1362
|
+
|
|
1363
|
+
# Override individual ports (takes precedence over offset):
|
|
1364
|
+
# DEV_VITE_PORT="5173"
|
|
1365
|
+
# DEV_API_PORT="3000"
|
|
1366
|
+
# DEV_KONG_PORT="8000"
|
|
1367
|
+
# DEV_KONG_SSL_PORT="8443"
|
|
1368
|
+
# DEV_TRAEFIK_PORT="80"
|
|
1369
|
+
|
|
1370
|
+
# =============================================================================
|
|
1371
|
+
# ENVIRONMENT
|
|
1372
|
+
# =============================================================================
|
|
1373
|
+
|
|
1374
|
+
NODE_ENV="development"
|
|
1375
|
+
PORT="3000"
|
|
1376
|
+
`.trim();
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function generateReadme(projectName, packageManager = 'pnpm') {
|
|
1380
|
+
const run = packageManager === 'npm' ? 'npm run' : packageManager;
|
|
1381
|
+
let readme = `# ${projectName}
|
|
1382
|
+
|
|
1383
|
+
A modern web app built with Vibecarbon.
|
|
1384
|
+
|
|
1385
|
+
## Stack
|
|
1386
|
+
|
|
1387
|
+
- **Hono** - Fast, lightweight web framework
|
|
1388
|
+
- **Vite** - Next-generation frontend tooling
|
|
1389
|
+
- **React 19** - UI library (SPA mode)
|
|
1390
|
+
- **Supabase** - Self-hosted backend (Auth, Database, Storage, Realtime)
|
|
1391
|
+
- **PostgreSQL** - Database (via Supabase)
|
|
1392
|
+
- **Tailwind CSS** - Utility-first styling
|
|
1393
|
+
- **Shadcn UI** - Component library
|
|
1394
|
+
- **Kubernetes** - Container orchestration & autoscaling
|
|
1395
|
+
- **Traefik** - Reverse proxy with automatic HTTPS
|
|
1396
|
+
|
|
1397
|
+
## Getting Started
|
|
1398
|
+
|
|
1399
|
+
Start everything with a single command:
|
|
1400
|
+
|
|
1401
|
+
\`\`\`bash
|
|
1402
|
+
vibecarbon up
|
|
1403
|
+
\`\`\`
|
|
1404
|
+
|
|
1405
|
+
This starts Docker containers, runs database migrations, and launches the dev servers.
|
|
1406
|
+
|
|
1407
|
+
## Services
|
|
1408
|
+
|
|
1409
|
+
| Service | URL | Description |
|
|
1410
|
+
|---------|-----|-------------|
|
|
1411
|
+
| Vite Dev | http://localhost:5173 | Frontend development server |
|
|
1412
|
+
| Hono API | http://localhost:3000 | Application API |
|
|
1413
|
+
| Supabase API | http://localhost:8000 | Supabase API Gateway (Kong) |
|
|
1414
|
+
| Supabase Studio | http://studio.localhost | Database management UI (requires admin login) |
|
|
1415
|
+
| Traefik Dashboard | http://traefik.localhost | Reverse proxy dashboard (requires admin login) |
|
|
1416
|
+
|
|
1417
|
+
## Available Scripts
|
|
1418
|
+
|
|
1419
|
+
- \`${run} dev:start\` - Full cold start (Docker + migrations + dev servers)
|
|
1420
|
+
- \`${run} dev\` - Start both API and frontend dev servers (fast restart)
|
|
1421
|
+
- \`${run} dev:server\` - Start Hono API server only
|
|
1422
|
+
- \`${run} dev:client\` - Start Vite dev server only
|
|
1423
|
+
- \`${run} build\` - Build for production
|
|
1424
|
+
- \`${run} start\` - Run production build
|
|
1425
|
+
- \`${run} docker:up\` - Start all services
|
|
1426
|
+
- \`${run} dev:stop\` - Stop all services
|
|
1427
|
+
- \`${run} docker:logs\` - View service logs
|
|
1428
|
+
- \`${run} db:migrate\` - Run database migrations
|
|
1429
|
+
`;
|
|
1430
|
+
|
|
1431
|
+
readme += `
|
|
1432
|
+
## Troubleshooting
|
|
1433
|
+
|
|
1434
|
+
### Database connection errors
|
|
1435
|
+
|
|
1436
|
+
If services fail with "password authentication failed", the database volume was created with different credentials. Reset it:
|
|
1437
|
+
|
|
1438
|
+
\`\`\`bash
|
|
1439
|
+
${run} docker:reset
|
|
1440
|
+
${run} db:migrate
|
|
1441
|
+
\`\`\`
|
|
1442
|
+
|
|
1443
|
+
> **Note:** This deletes all data. For production, back up first.
|
|
1444
|
+
|
|
1445
|
+
### Services not starting
|
|
1446
|
+
|
|
1447
|
+
Check logs for specific errors:
|
|
1448
|
+
|
|
1449
|
+
\`\`\`bash
|
|
1450
|
+
${run} docker:logs
|
|
1451
|
+
\`\`\`
|
|
1452
|
+
|
|
1453
|
+
### Port conflicts
|
|
1454
|
+
|
|
1455
|
+
If ports 5173, 3000, 3001, or 8000 are in use, stop conflicting services or modify \`docker-compose.yml\`.
|
|
1456
|
+
|
|
1457
|
+
## Learn More
|
|
1458
|
+
|
|
1459
|
+
- [Supabase Documentation](https://supabase.com/docs)
|
|
1460
|
+
- [Hono Documentation](https://hono.dev)
|
|
1461
|
+
- [Vite Documentation](https://vitejs.dev)
|
|
1462
|
+
`;
|
|
1463
|
+
|
|
1464
|
+
return readme.trim();
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// ============================================================================
|
|
1468
|
+
// RUN FUNCTION (called by CLI entry point)
|
|
1469
|
+
// ============================================================================
|
|
1470
|
+
|
|
1471
|
+
export async function run(args) {
|
|
1472
|
+
await bootstrap(args);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// ============================================================================
|
|
1476
|
+
// EXPORTS FOR TESTING
|
|
1477
|
+
// ============================================================================
|
|
1478
|
+
|
|
1479
|
+
export {
|
|
1480
|
+
bootstrap,
|
|
1481
|
+
checkPackageManagerVersion,
|
|
1482
|
+
copyTemplate,
|
|
1483
|
+
copyTemplateDir,
|
|
1484
|
+
detectPackageManager,
|
|
1485
|
+
generateEnvLocal,
|
|
1486
|
+
generateJWT,
|
|
1487
|
+
generatePassword,
|
|
1488
|
+
generateReadme,
|
|
1489
|
+
getInstallCommand,
|
|
1490
|
+
getPackageManagerVersion,
|
|
1491
|
+
isCI,
|
|
1492
|
+
PLACEHOLDERS,
|
|
1493
|
+
RECOMMENDED_VERSIONS,
|
|
1494
|
+
runCommand,
|
|
1495
|
+
SPEC,
|
|
1496
|
+
TEMPLATE_DIR,
|
|
1497
|
+
VERSION,
|
|
1498
|
+
validatePlaceholders,
|
|
1499
|
+
};
|