vibecheck-ai 2.0.1 → 5.0.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/bin/.generated +25 -0
- package/bin/_deprecations.js +463 -0
- package/bin/_router.js +46 -0
- package/bin/cli-hygiene.js +241 -0
- package/bin/dev/run-v2-torture.js +30 -0
- package/bin/registry.js +656 -0
- package/bin/runners/CLI_REFACTOR_SUMMARY.md +229 -0
- package/bin/runners/ENHANCEMENT_GUIDE.md +121 -0
- package/bin/runners/REPORT_AUDIT.md +64 -0
- package/bin/runners/cli-utils.js +1070 -0
- package/bin/runners/context/ai-task-decomposer.js +337 -0
- package/bin/runners/context/analyzer.js +513 -0
- package/bin/runners/context/api-contracts.js +427 -0
- package/bin/runners/context/context-diff.js +342 -0
- package/bin/runners/context/context-pruner.js +291 -0
- package/bin/runners/context/dependency-graph.js +414 -0
- package/bin/runners/context/generators/claude.js +107 -0
- package/bin/runners/context/generators/codex.js +108 -0
- package/bin/runners/context/generators/copilot.js +119 -0
- package/bin/runners/context/generators/cursor-enhanced.js +2525 -0
- package/bin/runners/context/generators/cursor.js +514 -0
- package/bin/runners/context/generators/mcp.js +169 -0
- package/bin/runners/context/generators/windsurf.js +180 -0
- package/bin/runners/context/git-context.js +304 -0
- package/bin/runners/context/index.js +1110 -0
- package/bin/runners/context/insights.js +173 -0
- package/bin/runners/context/mcp-server/generate-rules.js +337 -0
- package/bin/runners/context/mcp-server/index.js +1176 -0
- package/bin/runners/context/mcp-server/package.json +24 -0
- package/bin/runners/context/memory.js +200 -0
- package/bin/runners/context/monorepo.js +215 -0
- package/bin/runners/context/multi-repo-federation.js +404 -0
- package/bin/runners/context/patterns.js +253 -0
- package/bin/runners/context/proof-context.js +1264 -0
- package/bin/runners/context/security-scanner.js +541 -0
- package/bin/runners/context/semantic-search.js +350 -0
- package/bin/runners/context/shared.js +264 -0
- package/bin/runners/context/team-conventions.js +336 -0
- package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -0
- package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
- package/bin/runners/lib/agent-firewall/change-packet/builder.js +488 -0
- package/bin/runners/lib/agent-firewall/change-packet/schema.json +228 -0
- package/bin/runners/lib/agent-firewall/change-packet/store.js +200 -0
- package/bin/runners/lib/agent-firewall/claims/claim-types.js +21 -0
- package/bin/runners/lib/agent-firewall/claims/extractor.js +303 -0
- package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -0
- package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
- package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
- package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
- package/bin/runners/lib/agent-firewall/enforcement/gateway.js +1059 -0
- package/bin/runners/lib/agent-firewall/enforcement/index.js +98 -0
- package/bin/runners/lib/agent-firewall/enforcement/mode.js +318 -0
- package/bin/runners/lib/agent-firewall/enforcement/orchestrator.js +484 -0
- package/bin/runners/lib/agent-firewall/enforcement/proof-artifact.js +418 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/change-event.schema.json +173 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/intent.schema.json +181 -0
- package/bin/runners/lib/agent-firewall/enforcement/schemas/verdict.schema.json +222 -0
- package/bin/runners/lib/agent-firewall/enforcement/verdict-v2.js +333 -0
- package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +88 -0
- package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +75 -0
- package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +127 -0
- package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +213 -0
- package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +145 -0
- package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +19 -0
- package/bin/runners/lib/agent-firewall/fs-hook/installer.js +87 -0
- package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +184 -0
- package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +163 -0
- package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +107 -0
- package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +68 -0
- package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +66 -0
- package/bin/runners/lib/agent-firewall/index.js +200 -0
- package/bin/runners/lib/agent-firewall/integration/index.js +20 -0
- package/bin/runners/lib/agent-firewall/integration/ship-gate.js +437 -0
- package/bin/runners/lib/agent-firewall/intent/alignment-engine.js +634 -0
- package/bin/runners/lib/agent-firewall/intent/auto-detect.js +426 -0
- package/bin/runners/lib/agent-firewall/intent/index.js +102 -0
- package/bin/runners/lib/agent-firewall/intent/schema.js +352 -0
- package/bin/runners/lib/agent-firewall/intent/store.js +283 -0
- package/bin/runners/lib/agent-firewall/interception/fs-interceptor.js +502 -0
- package/bin/runners/lib/agent-firewall/interception/index.js +23 -0
- package/bin/runners/lib/agent-firewall/interceptor/base.js +308 -0
- package/bin/runners/lib/agent-firewall/interceptor/cursor.js +35 -0
- package/bin/runners/lib/agent-firewall/interceptor/vscode.js +35 -0
- package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +34 -0
- package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
- package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
- package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
- package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
- package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
- package/bin/runners/lib/agent-firewall/logger.js +141 -0
- package/bin/runners/lib/agent-firewall/policy/default-policy.json +90 -0
- package/bin/runners/lib/agent-firewall/policy/engine.js +103 -0
- package/bin/runners/lib/agent-firewall/policy/loader.js +451 -0
- package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +79 -0
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +227 -0
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +191 -0
- package/bin/runners/lib/agent-firewall/policy/rules/scope.js +93 -0
- package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +57 -0
- package/bin/runners/lib/agent-firewall/policy/schema.json +183 -0
- package/bin/runners/lib/agent-firewall/policy/verdict.js +54 -0
- package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
- package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
- package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
- package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
- package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
- package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
- package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
- package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
- package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
- package/bin/runners/lib/agent-firewall/risk/thresholds.js +322 -0
- package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
- package/bin/runners/lib/agent-firewall/session/collector.js +451 -0
- package/bin/runners/lib/agent-firewall/session/index.js +26 -0
- package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
- package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
- package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
- package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
- package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
- package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
- package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
- package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
- package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
- package/bin/runners/lib/agent-firewall/truthpack/index.js +67 -0
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +137 -0
- package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
- package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
- package/bin/runners/lib/ai-bridge.js +416 -0
- package/bin/runners/lib/analysis-core.js +309 -0
- package/bin/runners/lib/analyzers.js +2500 -0
- package/bin/runners/lib/api-client.js +269 -0
- package/bin/runners/lib/approve-output.js +235 -0
- package/bin/runners/lib/artifact-envelope.js +540 -0
- package/bin/runners/lib/assets/vibecheck-logo.png +0 -0
- package/bin/runners/lib/audit-bridge.js +391 -0
- package/bin/runners/lib/auth-shared.js +977 -0
- package/bin/runners/lib/auth-truth.js +193 -0
- package/bin/runners/lib/auth.js +215 -0
- package/bin/runners/lib/authority-badge.js +425 -0
- package/bin/runners/lib/backup.js +62 -0
- package/bin/runners/lib/billing.js +107 -0
- package/bin/runners/lib/checkpoint.js +941 -0
- package/bin/runners/lib/claims.js +118 -0
- package/bin/runners/lib/classify-output.js +204 -0
- package/bin/runners/lib/cleanup/engine.js +571 -0
- package/bin/runners/lib/cleanup/index.js +53 -0
- package/bin/runners/lib/cleanup/output.js +375 -0
- package/bin/runners/lib/cleanup/rules.js +1060 -0
- package/bin/runners/lib/cli-output.js +400 -0
- package/bin/runners/lib/cli-ui.js +540 -0
- package/bin/runners/lib/compliance-bridge-new.js +0 -0
- package/bin/runners/lib/compliance-bridge.js +165 -0
- package/bin/runners/lib/contracts/auth-contract.js +202 -0
- package/bin/runners/lib/contracts/env-contract.js +181 -0
- package/bin/runners/lib/contracts/external-contract.js +206 -0
- package/bin/runners/lib/contracts/guard.js +168 -0
- package/bin/runners/lib/contracts/index.js +89 -0
- package/bin/runners/lib/contracts/plan-validator.js +311 -0
- package/bin/runners/lib/contracts/route-contract.js +199 -0
- package/bin/runners/lib/contracts.js +804 -0
- package/bin/runners/lib/default-config.js +127 -0
- package/bin/runners/lib/detect.js +89 -0
- package/bin/runners/lib/detectors-v2.js +622 -0
- package/bin/runners/lib/doctor/autofix.js +254 -0
- package/bin/runners/lib/doctor/diagnosis-receipt.js +454 -0
- package/bin/runners/lib/doctor/failure-signatures.js +526 -0
- package/bin/runners/lib/doctor/fix-script.js +336 -0
- package/bin/runners/lib/doctor/index.js +37 -0
- package/bin/runners/lib/doctor/modules/build-tools.js +453 -0
- package/bin/runners/lib/doctor/modules/dependencies.js +325 -0
- package/bin/runners/lib/doctor/modules/index.js +105 -0
- package/bin/runners/lib/doctor/modules/network.js +250 -0
- package/bin/runners/lib/doctor/modules/os-quirks.js +706 -0
- package/bin/runners/lib/doctor/modules/project.js +312 -0
- package/bin/runners/lib/doctor/modules/repo-integrity.js +485 -0
- package/bin/runners/lib/doctor/modules/runtime.js +224 -0
- package/bin/runners/lib/doctor/modules/security.js +350 -0
- package/bin/runners/lib/doctor/modules/system.js +213 -0
- package/bin/runners/lib/doctor/modules/vibecheck.js +394 -0
- package/bin/runners/lib/doctor/reporter.js +262 -0
- package/bin/runners/lib/doctor/safe-repair.js +384 -0
- package/bin/runners/lib/doctor/service.js +262 -0
- package/bin/runners/lib/doctor/types.js +113 -0
- package/bin/runners/lib/doctor/ui.js +263 -0
- package/bin/runners/lib/doctor-enhanced.js +233 -0
- package/bin/runners/lib/doctor-output.js +226 -0
- package/bin/runners/lib/doctor-v2.js +608 -0
- package/bin/runners/lib/drift.js +425 -0
- package/bin/runners/lib/enforcement.js +72 -0
- package/bin/runners/lib/engine/ast-cache.js +210 -0
- package/bin/runners/lib/engine/auth-extractor.js +211 -0
- package/bin/runners/lib/engine/billing-extractor.js +112 -0
- package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
- package/bin/runners/lib/engine/env-extractor.js +207 -0
- package/bin/runners/lib/engine/express-extractor.js +208 -0
- package/bin/runners/lib/engine/extractors.js +849 -0
- package/bin/runners/lib/engine/index.js +207 -0
- package/bin/runners/lib/engine/repo-index.js +514 -0
- package/bin/runners/lib/engine/types.js +124 -0
- package/bin/runners/lib/engines/accessibility-engine.js +190 -0
- package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
- package/bin/runners/lib/engines/ast-cache.js +99 -0
- package/bin/runners/lib/engines/attack-detector.js +1192 -0
- package/bin/runners/lib/engines/code-quality-engine.js +255 -0
- package/bin/runners/lib/engines/console-logs-engine.js +115 -0
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
- package/bin/runners/lib/engines/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
- package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
- package/bin/runners/lib/engines/file-filter.js +131 -0
- package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
- package/bin/runners/lib/engines/mock-data-engine.js +272 -0
- package/bin/runners/lib/engines/parallel-processor.js +71 -0
- package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
- package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
- package/bin/runners/lib/engines/type-aware-engine.js +152 -0
- package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
- package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
- package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
- package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
- package/bin/runners/lib/enterprise-detect.js +603 -0
- package/bin/runners/lib/enterprise-init.js +942 -0
- package/bin/runners/lib/entitlements-v2.js +265 -0
- package/bin/runners/lib/entitlements.generated.js +0 -0
- package/bin/runners/lib/entitlements.js +340 -0
- package/bin/runners/lib/env-resolver.js +417 -0
- package/bin/runners/lib/env-template.js +66 -0
- package/bin/runners/lib/env.js +189 -0
- package/bin/runners/lib/error-handler.js +368 -0
- package/bin/runners/lib/error-messages.js +289 -0
- package/bin/runners/lib/evidence-pack.js +684 -0
- package/bin/runners/lib/exit-codes.js +275 -0
- package/bin/runners/lib/extractors/client-calls.js +990 -0
- package/bin/runners/lib/extractors/fastify-route-dump.js +573 -0
- package/bin/runners/lib/extractors/fastify-routes.js +426 -0
- package/bin/runners/lib/extractors/index.js +363 -0
- package/bin/runners/lib/extractors/next-routes.js +524 -0
- package/bin/runners/lib/extractors/proof-graph.js +431 -0
- package/bin/runners/lib/extractors/route-matcher.js +451 -0
- package/bin/runners/lib/extractors/truthpack-v2.js +377 -0
- package/bin/runners/lib/extractors/ui-bindings.js +547 -0
- package/bin/runners/lib/finding-id.js +69 -0
- package/bin/runners/lib/finding-sorter.js +89 -0
- package/bin/runners/lib/findings-schema.js +281 -0
- package/bin/runners/lib/fingerprint.js +377 -0
- package/bin/runners/lib/firewall-prompt.js +50 -0
- package/bin/runners/lib/fix-output.js +228 -0
- package/bin/runners/lib/global-flags.js +250 -0
- package/bin/runners/lib/graph/graph-builder.js +265 -0
- package/bin/runners/lib/graph/html-renderer.js +413 -0
- package/bin/runners/lib/graph/index.js +32 -0
- package/bin/runners/lib/graph/runtime-collector.js +215 -0
- package/bin/runners/lib/graph/static-extractor.js +518 -0
- package/bin/runners/lib/help-formatter.js +413 -0
- package/bin/runners/lib/html-proof-report.js +913 -0
- package/bin/runners/lib/html-report.js +650 -0
- package/bin/runners/lib/init-wizard.js +601 -0
- package/bin/runners/lib/interactive-menu.js +1496 -0
- package/bin/runners/lib/json-output.js +76 -0
- package/bin/runners/lib/llm.js +75 -0
- package/bin/runners/lib/logger.js +38 -0
- package/bin/runners/lib/meter.js +61 -0
- package/bin/runners/lib/missions/briefing.js +427 -0
- package/bin/runners/lib/missions/checkpoint.js +753 -0
- package/bin/runners/lib/missions/evidence.js +126 -0
- package/bin/runners/lib/missions/hardening.js +851 -0
- package/bin/runners/lib/missions/plan.js +648 -0
- package/bin/runners/lib/missions/safety-gates.js +645 -0
- package/bin/runners/lib/missions/schema.js +478 -0
- package/bin/runners/lib/missions/templates.js +317 -0
- package/bin/runners/lib/next-action.js +560 -0
- package/bin/runners/lib/packs/bundle.js +675 -0
- package/bin/runners/lib/packs/evidence-pack.js +671 -0
- package/bin/runners/lib/packs/pack-factory.js +837 -0
- package/bin/runners/lib/packs/permissions-pack.js +686 -0
- package/bin/runners/lib/packs/proof-graph-pack.js +779 -0
- package/bin/runners/lib/patch.js +40 -0
- package/bin/runners/lib/permissions/auth-model.js +213 -0
- package/bin/runners/lib/permissions/idor-prover.js +205 -0
- package/bin/runners/lib/permissions/index.js +45 -0
- package/bin/runners/lib/permissions/matrix-builder.js +198 -0
- package/bin/runners/lib/pkgjson.js +28 -0
- package/bin/runners/lib/policy.js +295 -0
- package/bin/runners/lib/polish/accessibility.js +62 -0
- package/bin/runners/lib/polish/analyzer.js +93 -0
- package/bin/runners/lib/polish/backend.js +87 -0
- package/bin/runners/lib/polish/configuration.js +83 -0
- package/bin/runners/lib/polish/documentation.js +83 -0
- package/bin/runners/lib/polish/frontend.js +817 -0
- package/bin/runners/lib/polish/index.js +27 -0
- package/bin/runners/lib/polish/infrastructure.js +80 -0
- package/bin/runners/lib/polish/internationalization.js +85 -0
- package/bin/runners/lib/polish/libraries.js +180 -0
- package/bin/runners/lib/polish/observability.js +75 -0
- package/bin/runners/lib/polish/performance.js +64 -0
- package/bin/runners/lib/polish/privacy.js +110 -0
- package/bin/runners/lib/polish/resilience.js +92 -0
- package/bin/runners/lib/polish/security.js +78 -0
- package/bin/runners/lib/polish/seo.js +71 -0
- package/bin/runners/lib/polish/styles.js +62 -0
- package/bin/runners/lib/polish/utils.js +104 -0
- package/bin/runners/lib/preflight.js +142 -0
- package/bin/runners/lib/prerequisites.js +149 -0
- package/bin/runners/lib/prove-output.js +220 -0
- package/bin/runners/lib/reality/correlation-detectors.js +359 -0
- package/bin/runners/lib/reality/index.js +318 -0
- package/bin/runners/lib/reality/request-hashing.js +416 -0
- package/bin/runners/lib/reality/request-mapper.js +453 -0
- package/bin/runners/lib/reality/safety-rails.js +463 -0
- package/bin/runners/lib/reality/semantic-snapshot.js +408 -0
- package/bin/runners/lib/reality/toast-detector.js +393 -0
- package/bin/runners/lib/reality-findings.js +84 -0
- package/bin/runners/lib/reality-output.js +231 -0
- package/bin/runners/lib/receipts.js +179 -0
- package/bin/runners/lib/redact.js +29 -0
- package/bin/runners/lib/replay/capsule-manager.js +154 -0
- package/bin/runners/lib/replay/index.js +263 -0
- package/bin/runners/lib/replay/player.js +348 -0
- package/bin/runners/lib/replay/recorder.js +331 -0
- package/bin/runners/lib/report-engine.js +626 -0
- package/bin/runners/lib/report-html.js +1233 -0
- package/bin/runners/lib/report-output.js +366 -0
- package/bin/runners/lib/report-templates.js +967 -0
- package/bin/runners/lib/report.js +135 -0
- package/bin/runners/lib/route-detection.js +1209 -0
- package/bin/runners/lib/route-truth.js +1322 -0
- package/bin/runners/lib/safelist/index.js +96 -0
- package/bin/runners/lib/safelist/integration.js +334 -0
- package/bin/runners/lib/safelist/matcher.js +696 -0
- package/bin/runners/lib/safelist/schema.js +948 -0
- package/bin/runners/lib/safelist/store.js +438 -0
- package/bin/runners/lib/sandbox/index.js +59 -0
- package/bin/runners/lib/sandbox/proof-chain.js +399 -0
- package/bin/runners/lib/sandbox/sandbox-runner.js +205 -0
- package/bin/runners/lib/sandbox/worktree.js +174 -0
- package/bin/runners/lib/scan-cache.js +330 -0
- package/bin/runners/lib/scan-output-schema.js +344 -0
- package/bin/runners/lib/scan-output.js +631 -0
- package/bin/runners/lib/scan-runner.js +135 -0
- package/bin/runners/lib/schema-validator.js +350 -0
- package/bin/runners/lib/schemas/ajv-validator.js +464 -0
- package/bin/runners/lib/schemas/contracts.schema.json +160 -0
- package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
- package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
- package/bin/runners/lib/schemas/finding.schema.json +100 -0
- package/bin/runners/lib/schemas/mission-pack.schema.json +206 -0
- package/bin/runners/lib/schemas/proof-graph.schema.json +176 -0
- package/bin/runners/lib/schemas/reality-report.schema.json +162 -0
- package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
- package/bin/runners/lib/schemas/run-request.schema.json +108 -0
- package/bin/runners/lib/schemas/share-pack.schema.json +180 -0
- package/bin/runners/lib/schemas/ship-manifest.schema.json +251 -0
- package/bin/runners/lib/schemas/ship-report.schema.json +117 -0
- package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -0
- package/bin/runners/lib/schemas/validator.js +465 -0
- package/bin/runners/lib/schemas/verdict.schema.json +140 -0
- package/bin/runners/lib/score-history.js +282 -0
- package/bin/runners/lib/security-bridge.js +249 -0
- package/bin/runners/lib/server-usage.js +513 -0
- package/bin/runners/lib/share-pack.js +239 -0
- package/bin/runners/lib/ship-gate.js +832 -0
- package/bin/runners/lib/ship-manifest.js +1153 -0
- package/bin/runners/lib/ship-output-enterprise.js +239 -0
- package/bin/runners/lib/ship-output.js +1128 -0
- package/bin/runners/lib/snippets.js +67 -0
- package/bin/runners/lib/status-output.js +340 -0
- package/bin/runners/lib/terminal-ui.js +356 -0
- package/bin/runners/lib/truth.js +1691 -0
- package/bin/runners/lib/ui.js +562 -0
- package/bin/runners/lib/unified-cli-output.js +947 -0
- package/bin/runners/lib/unified-output.js +197 -0
- package/bin/runners/lib/upsell.js +410 -0
- package/bin/runners/lib/usage.js +153 -0
- package/bin/runners/lib/validate-patch.js +156 -0
- package/bin/runners/lib/verdict-engine.js +628 -0
- package/bin/runners/lib/verification.js +345 -0
- package/bin/runners/lib/why-tree.js +650 -0
- package/bin/runners/reality/engine.js +917 -0
- package/bin/runners/reality/flows.js +122 -0
- package/bin/runners/reality/report.js +378 -0
- package/bin/runners/reality/session.js +193 -0
- package/bin/runners/runAIAgent.js +229 -0
- package/bin/runners/runAgent.d.ts +5 -0
- package/bin/runners/runAgent.js +161 -0
- package/bin/runners/runAllowlist.js +418 -0
- package/bin/runners/runApprove.js +320 -0
- package/bin/runners/runAudit.js +692 -0
- package/bin/runners/runAuth.js +731 -0
- package/bin/runners/runCI.js +353 -0
- package/bin/runners/runCheckpoint.js +530 -0
- package/bin/runners/runClassify.js +928 -0
- package/bin/runners/runCleanup.js +343 -0
- package/bin/runners/runContext.d.ts +4 -0
- package/bin/runners/runContext.js +175 -0
- package/bin/runners/runDoctor.js +877 -0
- package/bin/runners/runEvidencePack.js +362 -0
- package/bin/runners/runFirewall.d.ts +5 -0
- package/bin/runners/runFirewall.js +134 -0
- package/bin/runners/runFirewallHook.d.ts +5 -0
- package/bin/runners/runFirewallHook.js +56 -0
- package/bin/runners/runFix.js +1355 -0
- package/bin/runners/runForge.js +451 -0
- package/bin/runners/runGuard.js +262 -0
- package/bin/runners/runInit.js +1927 -0
- package/bin/runners/runIntent.js +906 -0
- package/bin/runners/runKickoff.js +878 -0
- package/bin/runners/runLabs.js +424 -0
- package/bin/runners/runLaunch.js +2000 -0
- package/bin/runners/runLink.js +785 -0
- package/bin/runners/runMcp.js +1875 -0
- package/bin/runners/runPacks.js +2089 -0
- package/bin/runners/runPolish.d.ts +4 -0
- package/bin/runners/runPolish.js +390 -0
- package/bin/runners/runPromptFirewall.js +211 -0
- package/bin/runners/runProve.js +1411 -0
- package/bin/runners/runQuickstart.js +531 -0
- package/bin/runners/runReality.js +2260 -0
- package/bin/runners/runReport.js +726 -0
- package/bin/runners/runRuntime.js +110 -0
- package/bin/runners/runSafelist.js +1190 -0
- package/bin/runners/runScan.js +688 -0
- package/bin/runners/runShield.js +1282 -0
- package/bin/runners/runShip.js +1660 -0
- package/bin/runners/runTruth.d.ts +5 -0
- package/bin/runners/runTruth.js +101 -0
- package/bin/runners/runValidate.js +179 -0
- package/bin/runners/runWatch.js +478 -0
- package/bin/runners/utils.js +360 -0
- package/bin/scan.js +617 -0
- package/bin/vibecheck.js +1617 -0
- package/dist/guardrail/index.d.ts +2405 -0
- package/dist/guardrail/index.js +9747 -0
- package/dist/guardrail/index.js.map +1 -0
- package/dist/scanner/index.d.ts +282 -0
- package/dist/scanner/index.js +3395 -0
- package/dist/scanner/index.js.map +1 -0
- package/package.json +123 -104
- package/README.md +0 -491
- package/dist/index.js +0 -99711
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,2000 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibecheck launch - Pre-Release Validation Wizard
|
|
3
|
+
*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
5
|
+
* THE LAST 10 MINUTES BEFORE RELEASE
|
|
6
|
+
* ═══════════════════════════════════════════════════════════════════════════════
|
|
7
|
+
*
|
|
8
|
+
* One command to validate everything:
|
|
9
|
+
* • Build & Compilation
|
|
10
|
+
* • Environment Wiring
|
|
11
|
+
* • Route Integrity
|
|
12
|
+
* • Auth Configuration
|
|
13
|
+
* • Integration Health
|
|
14
|
+
* • Security Posture
|
|
15
|
+
*
|
|
16
|
+
* @module bin/runners/runLaunch
|
|
17
|
+
* @version 2.0.0
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
"use strict";
|
|
21
|
+
|
|
22
|
+
const path = require("path");
|
|
23
|
+
const fs = require("fs");
|
|
24
|
+
const readline = require("readline");
|
|
25
|
+
const crypto = require("crypto");
|
|
26
|
+
const { execSync, spawn } = require("child_process");
|
|
27
|
+
|
|
28
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
29
|
+
// LAZY IMPORTS
|
|
30
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
31
|
+
|
|
32
|
+
let _globalFlags = null;
|
|
33
|
+
let _exitCodes = null;
|
|
34
|
+
let _errorHandler = null;
|
|
35
|
+
let _cliOutput = null;
|
|
36
|
+
|
|
37
|
+
function getGlobalFlags() {
|
|
38
|
+
if (!_globalFlags) _globalFlags = require("./lib/global-flags");
|
|
39
|
+
return _globalFlags;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getExitCodes() {
|
|
43
|
+
if (!_exitCodes) _exitCodes = require("./lib/exit-codes");
|
|
44
|
+
return _exitCodes;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getErrorHandler() {
|
|
48
|
+
if (!_errorHandler) _errorHandler = require("./lib/error-handler");
|
|
49
|
+
return _errorHandler;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getCliOutput() {
|
|
53
|
+
if (!_cliOutput) _cliOutput = require("./lib/unified-cli-output");
|
|
54
|
+
return _cliOutput;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
58
|
+
// CONSTANTS
|
|
59
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
60
|
+
|
|
61
|
+
const LAUNCH_VERSION = "2.0.0";
|
|
62
|
+
const DEFAULT_TIMEOUT = 600000; // 10 minutes
|
|
63
|
+
const CHECK_TIMEOUT = 60000; // 1 minute per check
|
|
64
|
+
|
|
65
|
+
const PHASES = {
|
|
66
|
+
build: {
|
|
67
|
+
name: "Build Validation",
|
|
68
|
+
description: "TypeScript compilation, artifacts, bundle analysis",
|
|
69
|
+
icon: "🏗️",
|
|
70
|
+
estimatedTime: "~2 min",
|
|
71
|
+
},
|
|
72
|
+
env: {
|
|
73
|
+
name: "Environment Wiring",
|
|
74
|
+
description: "Variables, secrets, URL validation",
|
|
75
|
+
icon: "🌍",
|
|
76
|
+
estimatedTime: "~1 min",
|
|
77
|
+
},
|
|
78
|
+
routes: {
|
|
79
|
+
name: "Route Integrity",
|
|
80
|
+
description: "Registration, handlers, contract drift",
|
|
81
|
+
icon: "🛤️",
|
|
82
|
+
estimatedTime: "~2 min",
|
|
83
|
+
},
|
|
84
|
+
auth: {
|
|
85
|
+
name: "Auth Configuration",
|
|
86
|
+
description: "Middleware, protection, CORS, rate limiting",
|
|
87
|
+
icon: "🔐",
|
|
88
|
+
estimatedTime: "~1 min",
|
|
89
|
+
},
|
|
90
|
+
integrations: {
|
|
91
|
+
name: "Integration Health",
|
|
92
|
+
description: "Database, Redis, billing, webhooks",
|
|
93
|
+
icon: "🔗",
|
|
94
|
+
estimatedTime: "~2 min",
|
|
95
|
+
},
|
|
96
|
+
security: {
|
|
97
|
+
name: "Security Posture",
|
|
98
|
+
description: "Secrets scan, dependency audit, mock detection",
|
|
99
|
+
icon: "🛡️",
|
|
100
|
+
estimatedTime: "~2 min",
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const PHASE_ORDER = ["build", "env", "routes", "auth", "integrations", "security"];
|
|
105
|
+
|
|
106
|
+
// Secret patterns (critical - must not be in code)
|
|
107
|
+
const SECRET_PATTERNS = [
|
|
108
|
+
{ name: "Stripe Live Key", pattern: /sk_live_[a-zA-Z0-9]{24,}/g, severity: "BLOCK" },
|
|
109
|
+
{ name: "AWS Access Key", pattern: /AKIA[A-Z0-9]{16}/g, severity: "BLOCK" },
|
|
110
|
+
{ name: "Private Key", pattern: /-----BEGIN\s+(RSA|EC|DSA|OPENSSH)?\s*PRIVATE KEY-----/g, severity: "BLOCK" },
|
|
111
|
+
{ name: "GitHub Token", pattern: /ghp_[a-zA-Z0-9]{36}/g, severity: "BLOCK" },
|
|
112
|
+
{ name: "Generic API Key", pattern: /['"]?api[_-]?key['"]?\s*[:=]\s*['"][a-zA-Z0-9_-]{20,}['"]/gi, severity: "WARN" },
|
|
113
|
+
{ name: "Password Assignment", pattern: /['"]?password['"]?\s*[:=]\s*['"][^'"]{8,}['"]/gi, severity: "WARN" },
|
|
114
|
+
{ name: "JWT Secret Hardcoded", pattern: /jwt[_-]?secret\s*[:=]\s*['"][^'"]+['"]/gi, severity: "BLOCK" },
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
// Mock data patterns
|
|
118
|
+
const MOCK_PATTERNS = [
|
|
119
|
+
{ name: "Demo User", pattern: /demo@|test@example|user@test|admin@localhost/gi },
|
|
120
|
+
{ name: "Placeholder Email", pattern: /your[_-]?email@|example\.com/gi },
|
|
121
|
+
{ name: "Fake Data", pattern: /FAKE_|MOCK_|PLACEHOLDER_|TODO_REPLACE/gi },
|
|
122
|
+
{ name: "Lorem Ipsum", pattern: /lorem\s+ipsum/gi },
|
|
123
|
+
{ name: "Test Mode Flag", pattern: /isTest\s*[:=]\s*true|testMode\s*[:=]\s*true/gi },
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
// Auth patterns to detect
|
|
127
|
+
const AUTH_PATTERNS = {
|
|
128
|
+
middleware: [
|
|
129
|
+
/authMiddleware/,
|
|
130
|
+
/authenticate\s*\(/,
|
|
131
|
+
/requireAuth/,
|
|
132
|
+
/isAuthenticated/,
|
|
133
|
+
/verifyToken/,
|
|
134
|
+
/passport\.authenticate/,
|
|
135
|
+
],
|
|
136
|
+
hashBeforeLookup: [
|
|
137
|
+
/hashApiKey/,
|
|
138
|
+
/hash.*before.*lookup/i,
|
|
139
|
+
/createHash.*apiKey/i,
|
|
140
|
+
/sha256.*apiKey/i,
|
|
141
|
+
],
|
|
142
|
+
rateLimiting: [
|
|
143
|
+
/rateLimit/i,
|
|
144
|
+
/rate.*limit/i,
|
|
145
|
+
/express-rate-limit/,
|
|
146
|
+
/@fastify\/rate-limit/,
|
|
147
|
+
/throttle/i,
|
|
148
|
+
],
|
|
149
|
+
cors: [
|
|
150
|
+
/cors\s*\(/,
|
|
151
|
+
/ALLOWED_ORIGINS/,
|
|
152
|
+
/Access-Control-Allow-Origin/,
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Files to scan for security issues
|
|
157
|
+
const SECURITY_PATHS = [
|
|
158
|
+
"src/middleware",
|
|
159
|
+
"src/auth",
|
|
160
|
+
"src/routes/auth",
|
|
161
|
+
"apps/api/src/middleware",
|
|
162
|
+
"apps/api/src/routes/auth",
|
|
163
|
+
"apps/api/src/services/auth",
|
|
164
|
+
"apps/api/src/services/api-key",
|
|
165
|
+
"apps/api/src/services/encryption",
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
169
|
+
// ANSI STYLING
|
|
170
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
171
|
+
|
|
172
|
+
const SUPPORTS_COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
173
|
+
|
|
174
|
+
const c = SUPPORTS_COLOR ? {
|
|
175
|
+
reset: "\x1b[0m",
|
|
176
|
+
bold: "\x1b[1m",
|
|
177
|
+
dim: "\x1b[2m",
|
|
178
|
+
red: "\x1b[31m",
|
|
179
|
+
green: "\x1b[32m",
|
|
180
|
+
yellow: "\x1b[33m",
|
|
181
|
+
blue: "\x1b[34m",
|
|
182
|
+
magenta: "\x1b[35m",
|
|
183
|
+
cyan: "\x1b[36m",
|
|
184
|
+
white: "\x1b[37m",
|
|
185
|
+
gray: "\x1b[90m",
|
|
186
|
+
bgGreen: "\x1b[42m",
|
|
187
|
+
bgYellow: "\x1b[43m",
|
|
188
|
+
bgRed: "\x1b[41m",
|
|
189
|
+
bgCyan: "\x1b[46m",
|
|
190
|
+
bgMagenta: "\x1b[45m",
|
|
191
|
+
} : Object.fromEntries([
|
|
192
|
+
"reset", "bold", "dim", "red", "green", "yellow", "blue",
|
|
193
|
+
"magenta", "cyan", "white", "gray", "bgGreen", "bgYellow", "bgRed", "bgCyan", "bgMagenta"
|
|
194
|
+
].map(k => [k, ""]));
|
|
195
|
+
|
|
196
|
+
const sym = {
|
|
197
|
+
check: "✓",
|
|
198
|
+
cross: "✗",
|
|
199
|
+
warning: "⚠",
|
|
200
|
+
arrow: "→",
|
|
201
|
+
rocket: "🚀",
|
|
202
|
+
block: "🚫",
|
|
203
|
+
spinner: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
|
204
|
+
box: {
|
|
205
|
+
topLeft: "╔", topRight: "╗", bottomLeft: "╚", bottomRight: "╝",
|
|
206
|
+
horizontal: "═", vertical: "║",
|
|
207
|
+
teeRight: "╠", teeLeft: "╣",
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
212
|
+
// BANNER (uses unified CLI output)
|
|
213
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
214
|
+
|
|
215
|
+
function printUnifiedBanner(projectRoot, opts) {
|
|
216
|
+
const cli = getCliOutput();
|
|
217
|
+
const projectName = path.basename(projectRoot);
|
|
218
|
+
|
|
219
|
+
// Determine integrity based on environment
|
|
220
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
221
|
+
const integrity = isProduction ? "PRODUCTION" : "DEVELOPMENT";
|
|
222
|
+
|
|
223
|
+
console.log(cli.renderFullHeader("launch", {
|
|
224
|
+
version: LAUNCH_VERSION,
|
|
225
|
+
tier: "PRO",
|
|
226
|
+
integrity,
|
|
227
|
+
target: projectRoot,
|
|
228
|
+
sessionId: cli.generateSessionId(),
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
233
|
+
// UTILITIES
|
|
234
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Recursively find files matching extensions
|
|
238
|
+
*/
|
|
239
|
+
function findFiles(dir, extensions, ignore = []) {
|
|
240
|
+
const results = [];
|
|
241
|
+
if (!fs.existsSync(dir)) return results;
|
|
242
|
+
|
|
243
|
+
const defaultIgnore = ["node_modules", ".git", "dist", "build", ".next", "coverage", "__pycache__"];
|
|
244
|
+
const ignoreSet = new Set([...defaultIgnore, ...ignore]);
|
|
245
|
+
|
|
246
|
+
function walk(currentDir) {
|
|
247
|
+
try {
|
|
248
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
249
|
+
for (const entry of entries) {
|
|
250
|
+
if (ignoreSet.has(entry.name)) continue;
|
|
251
|
+
|
|
252
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
253
|
+
if (entry.isDirectory()) {
|
|
254
|
+
walk(fullPath);
|
|
255
|
+
} else if (entry.isFile()) {
|
|
256
|
+
const ext = path.extname(entry.name);
|
|
257
|
+
if (extensions.includes(ext)) {
|
|
258
|
+
results.push(fullPath);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
// Permission denied or other errors - skip
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
walk(dir);
|
|
268
|
+
return results;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Scan file content for patterns
|
|
273
|
+
*/
|
|
274
|
+
function scanFileForPatterns(filePath, patterns) {
|
|
275
|
+
const findings = [];
|
|
276
|
+
try {
|
|
277
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
278
|
+
const lines = content.split("\n");
|
|
279
|
+
|
|
280
|
+
for (const { name, pattern, severity } of patterns) {
|
|
281
|
+
// Reset regex lastIndex for global patterns
|
|
282
|
+
if (pattern.global) pattern.lastIndex = 0;
|
|
283
|
+
|
|
284
|
+
let lineNum = 0;
|
|
285
|
+
for (const line of lines) {
|
|
286
|
+
lineNum++;
|
|
287
|
+
// Reset for each line
|
|
288
|
+
if (pattern.global) pattern.lastIndex = 0;
|
|
289
|
+
|
|
290
|
+
if (pattern.test(line)) {
|
|
291
|
+
findings.push({
|
|
292
|
+
pattern: name,
|
|
293
|
+
file: filePath,
|
|
294
|
+
line: lineNum,
|
|
295
|
+
content: line.trim().substring(0, 100),
|
|
296
|
+
severity: severity || "WARN",
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
// File read error - skip
|
|
303
|
+
}
|
|
304
|
+
return findings;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Check if any pattern matches in a file
|
|
309
|
+
*/
|
|
310
|
+
function fileContainsPatterns(filePath, patterns) {
|
|
311
|
+
try {
|
|
312
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
313
|
+
return patterns.some(p => p.test(content));
|
|
314
|
+
} catch {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Get git info for audit trail
|
|
321
|
+
*/
|
|
322
|
+
function getGitInfo(projectRoot) {
|
|
323
|
+
try {
|
|
324
|
+
const commit = execSync("git rev-parse HEAD", { cwd: projectRoot, stdio: "pipe" }).toString().trim();
|
|
325
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: projectRoot, stdio: "pipe" }).toString().trim();
|
|
326
|
+
const status = execSync("git status --porcelain", { cwd: projectRoot, stdio: "pipe" }).toString().trim();
|
|
327
|
+
return {
|
|
328
|
+
commit: commit.substring(0, 12),
|
|
329
|
+
branch,
|
|
330
|
+
dirty: status.length > 0,
|
|
331
|
+
};
|
|
332
|
+
} catch {
|
|
333
|
+
return { commit: "unknown", branch: "unknown", dirty: false };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Load environment from .env files
|
|
339
|
+
*/
|
|
340
|
+
function loadEnvFiles(projectRoot) {
|
|
341
|
+
const env = { ...process.env };
|
|
342
|
+
const envFiles = [".env", ".env.local", ".env.production"];
|
|
343
|
+
|
|
344
|
+
for (const file of envFiles) {
|
|
345
|
+
const filePath = path.join(projectRoot, file);
|
|
346
|
+
if (fs.existsSync(filePath)) {
|
|
347
|
+
try {
|
|
348
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
349
|
+
for (const line of content.split("\n")) {
|
|
350
|
+
const match = line.match(/^([^#=]+)=(.*)$/);
|
|
351
|
+
if (match) {
|
|
352
|
+
const key = match[1].trim();
|
|
353
|
+
const value = match[2].trim().replace(/^["']|["']$/g, "");
|
|
354
|
+
if (!env[key]) env[key] = value; // Don't override existing
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} catch {
|
|
358
|
+
// Skip unreadable files
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return env;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Detect package manager
|
|
368
|
+
*/
|
|
369
|
+
function detectPackageManager(projectRoot) {
|
|
370
|
+
if (fs.existsSync(path.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
|
|
371
|
+
if (fs.existsSync(path.join(projectRoot, "yarn.lock"))) return "yarn";
|
|
372
|
+
if (fs.existsSync(path.join(projectRoot, "bun.lockb"))) return "bun";
|
|
373
|
+
return "npm";
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Execute command with timeout
|
|
378
|
+
*/
|
|
379
|
+
function execWithTimeout(command, options = {}, timeout = CHECK_TIMEOUT) {
|
|
380
|
+
return new Promise((resolve, reject) => {
|
|
381
|
+
const timer = setTimeout(() => {
|
|
382
|
+
reject(new Error(`Command timed out after ${timeout}ms`));
|
|
383
|
+
}, timeout);
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
const result = execSync(command, { ...options, stdio: "pipe", timeout });
|
|
387
|
+
clearTimeout(timer);
|
|
388
|
+
resolve(result.toString());
|
|
389
|
+
} catch (err) {
|
|
390
|
+
clearTimeout(timer);
|
|
391
|
+
reject(err);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
397
|
+
// ARGS PARSING
|
|
398
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
399
|
+
|
|
400
|
+
function parseArgs(args) {
|
|
401
|
+
const { parseGlobalFlags } = getGlobalFlags();
|
|
402
|
+
const { flags: globalFlags, cleanArgs } = parseGlobalFlags(args);
|
|
403
|
+
|
|
404
|
+
const opts = {
|
|
405
|
+
path: globalFlags.path || process.cwd(),
|
|
406
|
+
json: globalFlags.json || false,
|
|
407
|
+
ci: globalFlags.ci || false,
|
|
408
|
+
quiet: globalFlags.quiet || false,
|
|
409
|
+
verbose: globalFlags.verbose || false,
|
|
410
|
+
noBanner: globalFlags.noBanner || false,
|
|
411
|
+
help: globalFlags.help || false,
|
|
412
|
+
// Launch-specific options
|
|
413
|
+
strict: false,
|
|
414
|
+
skipSlow: false,
|
|
415
|
+
timeout: DEFAULT_TIMEOUT,
|
|
416
|
+
failFast: false,
|
|
417
|
+
skipPhases: [],
|
|
418
|
+
onlyPhases: [],
|
|
419
|
+
bundle: false,
|
|
420
|
+
output: null,
|
|
421
|
+
dryRun: false,
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
for (let i = 0; i < cleanArgs.length; i++) {
|
|
425
|
+
const arg = cleanArgs[i];
|
|
426
|
+
switch (arg) {
|
|
427
|
+
case "--strict":
|
|
428
|
+
opts.strict = true;
|
|
429
|
+
break;
|
|
430
|
+
case "--skip-slow":
|
|
431
|
+
opts.skipSlow = true;
|
|
432
|
+
break;
|
|
433
|
+
case "--timeout":
|
|
434
|
+
opts.timeout = parseInt(cleanArgs[++i], 10) || DEFAULT_TIMEOUT;
|
|
435
|
+
break;
|
|
436
|
+
case "--fail-fast":
|
|
437
|
+
opts.failFast = true;
|
|
438
|
+
break;
|
|
439
|
+
case "--skip":
|
|
440
|
+
opts.skipPhases = (cleanArgs[++i] || "").split(",").map(s => s.trim()).filter(Boolean);
|
|
441
|
+
break;
|
|
442
|
+
case "--only":
|
|
443
|
+
opts.onlyPhases = (cleanArgs[++i] || "").split(",").map(s => s.trim()).filter(Boolean);
|
|
444
|
+
break;
|
|
445
|
+
case "--bundle":
|
|
446
|
+
opts.bundle = true;
|
|
447
|
+
break;
|
|
448
|
+
case "--output":
|
|
449
|
+
case "-o":
|
|
450
|
+
opts.output = cleanArgs[++i];
|
|
451
|
+
break;
|
|
452
|
+
case "--path":
|
|
453
|
+
case "-p":
|
|
454
|
+
opts.path = cleanArgs[++i] || process.cwd();
|
|
455
|
+
break;
|
|
456
|
+
case "--dry-run":
|
|
457
|
+
opts.dryRun = true;
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
opts.path = path.resolve(opts.path);
|
|
463
|
+
return opts;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
467
|
+
// HELP
|
|
468
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
469
|
+
|
|
470
|
+
function printHelp() {
|
|
471
|
+
console.log(`
|
|
472
|
+
${c.bold}${c.cyan}VIBECHECK LAUNCH${c.reset} - Pre-Release Validation Wizard (v${LAUNCH_VERSION})
|
|
473
|
+
|
|
474
|
+
${c.bold}USAGE${c.reset}
|
|
475
|
+
${c.cyan}vibecheck launch${c.reset} [options]
|
|
476
|
+
|
|
477
|
+
${c.bold}DESCRIPTION${c.reset}
|
|
478
|
+
Run all pre-release validations in one command. This is your "last 10 minutes
|
|
479
|
+
before release" checklist that ensures everything is ready for production.
|
|
480
|
+
|
|
481
|
+
${c.bold}OPTIONS${c.reset}
|
|
482
|
+
${c.cyan}--ci${c.reset} CI mode (non-interactive, machine-readable output)
|
|
483
|
+
${c.cyan}--json${c.reset} Output results as JSON
|
|
484
|
+
${c.cyan}--strict${c.reset} Treat warnings as blockers
|
|
485
|
+
${c.cyan}--skip-slow${c.reset} Skip checks that take >30s
|
|
486
|
+
${c.cyan}--timeout <ms>${c.reset} Maximum execution time ${c.dim}(default: 600000)${c.reset}
|
|
487
|
+
${c.cyan}--fail-fast${c.reset} Stop on first blocker
|
|
488
|
+
${c.cyan}--skip <phases>${c.reset} Skip specific phases ${c.dim}(comma-separated)${c.reset}
|
|
489
|
+
${c.cyan}--only <phases>${c.reset} Run only specific phases ${c.dim}(comma-separated)${c.reset}
|
|
490
|
+
${c.cyan}--bundle${c.reset} Auto-generate launch bundle after completion
|
|
491
|
+
${c.cyan}--output, -o <dir>${c.reset} Output directory for artifacts
|
|
492
|
+
${c.cyan}--path, -p <dir>${c.reset} Project path ${c.dim}(default: current directory)${c.reset}
|
|
493
|
+
${c.cyan}--dry-run${c.reset} Show what would be checked without running
|
|
494
|
+
${c.cyan}--quiet, -q${c.reset} Suppress non-essential output
|
|
495
|
+
${c.cyan}--verbose, -v${c.reset} Show detailed output
|
|
496
|
+
${c.cyan}--help, -h${c.reset} Show this help
|
|
497
|
+
|
|
498
|
+
${c.bold}PHASES${c.reset}
|
|
499
|
+
${c.cyan}build${c.reset} Build validation (TypeScript, artifacts, bundle)
|
|
500
|
+
${c.cyan}env${c.reset} Environment wiring (variables, secrets, URLs)
|
|
501
|
+
${c.cyan}routes${c.reset} Route integrity (registration, handlers, drift)
|
|
502
|
+
${c.cyan}auth${c.reset} Auth configuration (middleware, protection, CORS)
|
|
503
|
+
${c.cyan}integrations${c.reset} Integration health (database, Redis, billing)
|
|
504
|
+
${c.cyan}security${c.reset} Security posture (secrets scan, audit, mocks)
|
|
505
|
+
|
|
506
|
+
${c.bold}EXAMPLES${c.reset}
|
|
507
|
+
${c.dim}# Interactive wizard${c.reset}
|
|
508
|
+
vibecheck launch
|
|
509
|
+
|
|
510
|
+
${c.dim}# CI mode with JSON output${c.reset}
|
|
511
|
+
vibecheck launch --ci --json
|
|
512
|
+
|
|
513
|
+
${c.dim}# Strict mode (warnings = blockers)${c.reset}
|
|
514
|
+
vibecheck launch --strict
|
|
515
|
+
|
|
516
|
+
${c.dim}# Only run specific phases${c.reset}
|
|
517
|
+
vibecheck launch --only build,env,security
|
|
518
|
+
|
|
519
|
+
${c.dim}# Auto-bundle results${c.reset}
|
|
520
|
+
vibecheck launch --bundle
|
|
521
|
+
|
|
522
|
+
${c.bold}EXIT CODES${c.reset}
|
|
523
|
+
${c.green}0${c.reset} LAUNCH - Ready to ship, no blockers
|
|
524
|
+
${c.yellow}1${c.reset} WARN - Ready to ship, but warnings exist
|
|
525
|
+
${c.red}2${c.reset} BLOCK - Cannot ship, blockers found
|
|
526
|
+
${c.red}3${c.reset} ERROR - Execution error
|
|
527
|
+
|
|
528
|
+
${c.dim}Documentation: https://docs.vibecheckai.dev/commands/launch${c.reset}
|
|
529
|
+
`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
533
|
+
// CHECK RUNNER UTILITY
|
|
534
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
535
|
+
|
|
536
|
+
async function runCheck(name, description, fn, severity = "WARN", opts = {}) {
|
|
537
|
+
const start = Date.now();
|
|
538
|
+
const timeout = opts.timeout || CHECK_TIMEOUT;
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
// Wrap with timeout
|
|
542
|
+
const result = await Promise.race([
|
|
543
|
+
fn(),
|
|
544
|
+
new Promise((_, reject) =>
|
|
545
|
+
setTimeout(() => reject(new Error("Check timed out")), timeout)
|
|
546
|
+
),
|
|
547
|
+
]);
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
name,
|
|
551
|
+
description,
|
|
552
|
+
status: result.pass ? "pass" : (result.skip ? "skip" : "fail"),
|
|
553
|
+
severity: result.severity || severity,
|
|
554
|
+
duration: Date.now() - start,
|
|
555
|
+
details: result.details,
|
|
556
|
+
error: result.error,
|
|
557
|
+
fix: result.fix,
|
|
558
|
+
location: result.location,
|
|
559
|
+
findings: result.findings || [],
|
|
560
|
+
};
|
|
561
|
+
} catch (err) {
|
|
562
|
+
return {
|
|
563
|
+
name,
|
|
564
|
+
description,
|
|
565
|
+
status: "error",
|
|
566
|
+
severity,
|
|
567
|
+
duration: Date.now() - start,
|
|
568
|
+
error: err.message,
|
|
569
|
+
findings: [],
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
575
|
+
// PHASE 1: BUILD VALIDATION
|
|
576
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
577
|
+
|
|
578
|
+
async function runBuildPhase(ctx, opts) {
|
|
579
|
+
const checks = [];
|
|
580
|
+
const pm = detectPackageManager(ctx.projectRoot);
|
|
581
|
+
|
|
582
|
+
// TypeScript compilation
|
|
583
|
+
checks.push(await runCheck("typescript", "TypeScript compilation", async () => {
|
|
584
|
+
const tsconfigPath = path.join(ctx.projectRoot, "tsconfig.json");
|
|
585
|
+
if (!fs.existsSync(tsconfigPath)) {
|
|
586
|
+
return { skip: true, details: "No tsconfig.json found" };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
try {
|
|
590
|
+
await execWithTimeout("npx tsc --noEmit", { cwd: ctx.projectRoot }, opts.skipSlow ? 30000 : 120000);
|
|
591
|
+
return { pass: true };
|
|
592
|
+
} catch (err) {
|
|
593
|
+
const stderr = err.stderr?.toString() || err.message;
|
|
594
|
+
const errorCount = (stderr.match(/error TS/g) || []).length;
|
|
595
|
+
return {
|
|
596
|
+
pass: false,
|
|
597
|
+
error: `TypeScript compilation failed (${errorCount} errors)`,
|
|
598
|
+
details: stderr.substring(0, 500),
|
|
599
|
+
fix: "Run 'npx tsc --noEmit' to see all errors",
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
}, "BLOCK", { timeout: opts.skipSlow ? 30000 : 120000 }));
|
|
603
|
+
|
|
604
|
+
// Build artifacts present
|
|
605
|
+
checks.push(await runCheck("build-artifacts", "Build artifacts present", async () => {
|
|
606
|
+
const distPaths = [
|
|
607
|
+
{ path: "dist", framework: "generic" },
|
|
608
|
+
{ path: "build", framework: "CRA/generic" },
|
|
609
|
+
{ path: ".next", framework: "Next.js" },
|
|
610
|
+
{ path: "out", framework: "Next.js static" },
|
|
611
|
+
{ path: ".output", framework: "Nuxt" },
|
|
612
|
+
{ path: ".svelte-kit", framework: "SvelteKit" },
|
|
613
|
+
];
|
|
614
|
+
|
|
615
|
+
for (const { path: distPath, framework } of distPaths) {
|
|
616
|
+
const fullPath = path.join(ctx.projectRoot, distPath);
|
|
617
|
+
if (fs.existsSync(fullPath)) {
|
|
618
|
+
const stats = fs.statSync(fullPath);
|
|
619
|
+
const age = Date.now() - stats.mtimeMs;
|
|
620
|
+
const ageHours = Math.floor(age / (1000 * 60 * 60));
|
|
621
|
+
const isFresh = age < 24 * 60 * 60 * 1000;
|
|
622
|
+
|
|
623
|
+
return {
|
|
624
|
+
pass: true,
|
|
625
|
+
details: `Found ${distPath} (${framework}, ${isFresh ? "fresh" : `${ageHours}h old`})`,
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return {
|
|
631
|
+
pass: false,
|
|
632
|
+
error: "No build artifacts found",
|
|
633
|
+
fix: `Run '${pm} build' to create production build`,
|
|
634
|
+
};
|
|
635
|
+
}, "BLOCK"));
|
|
636
|
+
|
|
637
|
+
// Package.json scripts
|
|
638
|
+
checks.push(await runCheck("package-scripts", "Required scripts defined", async () => {
|
|
639
|
+
const pkgPath = path.join(ctx.projectRoot, "package.json");
|
|
640
|
+
if (!fs.existsSync(pkgPath)) {
|
|
641
|
+
return { pass: false, error: "No package.json found" };
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
646
|
+
const scripts = pkg.scripts || {};
|
|
647
|
+
const required = ["build", "start"];
|
|
648
|
+
const missing = required.filter(s => !scripts[s]);
|
|
649
|
+
|
|
650
|
+
if (missing.length > 0) {
|
|
651
|
+
return {
|
|
652
|
+
pass: false,
|
|
653
|
+
error: `Missing scripts: ${missing.join(", ")}`,
|
|
654
|
+
severity: "WARN",
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
return { pass: true };
|
|
658
|
+
} catch (err) {
|
|
659
|
+
return { pass: false, error: "Invalid package.json" };
|
|
660
|
+
}
|
|
661
|
+
}, "WARN"));
|
|
662
|
+
|
|
663
|
+
// Check for circular dependencies (if madge available)
|
|
664
|
+
if (!opts.skipSlow) {
|
|
665
|
+
checks.push(await runCheck("circular-deps", "No circular dependencies", async () => {
|
|
666
|
+
try {
|
|
667
|
+
// Try using madge if installed
|
|
668
|
+
const result = await execWithTimeout(
|
|
669
|
+
"npx madge --circular --extensions ts,tsx,js,jsx src/ 2>/dev/null || true",
|
|
670
|
+
{ cwd: ctx.projectRoot },
|
|
671
|
+
30000
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
if (result.includes("Found")) {
|
|
675
|
+
const count = (result.match(/Found (\d+)/)?.[1]) || "some";
|
|
676
|
+
return {
|
|
677
|
+
pass: false,
|
|
678
|
+
error: `${count} circular dependencies found`,
|
|
679
|
+
severity: "WARN",
|
|
680
|
+
details: result.substring(0, 300),
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
return { pass: true };
|
|
684
|
+
} catch {
|
|
685
|
+
return { skip: true, details: "madge not available" };
|
|
686
|
+
}
|
|
687
|
+
}, "WARN", { timeout: 30000 }));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return { phase: "build", checks };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
694
|
+
// PHASE 2: ENVIRONMENT WIRING
|
|
695
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
696
|
+
|
|
697
|
+
async function runEnvPhase(ctx, opts) {
|
|
698
|
+
const checks = [];
|
|
699
|
+
const env = loadEnvFiles(ctx.projectRoot);
|
|
700
|
+
const nodeEnv = env.NODE_ENV || "development";
|
|
701
|
+
const isProduction = nodeEnv === "production";
|
|
702
|
+
|
|
703
|
+
// Core required variables
|
|
704
|
+
const coreVars = [
|
|
705
|
+
{ key: "DATABASE_URL", required: true, minLength: 10, description: "Database connection" },
|
|
706
|
+
{ key: "JWT_SECRET", required: true, minLength: 32, description: "JWT signing secret" },
|
|
707
|
+
];
|
|
708
|
+
|
|
709
|
+
// Production-only variables
|
|
710
|
+
const prodVars = [
|
|
711
|
+
{ key: "ENCRYPTION_KEY", pattern: /^[a-f0-9]{64}$/i, description: "64-char hex encryption key" },
|
|
712
|
+
{ key: "FRONTEND_URL", pattern: /^https:\/\//, description: "HTTPS frontend URL" },
|
|
713
|
+
{ key: "ALLOWED_ORIGINS", minLength: 1, description: "CORS allowed origins" },
|
|
714
|
+
{ key: "COOKIE_SECRET", minLength: 32, description: "Cookie signing secret" },
|
|
715
|
+
];
|
|
716
|
+
|
|
717
|
+
// Check core variables
|
|
718
|
+
for (const { key, minLength, pattern, description } of coreVars) {
|
|
719
|
+
checks.push(await runCheck(`env-${key.toLowerCase()}`, `${key} is configured`, async () => {
|
|
720
|
+
const value = env[key];
|
|
721
|
+
if (!value) {
|
|
722
|
+
return { pass: false, error: `${key} is not set`, fix: `Add ${key} to .env file` };
|
|
723
|
+
}
|
|
724
|
+
if (minLength && value.length < minLength) {
|
|
725
|
+
return { pass: false, error: `${key} is too short (need ${minLength}+ chars)` };
|
|
726
|
+
}
|
|
727
|
+
if (pattern && !pattern.test(value)) {
|
|
728
|
+
return { pass: false, error: `${key} has invalid format (${description})` };
|
|
729
|
+
}
|
|
730
|
+
return { pass: true };
|
|
731
|
+
}, "BLOCK"));
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Check production variables (warning in dev, block in prod)
|
|
735
|
+
if (isProduction) {
|
|
736
|
+
for (const { key, minLength, pattern, description } of prodVars) {
|
|
737
|
+
checks.push(await runCheck(`env-${key.toLowerCase()}`, `${key} is configured (production)`, async () => {
|
|
738
|
+
const value = env[key];
|
|
739
|
+
if (!value) {
|
|
740
|
+
return { pass: false, error: `${key} required in production` };
|
|
741
|
+
}
|
|
742
|
+
if (minLength && value.length < minLength) {
|
|
743
|
+
return { pass: false, error: `${key} is too short (need ${minLength}+ chars)` };
|
|
744
|
+
}
|
|
745
|
+
if (pattern && !pattern.test(value)) {
|
|
746
|
+
return { pass: false, error: `${key} invalid format (${description})` };
|
|
747
|
+
}
|
|
748
|
+
return { pass: true };
|
|
749
|
+
}, "BLOCK"));
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Check for localhost in URLs (critical in production)
|
|
754
|
+
checks.push(await runCheck("no-localhost", "No localhost in URLs", async () => {
|
|
755
|
+
const urlVars = ["DATABASE_URL", "FRONTEND_URL", "API_URL", "REDIS_URL", "BACKEND_URL"];
|
|
756
|
+
const violations = [];
|
|
757
|
+
|
|
758
|
+
for (const key of urlVars) {
|
|
759
|
+
const value = env[key];
|
|
760
|
+
if (value && (value.includes("localhost") || value.includes("127.0.0.1"))) {
|
|
761
|
+
violations.push(key);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
if (violations.length > 0) {
|
|
766
|
+
return {
|
|
767
|
+
pass: false,
|
|
768
|
+
error: `Localhost found in: ${violations.join(", ")}`,
|
|
769
|
+
severity: isProduction ? "BLOCK" : "WARN",
|
|
770
|
+
fix: "Use production URLs for deployment",
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
return { pass: true };
|
|
774
|
+
}, isProduction ? "BLOCK" : "WARN"));
|
|
775
|
+
|
|
776
|
+
// Check for weak secrets
|
|
777
|
+
checks.push(await runCheck("secret-strength", "Secrets have sufficient entropy", async () => {
|
|
778
|
+
const secretVars = ["JWT_SECRET", "COOKIE_SECRET", "ENCRYPTION_KEY"];
|
|
779
|
+
const weak = [];
|
|
780
|
+
|
|
781
|
+
for (const key of secretVars) {
|
|
782
|
+
const value = env[key];
|
|
783
|
+
if (value) {
|
|
784
|
+
// Check for common weak patterns
|
|
785
|
+
if (value === "secret" || value === "password" || value === "changeme" ||
|
|
786
|
+
value.length < 16 || /^(.)\1+$/.test(value) || /^12345/.test(value)) {
|
|
787
|
+
weak.push(key);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (weak.length > 0) {
|
|
793
|
+
return {
|
|
794
|
+
pass: false,
|
|
795
|
+
error: `Weak secrets detected: ${weak.join(", ")}`,
|
|
796
|
+
fix: "Use cryptographically random secrets (e.g., openssl rand -hex 32)",
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
return { pass: true };
|
|
800
|
+
}, "BLOCK"));
|
|
801
|
+
|
|
802
|
+
// Check .env.example exists
|
|
803
|
+
checks.push(await runCheck("env-template", "Environment template exists", async () => {
|
|
804
|
+
const templates = [".env.example", ".env.template", ".env.sample"];
|
|
805
|
+
const found = templates.find(t => fs.existsSync(path.join(ctx.projectRoot, t)));
|
|
806
|
+
|
|
807
|
+
if (!found) {
|
|
808
|
+
return {
|
|
809
|
+
pass: false,
|
|
810
|
+
error: "No .env.example file",
|
|
811
|
+
severity: "WARN",
|
|
812
|
+
fix: "Create .env.example with required variables (without values)",
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
return { pass: true, details: `Found ${found}` };
|
|
816
|
+
}, "WARN"));
|
|
817
|
+
|
|
818
|
+
return { phase: "env", checks };
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
822
|
+
// PHASE 3: ROUTE INTEGRITY
|
|
823
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
824
|
+
|
|
825
|
+
async function runRoutesPhase(ctx, opts) {
|
|
826
|
+
const checks = [];
|
|
827
|
+
|
|
828
|
+
// Try to load and use existing route analysis
|
|
829
|
+
checks.push(await runCheck("route-analysis", "Route analysis", async () => {
|
|
830
|
+
try {
|
|
831
|
+
const { buildTruthpackSmart } = require("./lib/truth");
|
|
832
|
+
const { findMissingRoutes, findMethodMismatch, findDeadUI } = require("./lib/analyzers");
|
|
833
|
+
|
|
834
|
+
const truth = await buildTruthpackSmart({ projectRoot: ctx.projectRoot });
|
|
835
|
+
|
|
836
|
+
// Missing routes
|
|
837
|
+
const missing = findMissingRoutes ? findMissingRoutes(truth) : [];
|
|
838
|
+
// Method mismatch
|
|
839
|
+
const mismatch = findMethodMismatch ? findMethodMismatch(truth) : [];
|
|
840
|
+
// Dead routes
|
|
841
|
+
const dead = findDeadUI ? findDeadUI(truth) : [];
|
|
842
|
+
|
|
843
|
+
const issues = [];
|
|
844
|
+
if (missing.length > 0) issues.push(`${missing.length} missing routes`);
|
|
845
|
+
if (mismatch.length > 0) issues.push(`${mismatch.length} method mismatches`);
|
|
846
|
+
if (dead.length > 0) issues.push(`${dead.length} dead routes`);
|
|
847
|
+
|
|
848
|
+
if (missing.length > 0 || mismatch.length > 0) {
|
|
849
|
+
return {
|
|
850
|
+
pass: false,
|
|
851
|
+
error: issues.join(", "),
|
|
852
|
+
findings: [...missing, ...mismatch].slice(0, 5),
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (dead.length > 0) {
|
|
857
|
+
return {
|
|
858
|
+
pass: true,
|
|
859
|
+
details: `${dead.length} dead routes detected`,
|
|
860
|
+
severity: "WARN",
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
return { pass: true, details: "All routes verified" };
|
|
865
|
+
} catch (err) {
|
|
866
|
+
return { skip: true, details: `Route analysis unavailable: ${err.message}` };
|
|
867
|
+
}
|
|
868
|
+
}, "BLOCK"));
|
|
869
|
+
|
|
870
|
+
// Check for API route files
|
|
871
|
+
checks.push(await runCheck("api-routes-exist", "API routes defined", async () => {
|
|
872
|
+
const apiPaths = [
|
|
873
|
+
"src/routes",
|
|
874
|
+
"src/api",
|
|
875
|
+
"src/pages/api",
|
|
876
|
+
"app/api",
|
|
877
|
+
"apps/api/src/routes",
|
|
878
|
+
];
|
|
879
|
+
|
|
880
|
+
for (const apiPath of apiPaths) {
|
|
881
|
+
const fullPath = path.join(ctx.projectRoot, apiPath);
|
|
882
|
+
if (fs.existsSync(fullPath)) {
|
|
883
|
+
const files = findFiles(fullPath, [".ts", ".tsx", ".js", ".jsx"]);
|
|
884
|
+
return { pass: true, details: `Found ${files.length} route files in ${apiPath}` };
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return { skip: true, details: "No API routes directory found" };
|
|
889
|
+
}, "INFO"));
|
|
890
|
+
|
|
891
|
+
return { phase: "routes", checks };
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
895
|
+
// PHASE 4: AUTH CONFIGURATION
|
|
896
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
897
|
+
|
|
898
|
+
async function runAuthPhase(ctx, opts) {
|
|
899
|
+
const checks = [];
|
|
900
|
+
const sourceFiles = findFiles(ctx.projectRoot, [".ts", ".tsx", ".js", ".jsx"]);
|
|
901
|
+
|
|
902
|
+
// Check for auth middleware
|
|
903
|
+
checks.push(await runCheck("auth-middleware", "Auth middleware is implemented", async () => {
|
|
904
|
+
const authFiles = sourceFiles.filter(f =>
|
|
905
|
+
f.includes("middleware") || f.includes("auth") || f.includes("guard")
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
let found = false;
|
|
909
|
+
for (const file of authFiles) {
|
|
910
|
+
if (fileContainsPatterns(file, AUTH_PATTERNS.middleware)) {
|
|
911
|
+
found = true;
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (!found) {
|
|
917
|
+
return {
|
|
918
|
+
pass: false,
|
|
919
|
+
error: "No auth middleware detected",
|
|
920
|
+
fix: "Implement authentication middleware for protected routes",
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
return { pass: true, details: "Auth middleware patterns found" };
|
|
924
|
+
}, "BLOCK"));
|
|
925
|
+
|
|
926
|
+
// Check for hash-before-lookup pattern (API key security)
|
|
927
|
+
checks.push(await runCheck("hash-before-lookup", "API key hashing implemented", async () => {
|
|
928
|
+
const apiKeyFiles = sourceFiles.filter(f =>
|
|
929
|
+
f.includes("api-key") || f.includes("apikey") || f.includes("auth")
|
|
930
|
+
);
|
|
931
|
+
|
|
932
|
+
if (apiKeyFiles.length === 0) {
|
|
933
|
+
return { skip: true, details: "No API key handling detected" };
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
let found = false;
|
|
937
|
+
for (const file of apiKeyFiles) {
|
|
938
|
+
if (fileContainsPatterns(file, AUTH_PATTERNS.hashBeforeLookup)) {
|
|
939
|
+
found = true;
|
|
940
|
+
break;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (!found) {
|
|
945
|
+
return {
|
|
946
|
+
pass: false,
|
|
947
|
+
error: "API keys should be hashed before database lookup",
|
|
948
|
+
severity: "WARN",
|
|
949
|
+
fix: "Implement hash-before-lookup pattern for API key validation",
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
return { pass: true };
|
|
953
|
+
}, "WARN"));
|
|
954
|
+
|
|
955
|
+
// Check for rate limiting
|
|
956
|
+
checks.push(await runCheck("rate-limiting", "Rate limiting configured", async () => {
|
|
957
|
+
let found = false;
|
|
958
|
+
for (const file of sourceFiles.slice(0, 100)) { // Limit scan
|
|
959
|
+
if (fileContainsPatterns(file, AUTH_PATTERNS.rateLimiting)) {
|
|
960
|
+
found = true;
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Also check package.json for rate limit packages
|
|
966
|
+
const pkgPath = path.join(ctx.projectRoot, "package.json");
|
|
967
|
+
if (fs.existsSync(pkgPath)) {
|
|
968
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
969
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
970
|
+
if (deps["express-rate-limit"] || deps["@fastify/rate-limit"] || deps["rate-limiter-flexible"]) {
|
|
971
|
+
found = true;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (!found) {
|
|
976
|
+
return {
|
|
977
|
+
pass: false,
|
|
978
|
+
error: "No rate limiting detected",
|
|
979
|
+
fix: "Add rate limiting to prevent abuse",
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
return { pass: true };
|
|
983
|
+
}, "WARN"));
|
|
984
|
+
|
|
985
|
+
// Check CORS configuration
|
|
986
|
+
checks.push(await runCheck("cors-config", "CORS properly configured", async () => {
|
|
987
|
+
const env = loadEnvFiles(ctx.projectRoot);
|
|
988
|
+
|
|
989
|
+
if (!env.ALLOWED_ORIGINS) {
|
|
990
|
+
return {
|
|
991
|
+
pass: false,
|
|
992
|
+
error: "ALLOWED_ORIGINS not configured",
|
|
993
|
+
fix: "Set ALLOWED_ORIGINS in environment",
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Check for wildcard CORS (dangerous in production)
|
|
998
|
+
if (env.ALLOWED_ORIGINS === "*") {
|
|
999
|
+
return {
|
|
1000
|
+
pass: false,
|
|
1001
|
+
error: "CORS allows all origins (*) - dangerous in production",
|
|
1002
|
+
fix: "Specify explicit allowed origins",
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
return { pass: true };
|
|
1007
|
+
}, "BLOCK"));
|
|
1008
|
+
|
|
1009
|
+
return { phase: "auth", checks };
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1013
|
+
// PHASE 5: INTEGRATION HEALTH
|
|
1014
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1015
|
+
|
|
1016
|
+
async function runIntegrationsPhase(ctx, opts) {
|
|
1017
|
+
const checks = [];
|
|
1018
|
+
const env = loadEnvFiles(ctx.projectRoot);
|
|
1019
|
+
|
|
1020
|
+
// Database configuration
|
|
1021
|
+
checks.push(await runCheck("database-config", "Database is configured", async () => {
|
|
1022
|
+
const dbUrl = env.DATABASE_URL;
|
|
1023
|
+
if (!dbUrl) {
|
|
1024
|
+
return { pass: false, error: "DATABASE_URL not set" };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Parse and validate URL format
|
|
1028
|
+
try {
|
|
1029
|
+
const url = new URL(dbUrl);
|
|
1030
|
+
const protocol = url.protocol.replace(":", "");
|
|
1031
|
+
const validProtocols = ["postgresql", "postgres", "mysql", "mongodb", "mongodb+srv", "sqlite"];
|
|
1032
|
+
|
|
1033
|
+
if (!validProtocols.includes(protocol)) {
|
|
1034
|
+
return { pass: false, error: `Unknown database protocol: ${protocol}` };
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return { pass: true, details: `${protocol} database configured` };
|
|
1038
|
+
} catch {
|
|
1039
|
+
return { pass: false, error: "Invalid DATABASE_URL format" };
|
|
1040
|
+
}
|
|
1041
|
+
}, "BLOCK"));
|
|
1042
|
+
|
|
1043
|
+
// Redis configuration (optional)
|
|
1044
|
+
if (env.REDIS_URL) {
|
|
1045
|
+
checks.push(await runCheck("redis-config", "Redis is configured", async () => {
|
|
1046
|
+
try {
|
|
1047
|
+
const url = new URL(env.REDIS_URL);
|
|
1048
|
+
return { pass: true, details: `Redis at ${url.host}` };
|
|
1049
|
+
} catch {
|
|
1050
|
+
return { pass: false, error: "Invalid REDIS_URL format" };
|
|
1051
|
+
}
|
|
1052
|
+
}, "WARN"));
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Stripe configuration (if used)
|
|
1056
|
+
if (env.STRIPE_SECRET_KEY || env.STRIPE_PUBLISHABLE_KEY) {
|
|
1057
|
+
checks.push(await runCheck("stripe-config", "Stripe properly configured", async () => {
|
|
1058
|
+
const secretKey = env.STRIPE_SECRET_KEY;
|
|
1059
|
+
const publishableKey = env.STRIPE_PUBLISHABLE_KEY;
|
|
1060
|
+
const isProduction = env.NODE_ENV === "production";
|
|
1061
|
+
|
|
1062
|
+
if (!secretKey) {
|
|
1063
|
+
return { pass: false, error: "STRIPE_SECRET_KEY not set" };
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (isProduction && secretKey.startsWith("sk_test_")) {
|
|
1067
|
+
return {
|
|
1068
|
+
pass: false,
|
|
1069
|
+
error: "Using Stripe TEST key in production!",
|
|
1070
|
+
fix: "Use live Stripe keys for production deployment",
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if (isProduction && publishableKey?.startsWith("pk_test_")) {
|
|
1075
|
+
return {
|
|
1076
|
+
pass: false,
|
|
1077
|
+
error: "Using Stripe TEST publishable key in production!",
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Check webhook secret
|
|
1082
|
+
if (!env.STRIPE_WEBHOOK_SECRET) {
|
|
1083
|
+
return {
|
|
1084
|
+
pass: false,
|
|
1085
|
+
error: "STRIPE_WEBHOOK_SECRET not set",
|
|
1086
|
+
severity: "WARN",
|
|
1087
|
+
fix: "Configure webhook secret for secure webhook handling",
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
return { pass: true };
|
|
1092
|
+
}, "BLOCK"));
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Check for Prisma migrations
|
|
1096
|
+
const prismaPath = path.join(ctx.projectRoot, "prisma");
|
|
1097
|
+
if (fs.existsSync(prismaPath)) {
|
|
1098
|
+
checks.push(await runCheck("prisma-migrations", "Prisma migrations up to date", async () => {
|
|
1099
|
+
try {
|
|
1100
|
+
const result = await execWithTimeout(
|
|
1101
|
+
"npx prisma migrate status",
|
|
1102
|
+
{ cwd: ctx.projectRoot },
|
|
1103
|
+
30000
|
|
1104
|
+
);
|
|
1105
|
+
|
|
1106
|
+
if (result.includes("Database schema is up to date")) {
|
|
1107
|
+
return { pass: true };
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (result.includes("pending migration")) {
|
|
1111
|
+
return {
|
|
1112
|
+
pass: false,
|
|
1113
|
+
error: "Pending database migrations",
|
|
1114
|
+
fix: "Run 'npx prisma migrate deploy' before deployment",
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
return { pass: true, details: "Migrations checked" };
|
|
1119
|
+
} catch (err) {
|
|
1120
|
+
// Migration status check failed - could be DB connection issue
|
|
1121
|
+
return {
|
|
1122
|
+
pass: false,
|
|
1123
|
+
error: "Could not check migration status",
|
|
1124
|
+
severity: "WARN",
|
|
1125
|
+
details: err.message,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
}, "WARN"));
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
return { phase: "integrations", checks };
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1135
|
+
// PHASE 6: SECURITY POSTURE
|
|
1136
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1137
|
+
|
|
1138
|
+
async function runSecurityPhase(ctx, opts) {
|
|
1139
|
+
const checks = [];
|
|
1140
|
+
const sourceFiles = findFiles(ctx.projectRoot, [".ts", ".tsx", ".js", ".jsx"]);
|
|
1141
|
+
|
|
1142
|
+
// Scan for hardcoded secrets
|
|
1143
|
+
checks.push(await runCheck("hardcoded-secrets", "No hardcoded secrets in code", async () => {
|
|
1144
|
+
const findings = [];
|
|
1145
|
+
|
|
1146
|
+
// Limit files to scan for performance
|
|
1147
|
+
const filesToScan = sourceFiles.slice(0, 500);
|
|
1148
|
+
|
|
1149
|
+
for (const file of filesToScan) {
|
|
1150
|
+
// Skip test files
|
|
1151
|
+
if (file.includes(".test.") || file.includes(".spec.") || file.includes("__test__")) {
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const fileFindings = scanFileForPatterns(file, SECRET_PATTERNS);
|
|
1156
|
+
findings.push(...fileFindings);
|
|
1157
|
+
|
|
1158
|
+
// Stop early if we found critical issues
|
|
1159
|
+
if (findings.filter(f => f.severity === "BLOCK").length >= 3) {
|
|
1160
|
+
break;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const blockers = findings.filter(f => f.severity === "BLOCK");
|
|
1165
|
+
const warnings = findings.filter(f => f.severity === "WARN");
|
|
1166
|
+
|
|
1167
|
+
if (blockers.length > 0) {
|
|
1168
|
+
return {
|
|
1169
|
+
pass: false,
|
|
1170
|
+
error: `${blockers.length} hardcoded secrets found`,
|
|
1171
|
+
findings: blockers.slice(0, 5),
|
|
1172
|
+
fix: "Move secrets to environment variables",
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
if (warnings.length > 0) {
|
|
1177
|
+
return {
|
|
1178
|
+
pass: true,
|
|
1179
|
+
details: `${warnings.length} potential secrets (review recommended)`,
|
|
1180
|
+
severity: "WARN",
|
|
1181
|
+
findings: warnings.slice(0, 3),
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
return { pass: true };
|
|
1186
|
+
}, "BLOCK"));
|
|
1187
|
+
|
|
1188
|
+
// Scan for TODOs in security-critical code
|
|
1189
|
+
checks.push(await runCheck("security-todos", "No TODOs in security code", async () => {
|
|
1190
|
+
const todoPattern = { name: "TODO/FIXME", pattern: /TODO|FIXME|HACK|XXX/gi };
|
|
1191
|
+
const findings = [];
|
|
1192
|
+
|
|
1193
|
+
for (const secPath of SECURITY_PATHS) {
|
|
1194
|
+
const fullPath = path.join(ctx.projectRoot, secPath);
|
|
1195
|
+
if (!fs.existsSync(fullPath)) continue;
|
|
1196
|
+
|
|
1197
|
+
const files = findFiles(fullPath, [".ts", ".tsx", ".js", ".jsx"]);
|
|
1198
|
+
for (const file of files) {
|
|
1199
|
+
const fileFindings = scanFileForPatterns(file, [todoPattern]);
|
|
1200
|
+
findings.push(...fileFindings);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (findings.length > 0) {
|
|
1205
|
+
return {
|
|
1206
|
+
pass: false,
|
|
1207
|
+
error: `${findings.length} TODOs in security-critical code`,
|
|
1208
|
+
findings: findings.slice(0, 5),
|
|
1209
|
+
fix: "Resolve TODOs before deployment",
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
return { pass: true };
|
|
1214
|
+
}, "BLOCK"));
|
|
1215
|
+
|
|
1216
|
+
// Scan for mock/demo data
|
|
1217
|
+
checks.push(await runCheck("mock-data", "No mock/demo data in production code", async () => {
|
|
1218
|
+
const findings = [];
|
|
1219
|
+
|
|
1220
|
+
// Only scan source directories, not tests
|
|
1221
|
+
const srcDirs = ["src", "app", "apps", "lib", "server"];
|
|
1222
|
+
|
|
1223
|
+
for (const dir of srcDirs) {
|
|
1224
|
+
const fullPath = path.join(ctx.projectRoot, dir);
|
|
1225
|
+
if (!fs.existsSync(fullPath)) continue;
|
|
1226
|
+
|
|
1227
|
+
const files = findFiles(fullPath, [".ts", ".tsx", ".js", ".jsx"]).slice(0, 200);
|
|
1228
|
+
for (const file of files) {
|
|
1229
|
+
// Skip test files
|
|
1230
|
+
if (file.includes(".test.") || file.includes(".spec.")) continue;
|
|
1231
|
+
|
|
1232
|
+
const fileFindings = scanFileForPatterns(file, MOCK_PATTERNS);
|
|
1233
|
+
findings.push(...fileFindings);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
if (findings.length > 0) {
|
|
1238
|
+
return {
|
|
1239
|
+
pass: false,
|
|
1240
|
+
error: `${findings.length} mock/demo data patterns found`,
|
|
1241
|
+
findings: findings.slice(0, 5),
|
|
1242
|
+
fix: "Remove mock data before production deployment",
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
return { pass: true };
|
|
1247
|
+
}, "BLOCK"));
|
|
1248
|
+
|
|
1249
|
+
// Check for VIBECHECK_DEV_PRO bypass in production code
|
|
1250
|
+
checks.push(await runCheck("dev-bypass", "No development bypasses in production", async () => {
|
|
1251
|
+
const bypassPattern = {
|
|
1252
|
+
name: "DEV_PRO bypass",
|
|
1253
|
+
pattern: /VIBECHECK_DEV_PRO|DEV_BYPASS|SKIP_AUTH|DISABLE_AUTH/gi,
|
|
1254
|
+
severity: "BLOCK",
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
const findings = [];
|
|
1258
|
+
const srcFiles = sourceFiles.filter(f =>
|
|
1259
|
+
!f.includes(".test.") && !f.includes(".spec.") && !f.includes("node_modules")
|
|
1260
|
+
).slice(0, 300);
|
|
1261
|
+
|
|
1262
|
+
for (const file of srcFiles) {
|
|
1263
|
+
const fileFindings = scanFileForPatterns(file, [bypassPattern]);
|
|
1264
|
+
findings.push(...fileFindings);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
if (findings.length > 0) {
|
|
1268
|
+
return {
|
|
1269
|
+
pass: false,
|
|
1270
|
+
error: `${findings.length} development bypass patterns found`,
|
|
1271
|
+
findings: findings.slice(0, 5),
|
|
1272
|
+
fix: "Remove or properly gate development bypasses",
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
return { pass: true };
|
|
1277
|
+
}, "BLOCK"));
|
|
1278
|
+
|
|
1279
|
+
// Dependency audit
|
|
1280
|
+
if (!opts.skipSlow) {
|
|
1281
|
+
checks.push(await runCheck("dependency-audit", "No critical vulnerabilities", async () => {
|
|
1282
|
+
const pm = detectPackageManager(ctx.projectRoot);
|
|
1283
|
+
|
|
1284
|
+
try {
|
|
1285
|
+
let result;
|
|
1286
|
+
switch (pm) {
|
|
1287
|
+
case "pnpm":
|
|
1288
|
+
result = await execWithTimeout("pnpm audit --audit-level=critical 2>&1 || true", { cwd: ctx.projectRoot }, 60000);
|
|
1289
|
+
break;
|
|
1290
|
+
case "yarn":
|
|
1291
|
+
result = await execWithTimeout("yarn audit --level critical 2>&1 || true", { cwd: ctx.projectRoot }, 60000);
|
|
1292
|
+
break;
|
|
1293
|
+
default:
|
|
1294
|
+
result = await execWithTimeout("npm audit --audit-level=critical 2>&1 || true", { cwd: ctx.projectRoot }, 60000);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Check for critical vulnerabilities
|
|
1298
|
+
if (result.includes("critical") && !result.includes("0 critical")) {
|
|
1299
|
+
const criticalMatch = result.match(/(\d+)\s+critical/);
|
|
1300
|
+
const count = criticalMatch ? criticalMatch[1] : "some";
|
|
1301
|
+
return {
|
|
1302
|
+
pass: false,
|
|
1303
|
+
error: `${count} critical vulnerabilities found`,
|
|
1304
|
+
fix: `Run '${pm} audit fix' to attempt automatic fixes`,
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// Check for high vulnerabilities (warning)
|
|
1309
|
+
if (result.includes("high") && !result.includes("0 high")) {
|
|
1310
|
+
const highMatch = result.match(/(\d+)\s+high/);
|
|
1311
|
+
const count = highMatch ? highMatch[1] : "some";
|
|
1312
|
+
return {
|
|
1313
|
+
pass: true,
|
|
1314
|
+
details: `${count} high-severity vulnerabilities (review recommended)`,
|
|
1315
|
+
severity: "WARN",
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
return { pass: true };
|
|
1320
|
+
} catch (err) {
|
|
1321
|
+
return {
|
|
1322
|
+
pass: true,
|
|
1323
|
+
details: "Audit check skipped",
|
|
1324
|
+
severity: "WARN",
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
}, "BLOCK", { timeout: 60000 }));
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
return { phase: "security", checks };
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1334
|
+
// ORCHESTRATOR
|
|
1335
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1336
|
+
|
|
1337
|
+
async function runAllPhases(ctx, opts) {
|
|
1338
|
+
const results = {
|
|
1339
|
+
phases: [],
|
|
1340
|
+
blockers: [],
|
|
1341
|
+
warnings: [],
|
|
1342
|
+
passed: 0,
|
|
1343
|
+
skipped: 0,
|
|
1344
|
+
errors: 0,
|
|
1345
|
+
};
|
|
1346
|
+
|
|
1347
|
+
const phaseRunners = {
|
|
1348
|
+
build: runBuildPhase,
|
|
1349
|
+
env: runEnvPhase,
|
|
1350
|
+
routes: runRoutesPhase,
|
|
1351
|
+
auth: runAuthPhase,
|
|
1352
|
+
integrations: runIntegrationsPhase,
|
|
1353
|
+
security: runSecurityPhase,
|
|
1354
|
+
};
|
|
1355
|
+
|
|
1356
|
+
// Determine which phases to run
|
|
1357
|
+
let phasesToRun = [...PHASE_ORDER];
|
|
1358
|
+
if (opts.onlyPhases.length > 0) {
|
|
1359
|
+
phasesToRun = opts.onlyPhases.filter(p => PHASE_ORDER.includes(p));
|
|
1360
|
+
}
|
|
1361
|
+
phasesToRun = phasesToRun.filter(p => !opts.skipPhases.includes(p));
|
|
1362
|
+
|
|
1363
|
+
const totalPhases = phasesToRun.length;
|
|
1364
|
+
|
|
1365
|
+
for (let i = 0; i < phasesToRun.length; i++) {
|
|
1366
|
+
const phaseName = phasesToRun[i];
|
|
1367
|
+
const phaseConfig = PHASES[phaseName];
|
|
1368
|
+
const runner = phaseRunners[phaseName];
|
|
1369
|
+
|
|
1370
|
+
if (!opts.ci && !opts.quiet) {
|
|
1371
|
+
printPhaseStart(i + 1, totalPhases, phaseConfig);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
const start = Date.now();
|
|
1375
|
+
|
|
1376
|
+
try {
|
|
1377
|
+
const phaseResult = await runner(ctx, opts);
|
|
1378
|
+
phaseResult.duration = Date.now() - start;
|
|
1379
|
+
|
|
1380
|
+
// Aggregate results
|
|
1381
|
+
for (const check of phaseResult.checks) {
|
|
1382
|
+
if (check.status === "pass") {
|
|
1383
|
+
results.passed++;
|
|
1384
|
+
} else if (check.status === "skip") {
|
|
1385
|
+
results.skipped++;
|
|
1386
|
+
} else if (check.status === "error") {
|
|
1387
|
+
results.errors++;
|
|
1388
|
+
results.warnings.push({ phase: phaseName, ...check });
|
|
1389
|
+
} else if (check.status === "fail") {
|
|
1390
|
+
if (check.severity === "BLOCK") {
|
|
1391
|
+
results.blockers.push({ phase: phaseName, ...check });
|
|
1392
|
+
} else {
|
|
1393
|
+
results.warnings.push({ phase: phaseName, ...check });
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
if (!opts.ci && !opts.quiet) {
|
|
1398
|
+
printCheckResult(check, opts.verbose);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
results.phases.push(phaseResult);
|
|
1403
|
+
|
|
1404
|
+
// Fail fast
|
|
1405
|
+
if (opts.failFast && results.blockers.length > 0) {
|
|
1406
|
+
if (!opts.ci && !opts.quiet) {
|
|
1407
|
+
console.log(`\n ${c.yellow}${sym.warning} Stopping early (--fail-fast)${c.reset}\n`);
|
|
1408
|
+
}
|
|
1409
|
+
break;
|
|
1410
|
+
}
|
|
1411
|
+
} catch (err) {
|
|
1412
|
+
results.errors++;
|
|
1413
|
+
results.phases.push({
|
|
1414
|
+
phase: phaseName,
|
|
1415
|
+
duration: Date.now() - start,
|
|
1416
|
+
checks: [{
|
|
1417
|
+
name: "phase-error",
|
|
1418
|
+
description: `Phase ${phaseName} failed`,
|
|
1419
|
+
status: "error",
|
|
1420
|
+
error: err.message,
|
|
1421
|
+
}],
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
if (!opts.ci && !opts.quiet) {
|
|
1425
|
+
console.log(` ${c.red}${sym.cross} Phase failed: ${err.message}${c.reset}`);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Apply strict mode
|
|
1431
|
+
if (opts.strict) {
|
|
1432
|
+
results.blockers.push(...results.warnings);
|
|
1433
|
+
results.warnings = [];
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
return results;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1440
|
+
// OUTPUT RENDERING
|
|
1441
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1442
|
+
|
|
1443
|
+
function printPhaseStart(num, total, phase) {
|
|
1444
|
+
const header = `PHASE ${num}: ${phase.name.toUpperCase()}`;
|
|
1445
|
+
const suffix = `[${num}/${total}] ${phase.estimatedTime}`;
|
|
1446
|
+
const padding = Math.max(0, 77 - header.length - suffix.length - 4);
|
|
1447
|
+
|
|
1448
|
+
console.log();
|
|
1449
|
+
console.log(`${c.cyan}${sym.box.topLeft}${"═".repeat(77)}${sym.box.topRight}${c.reset}`);
|
|
1450
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.bold}${header}${c.reset}${" ".repeat(padding)}${c.dim}${suffix}${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1451
|
+
console.log(`${c.cyan}${sym.box.teeRight}${"═".repeat(77)}${sym.box.teeLeft}${c.reset}`);
|
|
1452
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1453
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.dim}${phase.description}${c.reset}`);
|
|
1454
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function printCheckResult(check, verbose = false) {
|
|
1458
|
+
let icon, color;
|
|
1459
|
+
switch (check.status) {
|
|
1460
|
+
case "pass":
|
|
1461
|
+
icon = sym.check;
|
|
1462
|
+
color = c.green;
|
|
1463
|
+
break;
|
|
1464
|
+
case "fail":
|
|
1465
|
+
case "error":
|
|
1466
|
+
icon = check.severity === "BLOCK" ? sym.cross : sym.warning;
|
|
1467
|
+
color = check.severity === "BLOCK" ? c.red : c.yellow;
|
|
1468
|
+
break;
|
|
1469
|
+
case "skip":
|
|
1470
|
+
icon = sym.arrow;
|
|
1471
|
+
color = c.gray;
|
|
1472
|
+
break;
|
|
1473
|
+
default:
|
|
1474
|
+
icon = "?";
|
|
1475
|
+
color = c.gray;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
const desc = check.description.substring(0, 50);
|
|
1479
|
+
const status = check.status.toUpperCase().padEnd(5);
|
|
1480
|
+
const padding = Math.max(0, 55 - desc.length);
|
|
1481
|
+
|
|
1482
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${color}${icon}${c.reset} ${desc}${" ".repeat(padding)}${color}${status}${c.reset}`);
|
|
1483
|
+
|
|
1484
|
+
if (check.error && check.status !== "pass") {
|
|
1485
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.dim}${sym.arrow} ${check.error}${c.reset}`);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
if (verbose && check.details) {
|
|
1489
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.dim}${check.details}${c.reset}`);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
if (verbose && check.findings && check.findings.length > 0) {
|
|
1493
|
+
for (const finding of check.findings.slice(0, 3)) {
|
|
1494
|
+
const loc = finding.file ? path.relative(process.cwd(), finding.file) : "";
|
|
1495
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.dim}📍 ${loc}:${finding.line || "?"}${c.reset}`);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function printBlockersSection(results) {
|
|
1501
|
+
const blockerCount = results.blockers.length;
|
|
1502
|
+
const warningCount = results.warnings.length;
|
|
1503
|
+
|
|
1504
|
+
console.log();
|
|
1505
|
+
console.log(`${c.cyan}${sym.box.topLeft}${"═".repeat(77)}${sym.box.topRight}${c.reset}`);
|
|
1506
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1507
|
+
|
|
1508
|
+
const blockerText = `${sym.block} LAUNCH BLOCKERS`;
|
|
1509
|
+
const blockerSuffix = `${blockerCount} found`;
|
|
1510
|
+
const blockerPadding = Math.max(0, 77 - blockerText.length - blockerSuffix.length - 10);
|
|
1511
|
+
|
|
1512
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${blockerText}${" ".repeat(blockerPadding)}${blockerCount > 0 ? c.red : c.green}${blockerSuffix}${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1513
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1514
|
+
console.log(`${c.cyan}${sym.box.teeRight}${"═".repeat(77)}${sym.box.teeLeft}${c.reset}`);
|
|
1515
|
+
|
|
1516
|
+
if (blockerCount === 0) {
|
|
1517
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1518
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.green}${sym.check} No blockers! You're ready to launch.${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1519
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1520
|
+
} else {
|
|
1521
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1522
|
+
results.blockers.forEach((blocker, i) => {
|
|
1523
|
+
const phaseLabel = `[${blocker.phase.toUpperCase()}]`;
|
|
1524
|
+
const desc = (blocker.description || blocker.name).substring(0, 50);
|
|
1525
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.bold}${i + 1}.${c.reset} ${c.red}${phaseLabel}${c.reset} ${desc}`);
|
|
1526
|
+
if (blocker.error) {
|
|
1527
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.dim}${sym.arrow} ${blocker.error.substring(0, 60)}${c.reset}`);
|
|
1528
|
+
}
|
|
1529
|
+
if (blocker.fix) {
|
|
1530
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.cyan}Fix: ${blocker.fix.substring(0, 55)}${c.reset}`);
|
|
1531
|
+
}
|
|
1532
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// Warnings section
|
|
1537
|
+
if (warningCount > 0) {
|
|
1538
|
+
console.log(`${c.cyan}${sym.box.teeRight}${"═".repeat(77)}${sym.box.teeLeft}${c.reset}`);
|
|
1539
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1540
|
+
|
|
1541
|
+
const warnText = `${sym.warning} WARNINGS (non-blocking)`;
|
|
1542
|
+
const warnSuffix = `${warningCount} found`;
|
|
1543
|
+
const warnPadding = Math.max(0, 77 - warnText.length - warnSuffix.length - 10);
|
|
1544
|
+
|
|
1545
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.yellow}${warnText}${c.reset}${" ".repeat(warnPadding)}${c.yellow}${warnSuffix}${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1546
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1547
|
+
results.warnings.slice(0, 10).forEach(warning => {
|
|
1548
|
+
const desc = (warning.description || warning.name).substring(0, 65);
|
|
1549
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.yellow}•${c.reset} ${desc}`);
|
|
1550
|
+
});
|
|
1551
|
+
if (warningCount > 10) {
|
|
1552
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.dim}... and ${warningCount - 10} more${c.reset}`);
|
|
1553
|
+
}
|
|
1554
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
console.log(`${c.cyan}${sym.box.bottomLeft}${"═".repeat(77)}${sym.box.bottomRight}${c.reset}`);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
function printVerdict(results, duration) {
|
|
1561
|
+
const verdict = results.blockers.length === 0 ?
|
|
1562
|
+
(results.warnings.length === 0 ? "LAUNCH" : "WARN") : "BLOCK";
|
|
1563
|
+
|
|
1564
|
+
const total = results.passed + results.blockers.length + results.warnings.length + results.skipped;
|
|
1565
|
+
const score = total > 0 ? Math.max(0, Math.round(100 * (results.passed / total))) : 0;
|
|
1566
|
+
|
|
1567
|
+
let verdictIcon;
|
|
1568
|
+
switch (verdict) {
|
|
1569
|
+
case "LAUNCH":
|
|
1570
|
+
verdictIcon = sym.rocket;
|
|
1571
|
+
break;
|
|
1572
|
+
case "WARN":
|
|
1573
|
+
verdictIcon = sym.warning;
|
|
1574
|
+
break;
|
|
1575
|
+
case "BLOCK":
|
|
1576
|
+
verdictIcon = sym.block;
|
|
1577
|
+
break;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
const verdictText = verdict === "LAUNCH" ? "READY TO LAUNCH" : verdict;
|
|
1581
|
+
|
|
1582
|
+
console.log();
|
|
1583
|
+
console.log(`${c.cyan}${sym.box.topLeft}${"═".repeat(77)}${sym.box.topRight}${c.reset}`);
|
|
1584
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1585
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${verdictIcon} ${c.bold}VERDICT: ${verdictText}${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1586
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1587
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.dim}┌─────────────────────────────────────────────────────────────────────┐${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1588
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.dim}│${c.reset} Score: ${score}/100 ${c.dim}│${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1589
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.dim}│${c.reset} Blockers: ${results.blockers.length} Warnings: ${results.warnings.length} Passed: ${results.passed} ${c.dim}│${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1590
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.dim}│${c.reset} Duration: ${formatDuration(duration)} ${c.dim}│${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1591
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.dim}└─────────────────────────────────────────────────────────────────────┘${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1592
|
+
console.log(`${c.cyan}${sym.box.vertical}${c.reset} ${c.cyan}${sym.box.vertical}${c.reset}`);
|
|
1593
|
+
console.log(`${c.cyan}${sym.box.bottomLeft}${"═".repeat(77)}${sym.box.bottomRight}${c.reset}`);
|
|
1594
|
+
|
|
1595
|
+
return { verdict, score };
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
function formatDuration(ms) {
|
|
1599
|
+
if (ms < 1000) return `${ms}ms`;
|
|
1600
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
1601
|
+
const minutes = Math.floor(ms / 60000);
|
|
1602
|
+
const seconds = Math.floor((ms % 60000) / 1000);
|
|
1603
|
+
return `${minutes}m ${seconds}s`;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1607
|
+
// CI OUTPUT
|
|
1608
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1609
|
+
|
|
1610
|
+
function printCIOutput(results, duration, packPath) {
|
|
1611
|
+
const verdict = results.blockers.length === 0 ?
|
|
1612
|
+
(results.warnings.length === 0 ? "LAUNCH" : "WARN") : "BLOCK";
|
|
1613
|
+
const total = results.passed + results.blockers.length + results.warnings.length + results.skipped;
|
|
1614
|
+
const score = total > 0 ? Math.max(0, Math.round(100 * (results.passed / total))) : 0;
|
|
1615
|
+
|
|
1616
|
+
console.log(`VERDICT=${verdict}`);
|
|
1617
|
+
console.log(`SCORE=${score}`);
|
|
1618
|
+
console.log(`BLOCKERS=${results.blockers.length}`);
|
|
1619
|
+
console.log(`WARNINGS=${results.warnings.length}`);
|
|
1620
|
+
console.log(`PASSED=${results.passed}`);
|
|
1621
|
+
console.log(`SKIPPED=${results.skipped}`);
|
|
1622
|
+
console.log(`DURATION_MS=${duration}`);
|
|
1623
|
+
if (packPath) {
|
|
1624
|
+
console.log(`PACK_PATH=${packPath}`);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
function buildJsonOutput(results, duration, ctx) {
|
|
1629
|
+
const verdict = results.blockers.length === 0 ?
|
|
1630
|
+
(results.warnings.length === 0 ? "LAUNCH" : "WARN") : "BLOCK";
|
|
1631
|
+
const total = results.passed + results.blockers.length + results.warnings.length + results.skipped;
|
|
1632
|
+
const score = total > 0 ? Math.max(0, Math.round(100 * (results.passed / total))) : 0;
|
|
1633
|
+
|
|
1634
|
+
return {
|
|
1635
|
+
version: LAUNCH_VERSION,
|
|
1636
|
+
runId: `launch-${crypto.randomBytes(6).toString("hex")}`,
|
|
1637
|
+
timestamp: new Date().toISOString(),
|
|
1638
|
+
verdict,
|
|
1639
|
+
score,
|
|
1640
|
+
duration: {
|
|
1641
|
+
ms: duration,
|
|
1642
|
+
formatted: formatDuration(duration),
|
|
1643
|
+
},
|
|
1644
|
+
summary: {
|
|
1645
|
+
blockers: results.blockers.length,
|
|
1646
|
+
warnings: results.warnings.length,
|
|
1647
|
+
passed: results.passed,
|
|
1648
|
+
skipped: results.skipped,
|
|
1649
|
+
errors: results.errors,
|
|
1650
|
+
},
|
|
1651
|
+
phases: results.phases.map(p => ({
|
|
1652
|
+
phase: p.phase,
|
|
1653
|
+
duration: p.duration,
|
|
1654
|
+
checks: p.checks.map(ch => ({
|
|
1655
|
+
name: ch.name,
|
|
1656
|
+
description: ch.description,
|
|
1657
|
+
status: ch.status,
|
|
1658
|
+
severity: ch.severity,
|
|
1659
|
+
duration: ch.duration,
|
|
1660
|
+
error: ch.error,
|
|
1661
|
+
fix: ch.fix,
|
|
1662
|
+
})),
|
|
1663
|
+
})),
|
|
1664
|
+
blockers: results.blockers.map(b => ({
|
|
1665
|
+
phase: b.phase,
|
|
1666
|
+
name: b.name,
|
|
1667
|
+
description: b.description,
|
|
1668
|
+
error: b.error,
|
|
1669
|
+
fix: b.fix,
|
|
1670
|
+
findings: b.findings?.slice(0, 5),
|
|
1671
|
+
})),
|
|
1672
|
+
warnings: results.warnings.map(w => ({
|
|
1673
|
+
phase: w.phase,
|
|
1674
|
+
name: w.name,
|
|
1675
|
+
description: w.description,
|
|
1676
|
+
error: w.error,
|
|
1677
|
+
})),
|
|
1678
|
+
git: getGitInfo(ctx.projectRoot),
|
|
1679
|
+
environment: {
|
|
1680
|
+
nodeVersion: process.version,
|
|
1681
|
+
platform: process.platform,
|
|
1682
|
+
ci: process.env.CI === "true",
|
|
1683
|
+
nodeEnv: process.env.NODE_ENV || "development",
|
|
1684
|
+
},
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1689
|
+
// ARTIFACT GENERATION
|
|
1690
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1691
|
+
|
|
1692
|
+
async function generateLaunchPack(results, duration, ctx, opts) {
|
|
1693
|
+
const outputDir = opts.output || path.join(ctx.projectRoot, ".vibecheck", "launch");
|
|
1694
|
+
|
|
1695
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
1696
|
+
|
|
1697
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
1698
|
+
const packId = `launch-${timestamp}`;
|
|
1699
|
+
|
|
1700
|
+
// Full results
|
|
1701
|
+
const fullResults = buildJsonOutput(results, duration, ctx);
|
|
1702
|
+
const fullPath = path.join(outputDir, `${packId}.json`);
|
|
1703
|
+
fs.writeFileSync(fullPath, JSON.stringify(fullResults, null, 2));
|
|
1704
|
+
|
|
1705
|
+
// Blockers only
|
|
1706
|
+
const blockersPath = path.join(outputDir, "blockers.json");
|
|
1707
|
+
fs.writeFileSync(blockersPath, JSON.stringify(results.blockers, null, 2));
|
|
1708
|
+
|
|
1709
|
+
// Human-readable checklist
|
|
1710
|
+
const checklistPath = path.join(outputDir, "checklist.md");
|
|
1711
|
+
fs.writeFileSync(checklistPath, generateChecklist(results, fullResults));
|
|
1712
|
+
|
|
1713
|
+
// Audit trail
|
|
1714
|
+
const auditPath = path.join(outputDir, "audit-trail.json");
|
|
1715
|
+
fs.writeFileSync(auditPath, JSON.stringify(generateAuditTrail(results, fullResults, ctx), null, 2));
|
|
1716
|
+
|
|
1717
|
+
// Manifest
|
|
1718
|
+
const manifestPath = path.join(outputDir, "manifest.json");
|
|
1719
|
+
fs.writeFileSync(manifestPath, JSON.stringify({
|
|
1720
|
+
id: packId,
|
|
1721
|
+
version: LAUNCH_VERSION,
|
|
1722
|
+
generated: new Date().toISOString(),
|
|
1723
|
+
verdict: fullResults.verdict,
|
|
1724
|
+
score: fullResults.score,
|
|
1725
|
+
files: [
|
|
1726
|
+
{ name: `${packId}.json`, type: "results" },
|
|
1727
|
+
{ name: "blockers.json", type: "blockers" },
|
|
1728
|
+
{ name: "checklist.md", type: "checklist" },
|
|
1729
|
+
{ name: "audit-trail.json", type: "audit" },
|
|
1730
|
+
],
|
|
1731
|
+
}, null, 2));
|
|
1732
|
+
|
|
1733
|
+
return { packId, outputDir, fullPath };
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function generateChecklist(results, fullResults) {
|
|
1737
|
+
let md = `# Launch Checklist\n\n`;
|
|
1738
|
+
md += `**Generated:** ${new Date().toISOString()}\n`;
|
|
1739
|
+
md += `**Verdict:** ${fullResults.verdict}\n`;
|
|
1740
|
+
md += `**Score:** ${fullResults.score}/100\n`;
|
|
1741
|
+
|
|
1742
|
+
if (fullResults.git) {
|
|
1743
|
+
md += `**Git:** ${fullResults.git.branch}@${fullResults.git.commit}${fullResults.git.dirty ? " (dirty)" : ""}\n`;
|
|
1744
|
+
}
|
|
1745
|
+
md += `\n`;
|
|
1746
|
+
|
|
1747
|
+
md += `## Summary\n\n`;
|
|
1748
|
+
md += `| Metric | Count |\n`;
|
|
1749
|
+
md += `|--------|-------|\n`;
|
|
1750
|
+
md += `| Blockers | ${results.blockers.length} |\n`;
|
|
1751
|
+
md += `| Warnings | ${results.warnings.length} |\n`;
|
|
1752
|
+
md += `| Passed | ${results.passed} |\n`;
|
|
1753
|
+
md += `| Skipped | ${results.skipped} |\n`;
|
|
1754
|
+
md += `\n`;
|
|
1755
|
+
|
|
1756
|
+
if (results.blockers.length > 0) {
|
|
1757
|
+
md += `## 🚫 Blockers\n\n`;
|
|
1758
|
+
results.blockers.forEach((b, i) => {
|
|
1759
|
+
md += `### ${i + 1}. [${b.phase.toUpperCase()}] ${b.description || b.name}\n\n`;
|
|
1760
|
+
if (b.error) md += `**Error:** ${b.error}\n\n`;
|
|
1761
|
+
if (b.fix) md += `**Fix:** ${b.fix}\n\n`;
|
|
1762
|
+
if (b.findings && b.findings.length > 0) {
|
|
1763
|
+
md += `**Locations:**\n`;
|
|
1764
|
+
for (const f of b.findings.slice(0, 5)) {
|
|
1765
|
+
if (f.file) {
|
|
1766
|
+
md += `- \`${path.relative(process.cwd(), f.file)}:${f.line || "?"}\`\n`;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
md += `\n`;
|
|
1770
|
+
}
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
if (results.warnings.length > 0) {
|
|
1775
|
+
md += `## ⚠️ Warnings\n\n`;
|
|
1776
|
+
results.warnings.forEach((w, i) => {
|
|
1777
|
+
md += `${i + 1}. **[${w.phase.toUpperCase()}]** ${w.description || w.name}`;
|
|
1778
|
+
if (w.error) md += ` - ${w.error}`;
|
|
1779
|
+
md += `\n`;
|
|
1780
|
+
});
|
|
1781
|
+
md += `\n`;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
md += `## All Checks\n\n`;
|
|
1785
|
+
for (const phase of results.phases) {
|
|
1786
|
+
const phaseConfig = PHASES[phase.phase];
|
|
1787
|
+
md += `### ${phaseConfig?.icon || "📋"} ${phaseConfig?.name || phase.phase}\n\n`;
|
|
1788
|
+
|
|
1789
|
+
for (const check of phase.checks) {
|
|
1790
|
+
const icon = check.status === "pass" ? "✅" :
|
|
1791
|
+
check.status === "skip" ? "⏭️" :
|
|
1792
|
+
check.severity === "BLOCK" ? "❌" : "⚠️";
|
|
1793
|
+
md += `- ${icon} ${check.description || check.name}`;
|
|
1794
|
+
if (check.status !== "pass" && check.error) {
|
|
1795
|
+
md += ` *(${check.error})*`;
|
|
1796
|
+
}
|
|
1797
|
+
md += `\n`;
|
|
1798
|
+
}
|
|
1799
|
+
md += `\n`;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
return md;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
function generateAuditTrail(results, fullResults, ctx) {
|
|
1806
|
+
return {
|
|
1807
|
+
auditVersion: "1.0.0",
|
|
1808
|
+
runId: fullResults.runId,
|
|
1809
|
+
timestamp: fullResults.timestamp,
|
|
1810
|
+
operator: {
|
|
1811
|
+
userId: process.env.USER || process.env.USERNAME || "unknown",
|
|
1812
|
+
email: process.env.GIT_AUTHOR_EMAIL || null,
|
|
1813
|
+
method: process.env.CI ? "ci" : "cli",
|
|
1814
|
+
},
|
|
1815
|
+
repository: {
|
|
1816
|
+
path: ctx.projectRoot,
|
|
1817
|
+
...fullResults.git,
|
|
1818
|
+
},
|
|
1819
|
+
environment: fullResults.environment,
|
|
1820
|
+
execution: {
|
|
1821
|
+
phases: results.phases.length,
|
|
1822
|
+
duration: fullResults.duration.ms,
|
|
1823
|
+
checks: results.phases.reduce((sum, p) => sum + p.checks.length, 0),
|
|
1824
|
+
},
|
|
1825
|
+
checks: results.phases.flatMap(phase =>
|
|
1826
|
+
phase.checks.map(check => ({
|
|
1827
|
+
phase: phase.phase,
|
|
1828
|
+
check: check.name,
|
|
1829
|
+
description: check.description,
|
|
1830
|
+
status: check.status,
|
|
1831
|
+
severity: check.severity,
|
|
1832
|
+
duration: check.duration,
|
|
1833
|
+
error: check.error || null,
|
|
1834
|
+
}))
|
|
1835
|
+
),
|
|
1836
|
+
verdict: fullResults.verdict,
|
|
1837
|
+
score: fullResults.score,
|
|
1838
|
+
attestation: {
|
|
1839
|
+
hash: crypto.createHash("sha256")
|
|
1840
|
+
.update(JSON.stringify({ verdict: fullResults.verdict, score: fullResults.score, timestamp: fullResults.timestamp }))
|
|
1841
|
+
.digest("hex"),
|
|
1842
|
+
signature: null, // Could be signed with private key
|
|
1843
|
+
},
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1848
|
+
// INTERACTIVE PROMPT
|
|
1849
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1850
|
+
|
|
1851
|
+
async function promptToContinue() {
|
|
1852
|
+
return new Promise((resolve) => {
|
|
1853
|
+
const rl = readline.createInterface({
|
|
1854
|
+
input: process.stdin,
|
|
1855
|
+
output: process.stdout,
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1858
|
+
rl.question(`\n ${c.dim}Press ENTER to start, or Ctrl+C to cancel...${c.reset}`, () => {
|
|
1859
|
+
rl.close();
|
|
1860
|
+
resolve(true);
|
|
1861
|
+
});
|
|
1862
|
+
|
|
1863
|
+
// Handle Ctrl+C gracefully
|
|
1864
|
+
rl.on("close", () => resolve(false));
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1869
|
+
// MAIN ENTRY POINT
|
|
1870
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1871
|
+
|
|
1872
|
+
async function runLaunch(args, context = {}) {
|
|
1873
|
+
const { EXIT } = getExitCodes();
|
|
1874
|
+
const opts = parseArgs(args);
|
|
1875
|
+
|
|
1876
|
+
// Help
|
|
1877
|
+
if (opts.help) {
|
|
1878
|
+
printHelp();
|
|
1879
|
+
return EXIT.SUCCESS;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// Validate project path
|
|
1883
|
+
if (!fs.existsSync(opts.path)) {
|
|
1884
|
+
console.error(`${c.red}Error: Project path does not exist: ${opts.path}${c.reset}`);
|
|
1885
|
+
return EXIT.USER_ERROR;
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// Context
|
|
1889
|
+
const ctx = {
|
|
1890
|
+
projectRoot: opts.path,
|
|
1891
|
+
runId: context.runId || `launch-${Date.now()}`,
|
|
1892
|
+
startTime: Date.now(),
|
|
1893
|
+
isProduction: process.env.NODE_ENV === "production",
|
|
1894
|
+
};
|
|
1895
|
+
|
|
1896
|
+
// Dry run - show what would be checked
|
|
1897
|
+
if (opts.dryRun) {
|
|
1898
|
+
console.log(`\n${c.bold}Dry run - would check:${c.reset}\n`);
|
|
1899
|
+
let phasesToRun = [...PHASE_ORDER];
|
|
1900
|
+
if (opts.onlyPhases.length > 0) {
|
|
1901
|
+
phasesToRun = opts.onlyPhases.filter(p => PHASE_ORDER.includes(p));
|
|
1902
|
+
}
|
|
1903
|
+
phasesToRun = phasesToRun.filter(p => !opts.skipPhases.includes(p));
|
|
1904
|
+
|
|
1905
|
+
for (const phase of phasesToRun) {
|
|
1906
|
+
const config = PHASES[phase];
|
|
1907
|
+
console.log(` ${config.icon} ${config.name}`);
|
|
1908
|
+
console.log(` ${c.dim}${config.description}${c.reset}`);
|
|
1909
|
+
}
|
|
1910
|
+
console.log();
|
|
1911
|
+
return EXIT.SUCCESS;
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// Banner (interactive mode only)
|
|
1915
|
+
if (!opts.ci && !opts.quiet && !opts.noBanner) {
|
|
1916
|
+
printUnifiedBanner(projectRoot, opts);
|
|
1917
|
+
const proceed = await promptToContinue();
|
|
1918
|
+
if (!proceed) {
|
|
1919
|
+
console.log(`\n ${c.dim}Cancelled.${c.reset}\n`);
|
|
1920
|
+
return EXIT.SUCCESS;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// Run all phases
|
|
1925
|
+
const startTime = Date.now();
|
|
1926
|
+
let results;
|
|
1927
|
+
|
|
1928
|
+
try {
|
|
1929
|
+
results = await runAllPhases(ctx, opts);
|
|
1930
|
+
} catch (err) {
|
|
1931
|
+
if (!opts.ci && !opts.quiet) {
|
|
1932
|
+
console.error(`\n${c.red}Fatal error: ${err.message}${c.reset}\n`);
|
|
1933
|
+
}
|
|
1934
|
+
if (opts.json) {
|
|
1935
|
+
console.log(JSON.stringify({ error: err.message, verdict: "ERROR" }));
|
|
1936
|
+
}
|
|
1937
|
+
return EXIT.INTERNAL_ERROR;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
const duration = Date.now() - startTime;
|
|
1941
|
+
|
|
1942
|
+
// Generate pack
|
|
1943
|
+
let pack = null;
|
|
1944
|
+
try {
|
|
1945
|
+
pack = await generateLaunchPack(results, duration, ctx, opts);
|
|
1946
|
+
} catch (err) {
|
|
1947
|
+
if (!opts.quiet) {
|
|
1948
|
+
console.error(`${c.yellow}Warning: Could not generate launch pack: ${err.message}${c.reset}`);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// Output
|
|
1953
|
+
if (opts.json) {
|
|
1954
|
+
console.log(JSON.stringify(buildJsonOutput(results, duration, ctx), null, 2));
|
|
1955
|
+
} else if (opts.ci) {
|
|
1956
|
+
printCIOutput(results, duration, pack?.fullPath);
|
|
1957
|
+
} else if (!opts.quiet) {
|
|
1958
|
+
printBlockersSection(results);
|
|
1959
|
+
const { verdict } = printVerdict(results, duration);
|
|
1960
|
+
|
|
1961
|
+
if (pack) {
|
|
1962
|
+
console.log();
|
|
1963
|
+
console.log(` ${c.dim}📦 Launch pack saved: ${pack.fullPath}${c.reset}`);
|
|
1964
|
+
}
|
|
1965
|
+
console.log();
|
|
1966
|
+
|
|
1967
|
+
if (verdict === "LAUNCH" || verdict === "WARN") {
|
|
1968
|
+
console.log(` ${c.bold}Next:${c.reset} Deploy with confidence! Your launch audit is ready.`);
|
|
1969
|
+
console.log(` Share: ${c.cyan}vibecheck packs bundle --launch${c.reset}`);
|
|
1970
|
+
} else {
|
|
1971
|
+
console.log(` ${c.bold}Next:${c.reset} Fix the blockers above before deploying.`);
|
|
1972
|
+
console.log(` Re-run: ${c.cyan}vibecheck launch${c.reset}`);
|
|
1973
|
+
}
|
|
1974
|
+
console.log();
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
// Exit code
|
|
1978
|
+
if (results.blockers.length > 0) {
|
|
1979
|
+
return EXIT.BLOCKING;
|
|
1980
|
+
} else if (results.warnings.length > 0) {
|
|
1981
|
+
return EXIT.WARNINGS;
|
|
1982
|
+
}
|
|
1983
|
+
return EXIT.SUCCESS;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1987
|
+
// EXPORTS
|
|
1988
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1989
|
+
|
|
1990
|
+
module.exports = { runLaunch };
|
|
1991
|
+
|
|
1992
|
+
// Direct execution
|
|
1993
|
+
if (require.main === module) {
|
|
1994
|
+
runLaunch(process.argv.slice(2))
|
|
1995
|
+
.then(exitCode => process.exit(exitCode))
|
|
1996
|
+
.catch(err => {
|
|
1997
|
+
console.error(err);
|
|
1998
|
+
process.exit(10);
|
|
1999
|
+
});
|
|
2000
|
+
}
|