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/diagnose.js
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `vibecarbon diagnose [env] [section]`
|
|
3
|
+
*
|
|
4
|
+
* One-command full cluster state dump. Writes a timestamped report to
|
|
5
|
+
* ~/.vibecarbon/diag-<env>-<ts>.txt and tees a summary to stdout. Companion
|
|
6
|
+
* to `vibecarbon shell` — when something is wrong, this is the single
|
|
7
|
+
* command that gathers every signal that's
|
|
8
|
+
* helped diagnose past failures (node IPs, CoreDNS config, Flux state,
|
|
9
|
+
* pod-level DNS probes, hcloud resource state).
|
|
10
|
+
*
|
|
11
|
+
* Sections:
|
|
12
|
+
* nodes — kubectl get nodes -o wide + ssh systemctl status k3s
|
|
13
|
+
* + journalctl -u k3s + ip addr + dmesg via SSH
|
|
14
|
+
* pods — kubectl get pods -A, describe non-Running pods, logs for
|
|
15
|
+
* crashlooping pods
|
|
16
|
+
* network — CoreDNS configmap + logs, Flannel + kube-proxy logs,
|
|
17
|
+
* hcloud-ccm + hcloud-csi logs, in-cluster busybox DNS probe
|
|
18
|
+
* flux — kubectl -n flux-system get gitrepository,kustomization,
|
|
19
|
+
* helmrelease + describe + controller logs
|
|
20
|
+
* hcloud — server/network/firewall/floating-ip/PG/SSH-key list
|
|
21
|
+
* all — every section (default)
|
|
22
|
+
*
|
|
23
|
+
* Each probe is wrapped so a failure in one section doesn't prevent the
|
|
24
|
+
* others from running — partial output is far more useful than nothing.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { execFileSync } from 'node:child_process';
|
|
28
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
29
|
+
import { homedir } from 'node:os';
|
|
30
|
+
import { join } from 'node:path';
|
|
31
|
+
import { renderHelp } from './lib/cli/help.js';
|
|
32
|
+
import { parseFlags } from './lib/cli/parse-flags.js';
|
|
33
|
+
import { c } from './lib/colors.js';
|
|
34
|
+
import { loadProjectConfig } from './lib/config.js';
|
|
35
|
+
import { ensureOperatorIpAccess } from './lib/operator-ip.js';
|
|
36
|
+
import { assertInProjectDir } from './lib/project-guard.js';
|
|
37
|
+
|
|
38
|
+
// Sections that run as part of `diagnose all`. Egress is intentionally
|
|
39
|
+
// excluded — its netshoot image pull + multi-probe battery can take 30-60s,
|
|
40
|
+
// which is fine for an explicit `diagnose <env> egress` invocation but
|
|
41
|
+
// disproportionate for the default sweep.
|
|
42
|
+
const SECTIONS = ['nodes', 'pods', 'network', 'flux', 'hcloud'];
|
|
43
|
+
const OPTIONAL_SECTIONS = ['egress'];
|
|
44
|
+
const ALL_VALID_SECTIONS = [...SECTIONS, ...OPTIONAL_SECTIONS];
|
|
45
|
+
|
|
46
|
+
/** @type {import('./lib/cli/parse-flags.js').CommandSpec & { summary?: string, description?: string, examples?: Array<{ command: string, description?: string }> }} */
|
|
47
|
+
const SPEC = {
|
|
48
|
+
name: 'diagnose',
|
|
49
|
+
summary: 'Dump full cluster state for an environment',
|
|
50
|
+
description: [
|
|
51
|
+
'Streams sections to stdout AND writes the full report to:',
|
|
52
|
+
' ~/.vibecarbon/diag-<env>-<timestamp>.txt',
|
|
53
|
+
].join('\n'),
|
|
54
|
+
positional: [
|
|
55
|
+
{
|
|
56
|
+
name: 'env',
|
|
57
|
+
optional: true,
|
|
58
|
+
description: 'Environment to inspect (default: prod)',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'section',
|
|
62
|
+
optional: true,
|
|
63
|
+
description: `One of: ${ALL_VALID_SECTIONS.join(', ')}, all (default: all — excludes egress)`,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
flags: [{ name: 'h', boolean: true, description: 'Show this help' }],
|
|
67
|
+
examples: [
|
|
68
|
+
{ command: 'vibecarbon diagnose e3', description: 'full dump (excludes heavy egress probe)' },
|
|
69
|
+
{ command: 'vibecarbon diagnose e3 network', description: 'CoreDNS + CCM + DNS probe' },
|
|
70
|
+
{
|
|
71
|
+
command: 'vibecarbon diagnose e3 egress',
|
|
72
|
+
description: 'netshoot DNS + MTU + route battery',
|
|
73
|
+
},
|
|
74
|
+
{ command: 'vibecarbon diagnose e3 flux', description: 'just Flux state' },
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function loadHcloudToken() {
|
|
79
|
+
const credPath = join(homedir(), '.vibecarbon', 'credentials.json');
|
|
80
|
+
if (!existsSync(credPath)) return null;
|
|
81
|
+
try {
|
|
82
|
+
const data = JSON.parse(readFileSync(credPath, 'utf-8'));
|
|
83
|
+
return data?.hetzner?.apiToken ?? data?.hetznerApiToken ?? null;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Run a command, returning { ok, stdout, stderr }. Never throws —
|
|
91
|
+
* failures get captured into stderr/exitCode so a single broken command
|
|
92
|
+
* doesn't break the whole report.
|
|
93
|
+
*/
|
|
94
|
+
function safeRun(cmd, args, env) {
|
|
95
|
+
try {
|
|
96
|
+
const out = execFileSync(cmd, args, {
|
|
97
|
+
env,
|
|
98
|
+
encoding: 'utf-8',
|
|
99
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
100
|
+
timeout: 60_000,
|
|
101
|
+
});
|
|
102
|
+
return { ok: true, stdout: out, stderr: '' };
|
|
103
|
+
} catch (err) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
stdout: err?.stdout?.toString() ?? '',
|
|
107
|
+
stderr: err?.stderr?.toString() ?? err?.message ?? String(err),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
class Report {
|
|
113
|
+
constructor() {
|
|
114
|
+
this.lines = [];
|
|
115
|
+
}
|
|
116
|
+
section(title) {
|
|
117
|
+
const banner = `\n${'='.repeat(72)}\n ${title}\n${'='.repeat(72)}\n`;
|
|
118
|
+
this.lines.push(banner);
|
|
119
|
+
console.log(banner);
|
|
120
|
+
}
|
|
121
|
+
subsection(title) {
|
|
122
|
+
const banner = `\n--- ${title} ---`;
|
|
123
|
+
this.lines.push(banner);
|
|
124
|
+
console.log(banner);
|
|
125
|
+
}
|
|
126
|
+
command(label, cmd, args) {
|
|
127
|
+
this.lines.push(`\n# $ ${cmd} ${args.join(' ')} (${label})`);
|
|
128
|
+
console.log(c.dim(`# $ ${cmd} ${args.join(' ')}`));
|
|
129
|
+
}
|
|
130
|
+
output(text) {
|
|
131
|
+
if (!text) return;
|
|
132
|
+
this.lines.push(text.trimEnd());
|
|
133
|
+
process.stdout.write(text);
|
|
134
|
+
if (!text.endsWith('\n')) process.stdout.write('\n');
|
|
135
|
+
}
|
|
136
|
+
warn(text) {
|
|
137
|
+
const line = `${c.warning('!')} ${text}`;
|
|
138
|
+
this.lines.push(`! ${text}`);
|
|
139
|
+
console.log(line);
|
|
140
|
+
}
|
|
141
|
+
toString() {
|
|
142
|
+
return this.lines.join('\n');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Run a command, push it to the report with a label, and dump output.
|
|
148
|
+
*/
|
|
149
|
+
function probe(report, label, cmd, args, env) {
|
|
150
|
+
report.command(label, cmd, args);
|
|
151
|
+
const r = safeRun(cmd, args, env);
|
|
152
|
+
if (r.ok) {
|
|
153
|
+
report.output(r.stdout);
|
|
154
|
+
} else {
|
|
155
|
+
report.output(r.stdout);
|
|
156
|
+
report.warn(`${cmd} ${args.join(' ')} failed: ${r.stderr.split('\n')[0]}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function diagnoseHcloud(report, env) {
|
|
161
|
+
report.section('HCLOUD RESOURCES');
|
|
162
|
+
for (const kind of [
|
|
163
|
+
'server',
|
|
164
|
+
'network',
|
|
165
|
+
'firewall',
|
|
166
|
+
'floating-ip',
|
|
167
|
+
'placement-group',
|
|
168
|
+
'ssh-key',
|
|
169
|
+
]) {
|
|
170
|
+
probe(report, kind, 'hcloud', [kind, 'list'], env);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function diagnoseNodes(report, env, kubeconfig, sshKey, controlplaneIp) {
|
|
175
|
+
report.section('NODES');
|
|
176
|
+
probe(
|
|
177
|
+
report,
|
|
178
|
+
'nodes wide',
|
|
179
|
+
'kubectl',
|
|
180
|
+
['--kubeconfig', kubeconfig, 'get', 'nodes', '-o', 'wide'],
|
|
181
|
+
env,
|
|
182
|
+
);
|
|
183
|
+
probe(report, 'node describe', 'kubectl', ['--kubeconfig', kubeconfig, 'describe', 'nodes'], env);
|
|
184
|
+
if (existsSync(sshKey) && controlplaneIp) {
|
|
185
|
+
const sshArgs = (cmd) => [
|
|
186
|
+
'-i',
|
|
187
|
+
sshKey,
|
|
188
|
+
'-o',
|
|
189
|
+
'StrictHostKeyChecking=accept-new',
|
|
190
|
+
'-o',
|
|
191
|
+
'BatchMode=yes',
|
|
192
|
+
'-o',
|
|
193
|
+
'ConnectTimeout=10',
|
|
194
|
+
`root@${controlplaneIp}`,
|
|
195
|
+
cmd,
|
|
196
|
+
];
|
|
197
|
+
probe(report, 'k3s service status', 'ssh', sshArgs('systemctl status k3s --no-pager'), env);
|
|
198
|
+
probe(
|
|
199
|
+
report,
|
|
200
|
+
'k3s journal (last 50)',
|
|
201
|
+
'ssh',
|
|
202
|
+
sshArgs('journalctl -u k3s --no-pager -n 50'),
|
|
203
|
+
env,
|
|
204
|
+
);
|
|
205
|
+
probe(
|
|
206
|
+
report,
|
|
207
|
+
'containerd containers',
|
|
208
|
+
'ssh',
|
|
209
|
+
sshArgs('k3s ctr containers ls 2>&1 | head -20'),
|
|
210
|
+
env,
|
|
211
|
+
);
|
|
212
|
+
probe(report, 'host addresses', 'ssh', sshArgs('ip addr show'), env);
|
|
213
|
+
probe(report, 'host dmesg (tail)', 'ssh', sshArgs('dmesg | tail -30'), env);
|
|
214
|
+
} else {
|
|
215
|
+
report.warn(
|
|
216
|
+
`ssh skipped — sshKey=${sshKey} (${existsSync(sshKey) ? 'found' : 'MISSING'}) controlplaneIp=${controlplaneIp ?? '(none)'}`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function diagnosePods(report, env, kubeconfig) {
|
|
222
|
+
report.section('PODS');
|
|
223
|
+
probe(
|
|
224
|
+
report,
|
|
225
|
+
'pods all-ns wide',
|
|
226
|
+
'kubectl',
|
|
227
|
+
['--kubeconfig', kubeconfig, 'get', 'pods', '-A', '-o', 'wide'],
|
|
228
|
+
env,
|
|
229
|
+
);
|
|
230
|
+
probe(
|
|
231
|
+
report,
|
|
232
|
+
'non-running pods',
|
|
233
|
+
'kubectl',
|
|
234
|
+
[
|
|
235
|
+
'--kubeconfig',
|
|
236
|
+
kubeconfig,
|
|
237
|
+
'get',
|
|
238
|
+
'pods',
|
|
239
|
+
'-A',
|
|
240
|
+
'--field-selector=status.phase!=Running,status.phase!=Succeeded',
|
|
241
|
+
],
|
|
242
|
+
env,
|
|
243
|
+
);
|
|
244
|
+
// Describe pods in non-running state
|
|
245
|
+
const listResult = safeRun(
|
|
246
|
+
'kubectl',
|
|
247
|
+
[
|
|
248
|
+
'--kubeconfig',
|
|
249
|
+
kubeconfig,
|
|
250
|
+
'get',
|
|
251
|
+
'pods',
|
|
252
|
+
'-A',
|
|
253
|
+
'--field-selector=status.phase!=Running,status.phase!=Succeeded',
|
|
254
|
+
'-o',
|
|
255
|
+
'jsonpath={range .items[*]}{.metadata.namespace} {.metadata.name}{"\\n"}{end}',
|
|
256
|
+
],
|
|
257
|
+
env,
|
|
258
|
+
);
|
|
259
|
+
if (listResult.ok) {
|
|
260
|
+
const pairs = listResult.stdout
|
|
261
|
+
.trim()
|
|
262
|
+
.split('\n')
|
|
263
|
+
.filter(Boolean)
|
|
264
|
+
.map((l) => l.split(' '));
|
|
265
|
+
for (const [ns, name] of pairs) {
|
|
266
|
+
if (!ns || !name) continue;
|
|
267
|
+
report.subsection(`${ns}/${name}`);
|
|
268
|
+
probe(
|
|
269
|
+
report,
|
|
270
|
+
'describe',
|
|
271
|
+
'kubectl',
|
|
272
|
+
['--kubeconfig', kubeconfig, '-n', ns, 'describe', `pod/${name}`],
|
|
273
|
+
env,
|
|
274
|
+
);
|
|
275
|
+
probe(
|
|
276
|
+
report,
|
|
277
|
+
'logs (current)',
|
|
278
|
+
'kubectl',
|
|
279
|
+
['--kubeconfig', kubeconfig, '-n', ns, 'logs', name, '--tail=40'],
|
|
280
|
+
env,
|
|
281
|
+
);
|
|
282
|
+
probe(
|
|
283
|
+
report,
|
|
284
|
+
'logs (previous)',
|
|
285
|
+
'kubectl',
|
|
286
|
+
['--kubeconfig', kubeconfig, '-n', ns, 'logs', name, '--previous', '--tail=40'],
|
|
287
|
+
env,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function diagnoseNetwork(report, env, kubeconfig) {
|
|
294
|
+
report.section('NETWORK');
|
|
295
|
+
probe(
|
|
296
|
+
report,
|
|
297
|
+
'CoreDNS configmap',
|
|
298
|
+
'kubectl',
|
|
299
|
+
['--kubeconfig', kubeconfig, '-n', 'kube-system', 'get', 'cm', 'coredns', '-o', 'yaml'],
|
|
300
|
+
env,
|
|
301
|
+
);
|
|
302
|
+
probe(
|
|
303
|
+
report,
|
|
304
|
+
'kube-system pods',
|
|
305
|
+
'kubectl',
|
|
306
|
+
['--kubeconfig', kubeconfig, '-n', 'kube-system', 'get', 'pods', '-o', 'wide'],
|
|
307
|
+
env,
|
|
308
|
+
);
|
|
309
|
+
for (const labelSel of [
|
|
310
|
+
{ name: 'CoreDNS', sel: 'k8s-app=kube-dns' },
|
|
311
|
+
{ name: 'Flannel', sel: 'app=flannel' },
|
|
312
|
+
{ name: 'kube-proxy', sel: 'k8s-app=kube-proxy' },
|
|
313
|
+
{ name: 'hcloud-CCM', sel: 'app.kubernetes.io/name=hcloud-cloud-controller-manager' },
|
|
314
|
+
{
|
|
315
|
+
name: 'hcloud-CSI controller',
|
|
316
|
+
sel: 'app.kubernetes.io/component=controller,app.kubernetes.io/name=hcloud-csi',
|
|
317
|
+
},
|
|
318
|
+
]) {
|
|
319
|
+
report.subsection(`${labelSel.name} logs`);
|
|
320
|
+
const list = safeRun(
|
|
321
|
+
'kubectl',
|
|
322
|
+
[
|
|
323
|
+
'--kubeconfig',
|
|
324
|
+
kubeconfig,
|
|
325
|
+
'-n',
|
|
326
|
+
'kube-system',
|
|
327
|
+
'get',
|
|
328
|
+
'pods',
|
|
329
|
+
'-l',
|
|
330
|
+
labelSel.sel,
|
|
331
|
+
'-o',
|
|
332
|
+
'jsonpath={.items[*].metadata.name}',
|
|
333
|
+
],
|
|
334
|
+
env,
|
|
335
|
+
);
|
|
336
|
+
if (list.ok && list.stdout.trim()) {
|
|
337
|
+
for (const pod of list.stdout.trim().split(/\s+/)) {
|
|
338
|
+
probe(
|
|
339
|
+
report,
|
|
340
|
+
pod,
|
|
341
|
+
'kubectl',
|
|
342
|
+
['--kubeconfig', kubeconfig, '-n', 'kube-system', 'logs', pod, '--tail=30'],
|
|
343
|
+
env,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
report.warn(`No pods found matching ${labelSel.sel}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// In-cluster DNS + egress probe
|
|
351
|
+
report.subsection('busybox in-cluster probe (nslookup + wget)');
|
|
352
|
+
const probeName = `vc-diag-${Math.random().toString(36).slice(2, 8)}`;
|
|
353
|
+
probe(
|
|
354
|
+
report,
|
|
355
|
+
'pod probe',
|
|
356
|
+
'kubectl',
|
|
357
|
+
[
|
|
358
|
+
'--kubeconfig',
|
|
359
|
+
kubeconfig,
|
|
360
|
+
'-n',
|
|
361
|
+
'kube-system',
|
|
362
|
+
'run',
|
|
363
|
+
probeName,
|
|
364
|
+
'--image=busybox:1.36',
|
|
365
|
+
'--restart=Never',
|
|
366
|
+
'--rm',
|
|
367
|
+
'--attach',
|
|
368
|
+
'--timeout=60s',
|
|
369
|
+
'--command',
|
|
370
|
+
'--',
|
|
371
|
+
'sh',
|
|
372
|
+
'-c',
|
|
373
|
+
'echo "--- /etc/resolv.conf ---"; cat /etc/resolv.conf; echo; echo "--- nslookup github.com (cluster DNS) ---"; nslookup github.com 2>&1 | head -10; echo; echo "--- nslookup github.com @1.1.1.1 ---"; nslookup github.com 1.1.1.1 2>&1 | head -10; echo; echo "--- wget https://github.com (5s) ---"; wget -qO- --timeout=5 https://github.com 2>&1 | head -3 || echo wget-failed',
|
|
374
|
+
],
|
|
375
|
+
env,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Deep DNS + pod-network egress battery. Runs a netshoot pod with sleep,
|
|
381
|
+
* execs a multi-layered probe script, then deletes the pod. Distinguishes
|
|
382
|
+
* between "DNS broken", "pod-network broken", "MTU mismatch", and "external
|
|
383
|
+
* egress broken" — the four classes of bug that masquerade as the same
|
|
384
|
+
* "i/o timeout to github.com" symptom that bit us 2026-04-24.
|
|
385
|
+
*
|
|
386
|
+
* Heavy enough (~30s netshoot pull, ~60s battery) that it's excluded from
|
|
387
|
+
* `diagnose all`. Run explicitly when network is suspect:
|
|
388
|
+
* vibecarbon diagnose <env> egress
|
|
389
|
+
*/
|
|
390
|
+
function diagnoseEgress(report, env, kubeconfig, sshKey, controlplaneIp) {
|
|
391
|
+
report.section('EGRESS (DNS + pod-network battery)');
|
|
392
|
+
|
|
393
|
+
// CCM env vars: HCLOUD_NETWORK should resolve via secretKeyRef:hcloud/network.
|
|
394
|
+
// If it's empty here the route-controller is silently disabled — see
|
|
395
|
+
// hcloud-cloud-controller-manager #115/#630.
|
|
396
|
+
report.subsection('CCM env vars (HCLOUD_NETWORK present?)');
|
|
397
|
+
probe(
|
|
398
|
+
report,
|
|
399
|
+
'CCM deploy env',
|
|
400
|
+
'kubectl',
|
|
401
|
+
[
|
|
402
|
+
'--kubeconfig',
|
|
403
|
+
kubeconfig,
|
|
404
|
+
'-n',
|
|
405
|
+
'kube-system',
|
|
406
|
+
'get',
|
|
407
|
+
'deploy',
|
|
408
|
+
'hcloud-cloud-controller-manager',
|
|
409
|
+
'-o',
|
|
410
|
+
'jsonpath={range .spec.template.spec.containers[*].env[*]}{.name}\t{.value}\t{.valueFrom.secretKeyRef.name}/{.valueFrom.secretKeyRef.key}{"\\n"}{end}',
|
|
411
|
+
],
|
|
412
|
+
env,
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
// CoreDNS Corefile — forwarders are the upstream resolver pod-DNS depends on.
|
|
416
|
+
report.subsection('CoreDNS Corefile');
|
|
417
|
+
probe(
|
|
418
|
+
report,
|
|
419
|
+
'coredns Corefile',
|
|
420
|
+
'kubectl',
|
|
421
|
+
[
|
|
422
|
+
'--kubeconfig',
|
|
423
|
+
kubeconfig,
|
|
424
|
+
'-n',
|
|
425
|
+
'kube-system',
|
|
426
|
+
'get',
|
|
427
|
+
'cm',
|
|
428
|
+
'coredns',
|
|
429
|
+
'-o',
|
|
430
|
+
'jsonpath={.data.Corefile}',
|
|
431
|
+
],
|
|
432
|
+
env,
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
// Node-level state: routes, addresses, links, resolv.conf. If the host
|
|
436
|
+
// can't egress, no pod can either.
|
|
437
|
+
if (existsSync(sshKey) && controlplaneIp) {
|
|
438
|
+
report.subsection('node-level routes / addresses / links / resolv.conf');
|
|
439
|
+
const sshArgs = (cmd) => [
|
|
440
|
+
'-i',
|
|
441
|
+
sshKey,
|
|
442
|
+
'-o',
|
|
443
|
+
'StrictHostKeyChecking=accept-new',
|
|
444
|
+
'-o',
|
|
445
|
+
'BatchMode=yes',
|
|
446
|
+
'-o',
|
|
447
|
+
'ConnectTimeout=10',
|
|
448
|
+
`root@${controlplaneIp}`,
|
|
449
|
+
cmd,
|
|
450
|
+
];
|
|
451
|
+
probe(report, 'host routes', 'ssh', sshArgs('ip route'), env);
|
|
452
|
+
probe(report, 'host addresses', 'ssh', sshArgs('ip addr'), env);
|
|
453
|
+
probe(report, 'host links', 'ssh', sshArgs('ip link'), env);
|
|
454
|
+
probe(report, 'host /etc/resolv.conf', 'ssh', sshArgs('cat /etc/resolv.conf'), env);
|
|
455
|
+
} else {
|
|
456
|
+
report.warn(
|
|
457
|
+
`host node probes skipped — sshKey=${existsSync(sshKey) ? 'found' : 'MISSING'} controlplaneIp=${controlplaneIp ?? '(none)'}`,
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Pod-level battery — the part that actually distinguishes failure modes.
|
|
462
|
+
// Each line is labeled so a failed step localizes the layer:
|
|
463
|
+
// A fails → cluster DNS (CoreDNS) broken
|
|
464
|
+
// B fails → external DNS reachability (node→1.1.1.1) broken
|
|
465
|
+
// C fails → public-IP egress broken (CNI masquerade / default route)
|
|
466
|
+
// D fails → github.com unreachable by IP (egress to that AS broken)
|
|
467
|
+
// E fails → DNS-driven egress broken specifically (combo of A or B)
|
|
468
|
+
// I fails → MTU clamp not applied (vxlan-on-Hetzner=1450); fragments dropped
|
|
469
|
+
//
|
|
470
|
+
// We can't use `kubectl run --rm --attach` with a one-shot script — it
|
|
471
|
+
// truncates stdout when the container exits quickly (validated against a
|
|
472
|
+
// live cluster). Use the create+wait+exec+delete pattern: create with a
|
|
473
|
+
// long sleep, wait for Ready, exec the battery (stdout is captured cleanly),
|
|
474
|
+
// delete in finally-ish fashion (probe() never throws so cleanup always runs).
|
|
475
|
+
report.subsection('netshoot pod battery (DNS + curl + MTU)');
|
|
476
|
+
const podName = `vc-egress-${Math.random().toString(36).slice(2, 8)}`;
|
|
477
|
+
const battery = [
|
|
478
|
+
'set +e',
|
|
479
|
+
'echo "## /etc/resolv.conf"; cat /etc/resolv.conf; echo',
|
|
480
|
+
'echo "## A. dig @cluster-DNS (10.96.0.10) github.com"; dig +time=3 +tries=1 @10.96.0.10 github.com +short; echo "exit=$?"; echo',
|
|
481
|
+
'echo "## B. dig @1.1.1.1 github.com"; dig +time=3 +tries=1 @1.1.1.1 github.com +short; echo "exit=$?"; echo',
|
|
482
|
+
'echo "## C. curl https://1.1.1.1 (public IP)"; curl -m5 -sI https://1.1.1.1 -o /dev/null -w "http=%{http_code} t=%{time_total}s\\n" || echo FAIL_PUBLIC; echo',
|
|
483
|
+
'echo "## D. curl github.com via direct IP (bypass DNS)"; curl -m5 -skI --resolve github.com:443:140.82.121.4 https://github.com/ -o /dev/null -w "http=%{http_code} t=%{time_total}s\\n" || echo FAIL_GITHUB_IP; echo',
|
|
484
|
+
'echo "## E. curl https://github.com (DNS-driven)"; curl -m5 -sI https://github.com -o /dev/null -w "http=%{http_code} t=%{time_total}s\\n" || echo FAIL_GITHUB_DNS; echo',
|
|
485
|
+
'echo "## F. curl https://raw.githubusercontent.com (Flux source target)"; curl -m5 -sI https://raw.githubusercontent.com -o /dev/null -w "http=%{http_code} t=%{time_total}s\\n" || echo FAIL_RAWGH; echo',
|
|
486
|
+
'echo "## G. ip route"; ip route; echo',
|
|
487
|
+
'echo "## H. ip -br link (MTU on each iface)"; ip -br link; echo',
|
|
488
|
+
'echo "## I. ping -M do -s 1372 1.1.1.1 (vxlan-MTU 1400 OK)"; ping -M do -s 1372 -c1 -W2 1.1.1.1 2>&1 | tail -2; echo',
|
|
489
|
+
'echo "## J. ping -M do -s 1444 1.1.1.1 (full-MTU 1472 — expect FAIL on Hetzner private)"; ping -M do -s 1444 -c1 -W2 1.1.1.1 2>&1 | tail -2; echo',
|
|
490
|
+
].join('; ');
|
|
491
|
+
probe(
|
|
492
|
+
report,
|
|
493
|
+
'create pod',
|
|
494
|
+
'kubectl',
|
|
495
|
+
[
|
|
496
|
+
'--kubeconfig',
|
|
497
|
+
kubeconfig,
|
|
498
|
+
'-n',
|
|
499
|
+
'default',
|
|
500
|
+
'run',
|
|
501
|
+
podName,
|
|
502
|
+
'--image=nicolaka/netshoot',
|
|
503
|
+
'--restart=Never',
|
|
504
|
+
'--command',
|
|
505
|
+
'--',
|
|
506
|
+
'sleep',
|
|
507
|
+
'300',
|
|
508
|
+
],
|
|
509
|
+
env,
|
|
510
|
+
);
|
|
511
|
+
probe(
|
|
512
|
+
report,
|
|
513
|
+
'wait pod ready',
|
|
514
|
+
'kubectl',
|
|
515
|
+
[
|
|
516
|
+
'--kubeconfig',
|
|
517
|
+
kubeconfig,
|
|
518
|
+
'-n',
|
|
519
|
+
'default',
|
|
520
|
+
'wait',
|
|
521
|
+
`pod/${podName}`,
|
|
522
|
+
'--for=condition=Ready',
|
|
523
|
+
'--timeout=180s',
|
|
524
|
+
],
|
|
525
|
+
env,
|
|
526
|
+
);
|
|
527
|
+
probe(
|
|
528
|
+
report,
|
|
529
|
+
'exec battery',
|
|
530
|
+
'kubectl',
|
|
531
|
+
['--kubeconfig', kubeconfig, '-n', 'default', 'exec', podName, '--', 'sh', '-c', battery],
|
|
532
|
+
env,
|
|
533
|
+
);
|
|
534
|
+
probe(
|
|
535
|
+
report,
|
|
536
|
+
'cleanup pod',
|
|
537
|
+
'kubectl',
|
|
538
|
+
[
|
|
539
|
+
'--kubeconfig',
|
|
540
|
+
kubeconfig,
|
|
541
|
+
'-n',
|
|
542
|
+
'default',
|
|
543
|
+
'delete',
|
|
544
|
+
'pod',
|
|
545
|
+
podName,
|
|
546
|
+
'--wait=false',
|
|
547
|
+
'--grace-period=1',
|
|
548
|
+
],
|
|
549
|
+
env,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function diagnoseFlux(report, env, kubeconfig) {
|
|
554
|
+
report.section('FLUX');
|
|
555
|
+
probe(
|
|
556
|
+
report,
|
|
557
|
+
'flux resources',
|
|
558
|
+
'kubectl',
|
|
559
|
+
[
|
|
560
|
+
'--kubeconfig',
|
|
561
|
+
kubeconfig,
|
|
562
|
+
'-n',
|
|
563
|
+
'flux-system',
|
|
564
|
+
'get',
|
|
565
|
+
'gitrepository,kustomization,helmrelease',
|
|
566
|
+
],
|
|
567
|
+
env,
|
|
568
|
+
);
|
|
569
|
+
probe(
|
|
570
|
+
report,
|
|
571
|
+
'flux events',
|
|
572
|
+
'kubectl',
|
|
573
|
+
['--kubeconfig', kubeconfig, '-n', 'flux-system', 'get', 'events', '--sort-by=.lastTimestamp'],
|
|
574
|
+
env,
|
|
575
|
+
);
|
|
576
|
+
for (const controller of [
|
|
577
|
+
'source-controller',
|
|
578
|
+
'kustomize-controller',
|
|
579
|
+
'helm-controller',
|
|
580
|
+
'notification-controller',
|
|
581
|
+
]) {
|
|
582
|
+
report.subsection(`${controller} logs`);
|
|
583
|
+
probe(
|
|
584
|
+
report,
|
|
585
|
+
controller,
|
|
586
|
+
'kubectl',
|
|
587
|
+
[
|
|
588
|
+
'--kubeconfig',
|
|
589
|
+
kubeconfig,
|
|
590
|
+
'-n',
|
|
591
|
+
'flux-system',
|
|
592
|
+
'logs',
|
|
593
|
+
`deployment/${controller}`,
|
|
594
|
+
'--tail=40',
|
|
595
|
+
],
|
|
596
|
+
env,
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
// Describe the GitRepository — this is where stale-URL / auth errors appear
|
|
600
|
+
probe(
|
|
601
|
+
report,
|
|
602
|
+
'GitRepository describe',
|
|
603
|
+
'kubectl',
|
|
604
|
+
['--kubeconfig', kubeconfig, '-n', 'flux-system', 'describe', 'gitrepository'],
|
|
605
|
+
env,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function readControlplaneIp(envName) {
|
|
610
|
+
const projConfigPath = join(process.cwd(), '.vibecarbon.json');
|
|
611
|
+
if (!existsSync(projConfigPath)) return null;
|
|
612
|
+
try {
|
|
613
|
+
const data = JSON.parse(readFileSync(projConfigPath, 'utf-8'));
|
|
614
|
+
const envCfg = data?.environments?.[envName];
|
|
615
|
+
if (!envCfg) return null;
|
|
616
|
+
const masterServer = envCfg.servers?.find((s) => s.name === 'master' || s.name === 'primary');
|
|
617
|
+
return masterServer?.ip ?? null;
|
|
618
|
+
} catch {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export async function run(args) {
|
|
624
|
+
const { values, positional, errors } = parseFlags(args, SPEC);
|
|
625
|
+
|
|
626
|
+
if (errors.length > 0) {
|
|
627
|
+
for (const e of errors) {
|
|
628
|
+
process.stderr.write(`${c.error('✗')} ${e}\n`);
|
|
629
|
+
}
|
|
630
|
+
process.stderr.write(`Run ${c.info('vibecarbon diagnose -h')} for usage.\n`);
|
|
631
|
+
process.exit(1);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (values.h) {
|
|
635
|
+
process.stdout.write(renderHelp(SPEC));
|
|
636
|
+
process.exit(0);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Project guard runs first so an accidental `vibecarbon diagnose` from
|
|
640
|
+
// a parent directory emits the canonical message instead of stumbling
|
|
641
|
+
// through kubectl probes against a missing kubeconfig.
|
|
642
|
+
assertInProjectDir();
|
|
643
|
+
|
|
644
|
+
const envName = /** @type {string|undefined} */ (positional.env) ?? 'prod';
|
|
645
|
+
const requested = /** @type {string|undefined} */ (positional.section) ?? 'all';
|
|
646
|
+
if (!ALL_VALID_SECTIONS.includes(requested) && requested !== 'all') {
|
|
647
|
+
console.error(
|
|
648
|
+
c.error(`Unknown section "${requested}". Valid: ${ALL_VALID_SECTIONS.join(', ')}, all.`),
|
|
649
|
+
);
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
const sectionsToRun = requested === 'all' ? SECTIONS : [requested];
|
|
653
|
+
|
|
654
|
+
const cwd = process.cwd();
|
|
655
|
+
const kubeconfig = join(cwd, '.vibecarbon', `kubeconfig-${envName}`);
|
|
656
|
+
const sshKey = join(cwd, '.vibecarbon', `ssh-${envName}`);
|
|
657
|
+
const controlplaneIp = readControlplaneIp(envName);
|
|
658
|
+
|
|
659
|
+
const hcloudToken = loadHcloudToken();
|
|
660
|
+
const env = { ...process.env };
|
|
661
|
+
if (hcloudToken) env.HCLOUD_TOKEN = hcloudToken;
|
|
662
|
+
|
|
663
|
+
// Auto-detect operator IP and ensure firewall lets us in. Diagnose runs
|
|
664
|
+
// many kubectl + ssh probes; if the firewall blocks our IP every probe
|
|
665
|
+
// hangs to its timeout. Refresh lastUsedAt and apply on first miss.
|
|
666
|
+
const projectConfig = loadProjectConfig();
|
|
667
|
+
if (projectConfig && hcloudToken) {
|
|
668
|
+
try {
|
|
669
|
+
const isHA = !!projectConfig.environments?.[envName]?.ha?.enabled;
|
|
670
|
+
const result = await ensureOperatorIpAccess({
|
|
671
|
+
projectConfig,
|
|
672
|
+
environment: envName,
|
|
673
|
+
isHA,
|
|
674
|
+
apiToken: hcloudToken,
|
|
675
|
+
yes: false,
|
|
676
|
+
onMessage: (msg) => console.error(`${c.info('i')} ${msg}`),
|
|
677
|
+
});
|
|
678
|
+
if (result.added) {
|
|
679
|
+
console.error(`${c.success('✓')} Firewall updated: SSH/k8s API now allow ${result.cidr}`);
|
|
680
|
+
}
|
|
681
|
+
} catch (err) {
|
|
682
|
+
console.error(`${c.warning('!')} Operator-IP check failed: ${err.message}`);
|
|
683
|
+
console.error(
|
|
684
|
+
' Continuing — kubectl/SSH probes may time out if your IP is not allowlisted.',
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const report = new Report();
|
|
690
|
+
|
|
691
|
+
// Header so the saved report is self-describing
|
|
692
|
+
const ts = new Date().toISOString();
|
|
693
|
+
const header = [
|
|
694
|
+
'# vibecarbon diagnose',
|
|
695
|
+
`# env: ${envName}`,
|
|
696
|
+
`# sections: ${sectionsToRun.join(', ')}`,
|
|
697
|
+
`# timestamp: ${ts}`,
|
|
698
|
+
`# cwd: ${cwd}`,
|
|
699
|
+
`# kubeconfig: ${kubeconfig} ${existsSync(kubeconfig) ? '(found)' : '(MISSING)'}`,
|
|
700
|
+
`# sshKey: ${sshKey} ${existsSync(sshKey) ? '(found)' : '(MISSING)'}`,
|
|
701
|
+
`# masterIP: ${controlplaneIp ?? '(unknown)'}`,
|
|
702
|
+
`# hcloud: ${hcloudToken ? 'authenticated' : 'NOT AUTHENTICATED'}`,
|
|
703
|
+
].join('\n');
|
|
704
|
+
console.log(header);
|
|
705
|
+
report.lines.push(header);
|
|
706
|
+
|
|
707
|
+
const includesHcloud = sectionsToRun.includes('hcloud');
|
|
708
|
+
const needsKubectl = sectionsToRun.some((s) =>
|
|
709
|
+
['nodes', 'pods', 'network', 'flux', 'egress'].includes(s),
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
if (includesHcloud) diagnoseHcloud(report, env);
|
|
713
|
+
if (needsKubectl) {
|
|
714
|
+
if (!existsSync(kubeconfig)) {
|
|
715
|
+
report.warn(
|
|
716
|
+
`Skipping kubectl-based sections: kubeconfig not found at ${kubeconfig}. Run a deploy first.`,
|
|
717
|
+
);
|
|
718
|
+
} else {
|
|
719
|
+
if (sectionsToRun.includes('nodes'))
|
|
720
|
+
diagnoseNodes(report, env, kubeconfig, sshKey, controlplaneIp);
|
|
721
|
+
if (sectionsToRun.includes('pods')) diagnosePods(report, env, kubeconfig);
|
|
722
|
+
if (sectionsToRun.includes('network')) diagnoseNetwork(report, env, kubeconfig);
|
|
723
|
+
if (sectionsToRun.includes('egress'))
|
|
724
|
+
diagnoseEgress(report, env, kubeconfig, sshKey, controlplaneIp);
|
|
725
|
+
if (sectionsToRun.includes('flux')) diagnoseFlux(report, env, kubeconfig);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Persist
|
|
730
|
+
const diagDir = join(homedir(), '.vibecarbon');
|
|
731
|
+
if (!existsSync(diagDir)) mkdirSync(diagDir, { recursive: true });
|
|
732
|
+
const outFile = join(diagDir, `diag-${envName}-${ts.replace(/[:.]/g, '-')}.txt`);
|
|
733
|
+
writeFileSync(outFile, report.toString());
|
|
734
|
+
console.log(`\n${c.success('✓')} Report saved: ${outFile}`);
|
|
735
|
+
}
|