forgeos 0.1.0-alpha.7 → 0.1.0-alpha.71
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/.agents/skills/forge-agent-fabric/SKILL.md +178 -0
- package/.agents/skills/forge-agent-fabric/references/managed-workflows.md +95 -0
- package/.agents/skills/forge-agent-fabric/references/repository-analysis.md +102 -0
- package/.agents/skills/forge-agent-fabric/scripts/fabric.mjs +46 -0
- package/.agents/skills/forge-agent-fabric/scripts/repository.mjs +44 -0
- package/.npmignore +10 -0
- package/AGENTS.md +142 -90
- package/CHANGELOG.md +870 -9
- package/LICENSE +21 -0
- package/README.md +121 -6
- package/adapters/go/README.md +23 -0
- package/adapters/go/go.mod +3 -0
- package/adapters/go/http.go +149 -0
- package/adapters/go/registry.go +234 -0
- package/adapters/go/types.go +136 -0
- package/adapters/java/README.md +68 -0
- package/adapters/java/pom.xml +47 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Auth.java +20 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Diagnostic.java +16 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Entry.java +38 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/EntryKind.java +16 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ErrorInfo.java +4 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Forge.java +94 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeCall.java +12 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeContext.java +11 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHandler.java +8 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeHttpHandler.java +179 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ForgeRegistry.java +121 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Json.java +14 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Manifest.java +14 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/RequestEnvelope.java +6 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/ResponseEnvelope.java +25 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Risk.java +18 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Schemas.java +36 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/Service.java +65 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/TransactionMode.java +18 -0
- package/adapters/java/src/main/java/dev/forgeos/adapter/TypedForgeHandler.java +6 -0
- package/adapters/java-spring-boot-starter/README.md +32 -0
- package/adapters/java-spring-boot-starter/pom.xml +49 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeCommand.java +22 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeExternalService.java +15 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeQuery.java +16 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeServiceBeanCondition.java +18 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringAutoConfiguration.java +16 -0
- package/adapters/java-spring-boot-starter/src/main/java/dev/forgeos/adapter/spring/ForgeSpringRuntime.java +104 -0
- package/adapters/java-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +1 -0
- package/bin/forge.mjs +18 -0
- package/docs/agent-fabric-programs.md +151 -0
- package/docs/agent-fabric.md +582 -0
- package/docs/architecture/agent-fabric/PORTABLE_PROJECTS_PLAN.md +136 -0
- package/docs/architecture/agent-fabric/README.md +180 -0
- package/docs/architecture/agent-fabric/REPOSITORY_MANIFEST_PLAN.md +284 -0
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_ADR.md +81 -0
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS.md +39 -0
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_CORRECTIONS_EVIDENCE.md +53 -0
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_EVIDENCE.md +49 -0
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_V2_PLAN.md +590 -0
- package/docs/cair-protocol.md +103 -0
- package/docs/changelog.md +1070 -0
- package/docs/forge-protocol.md +189 -0
- package/docs/repository-agent-benchmark.md +179 -0
- package/docs/repository-analysis.md +139 -0
- package/docs/repository-context-quality.md +179 -0
- package/docs/repository-runtime-observation.md +82 -0
- package/examples/agent-fabric-v2/README.md +70 -0
- package/examples/agent-fabric-v2/item-child.workflow.ts +11 -0
- package/examples/agent-fabric-v2/make-request.mjs +4 -0
- package/examples/agent-fabric-v2/map-child.workflow.ts +10 -0
- package/examples/agent-fabric-v2/migrate.workflow.ts +35 -0
- package/examples/agent-fabric-v2/registry.example.json +256 -0
- package/examples/agent-fabric-v2/run-ui-fixture.ts +48 -0
- package/examples/agent-fabric-v2/ui-audit.workflow.ts +28 -0
- package/examples/agent-fabric-v2/ui-fixture-worker.cjs +36 -0
- package/examples/agent-fabric-v2/visual-population.json +13 -0
- package/examples/agent-fabric-v2/workers/check.mjs +2 -0
- package/examples/agent-fabric-v2/workers/common.mjs +7 -0
- package/examples/agent-fabric-v2/workers/discover.mjs +2 -0
- package/examples/agent-fabric-v2/workers/migrate.mjs +4 -0
- package/examples/agent-fabric-v2/workers/review.mjs +3 -0
- package/examples/go-billing/go.mod +7 -0
- package/examples/go-billing/main.go +120 -0
- package/examples/java-billing/pom.xml +60 -0
- package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/CreateInvoiceInput.java +4 -0
- package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Invoice.java +11 -0
- package/examples/java-billing/src/main/java/dev/forgeos/examples/billing/Main.java +127 -0
- package/package.json +60 -5
- package/schemas/agent-fabric/v0.1/control-event.schema.json +281 -0
- package/schemas/forge-manifest.schema.json +389 -0
- package/scripts/field-test-forgeos.mjs +1021 -0
- package/scripts/install-agent-fabric-skill.mjs +105 -0
- package/src/forge/_generated/releaseManifest.json +1 -2
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/agent-adapters/index.ts +1592 -125
- package/src/forge/agent-adapters/types.ts +225 -1
- package/src/forge/agent-fabric/adapter.ts +131 -0
- package/src/forge/agent-fabric/attached-snapshot.ts +55 -0
- package/src/forge/agent-fabric/attached-task-contract.ts +105 -0
- package/src/forge/agent-fabric/attached-task-service.ts +127 -0
- package/src/forge/agent-fabric/attached-task-store.ts +109 -0
- package/src/forge/agent-fabric/authority.ts +298 -0
- package/src/forge/agent-fabric/canonical.ts +135 -0
- package/src/forge/agent-fabric/codex-adversarial-review.ts +229 -0
- package/src/forge/agent-fabric/codex-sdk-worker.ts +203 -0
- package/src/forge/agent-fabric/conductor.ts +754 -0
- package/src/forge/agent-fabric/dictionary.ts +12 -0
- package/src/forge/agent-fabric/errors.ts +32 -0
- package/src/forge/agent-fabric/hardened-conductor.ts +485 -0
- package/src/forge/agent-fabric/hardened-reducer.ts +211 -0
- package/src/forge/agent-fabric/index.ts +48 -0
- package/src/forge/agent-fabric/journal.ts +116 -0
- package/src/forge/agent-fabric/local-adaptive-digest-worker.mjs +26 -0
- package/src/forge/agent-fabric/local-adaptive-harness.ts +343 -0
- package/src/forge/agent-fabric/local-adaptive-service.ts +398 -0
- package/src/forge/agent-fabric/local-adaptive-worker.ts +146 -0
- package/src/forge/agent-fabric/local-approval-window.ts +211 -0
- package/src/forge/agent-fabric/local-change-review-service.ts +374 -0
- package/src/forge/agent-fabric/local-coding-worker.ts +285 -0
- package/src/forge/agent-fabric/local-control-store.ts +239 -0
- package/src/forge/agent-fabric/local-effect-broker.ts +348 -0
- package/src/forge/agent-fabric/local-evolution-approval.ts +81 -0
- package/src/forge/agent-fabric/local-evolution-profile.ts +87 -0
- package/src/forge/agent-fabric/local-evolution-registry.ts +427 -0
- package/src/forge/agent-fabric/local-evolution-service.ts +200 -0
- package/src/forge/agent-fabric/local-intelligence.ts +281 -0
- package/src/forge/agent-fabric/local-paths.ts +18 -0
- package/src/forge/agent-fabric/local-task-contract.ts +203 -0
- package/src/forge/agent-fabric/local-task-inbox.ts +543 -0
- package/src/forge/agent-fabric/local-task-server.ts +374 -0
- package/src/forge/agent-fabric/local-task-service.ts +982 -0
- package/src/forge/agent-fabric/local-verification.ts +551 -0
- package/src/forge/agent-fabric/managed-environment.ts +205 -0
- package/src/forge/agent-fabric/managed-run-contract.ts +103 -0
- package/src/forge/agent-fabric/managed-run-service.ts +401 -0
- package/src/forge/agent-fabric/managed-run-store.ts +165 -0
- package/src/forge/agent-fabric/managed-workspace.ts +253 -0
- package/src/forge/agent-fabric/p0a.ts +104 -0
- package/src/forge/agent-fabric/p0b-model-adapter.ts +434 -0
- package/src/forge/agent-fabric/planning.ts +155 -0
- package/src/forge/agent-fabric/program-api.ts +37 -0
- package/src/forge/agent-fabric/program-contract.ts +283 -0
- package/src/forge/agent-fabric/program-dsl.ts +126 -0
- package/src/forge/agent-fabric/program-evidence.ts +22 -0
- package/src/forge/agent-fabric/program-image.ts +15 -0
- package/src/forge/agent-fabric/program-lock.ts +50 -0
- package/src/forge/agent-fabric/program-owner.ts +16 -0
- package/src/forge/agent-fabric/program-scheduler.ts +38 -0
- package/src/forge/agent-fabric/program-service.ts +793 -0
- package/src/forge/agent-fabric/program-store.ts +142 -0
- package/src/forge/agent-fabric/program-structure.ts +65 -0
- package/src/forge/agent-fabric/program-types.ts +110 -0
- package/src/forge/agent-fabric/program-view.ts +12 -0
- package/src/forge/agent-fabric/program-worker.ts +108 -0
- package/src/forge/agent-fabric/project-profile.ts +50 -0
- package/src/forge/agent-fabric/project-registry.ts +120 -0
- package/src/forge/agent-fabric/project-runtime.ts +85 -0
- package/src/forge/agent-fabric/readiness.ts +26 -0
- package/src/forge/agent-fabric/reducer.ts +863 -0
- package/src/forge/agent-fabric/repository-context.ts +83 -0
- package/src/forge/agent-fabric/resource-ledger.ts +266 -0
- package/src/forge/agent-fabric/serialized-local-adapter.ts +60 -0
- package/src/forge/agent-fabric/types.ts +424 -0
- package/src/forge/agent-fabric/validation.ts +500 -0
- package/src/forge/agent-fabric/workflow-engine.ts +218 -0
- package/src/forge/agent-fabric/workflow-task-actions.ts +104 -0
- package/src/forge/agent-fabric/workflow-templates.ts +11 -0
- package/src/forge/agent-memory/bridge.ts +1728 -0
- package/src/forge/agent-memory/context-pack.ts +284 -0
- package/src/forge/agent-memory/hook-runner.ts +331 -0
- package/src/forge/agent-memory/mcp.ts +597 -0
- package/src/forge/agent-memory/normalize.ts +498 -0
- package/src/forge/agent-memory/redaction.ts +103 -0
- package/src/forge/agent-memory/sources/claude-code.ts +51 -0
- package/src/forge/agent-memory/sources/codex-hook-runner.mjs +343 -0
- package/src/forge/agent-memory/sources/codex.ts +161 -0
- package/src/forge/agent-memory/sources/cursor.ts +35 -0
- package/src/forge/agent-memory/types.ts +215 -0
- package/src/forge/bench.ts +248 -0
- package/src/forge/brownfield-import/index.ts +896 -0
- package/src/forge/brownfield-import/types.ts +127 -0
- package/src/forge/cair/action-journal.ts +61 -0
- package/src/forge/cair/action-parser.ts +314 -0
- package/src/forge/cair/action-validator.ts +40 -0
- package/src/forge/cair/actions.ts +1818 -0
- package/src/forge/cair/format.ts +77 -0
- package/src/forge/cair/index.ts +118 -0
- package/src/forge/cair/query.ts +478 -0
- package/src/forge/cair/snapshot.ts +315 -0
- package/src/forge/cair/types.ts +255 -0
- package/src/forge/cli/adaptive.ts +51 -0
- package/src/forge/cli/ai.ts +16 -1
- package/src/forge/cli/auth.ts +459 -15
- package/src/forge/cli/authmd.ts +441 -0
- package/src/forge/cli/baseline.ts +112 -0
- package/src/forge/cli/build.ts +20 -4
- package/src/forge/cli/changed.ts +418 -0
- package/src/forge/cli/codex-app-server.ts +877 -0
- package/src/forge/cli/commands.ts +1859 -20
- package/src/forge/cli/db.ts +209 -2
- package/src/forge/cli/deploy.ts +1339 -0
- package/src/forge/cli/deps.ts +150 -23
- package/src/forge/cli/dev.ts +1137 -53
- package/src/forge/cli/docs.ts +265 -0
- package/src/forge/cli/doctor.ts +297 -0
- package/src/forge/cli/env-placeholders.ts +62 -0
- package/src/forge/cli/evolution.ts +40 -0
- package/src/forge/cli/fabric.ts +253 -0
- package/src/forge/cli/field-test.ts +568 -0
- package/src/forge/cli/golden-path.ts +487 -0
- package/src/forge/cli/handoff.ts +336 -0
- package/src/forge/cli/index.ts +1 -0
- package/src/forge/cli/last-run.ts +84 -0
- package/src/forge/cli/main.ts +196 -3
- package/src/forge/cli/new.ts +169 -17
- package/src/forge/cli/next-actions.ts +23 -0
- package/src/forge/cli/output.ts +398 -6
- package/src/forge/cli/parse.ts +1377 -38
- package/src/forge/cli/progress.ts +51 -0
- package/src/forge/cli/query.ts +32 -0
- package/src/forge/cli/release.ts +35 -11
- package/src/forge/cli/repository.ts +151 -0
- package/src/forge/cli/run.ts +42 -0
- package/src/forge/cli/secrets.ts +137 -1
- package/src/forge/cli/seed.ts +581 -0
- package/src/forge/cli/self-host.ts +56 -14
- package/src/forge/cli/studio.ts +2282 -0
- package/src/forge/cli/verify.ts +1458 -32
- package/src/forge/cli/windows.ts +23 -0
- package/src/forge/cli/workos.ts +3509 -0
- package/src/forge/compiler/agent-contract/build.ts +728 -64
- package/src/forge/compiler/agent-contract/types.ts +45 -2
- package/src/forge/compiler/ai-registry/parse.ts +71 -2
- package/src/forge/compiler/api-surface/build.ts +47 -0
- package/src/forge/compiler/app-graph/build.ts +112 -9
- package/src/forge/compiler/app-graph/extract.ts +107 -0
- package/src/forge/compiler/app-graph/module-graph.ts +73 -78
- package/src/forge/compiler/app-graph/parser.ts +24 -24
- package/src/forge/compiler/app-graph/profile.ts +26 -0
- package/src/forge/compiler/app-graph/tsconfig-hash.ts +41 -4
- package/src/forge/compiler/app-graph/versions.ts +1 -1
- package/src/forge/compiler/classifier/capabilities.ts +3 -2
- package/src/forge/compiler/classifier/classify.ts +32 -8
- package/src/forge/compiler/classifier/secrets.ts +3 -2
- package/src/forge/compiler/classifier/signals.ts +91 -1
- package/src/forge/compiler/client-sdk/build-manifest.ts +59 -0
- package/src/forge/compiler/client-sdk/render-client.ts +295 -13
- package/src/forge/compiler/data-graph/build.ts +29 -1
- package/src/forge/compiler/data-graph/parse.ts +20 -4
- package/src/forge/compiler/data-graph/sql/ddl.ts +144 -31
- package/src/forge/compiler/data-graph/sql/serialize.ts +4 -0
- package/src/forge/compiler/data-graph/sql/types.ts +1 -0
- package/src/forge/compiler/diagnostics/codes.ts +38 -0
- package/src/forge/compiler/diagnostics/create.ts +28 -2
- package/src/forge/compiler/diagnostics/index.ts +2 -0
- package/src/forge/compiler/emitter/barrel.ts +3 -0
- package/src/forge/compiler/emitter/render.ts +9 -0
- package/src/forge/compiler/external-manifest/registry.ts +217 -0
- package/src/forge/compiler/external-manifest/types.ts +91 -0
- package/src/forge/compiler/external-manifest/validate.ts +373 -0
- package/src/forge/compiler/frontend-graph/build.ts +142 -14
- package/src/forge/compiler/integration/add.ts +991 -33
- package/src/forge/compiler/integration/plan.ts +43 -12
- package/src/forge/compiler/integration/render.ts +29 -0
- package/src/forge/compiler/integration/snapshot.ts +2 -0
- package/src/forge/compiler/integration/templates/convex.ts +70 -0
- package/src/forge/compiler/integration/templates/index.ts +1 -0
- package/src/forge/compiler/integration/templates/render.ts +38 -0
- package/src/forge/compiler/integration/templates/types.ts +3 -0
- package/src/forge/compiler/integration/templates/workos.ts +1439 -0
- package/src/forge/compiler/make-registry/build.ts +8 -9
- package/src/forge/compiler/orchestrator/fast-check.ts +4 -0
- package/src/forge/compiler/orchestrator/generate-lock.ts +14 -3
- package/src/forge/compiler/orchestrator/manifest.ts +1 -1
- package/src/forge/compiler/orchestrator/plan-profile.ts +23 -0
- package/src/forge/compiler/orchestrator/plan.ts +249 -10
- package/src/forge/compiler/orchestrator/profile.ts +65 -0
- package/src/forge/compiler/orchestrator/run.ts +101 -33
- package/src/forge/compiler/orchestrator/serialize.ts +33 -8
- package/src/forge/compiler/orchestrator/types.ts +1 -1
- package/src/forge/compiler/package-graph/compiler.ts +3 -1
- package/src/forge/compiler/package-graph/constants.ts +1 -1
- package/src/forge/compiler/package-graph/dts-extractor.ts +14 -1
- package/src/forge/compiler/package-manager/adapter.ts +4 -1
- package/src/forge/compiler/package-manager/commands.ts +4 -0
- package/src/forge/compiler/package-manager/executor.ts +30 -1
- package/src/forge/compiler/package-manager/version.ts +36 -12
- package/src/forge/compiler/package-upgrades/planner.ts +155 -18
- package/src/forge/compiler/package-upgrades/types.ts +2 -0
- package/src/forge/compiler/policy-registry/build.ts +47 -2
- package/src/forge/compiler/policy-registry/parse.ts +32 -2
- package/src/forge/compiler/recipes/definitions.ts +63 -0
- package/src/forge/compiler/recipes/index.ts +2 -0
- package/src/forge/compiler/recipes/profiles.ts +81 -0
- package/src/forge/compiler/recipes/registry.ts +6 -0
- package/src/forge/compiler/test-graph/build.ts +11 -3
- package/src/forge/compiler/types/app-graph.ts +8 -2
- package/src/forge/compiler/types/cli.ts +76 -1
- package/src/forge/compiler/types/dev-manifest.ts +4 -0
- package/src/forge/compiler/types/emit.ts +2 -0
- package/src/forge/compiler/types/frontend-graph.ts +2 -2
- package/src/forge/compiler/types/integration.ts +1 -0
- package/src/forge/compiler/types/package-graph.ts +6 -0
- package/src/forge/compiler/types/policy-registry.ts +3 -1
- package/src/forge/delta/broker-runner.mjs +5 -0
- package/src/forge/delta/broker.ts +429 -0
- package/src/forge/delta/classifier.ts +52 -0
- package/src/forge/delta/explain.ts +238 -0
- package/src/forge/delta/git-observer.ts +67 -0
- package/src/forge/delta/ids.ts +44 -0
- package/src/forge/delta/index.ts +25 -0
- package/src/forge/delta/process-identity.ts +37 -0
- package/src/forge/delta/queue-status.ts +190 -0
- package/src/forge/delta/recorder.ts +540 -0
- package/src/forge/delta/redaction.ts +50 -0
- package/src/forge/delta/schema.ts +240 -0
- package/src/forge/delta/session.ts +142 -0
- package/src/forge/delta/status.ts +1139 -0
- package/src/forge/delta/store.ts +3329 -0
- package/src/forge/delta/timeline.ts +178 -0
- package/src/forge/dev/server.ts +1061 -22
- package/src/forge/dev/types.ts +15 -1
- package/src/forge/dev/watch.ts +17 -7
- package/src/forge/dev-console/cycle.ts +285 -33
- package/src/forge/dev-console/types.ts +47 -1
- package/src/forge/impact/index.ts +222 -30
- package/src/forge/impact/types.ts +33 -1
- package/src/forge/intent/index.ts +35 -16
- package/src/forge/make/fields.ts +26 -0
- package/src/forge/make/index.ts +42 -6
- package/src/forge/make/templates.ts +211 -4
- package/src/forge/make/types.ts +2 -1
- package/src/forge/policy.ts +1 -1
- package/src/forge/react/index.ts +160 -2
- package/src/forge/refactor/index.ts +1 -0
- package/src/forge/repair/rules/index.ts +2 -2
- package/src/forge/repository-analysis/adapters/common.ts +72 -0
- package/src/forge/repository-analysis/adapters/docker.ts +185 -0
- package/src/forge/repository-analysis/adapters/java.ts +133 -0
- package/src/forge/repository-analysis/adapters/typescript.ts +350 -0
- package/src/forge/repository-analysis/adapters/vue.ts +71 -0
- package/src/forge/repository-analysis/analyze.ts +434 -0
- package/src/forge/repository-analysis/benchmark.ts +336 -0
- package/src/forge/repository-analysis/cache-gc.ts +154 -0
- package/src/forge/repository-analysis/check-selection.ts +123 -0
- package/src/forge/repository-analysis/context-node.ts +17 -0
- package/src/forge/repository-analysis/context.ts +456 -0
- package/src/forge/repository-analysis/http-resolution.ts +135 -0
- package/src/forge/repository-analysis/identity.ts +45 -0
- package/src/forge/repository-analysis/index.ts +18 -0
- package/src/forge/repository-analysis/nuxt-resolution.ts +188 -0
- package/src/forge/repository-analysis/quality.ts +60 -0
- package/src/forge/repository-analysis/retrieval.ts +309 -0
- package/src/forge/repository-analysis/runtime-artifacts.ts +195 -0
- package/src/forge/repository-analysis/runtime-observation.ts +325 -0
- package/src/forge/repository-analysis/scanner.ts +174 -0
- package/src/forge/repository-analysis/storage.ts +193 -0
- package/src/forge/repository-analysis/test-associations.ts +59 -0
- package/src/forge/repository-analysis/types.ts +99 -0
- package/src/forge/repository-manifest/index.ts +178 -0
- package/src/forge/repository-manifest/types.ts +63 -0
- package/src/forge/review/index.ts +162 -12
- package/src/forge/review/types.ts +15 -0
- package/src/forge/runtime/ai/providers.ts +19 -4
- package/src/forge/runtime/auth/config.ts +17 -0
- package/src/forge/runtime/auth/evaluate.ts +15 -2
- package/src/forge/runtime/auth/resolve.ts +61 -6
- package/src/forge/runtime/auth/types.ts +1 -0
- package/src/forge/runtime/db/factory.ts +1 -3
- package/src/forge/runtime/db/generated-client.ts +13 -2
- package/src/forge/runtime/db/memory-adapter.ts +139 -32
- package/src/forge/runtime/db/pglite-adapter.ts +285 -2
- package/src/forge/runtime/executor.ts +129 -16
- package/src/forge/runtime/external/bridge.ts +663 -0
- package/src/forge/runtime/runner/run-entry.ts +16 -7
- package/src/forge/runtime/telemetry/buffer.ts +4 -1
- package/src/forge/runtime/telemetry/scrubber.ts +35 -5
- package/src/forge/runtime/webhooks/security.ts +12 -7
- package/src/forge/server.ts +71 -7
- package/src/forge/ui/index.ts +831 -19
- package/src/forge/ui/types.ts +3 -1
- package/src/forge/version.ts +1 -1
- package/src/forge/vue/index.ts +422 -0
- package/src/forge/workspace/baseline.ts +112 -0
- package/src/forge/workspace/change-summary.ts +292 -0
- package/src/forge/workspace/forge-cli.ts +42 -0
- package/src/forge/workspace/git-summary.ts +393 -0
- package/templates/agent-workroom/AGENTS.md +29 -0
- package/templates/agent-workroom/README.md +34 -0
- package/templates/agent-workroom/forge.config.ts +3 -0
- package/templates/agent-workroom/package.json +34 -0
- package/templates/agent-workroom/pnpm-workspace.yaml +2 -0
- package/templates/agent-workroom/src/actions/indexAgentSignal.ts +10 -0
- package/templates/agent-workroom/src/commands/openWorkroom.ts +61 -0
- package/templates/agent-workroom/src/commands/recordAgentSignal.ts +119 -0
- package/templates/agent-workroom/src/commands/recordCheckRun.ts +52 -0
- package/templates/agent-workroom/src/forge/schema.ts +54 -0
- package/templates/agent-workroom/src/policies.ts +6 -0
- package/templates/agent-workroom/src/queries/listWorkrooms.ts +11 -0
- package/templates/agent-workroom/src/queries/liveWorkroom.ts +63 -0
- package/templates/agent-workroom/tsconfig.json +16 -0
- package/templates/agent-workroom/web/index.html +13 -0
- package/templates/agent-workroom/web/package.json +21 -0
- package/templates/agent-workroom/web/src/App.tsx +345 -0
- package/templates/agent-workroom/web/src/lib/forge.ts +20 -0
- package/templates/agent-workroom/web/src/main.tsx +13 -0
- package/templates/agent-workroom/web/src/styles.css +545 -0
- package/templates/agent-workroom/web/tsconfig.json +27 -0
- package/templates/agent-workroom/web/vite.config.ts +36 -0
- package/templates/b2b-support-web/package.json +2 -0
- package/templates/b2b-support-web/pnpm-workspace.yaml +2 -0
- package/templates/b2b-support-web/web/app/page.tsx +2 -2
- package/templates/b2b-support-web/web/lib/forge.ts +9 -2
- package/templates/b2b-support-web/web/next.config.ts +24 -0
- package/templates/b2b-support-web/web/package.json +1 -1
- package/templates/minimal-web/package.json +2 -1
- package/templates/minimal-web/pnpm-workspace.yaml +2 -0
- package/templates/minimal-web/web/index.html +1 -0
- package/templates/minimal-web/web/src/App.tsx +12 -6
- package/templates/minimal-web/web/src/lib/forge.ts +9 -2
- package/templates/minimal-web/web/src/styles.css +34 -0
- package/templates/minimal-web/web/vite.config.ts +36 -0
- package/templates/nuxt-web/.vscode/settings.json +14 -0
- package/templates/nuxt-web/.yarnrc.yml +1 -0
- package/templates/nuxt-web/README.md +30 -0
- package/templates/nuxt-web/forge.config.ts +3 -0
- package/templates/nuxt-web/package.json +37 -0
- package/templates/nuxt-web/pnpm-workspace.yaml +2 -0
- package/templates/nuxt-web/src/actions/logNoteCreated.ts +11 -0
- package/templates/nuxt-web/src/commands/createNote.ts +26 -0
- package/templates/nuxt-web/src/forge/schema.ts +12 -0
- package/templates/nuxt-web/src/policies.ts +6 -0
- package/templates/nuxt-web/src/queries/listNotes.ts +8 -0
- package/templates/nuxt-web/src/queries/liveNotes.ts +8 -0
- package/templates/nuxt-web/tsconfig.json +17 -0
- package/templates/nuxt-web/web/app.vue +67 -0
- package/templates/nuxt-web/web/components/LiveNotes.vue +89 -0
- package/templates/nuxt-web/web/components/NoteComposer.vue +100 -0
- package/templates/nuxt-web/web/composables/forge.ts +13 -0
- package/templates/nuxt-web/web/composables/useNotes.ts +24 -0
- package/templates/nuxt-web/web/nuxt.config.ts +11 -0
- package/templates/nuxt-web/web/package.json +22 -0
- package/templates/nuxt-web/web/plugins/forge.client.ts +10 -0
- package/templates/nuxt-web/web/plugins/forge.server.ts +10 -0
- package/templates/nuxt-web/web/server/api/forge-health.get.ts +7 -0
- package/templates/nuxt-web/web/tsconfig.json +6 -0
- package/templates/vendor-access/.vscode/settings.json +14 -0
- package/templates/vendor-access/README.md +30 -0
- package/templates/vendor-access/forge.config.ts +3 -0
- package/templates/vendor-access/package.json +35 -0
- package/templates/vendor-access/pnpm-workspace.yaml +2 -0
- package/templates/vendor-access/src/commands/addEvidence.ts +42 -0
- package/templates/vendor-access/src/commands/approveAccessRequest.ts +43 -0
- package/templates/vendor-access/src/commands/createAccessRequest.ts +49 -0
- package/templates/vendor-access/src/commands/seedVendorAccessDemo.ts +200 -0
- package/templates/vendor-access/src/forge/schema.ts +74 -0
- package/templates/vendor-access/src/policies.ts +11 -0
- package/templates/vendor-access/src/queries/listVendorAccessDashboard.ts +24 -0
- package/templates/vendor-access/src/queries/liveVendorAccessDashboard.ts +24 -0
- package/templates/vendor-access/tsconfig.json +17 -0
- package/templates/vendor-access/web/index.html +13 -0
- package/templates/vendor-access/web/package.json +21 -0
- package/templates/vendor-access/web/src/App.tsx +573 -0
- package/templates/vendor-access/web/src/lib/forge.ts +20 -0
- package/templates/vendor-access/web/src/main.tsx +205 -0
- package/templates/vendor-access/web/src/styles.css +682 -0
- package/templates/vendor-access/web/tsconfig.json +18 -0
- package/templates/vendor-access/web/vite.config.ts +36 -0
- package/src/forge/_generated/actionSubscriptions.json +0 -2
- package/src/forge/_generated/actionSubscriptions.ts +0 -10
- package/src/forge/_generated/agentAdapterManifest.json +0 -2
- package/src/forge/_generated/agentAdapterManifest.ts +0 -73
- package/src/forge/_generated/agentContract.json +0 -2
- package/src/forge/_generated/agentContract.ts +0 -7829
- package/src/forge/_generated/agentQuickstart.md +0 -34
- package/src/forge/_generated/agentTools.json +0 -2
- package/src/forge/_generated/agentTools.md +0 -16
- package/src/forge/_generated/agentTools.ts +0 -12
- package/src/forge/_generated/aiContext.ts +0 -125
- package/src/forge/_generated/aiModels.json +0 -2
- package/src/forge/_generated/aiModels.ts +0 -51
- package/src/forge/_generated/aiProviders.json +0 -2
- package/src/forge/_generated/aiProviders.ts +0 -23
- package/src/forge/_generated/aiRegistry.json +0 -2
- package/src/forge/_generated/aiRegistry.ts +0 -31
- package/src/forge/_generated/api.json +0 -2
- package/src/forge/_generated/api.ts +0 -8
- package/src/forge/_generated/appGraph.json +0 -2
- package/src/forge/_generated/appGraph.ts +0 -14937
- package/src/forge/_generated/appMap.md +0 -55
- package/src/forge/_generated/artifactManifest.json +0 -2
- package/src/forge/_generated/artifactManifest.ts +0 -7
- package/src/forge/_generated/authClaims.json +0 -2
- package/src/forge/_generated/authClaims.ts +0 -13
- package/src/forge/_generated/authConfig.json +0 -2
- package/src/forge/_generated/authConfig.ts +0 -17
- package/src/forge/_generated/authContext.ts +0 -23
- package/src/forge/_generated/authRegistry.json +0 -2
- package/src/forge/_generated/authRegistry.ts +0 -25
- package/src/forge/_generated/buildInfo.json +0 -2
- package/src/forge/_generated/buildInfo.ts +0 -9
- package/src/forge/_generated/capabilityMap.json +0 -2
- package/src/forge/_generated/capabilityMap.md +0 -15
- package/src/forge/_generated/capabilityMap.ts +0 -17
- package/src/forge/_generated/client.ts +0 -282
- package/src/forge/_generated/clientApi.ts +0 -9
- package/src/forge/_generated/clientManifest.json +0 -2
- package/src/forge/_generated/clientManifest.ts +0 -39
- package/src/forge/_generated/clientTypes.ts +0 -78
- package/src/forge/_generated/configRegistry.json +0 -2
- package/src/forge/_generated/configRegistry.ts +0 -4
- package/src/forge/_generated/dataGraph.json +0 -2
- package/src/forge/_generated/dataGraph.ts +0 -8
- package/src/forge/_generated/db.json +0 -2
- package/src/forge/_generated/db.ts +0 -2
- package/src/forge/_generated/dbSecurityManifest.json +0 -2
- package/src/forge/_generated/dbSecurityManifest.ts +0 -15
- package/src/forge/_generated/dbSessionContext.json +0 -2
- package/src/forge/_generated/dbSessionContext.ts +0 -39
- package/src/forge/_generated/deployManifest.json +0 -2
- package/src/forge/_generated/deployManifest.ts +0 -14
- package/src/forge/_generated/devManifest.json +0 -2
- package/src/forge/_generated/devManifest.ts +0 -62
- package/src/forge/_generated/envSchema.json +0 -2
- package/src/forge/_generated/envSchema.ts +0 -59
- package/src/forge/_generated/frontendGraph.json +0 -2
- package/src/forge/_generated/frontendGraph.ts +0 -27
- package/src/forge/_generated/importGuards.json +0 -2
- package/src/forge/_generated/importGuards.ts +0 -686
- package/src/forge/_generated/index.ts +0 -68
- package/src/forge/_generated/liveProductionManifest.json +0 -2
- package/src/forge/_generated/liveProductionManifest.ts +0 -23
- package/src/forge/_generated/liveProtocol.json +0 -2
- package/src/forge/_generated/liveProtocol.ts +0 -21
- package/src/forge/_generated/liveQueryRegistry.json +0 -2
- package/src/forge/_generated/liveQueryRegistry.ts +0 -9
- package/src/forge/_generated/liveTransportConfig.json +0 -2
- package/src/forge/_generated/liveTransportConfig.ts +0 -19
- package/src/forge/_generated/makeRegistry.json +0 -2
- package/src/forge/_generated/makeRegistry.ts +0 -177
- package/src/forge/_generated/makeTemplates.json +0 -2
- package/src/forge/_generated/makeTemplates.ts +0 -66
- package/src/forge/_generated/mockMap.json +0 -2
- package/src/forge/_generated/mockMap.ts +0 -7
- package/src/forge/_generated/operationPlaybooks.md +0 -167
- package/src/forge/_generated/packageGraph.json +0 -2
- package/src/forge/_generated/packageGraph.ts +0 -249334
- package/src/forge/_generated/packageUpgradeRegistry.json +0 -2
- package/src/forge/_generated/packageUpgradeRegistry.ts +0 -15
- package/src/forge/_generated/permissionMatrix.json +0 -2
- package/src/forge/_generated/permissionMatrix.ts +0 -7
- package/src/forge/_generated/policyRegistry.json +0 -2
- package/src/forge/_generated/policyRegistry.ts +0 -11
- package/src/forge/_generated/queryRegistry.json +0 -2
- package/src/forge/_generated/queryRegistry.ts +0 -9
- package/src/forge/_generated/react.d.ts +0 -22
- package/src/forge/_generated/react.ts +0 -29
- package/src/forge/_generated/reactManifest.json +0 -2
- package/src/forge/_generated/reactManifest.ts +0 -19
- package/src/forge/_generated/rlsPolicies.json +0 -2
- package/src/forge/_generated/rlsPolicies.sql +0 -34
- package/src/forge/_generated/rlsPolicies.ts +0 -6
- package/src/forge/_generated/runtimeGraph.json +0 -2
- package/src/forge/_generated/runtimeGraph.ts +0 -8
- package/src/forge/_generated/runtimeMatrix.json +0 -2
- package/src/forge/_generated/runtimeMatrix.ts +0 -334130
- package/src/forge/_generated/runtimeRegistry.ts +0 -2
- package/src/forge/_generated/runtimeRules.md +0 -91
- package/src/forge/_generated/secretRegistry.json +0 -2
- package/src/forge/_generated/secretRegistry.ts +0 -50
- package/src/forge/_generated/secretsContext.ts +0 -11
- package/src/forge/_generated/serverApi.ts +0 -10
- package/src/forge/_generated/sourceMapManifest.json +0 -2
- package/src/forge/_generated/sourceMapManifest.ts +0 -7
- package/src/forge/_generated/sqlPlan.json +0 -2
- package/src/forge/_generated/sqlPlan.ts +0 -88
- package/src/forge/_generated/subscriptionManifest.json +0 -2
- package/src/forge/_generated/subscriptionManifest.ts +0 -7
- package/src/forge/_generated/symbolicationManifest.json +0 -2
- package/src/forge/_generated/symbolicationManifest.ts +0 -17
- package/src/forge/_generated/telemetryRegistry.json +0 -2
- package/src/forge/_generated/telemetryRegistry.ts +0 -9
- package/src/forge/_generated/telemetrySinks.json +0 -2
- package/src/forge/_generated/telemetrySinks.ts +0 -11
- package/src/forge/_generated/tenantScope.json +0 -2
- package/src/forge/_generated/tenantScope.ts +0 -8
- package/src/forge/_generated/testGraph.json +0 -2
- package/src/forge/_generated/testGraph.ts +0 -3452
- package/src/forge/_generated/testPlanRegistry.json +0 -2
- package/src/forge/_generated/testPlanRegistry.ts +0 -33
- package/src/forge/_generated/uiRoutes.json +0 -2
- package/src/forge/_generated/uiRoutes.ts +0 -16
- package/src/forge/_generated/uiScenarios.json +0 -2
- package/src/forge/_generated/uiScenarios.ts +0 -30
- package/src/forge/_generated/uiTestManifest.json +0 -2
- package/src/forge/_generated/uiTestManifest.ts +0 -27
- package/src/forge/_generated/workflowRegistry.json +0 -2
- package/src/forge/_generated/workflowRegistry.ts +0 -9
- package/src/forge/_generated/workflowSubscriptions.json +0 -2
- package/src/forge/_generated/workflowSubscriptions.ts +0 -10
|
@@ -0,0 +1,1339 @@
|
|
|
1
|
+
import { nodeFileSystem } from "../compiler/fs/index.ts";
|
|
2
|
+
import { GENERATED_DIR } from "../compiler/emitter/constants.ts";
|
|
3
|
+
import { stripDeterministicHeader } from "../compiler/primitives/header.ts";
|
|
4
|
+
import { run as runGenerate } from "../compiler/orchestrator/run.ts";
|
|
5
|
+
import { detectPackageManager, getLockfileCandidates } from "../compiler/package-manager/detect.ts";
|
|
6
|
+
import type { FrontendGraph } from "../compiler/types/frontend-graph.ts";
|
|
7
|
+
import type { PackageManager } from "../compiler/types/runtime.ts";
|
|
8
|
+
import { loadAuthConfigFromEnv } from "../runtime/auth/config.ts";
|
|
9
|
+
import { loadSecretRegistry } from "../runtime/secrets/check.ts";
|
|
10
|
+
import { normalizeForgeCliCommandsInValue } from "../workspace/forge-cli.ts";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { runAuthCommand } from "./auth.ts";
|
|
13
|
+
import { runAuthMdCommand } from "./authmd.ts";
|
|
14
|
+
import { runWorkOSCommand } from "./workos.ts";
|
|
15
|
+
import { productionPlaceholderEnvProblems } from "./env-placeholders.ts";
|
|
16
|
+
|
|
17
|
+
export type DeploySubcommand = "plan" | "init" | "check" | "readiness" | "render" | "package" | "verify";
|
|
18
|
+
export type DeployTarget = "docker" | "forge-cloud";
|
|
19
|
+
|
|
20
|
+
export interface DeployCommandOptions {
|
|
21
|
+
subcommand: DeploySubcommand;
|
|
22
|
+
workspaceRoot: string;
|
|
23
|
+
json: boolean;
|
|
24
|
+
target: DeployTarget;
|
|
25
|
+
production: boolean;
|
|
26
|
+
url?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DeployCheck {
|
|
30
|
+
name: string;
|
|
31
|
+
ok: boolean;
|
|
32
|
+
severity: "error" | "warning";
|
|
33
|
+
message: string;
|
|
34
|
+
command?: string;
|
|
35
|
+
details?: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DeployCommandResult {
|
|
39
|
+
schemaVersion: "0.1.0";
|
|
40
|
+
ok: boolean;
|
|
41
|
+
kind: "deploy";
|
|
42
|
+
action: DeploySubcommand;
|
|
43
|
+
target: DeployTarget;
|
|
44
|
+
production: boolean;
|
|
45
|
+
checks: DeployCheck[];
|
|
46
|
+
blocking?: string[];
|
|
47
|
+
warnings?: string[];
|
|
48
|
+
files?: string[];
|
|
49
|
+
plan?: {
|
|
50
|
+
summary: string;
|
|
51
|
+
commands: string[];
|
|
52
|
+
gates: string[];
|
|
53
|
+
notes: string[];
|
|
54
|
+
};
|
|
55
|
+
probes?: Array<{ method: string; url: string; ok: boolean; status?: number; contentType?: string; error?: string; jsonValid?: boolean }>;
|
|
56
|
+
readiness?: DeployReadinessSummary;
|
|
57
|
+
nextActions: string[];
|
|
58
|
+
exitCode: 0 | 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface DeployProbeExpectation {
|
|
62
|
+
contentTypeIncludes?: string;
|
|
63
|
+
json?: boolean;
|
|
64
|
+
required?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface DeployReadinessSummary {
|
|
68
|
+
score: number;
|
|
69
|
+
status: "ready" | "blocked" | "warning";
|
|
70
|
+
blocking: string[];
|
|
71
|
+
warnings: string[];
|
|
72
|
+
stages: Array<{
|
|
73
|
+
name: string;
|
|
74
|
+
ok: boolean;
|
|
75
|
+
score: number;
|
|
76
|
+
checks: string[];
|
|
77
|
+
blocking: string[];
|
|
78
|
+
warnings: string[];
|
|
79
|
+
}>;
|
|
80
|
+
answers: {
|
|
81
|
+
canPublish: boolean;
|
|
82
|
+
ifNotWhatIsMissing: string[];
|
|
83
|
+
nextCommand: string | null;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readGeneratedJson<T>(workspaceRoot: string, relative: string): T | null {
|
|
88
|
+
const absolute = join(workspaceRoot, relative);
|
|
89
|
+
if (!nodeFileSystem.exists(absolute)) return null;
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(stripDeterministicHeader(nodeFileSystem.readText(absolute) ?? "")) as T;
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function packageJson(workspaceRoot: string): Record<string, unknown> {
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(nodeFileSystem.readText(join(workspaceRoot, "package.json")) ?? "{}") as Record<string, unknown>;
|
|
100
|
+
} catch {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function hasScript(workspaceRoot: string, name: string): boolean {
|
|
106
|
+
const scripts = packageJson(workspaceRoot).scripts;
|
|
107
|
+
return Boolean(scripts && typeof scripts === "object" && name in scripts);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function requiredSecretNames(workspaceRoot: string): string[] {
|
|
111
|
+
return (loadSecretRegistry(workspaceRoot)?.secrets ?? [])
|
|
112
|
+
.map((secret) => secret.name)
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.sort();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function unique(values: string[]): string[] {
|
|
118
|
+
return [...new Set(values)];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function deployStageForCheck(name: string): string {
|
|
122
|
+
if (["generated", "package-lockfile", "frontend-build-script"].includes(name)) return "build";
|
|
123
|
+
if (["database-url"].includes(name)) return "database";
|
|
124
|
+
if (name.startsWith("auth-") || ["production-auth-mode", "auth-issuer", "auth-audience", "auth-jwks", "tenant-claim"].includes(name)) return "auth";
|
|
125
|
+
if (name.startsWith("workos")) return "workos";
|
|
126
|
+
if (name.startsWith("auth-md") || name === "oauth-protected-resource") return "auth metadata";
|
|
127
|
+
if (name.startsWith("field-test")) return "field test";
|
|
128
|
+
if (name === "live-production") return "live/runtime";
|
|
129
|
+
if (name === "deploy-env-sources" || name === "production-env-values") return "environment";
|
|
130
|
+
return "production";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function summarizeDeployReadiness(checks: DeployCheck[], nextActions: string[]): DeployReadinessSummary {
|
|
134
|
+
const blocking = checks
|
|
135
|
+
.filter((check) => !check.ok && check.severity === "error")
|
|
136
|
+
.map((check) => `${check.name}: ${check.message}`);
|
|
137
|
+
const warnings = checks
|
|
138
|
+
.filter((check) => !check.ok && check.severity === "warning")
|
|
139
|
+
.map((check) => `${check.name}: ${check.message}`);
|
|
140
|
+
const stageNames = [
|
|
141
|
+
"build",
|
|
142
|
+
"environment",
|
|
143
|
+
"database",
|
|
144
|
+
"auth",
|
|
145
|
+
"workos",
|
|
146
|
+
"auth metadata",
|
|
147
|
+
"field test",
|
|
148
|
+
"live/runtime",
|
|
149
|
+
"production",
|
|
150
|
+
];
|
|
151
|
+
const stages = stageNames
|
|
152
|
+
.map((stage) => {
|
|
153
|
+
const stageChecks = checks.filter((check) => deployStageForCheck(check.name) === stage);
|
|
154
|
+
const stageBlocking = stageChecks
|
|
155
|
+
.filter((check) => !check.ok && check.severity === "error")
|
|
156
|
+
.map((check) => `${check.name}: ${check.message}`);
|
|
157
|
+
const stageWarnings = stageChecks
|
|
158
|
+
.filter((check) => !check.ok && check.severity === "warning")
|
|
159
|
+
.map((check) => `${check.name}: ${check.message}`);
|
|
160
|
+
const passed = stageChecks.filter((check) => check.ok || check.severity === "warning").length;
|
|
161
|
+
const score = stageChecks.length === 0 ? 100 : Math.round((passed / stageChecks.length) * 100);
|
|
162
|
+
return {
|
|
163
|
+
name: stage,
|
|
164
|
+
ok: stageBlocking.length === 0,
|
|
165
|
+
score,
|
|
166
|
+
checks: stageChecks.map((check) => check.name),
|
|
167
|
+
blocking: stageBlocking,
|
|
168
|
+
warnings: stageWarnings,
|
|
169
|
+
};
|
|
170
|
+
})
|
|
171
|
+
.filter((stage) => stage.checks.length > 0);
|
|
172
|
+
const passed = checks.filter((check) => check.ok || check.severity === "warning").length;
|
|
173
|
+
const score = checks.length === 0 ? 100 : Math.round((passed / checks.length) * 100);
|
|
174
|
+
const canPublish = blocking.length === 0;
|
|
175
|
+
return {
|
|
176
|
+
score,
|
|
177
|
+
status: blocking.length > 0 ? "blocked" : warnings.length > 0 ? "warning" : "ready",
|
|
178
|
+
blocking,
|
|
179
|
+
warnings,
|
|
180
|
+
stages,
|
|
181
|
+
answers: {
|
|
182
|
+
canPublish,
|
|
183
|
+
ifNotWhatIsMissing: blocking,
|
|
184
|
+
nextCommand: canPublish ? "forge deploy verify --production --url https://app.example.com --json" : nextActions[0] ?? null,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function hasWorkOSIntegration(workspaceRoot: string): boolean {
|
|
190
|
+
const secrets = new Set(requiredSecretNames(workspaceRoot));
|
|
191
|
+
return secrets.has("WORKOS_API_KEY") ||
|
|
192
|
+
secrets.has("WORKOS_CLIENT_ID") ||
|
|
193
|
+
nodeFileSystem.exists(join(workspaceRoot, `${GENERATED_DIR}/integrations/workos`)) ||
|
|
194
|
+
nodeFileSystem.exists(join(workspaceRoot, "src/policies.workos.ts"));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function hasWorkOSFgaIntegration(workspaceRoot: string): boolean {
|
|
198
|
+
return nodeFileSystem.exists(join(workspaceRoot, `${GENERATED_DIR}/integrations/workos/fga.ts`)) ||
|
|
199
|
+
nodeFileSystem.exists(join(workspaceRoot, `${GENERATED_DIR}/integrations/workos/resource-map.ts`)) ||
|
|
200
|
+
nodeFileSystem.exists(join(workspaceRoot, ".workos-fga-state.json"));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function readJsonFile<T>(workspaceRoot: string, relative: string): T | null {
|
|
204
|
+
const absolute = join(workspaceRoot, relative);
|
|
205
|
+
if (!nodeFileSystem.exists(absolute)) return null;
|
|
206
|
+
try {
|
|
207
|
+
return JSON.parse(nodeFileSystem.readText(absolute) ?? "null") as T;
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function fieldTestReportCandidates(workspaceRoot: string): Array<{ path: string; data: Record<string, unknown> }> {
|
|
214
|
+
const paths = [".forge/field-test-report.json", "field-reports/full-alpha.json"];
|
|
215
|
+
return paths.flatMap((path) => {
|
|
216
|
+
const data = readJsonFile<Record<string, unknown>>(workspaceRoot, path);
|
|
217
|
+
return data ? [{ path, data }] : [];
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const FIELD_TEST_PRODUCTION_COMMAND = "forge field-test run --realistic --json";
|
|
222
|
+
const DEPLOY_ENV_FILE = "deploy/.env.production";
|
|
223
|
+
const LOCAL_ENV_FILES = [".env", ".env.local"];
|
|
224
|
+
const DEPLOY_ENV_KEYS = [
|
|
225
|
+
"DATABASE_URL",
|
|
226
|
+
"FORGE_AUTH_MODE",
|
|
227
|
+
"FORGE_AUTH_ISSUER",
|
|
228
|
+
"FORGE_AUTH_AUDIENCE",
|
|
229
|
+
"FORGE_AUTH_JWKS_URI",
|
|
230
|
+
"FORGE_AUTH_DISCOVERY_URL",
|
|
231
|
+
"WORKOS_API_KEY",
|
|
232
|
+
"WORKOS_CLIENT_ID",
|
|
233
|
+
"WORKOS_COOKIE_PASSWORD",
|
|
234
|
+
"WORKOS_REDIRECT_URI",
|
|
235
|
+
"WORKOS_POST_LOGIN_REDIRECT_URI",
|
|
236
|
+
"WORKOS_POST_LOGOUT_REDIRECT_URI",
|
|
237
|
+
"WORKOS_WEBHOOK_SECRET",
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
function summarizeFieldTestReport(data: Record<string, unknown>) {
|
|
241
|
+
const results = Array.isArray(data.results) ? data.results as Array<Record<string, unknown>> : [];
|
|
242
|
+
const failed = results.filter((result) => result.ok === false && result.skipped !== true);
|
|
243
|
+
const skipped = results.filter((result) => result.skipped === true);
|
|
244
|
+
const executed = results.filter((result) => result.skipped !== true);
|
|
245
|
+
const topLevelSteps = results.flatMap((result) =>
|
|
246
|
+
Array.isArray(result.steps) ? result.steps as Array<Record<string, unknown>> : [],
|
|
247
|
+
);
|
|
248
|
+
const runtimeSteps = results.flatMap((result) => {
|
|
249
|
+
const runtime = result.runtime && typeof result.runtime === "object"
|
|
250
|
+
? result.runtime as Record<string, unknown>
|
|
251
|
+
: {};
|
|
252
|
+
return Array.isArray(runtime.steps) ? runtime.steps as Array<Record<string, unknown>> : [];
|
|
253
|
+
});
|
|
254
|
+
const commandOf = (step: unknown): string =>
|
|
255
|
+
step && typeof step === "object" && typeof (step as { command?: unknown }).command === "string"
|
|
256
|
+
? (step as { command: string }).command
|
|
257
|
+
: "";
|
|
258
|
+
const okStep = (step: unknown): boolean =>
|
|
259
|
+
Boolean(step && typeof step === "object" && (step as { ok?: unknown }).ok === true);
|
|
260
|
+
const hasOkRuntimeCommand = (pattern: RegExp): boolean =>
|
|
261
|
+
runtimeSteps.some((step) => okStep(step) && pattern.test(commandOf(step)));
|
|
262
|
+
const hasOkTopLevelCommand = (pattern: RegExp): boolean =>
|
|
263
|
+
topLevelSteps.some((step) => okStep(step) && pattern.test(commandOf(step)));
|
|
264
|
+
const authSetupProbeSteps = topLevelSteps.filter((step) =>
|
|
265
|
+
/forge\s+(?:--\s+)?(add\s+auth\s+workos|authmd\s+generate|authmd\s+check|workos\s+doctor|workos\s+seed|workos\s+prove|auth\s+prove)/.test(commandOf(step)),
|
|
266
|
+
);
|
|
267
|
+
const authMetadataProbeSteps = runtimeSteps.filter((step) =>
|
|
268
|
+
/^(HEAD|GET)\s+https?:\/\/[^/]+\/(auth\.md|\.well-known\/oauth-protected-resource)\b/i.test(commandOf(step)),
|
|
269
|
+
);
|
|
270
|
+
const uiErgonomicsResults = results
|
|
271
|
+
.map((result) => result.uiErgonomics)
|
|
272
|
+
.filter((value): value is Record<string, unknown> => Boolean(value && typeof value === "object"));
|
|
273
|
+
const uiErgonomicsWarnings = uiErgonomicsResults.reduce((total, result) =>
|
|
274
|
+
total + (typeof result.warnings === "number" ? result.warnings : 0), 0);
|
|
275
|
+
const uiErgonomicsErrors = uiErgonomicsResults.reduce((total, result) =>
|
|
276
|
+
total + (typeof result.errors === "number" ? result.errors : 0), 0);
|
|
277
|
+
const runtimeProbes = data.runtimeProbes === true;
|
|
278
|
+
const authProbes = data.authProbes === true;
|
|
279
|
+
const uiProbes = data.uiProbes === true;
|
|
280
|
+
const uiProbeSteps = runtimeSteps.filter((step) =>
|
|
281
|
+
/^GET\s+https?:\/\/[^/]+\/$/i.test(commandOf(step)),
|
|
282
|
+
);
|
|
283
|
+
const uiErgonomics = uiProbes !== true || (executed.length > 0 && uiErgonomicsResults.length === executed.length);
|
|
284
|
+
const runtimeHealthEvidence = !runtimeProbes || hasOkRuntimeCommand(/\bGET\s+.*\/health\b/i);
|
|
285
|
+
const runtimeEntriesEvidence = !runtimeProbes || hasOkRuntimeCommand(/\bGET\s+.*\/entries\b/i);
|
|
286
|
+
const authSetupEvidence = !authProbes || [
|
|
287
|
+
/forge\s+(?:--\s+)?add\s+auth\s+workos\b/,
|
|
288
|
+
/forge\s+(?:--\s+)?authmd\s+generate\b/,
|
|
289
|
+
/forge\s+(?:--\s+)?authmd\s+check\b/,
|
|
290
|
+
/forge\s+(?:--\s+)?workos\s+doctor\b/,
|
|
291
|
+
/forge\s+(?:--\s+)?workos\s+seed\b/,
|
|
292
|
+
/forge\s+(?:--\s+)?workos\s+prove\b/,
|
|
293
|
+
/forge\s+(?:--\s+)?auth\s+prove\b/,
|
|
294
|
+
].every((pattern) => hasOkTopLevelCommand(pattern));
|
|
295
|
+
const authMetadataEvidence = !authProbes || [
|
|
296
|
+
/^HEAD\s+.*\/auth\.md\b/i,
|
|
297
|
+
/^GET\s+.*\/auth\.md\b/i,
|
|
298
|
+
/^HEAD\s+.*\/\.well-known\/oauth-protected-resource\b/i,
|
|
299
|
+
/^GET\s+.*\/\.well-known\/oauth-protected-resource\b/i,
|
|
300
|
+
].every((pattern) => hasOkRuntimeCommand(pattern));
|
|
301
|
+
const uiProbeEvidence = !uiProbes || uiProbeSteps.some((step) => okStep(step));
|
|
302
|
+
const productionEvidenceMissing = [
|
|
303
|
+
...(data.ok !== true ? ["passing field-test report"] : []),
|
|
304
|
+
...(!runtimeProbes ? ["runtime probes"] : []),
|
|
305
|
+
...(!authProbes ? ["auth probes"] : []),
|
|
306
|
+
...(!uiProbes ? ["ui probes"] : []),
|
|
307
|
+
...(!runtimeHealthEvidence ? ["runtime health probe"] : []),
|
|
308
|
+
...(!runtimeEntriesEvidence ? ["runtime entries probe"] : []),
|
|
309
|
+
...(!authSetupEvidence ? ["auth setup probes"] : []),
|
|
310
|
+
...(!authMetadataEvidence ? ["auth metadata endpoint probes"] : []),
|
|
311
|
+
...(!uiProbeEvidence ? ["web UI probe"] : []),
|
|
312
|
+
...(!uiErgonomics ? ["UI ergonomics audit"] : []),
|
|
313
|
+
...(uiErgonomicsErrors > 0 ? ["zero UI ergonomics errors"] : []),
|
|
314
|
+
...(failed.length > 0 ? ["zero failed cases"] : []),
|
|
315
|
+
];
|
|
316
|
+
return {
|
|
317
|
+
ok: data.ok === true,
|
|
318
|
+
cases: results.length,
|
|
319
|
+
passed: results.filter((result) => result.ok === true && result.skipped !== true).length,
|
|
320
|
+
failed: failed.length,
|
|
321
|
+
skipped: skipped.length,
|
|
322
|
+
runtimeProbes,
|
|
323
|
+
authProbes,
|
|
324
|
+
uiProbes,
|
|
325
|
+
uiErgonomics,
|
|
326
|
+
uiErgonomicsWarnings,
|
|
327
|
+
uiErgonomicsErrors,
|
|
328
|
+
runtimeProbeSteps: runtimeSteps.length,
|
|
329
|
+
authSetupProbeSteps: authSetupProbeSteps.length,
|
|
330
|
+
authMetadataProbeSteps: authMetadataProbeSteps.length,
|
|
331
|
+
uiProbeSteps: uiProbeSteps.length,
|
|
332
|
+
productionEvidence: {
|
|
333
|
+
readyForDeployCheck: productionEvidenceMissing.length === 0,
|
|
334
|
+
missing: productionEvidenceMissing,
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function shellCommand(command: string): string {
|
|
340
|
+
return `["sh", "-lc", ${JSON.stringify(command)}]`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function runPackageScript(packageManager: PackageManager, script: string): string {
|
|
344
|
+
switch (packageManager) {
|
|
345
|
+
case "npm":
|
|
346
|
+
return `npm run ${script}`;
|
|
347
|
+
case "pnpm":
|
|
348
|
+
return `pnpm run ${script}`;
|
|
349
|
+
case "yarn":
|
|
350
|
+
return `yarn run ${script}`;
|
|
351
|
+
case "bun":
|
|
352
|
+
return `bun run ${script}`;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function runForgeScript(packageManager: PackageManager, args: string): string {
|
|
357
|
+
switch (packageManager) {
|
|
358
|
+
case "npm":
|
|
359
|
+
return `npm run forge -- ${args}`;
|
|
360
|
+
case "pnpm":
|
|
361
|
+
return `pnpm run forge -- ${args}`;
|
|
362
|
+
case "yarn":
|
|
363
|
+
return `yarn run forge ${args}`;
|
|
364
|
+
case "bun":
|
|
365
|
+
return `bun run forge -- ${args}`;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function packageManagerSpec(workspaceRoot: string, packageManager: PackageManager): string {
|
|
370
|
+
try {
|
|
371
|
+
const pkg = JSON.parse(nodeFileSystem.readText(join(workspaceRoot, "package.json")) ?? "{}") as { packageManager?: string };
|
|
372
|
+
if (pkg.packageManager?.startsWith(`${packageManager}@`)) return pkg.packageManager;
|
|
373
|
+
} catch {
|
|
374
|
+
// fall through to package manager name
|
|
375
|
+
}
|
|
376
|
+
return packageManager;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function dockerPackageCopyLine(workspaceRoot: string, packageManager: PackageManager): string {
|
|
380
|
+
const files = [
|
|
381
|
+
"package.json",
|
|
382
|
+
...activeLockfiles(workspaceRoot, packageManager),
|
|
383
|
+
];
|
|
384
|
+
return `COPY ${files.join(" ")} ./`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function activeLockfiles(workspaceRoot: string, packageManager = detectPackageManager(workspaceRoot)): string[] {
|
|
388
|
+
return getLockfileCandidates(packageManager).filter((file) => nodeFileSystem.exists(join(workspaceRoot, file)));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function packageInstallCommand(packageManager: PackageManager): string {
|
|
392
|
+
switch (packageManager) {
|
|
393
|
+
case "npm":
|
|
394
|
+
return "npm install";
|
|
395
|
+
case "pnpm":
|
|
396
|
+
return "pnpm install";
|
|
397
|
+
case "yarn":
|
|
398
|
+
return "yarn install";
|
|
399
|
+
case "bun":
|
|
400
|
+
return "bun install";
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function dockerPackageSetup(workspaceRoot: string, packageManager: PackageManager): string[] {
|
|
405
|
+
if (packageManager === "npm") return [];
|
|
406
|
+
if (packageManager === "bun") {
|
|
407
|
+
return [`RUN npm install -g ${packageManagerSpec(workspaceRoot, packageManager)}`];
|
|
408
|
+
}
|
|
409
|
+
return ["RUN corepack enable"];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function dockerInstallCommand(workspaceRoot: string, packageManager: PackageManager): string {
|
|
413
|
+
const hasLockfile = activeLockfiles(workspaceRoot, packageManager).length > 0;
|
|
414
|
+
switch (packageManager) {
|
|
415
|
+
case "npm":
|
|
416
|
+
return hasLockfile ? "RUN npm ci" : "RUN npm install";
|
|
417
|
+
case "pnpm":
|
|
418
|
+
return hasLockfile ? "RUN pnpm install --frozen-lockfile" : "RUN pnpm install";
|
|
419
|
+
case "yarn":
|
|
420
|
+
return hasLockfile ? "RUN yarn install --immutable || yarn install --frozen-lockfile" : "RUN yarn install";
|
|
421
|
+
case "bun":
|
|
422
|
+
return hasLockfile ? "RUN bun install --frozen-lockfile" : "RUN bun install";
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function dockerRunIfScriptPresent(packageManager: PackageManager, script: string): string {
|
|
427
|
+
return `RUN if node -e "process.exit(require('./package.json').scripts?.[${JSON.stringify(script)}] ? 0 : 1)"; then ${runPackageScript(packageManager, script)}; fi`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function renderDockerCompose(workspaceRoot: string): string {
|
|
431
|
+
const packageManager = detectPackageManager(workspaceRoot);
|
|
432
|
+
return `services:
|
|
433
|
+
postgres:
|
|
434
|
+
image: postgres:16
|
|
435
|
+
environment:
|
|
436
|
+
POSTGRES_USER: forge
|
|
437
|
+
POSTGRES_PASSWORD: forge
|
|
438
|
+
POSTGRES_DB: forge_app
|
|
439
|
+
volumes:
|
|
440
|
+
- postgres_data:/var/lib/postgresql/data
|
|
441
|
+
healthcheck:
|
|
442
|
+
test: ["CMD-SHELL", "pg_isready -U forge -d forge_app"]
|
|
443
|
+
interval: 5s
|
|
444
|
+
timeout: 5s
|
|
445
|
+
retries: 10
|
|
446
|
+
|
|
447
|
+
forge-migrate:
|
|
448
|
+
build:
|
|
449
|
+
context: ..
|
|
450
|
+
dockerfile: deploy/Dockerfile.runtime
|
|
451
|
+
command: ${shellCommand(runForgeScript(packageManager, "db migrate --db postgres"))}
|
|
452
|
+
env_file:
|
|
453
|
+
- .env.production
|
|
454
|
+
depends_on:
|
|
455
|
+
postgres:
|
|
456
|
+
condition: service_healthy
|
|
457
|
+
|
|
458
|
+
forge-runtime:
|
|
459
|
+
build:
|
|
460
|
+
context: ..
|
|
461
|
+
dockerfile: deploy/Dockerfile.runtime
|
|
462
|
+
command: ${shellCommand(runForgeScript(packageManager, "serve --host 0.0.0.0 --port 3765"))}
|
|
463
|
+
env_file:
|
|
464
|
+
- .env.production
|
|
465
|
+
environment:
|
|
466
|
+
FORGE_ENV: production
|
|
467
|
+
FORGE_DEPLOY_ENV: production
|
|
468
|
+
FORGE_LIVE_TRANSPORT: sse
|
|
469
|
+
FORGE_LIVE_INVALIDATION: polling,postgres-notify
|
|
470
|
+
depends_on:
|
|
471
|
+
postgres:
|
|
472
|
+
condition: service_healthy
|
|
473
|
+
forge-migrate:
|
|
474
|
+
condition: service_completed_successfully
|
|
475
|
+
ports:
|
|
476
|
+
- "3765:3765"
|
|
477
|
+
|
|
478
|
+
forge-worker:
|
|
479
|
+
build:
|
|
480
|
+
context: ..
|
|
481
|
+
dockerfile: deploy/Dockerfile.runtime
|
|
482
|
+
command: ${shellCommand(runForgeScript(packageManager, "worker --db postgres"))}
|
|
483
|
+
env_file:
|
|
484
|
+
- .env.production
|
|
485
|
+
environment:
|
|
486
|
+
FORGE_ENV: production
|
|
487
|
+
FORGE_DEPLOY_ENV: production
|
|
488
|
+
depends_on:
|
|
489
|
+
postgres:
|
|
490
|
+
condition: service_healthy
|
|
491
|
+
forge-migrate:
|
|
492
|
+
condition: service_completed_successfully
|
|
493
|
+
|
|
494
|
+
volumes:
|
|
495
|
+
postgres_data:
|
|
496
|
+
`;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function renderRuntimeDockerfile(workspaceRoot: string): string {
|
|
500
|
+
const packageManager = detectPackageManager(workspaceRoot);
|
|
501
|
+
return `FROM node:22-slim AS deps
|
|
502
|
+
WORKDIR /app
|
|
503
|
+
${dockerPackageCopyLine(workspaceRoot, packageManager)}
|
|
504
|
+
${[...dockerPackageSetup(workspaceRoot, packageManager), dockerInstallCommand(workspaceRoot, packageManager)].join("\n")}
|
|
505
|
+
|
|
506
|
+
FROM deps AS build
|
|
507
|
+
COPY . .
|
|
508
|
+
RUN ${runForgeScript(packageManager, "generate")}
|
|
509
|
+
${dockerRunIfScriptPresent(packageManager, "typecheck")}
|
|
510
|
+
|
|
511
|
+
FROM node:22-slim AS runtime
|
|
512
|
+
WORKDIR /app
|
|
513
|
+
ENV NODE_ENV=production
|
|
514
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
515
|
+
COPY --from=build /app ./
|
|
516
|
+
EXPOSE 3765
|
|
517
|
+
CMD ${shellCommand(runForgeScript(packageManager, "serve --host 0.0.0.0 --port 3765"))}
|
|
518
|
+
`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function renderEnvExample(workspaceRoot: string): string {
|
|
522
|
+
const secrets = requiredSecretNames(workspaceRoot);
|
|
523
|
+
const base = [
|
|
524
|
+
"DATABASE_URL=postgres://forge:forge@postgres:5432/forge_app",
|
|
525
|
+
"FORGE_ENV=production",
|
|
526
|
+
"FORGE_DEPLOY_ENV=production",
|
|
527
|
+
"FORGE_PORT=3765",
|
|
528
|
+
"FORGE_AUTH_MODE=oidc",
|
|
529
|
+
"FORGE_AUTH_ISSUER=",
|
|
530
|
+
"FORGE_AUTH_AUDIENCE=",
|
|
531
|
+
"FORGE_AUTH_JWKS_URI=",
|
|
532
|
+
"FORGE_AUTH_ALGORITHMS=RS256",
|
|
533
|
+
"FORGE_LIVE_TRANSPORT=sse",
|
|
534
|
+
"FORGE_LIVE_INVALIDATION=polling,postgres-notify",
|
|
535
|
+
"FORGE_LIVE_POLL_INTERVAL_MS=1000",
|
|
536
|
+
"FORGE_LIVE_HEARTBEAT_MS=15000",
|
|
537
|
+
"FORGE_CORS_ORIGINS=https://app.example.com",
|
|
538
|
+
];
|
|
539
|
+
const present = new Set(base.map((line) => line.split("=")[0]));
|
|
540
|
+
for (const secret of secrets) {
|
|
541
|
+
if (!present.has(secret)) base.push(`${secret}=`);
|
|
542
|
+
}
|
|
543
|
+
return `${base.join("\n")}\n`;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function hasProductionEnvFile(workspaceRoot: string): boolean {
|
|
547
|
+
return nodeFileSystem.exists(join(workspaceRoot, DEPLOY_ENV_FILE));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function parseEnvValue(value: string): string {
|
|
551
|
+
const trimmed = value.trim();
|
|
552
|
+
if (
|
|
553
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
554
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
555
|
+
) {
|
|
556
|
+
return trimmed.slice(1, -1);
|
|
557
|
+
}
|
|
558
|
+
return trimmed;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function readEnvFile(workspaceRoot: string, relative: string): Record<string, string> {
|
|
562
|
+
const absolute = join(workspaceRoot, relative);
|
|
563
|
+
if (!nodeFileSystem.exists(absolute)) return {};
|
|
564
|
+
const text = nodeFileSystem.readText(absolute) ?? "";
|
|
565
|
+
const values: Record<string, string> = {};
|
|
566
|
+
for (const line of text.split(/\r?\n/)) {
|
|
567
|
+
const trimmed = line.trim();
|
|
568
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
569
|
+
const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
570
|
+
if (!match) continue;
|
|
571
|
+
values[match[1]!] = parseEnvValue(match[2] ?? "");
|
|
572
|
+
}
|
|
573
|
+
return values;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function readDeployEnvFile(workspaceRoot: string): Record<string, string> {
|
|
577
|
+
return readEnvFile(workspaceRoot, DEPLOY_ENV_FILE);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function envKeys(values: Record<string, string>): string[] {
|
|
581
|
+
return DEPLOY_ENV_KEYS.filter((key) => Boolean(values[key])).sort();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function processDeployEnv(): Record<string, string> {
|
|
585
|
+
return Object.fromEntries(
|
|
586
|
+
DEPLOY_ENV_KEYS
|
|
587
|
+
.filter((key) => Boolean(process.env[key]))
|
|
588
|
+
.map((key) => [key, process.env[key]!]),
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function productionEnvValues(workspaceRoot: string): Record<string, string> {
|
|
593
|
+
return {
|
|
594
|
+
...readDeployEnvFile(workspaceRoot),
|
|
595
|
+
...processDeployEnv(),
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function deployEnvSources(workspaceRoot: string): Record<string, unknown> {
|
|
600
|
+
const processEnv = processDeployEnv();
|
|
601
|
+
const deployEnv = readDeployEnvFile(workspaceRoot);
|
|
602
|
+
const localSources = LOCAL_ENV_FILES.map((path) => {
|
|
603
|
+
const values = readEnvFile(workspaceRoot, path);
|
|
604
|
+
return {
|
|
605
|
+
path,
|
|
606
|
+
present: nodeFileSystem.exists(join(workspaceRoot, path)),
|
|
607
|
+
role: "local-guidance",
|
|
608
|
+
readForProduction: false,
|
|
609
|
+
keys: envKeys(values),
|
|
610
|
+
};
|
|
611
|
+
});
|
|
612
|
+
const productionEvidence = productionEnvValues(workspaceRoot);
|
|
613
|
+
return {
|
|
614
|
+
readOrder: ["process.env", DEPLOY_ENV_FILE],
|
|
615
|
+
note: `${DEPLOY_ENV_FILE} and current process env are production deploy evidence; .env/.env.local are reported only as local guidance and are not used to pass production gates.`,
|
|
616
|
+
sources: [
|
|
617
|
+
{
|
|
618
|
+
path: "process.env",
|
|
619
|
+
present: envKeys(processEnv).length > 0,
|
|
620
|
+
role: "production-evidence",
|
|
621
|
+
readForProduction: true,
|
|
622
|
+
keys: envKeys(processEnv),
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
path: DEPLOY_ENV_FILE,
|
|
626
|
+
present: hasProductionEnvFile(workspaceRoot),
|
|
627
|
+
role: "production-evidence",
|
|
628
|
+
readForProduction: true,
|
|
629
|
+
keys: envKeys(deployEnv),
|
|
630
|
+
},
|
|
631
|
+
...localSources,
|
|
632
|
+
],
|
|
633
|
+
missingProductionKeys: DEPLOY_ENV_KEYS.filter((key) => !productionEvidence[key]),
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function deployEnvValue(workspaceRoot: string, name: string): string | undefined {
|
|
638
|
+
return process.env[name] || readDeployEnvFile(workspaceRoot)[name];
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function withDeployEnv<T>(workspaceRoot: string, fn: () => Promise<T>): Promise<T> {
|
|
642
|
+
const deployEnv = readDeployEnvFile(workspaceRoot);
|
|
643
|
+
const previous = new Map<string, string | undefined>();
|
|
644
|
+
for (const [name, value] of Object.entries(deployEnv)) {
|
|
645
|
+
if (process.env[name]) continue;
|
|
646
|
+
previous.set(name, process.env[name]);
|
|
647
|
+
process.env[name] = value;
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
return await fn();
|
|
651
|
+
} finally {
|
|
652
|
+
for (const [name, value] of previous) {
|
|
653
|
+
if (value === undefined) {
|
|
654
|
+
delete process.env[name];
|
|
655
|
+
} else {
|
|
656
|
+
process.env[name] = value;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function hasRenderedProductionEnvExample(workspaceRoot: string): boolean {
|
|
663
|
+
return nodeFileSystem.exists(join(workspaceRoot, "deploy/.env.production.example"));
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function databaseReady(options: DeployCommandOptions): boolean {
|
|
667
|
+
if (deployEnvValue(options.workspaceRoot, "DATABASE_URL")) return true;
|
|
668
|
+
if (!options.production) return hasRenderedProductionEnvExample(options.workspaceRoot);
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function databaseReadyMessage(options: DeployCommandOptions): string {
|
|
673
|
+
if (process.env.DATABASE_URL) return "DATABASE_URL is set in the current environment";
|
|
674
|
+
if (readDeployEnvFile(options.workspaceRoot).DATABASE_URL) return `DATABASE_URL is set in ${DEPLOY_ENV_FILE}`;
|
|
675
|
+
if (hasProductionEnvFile(options.workspaceRoot)) return `${DEPLOY_ENV_FILE} is present but does not set DATABASE_URL`;
|
|
676
|
+
if (hasRenderedProductionEnvExample(options.workspaceRoot)) {
|
|
677
|
+
return options.production
|
|
678
|
+
? `deploy/.env.production.example is only a template; copy it to ${DEPLOY_ENV_FILE} with DATABASE_URL or set DATABASE_URL`
|
|
679
|
+
: "deploy/.env.production.example is present";
|
|
680
|
+
}
|
|
681
|
+
return options.production
|
|
682
|
+
? `production deploy requires DATABASE_URL or ${DEPLOY_ENV_FILE}`
|
|
683
|
+
: "deploy readiness requires DATABASE_URL or rendered deploy/.env.production.example";
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function databaseReadyCommand(options: DeployCommandOptions): string {
|
|
687
|
+
if (hasRenderedProductionEnvExample(options.workspaceRoot)) {
|
|
688
|
+
return "cp deploy/.env.production.example deploy/.env.production";
|
|
689
|
+
}
|
|
690
|
+
return "forge deploy package --target docker";
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function renderProductionReadme(): string {
|
|
694
|
+
return `# ForgeOS Production Deploy
|
|
695
|
+
|
|
696
|
+
This directory is generated by \`forge deploy init --target docker\` or \`forge deploy package --target docker\`.
|
|
697
|
+
|
|
698
|
+
Start with the golden-path status gate when you are unsure what comes next:
|
|
699
|
+
|
|
700
|
+
\`\`\`bash
|
|
701
|
+
forge golden-path status --real --production --json
|
|
702
|
+
\`\`\`
|
|
703
|
+
|
|
704
|
+
It reads WorkOS hosted setup evidence, field-test evidence, and deploy readiness
|
|
705
|
+
without hiding the lower-level commands. If it cannot publish, it returns the
|
|
706
|
+
current blocking stage and the next command to run.
|
|
707
|
+
|
|
708
|
+
## 1. Prepare production env
|
|
709
|
+
|
|
710
|
+
\`\`\`bash
|
|
711
|
+
cp deploy/.env.production.example deploy/.env.production
|
|
712
|
+
\`\`\`
|
|
713
|
+
|
|
714
|
+
Fill \`deploy/.env.production\` with real values before public traffic. \`forge deploy check --production\` reads this file as deploy evidence, so you do not need to export every value into the shell. Do not commit real secrets.
|
|
715
|
+
|
|
716
|
+
The generated Docker Compose stack also reads \`deploy/.env.production\` through \`env_file\`. It does not inject a hidden \`DATABASE_URL\` override into the Forge runtime, migrate, or worker services.
|
|
717
|
+
|
|
718
|
+
Required production posture:
|
|
719
|
+
|
|
720
|
+
- \`FORGE_AUTH_MODE=jwt\` or \`FORGE_AUTH_MODE=oidc\`
|
|
721
|
+
- \`DATABASE_URL\` points at a production Postgres database
|
|
722
|
+
- issuer, audience, and JWKS/discovery settings match the auth provider
|
|
723
|
+
- any provider secrets listed in the example are set in the deployment secret store
|
|
724
|
+
|
|
725
|
+
For WorkOS-backed apps, local dry-runs are not enough for production deploy
|
|
726
|
+
readiness. Run:
|
|
727
|
+
|
|
728
|
+
\`\`\`bash
|
|
729
|
+
forge workos setup --real --file workos-seed.yml --json
|
|
730
|
+
forge workos prove --real --file workos-seed.yml --json
|
|
731
|
+
\`\`\`
|
|
732
|
+
|
|
733
|
+
This applies or confirms hosted redirect/CORS/webhook config and seed data
|
|
734
|
+
through the WorkOS CLI and writes \`.workos-seed-state.json\`. \`forge deploy
|
|
735
|
+
check --production\` requires that state to match the current \`workos-seed.yml\`
|
|
736
|
+
before treating WorkOS posture as production evidence. WorkOS FGA is optional:
|
|
737
|
+
most apps can ship with AuthKit, RBAC permission claims, Forge policies, and
|
|
738
|
+
tenant isolation; use \`forge add auth workos --with-fga\` only when the app needs
|
|
739
|
+
resource-level authorization outside normal role/permission checks.
|
|
740
|
+
|
|
741
|
+
## 2. Prove the app before traffic
|
|
742
|
+
|
|
743
|
+
\`\`\`bash
|
|
744
|
+
forge generate --check --json
|
|
745
|
+
forge check --json
|
|
746
|
+
forge authmd generate --json
|
|
747
|
+
forge env doctor --target production --json
|
|
748
|
+
forge auth prove --scenario multi-tenant --json
|
|
749
|
+
forge workos setup --real --file workos-seed.yml --json
|
|
750
|
+
forge workos prove --real --file workos-seed.yml --json
|
|
751
|
+
forge field-test run --realistic --json
|
|
752
|
+
forge deploy readiness --production --json
|
|
753
|
+
forge deploy check --production --json
|
|
754
|
+
\`\`\`
|
|
755
|
+
|
|
756
|
+
\`forge deploy check --production\` requires real deploy env evidence. A template file alone is not enough.
|
|
757
|
+
|
|
758
|
+
## 3. Run Docker
|
|
759
|
+
|
|
760
|
+
\`\`\`bash
|
|
761
|
+
docker compose -f deploy/docker-compose.yml up --build
|
|
762
|
+
\`\`\`
|
|
763
|
+
|
|
764
|
+
## 4. Verify the public runtime
|
|
765
|
+
|
|
766
|
+
\`\`\`bash
|
|
767
|
+
forge auth prove --prod --token <jwt> --json
|
|
768
|
+
forge deploy verify --production --url https://app.example.com --json
|
|
769
|
+
\`\`\`
|
|
770
|
+
|
|
771
|
+
\`forge auth prove --prod\` verifies a real JWT/OIDC token against the configured issuer/audience/JWKS. \`forge deploy verify --production\` probes required endpoints \`GET /health\`, \`HEAD /auth.md\`, \`GET /auth.md\`, \`HEAD /.well-known/oauth-protected-resource\`, and \`GET /.well-known/oauth-protected-resource\`; it also validates auth metadata content-types and requires the OAuth protected-resource metadata body to be valid JSON. Optional probes check \`GET /ready\`, \`GET /live/status\`, \`GET /outbox/status\`, and \`HEAD /webhooks/workos\` when the app exposes those operational endpoints.
|
|
772
|
+
`;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function buildPlan(options: DeployCommandOptions): DeployCommandResult {
|
|
776
|
+
const commands = options.target === "forge-cloud"
|
|
777
|
+
? [
|
|
778
|
+
"forge golden-path status --real --production --json",
|
|
779
|
+
"forge deploy check --production --json",
|
|
780
|
+
"forge auth check --production --json",
|
|
781
|
+
"forge authmd check --json",
|
|
782
|
+
FIELD_TEST_PRODUCTION_COMMAND,
|
|
783
|
+
"forge field-test report --json",
|
|
784
|
+
"forge deploy verify --production --url https://<your-forge-cloud-app> --json",
|
|
785
|
+
]
|
|
786
|
+
: [
|
|
787
|
+
"forge golden-path status --real --production --json",
|
|
788
|
+
"forge deploy init --target docker",
|
|
789
|
+
"cp deploy/.env.production.example deploy/.env.production",
|
|
790
|
+
"forge authmd generate --json",
|
|
791
|
+
"forge env doctor --target production --json",
|
|
792
|
+
"forge auth prove --scenario multi-tenant --json",
|
|
793
|
+
"forge workos setup --real --file workos-seed.yml --json",
|
|
794
|
+
"forge workos prove --real --file workos-seed.yml --json",
|
|
795
|
+
FIELD_TEST_PRODUCTION_COMMAND,
|
|
796
|
+
"forge deploy readiness --production --json",
|
|
797
|
+
"forge deploy check --production --json",
|
|
798
|
+
"forge deploy package --target docker",
|
|
799
|
+
"docker compose -f deploy/docker-compose.yml up --build",
|
|
800
|
+
"forge deploy verify --production --url https://app.example.com --json",
|
|
801
|
+
];
|
|
802
|
+
return normalizeForgeCliCommandsInValue(options.workspaceRoot, {
|
|
803
|
+
schemaVersion: "0.1.0",
|
|
804
|
+
ok: true,
|
|
805
|
+
kind: "deploy",
|
|
806
|
+
action: "plan",
|
|
807
|
+
target: options.target,
|
|
808
|
+
production: options.production,
|
|
809
|
+
checks: [],
|
|
810
|
+
plan: {
|
|
811
|
+
summary: options.target === "forge-cloud"
|
|
812
|
+
? "Forge Cloud should be treated as a future managed target; current production proof still uses the same readiness gates."
|
|
813
|
+
: "Docker production deploy uses Postgres, explicit migration, runtime, worker, production auth, and public metadata checks.",
|
|
814
|
+
commands,
|
|
815
|
+
gates: [
|
|
816
|
+
"generated artifacts fresh",
|
|
817
|
+
"forge check passes",
|
|
818
|
+
"auth mode is jwt or oidc",
|
|
819
|
+
"production database env evidence is present",
|
|
820
|
+
"required secret names are present",
|
|
821
|
+
"auth.md and protected-resource metadata are published",
|
|
822
|
+
"field-test report exists with runtime/auth/UI probes",
|
|
823
|
+
"runtime /health responds",
|
|
824
|
+
"tenant and policy proof is run before public traffic",
|
|
825
|
+
"readiness score has no blocking items",
|
|
826
|
+
],
|
|
827
|
+
notes: [
|
|
828
|
+
"dev-headers auth is local-only and must not be enabled for public runtime.",
|
|
829
|
+
"Use forge workos doctor/seed when WorkOS is configured.",
|
|
830
|
+
"Use forge golden-path status --real --production --json when the next blocking stage is unclear.",
|
|
831
|
+
"WorkOS FGA is optional; ordinary WorkOS apps can be production-ready with AuthKit/RBAC permission claims, Forge policies, and tenant isolation.",
|
|
832
|
+
"Use forge test authz or HTTP probes to prove cross-tenant denial.",
|
|
833
|
+
],
|
|
834
|
+
},
|
|
835
|
+
nextActions: commands,
|
|
836
|
+
exitCode: 0,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
async function buildChecks(options: DeployCommandOptions): Promise<DeployCommandResult> {
|
|
841
|
+
const checks: DeployCheck[] = [];
|
|
842
|
+
const generated = await runGenerate({
|
|
843
|
+
workspaceRoot: options.workspaceRoot,
|
|
844
|
+
check: true,
|
|
845
|
+
dryRun: false,
|
|
846
|
+
json: false,
|
|
847
|
+
concurrency: 4,
|
|
848
|
+
});
|
|
849
|
+
checks.push({
|
|
850
|
+
name: "generated",
|
|
851
|
+
ok: generated.exitCode === 0,
|
|
852
|
+
severity: "error",
|
|
853
|
+
message: generated.exitCode === 0 ? "generated artifacts are fresh" : "generated artifacts are stale; run forge generate",
|
|
854
|
+
command: "forge generate --check --json",
|
|
855
|
+
});
|
|
856
|
+
const packageManager = detectPackageManager(options.workspaceRoot);
|
|
857
|
+
const lockfiles = activeLockfiles(options.workspaceRoot, packageManager);
|
|
858
|
+
checks.push({
|
|
859
|
+
name: "package-lockfile",
|
|
860
|
+
ok: lockfiles.length > 0,
|
|
861
|
+
severity: options.production ? "error" : "warning",
|
|
862
|
+
message: lockfiles.length > 0
|
|
863
|
+
? `${packageManager} lockfile present: ${lockfiles.join(", ")}`
|
|
864
|
+
: `no ${packageManager} lockfile found; production Docker builds should be reproducible`,
|
|
865
|
+
command: packageInstallCommand(packageManager),
|
|
866
|
+
details: {
|
|
867
|
+
packageManager,
|
|
868
|
+
expected: getLockfileCandidates(packageManager),
|
|
869
|
+
found: lockfiles,
|
|
870
|
+
},
|
|
871
|
+
});
|
|
872
|
+
checks.push({
|
|
873
|
+
name: "deploy-env-sources",
|
|
874
|
+
ok: true,
|
|
875
|
+
severity: "warning",
|
|
876
|
+
message: `${DEPLOY_ENV_FILE} and process.env are production deploy evidence; .env/.env.local are local guidance only`,
|
|
877
|
+
details: deployEnvSources(options.workspaceRoot),
|
|
878
|
+
});
|
|
879
|
+
const placeholderEnvProblems = options.production
|
|
880
|
+
? productionPlaceholderEnvProblems(productionEnvValues(options.workspaceRoot), DEPLOY_ENV_KEYS as unknown as string[])
|
|
881
|
+
: [];
|
|
882
|
+
checks.push({
|
|
883
|
+
name: "production-env-values",
|
|
884
|
+
ok: placeholderEnvProblems.length === 0,
|
|
885
|
+
severity: "error",
|
|
886
|
+
message: placeholderEnvProblems.length === 0
|
|
887
|
+
? "production env values do not contain known placeholders"
|
|
888
|
+
: `production env contains placeholder value(s): ${placeholderEnvProblems.map((problem) => problem.key).join(", ")}`,
|
|
889
|
+
command: "edit deploy/.env.production with real production values",
|
|
890
|
+
details: {
|
|
891
|
+
placeholders: placeholderEnvProblems,
|
|
892
|
+
valuesRedacted: true,
|
|
893
|
+
},
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
const auth = await withDeployEnv(options.workspaceRoot, async () => loadAuthConfigFromEnv(options.workspaceRoot));
|
|
897
|
+
const productionAuth = auth.mode === "jwt" || auth.mode === "oidc";
|
|
898
|
+
checks.push({
|
|
899
|
+
name: "production-auth-mode",
|
|
900
|
+
ok: productionAuth,
|
|
901
|
+
severity: "error",
|
|
902
|
+
message: productionAuth
|
|
903
|
+
? `auth mode ${auth.mode} is production-capable`
|
|
904
|
+
: `auth mode ${auth.mode} is local-only; set FORGE_AUTH_MODE=jwt or oidc`,
|
|
905
|
+
command: "forge auth check --production --json",
|
|
906
|
+
});
|
|
907
|
+
checks.push({
|
|
908
|
+
name: "auth-issuer",
|
|
909
|
+
ok: !productionAuth || Boolean(auth.issuer),
|
|
910
|
+
severity: "error",
|
|
911
|
+
message: !productionAuth || auth.issuer ? "issuer configured or not applicable" : "FORGE_AUTH_ISSUER is required",
|
|
912
|
+
});
|
|
913
|
+
checks.push({
|
|
914
|
+
name: "auth-audience",
|
|
915
|
+
ok: !productionAuth || Boolean(auth.audience),
|
|
916
|
+
severity: "error",
|
|
917
|
+
message: !productionAuth || auth.audience ? "audience configured or not applicable" : "FORGE_AUTH_AUDIENCE is required",
|
|
918
|
+
});
|
|
919
|
+
checks.push({
|
|
920
|
+
name: "auth-jwks",
|
|
921
|
+
ok: !productionAuth || auth.mode === "oidc" || Boolean(auth.jwksUri),
|
|
922
|
+
severity: "error",
|
|
923
|
+
message: !productionAuth || auth.mode === "oidc" || auth.jwksUri ? "JWKS/discovery configured or not applicable" : "FORGE_AUTH_JWKS_URI is required for jwt mode",
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
checks.push({
|
|
927
|
+
name: "database-url",
|
|
928
|
+
ok: databaseReady(options),
|
|
929
|
+
severity: "error",
|
|
930
|
+
message: databaseReadyMessage(options),
|
|
931
|
+
command: databaseReadyCommand(options),
|
|
932
|
+
});
|
|
933
|
+
checks.push({
|
|
934
|
+
name: "auth-md",
|
|
935
|
+
ok: nodeFileSystem.exists(join(options.workspaceRoot, "public/auth.md")),
|
|
936
|
+
severity: options.production ? "error" : "warning",
|
|
937
|
+
message: options.production
|
|
938
|
+
? "public/auth.md must be published before production traffic so agents can discover the protected resource contract"
|
|
939
|
+
: "public/auth.md should be published for agent-ready apps",
|
|
940
|
+
command: "forge authmd generate --json",
|
|
941
|
+
});
|
|
942
|
+
checks.push({
|
|
943
|
+
name: "oauth-protected-resource",
|
|
944
|
+
ok: nodeFileSystem.exists(join(options.workspaceRoot, "public/.well-known/oauth-protected-resource")),
|
|
945
|
+
severity: options.production ? "error" : "warning",
|
|
946
|
+
message: options.production
|
|
947
|
+
? "OAuth protected-resource metadata must be published before production traffic"
|
|
948
|
+
: "protected-resource metadata should be published for agent-ready apps",
|
|
949
|
+
command: "forge authmd generate --json",
|
|
950
|
+
});
|
|
951
|
+
const authMdCheck = runAuthMdCommand({
|
|
952
|
+
subcommand: "check",
|
|
953
|
+
workspaceRoot: options.workspaceRoot,
|
|
954
|
+
json: true,
|
|
955
|
+
});
|
|
956
|
+
checks.push({
|
|
957
|
+
name: "auth-md-check",
|
|
958
|
+
ok: authMdCheck.exitCode === 0,
|
|
959
|
+
severity: options.production ? "error" : "warning",
|
|
960
|
+
message: authMdCheck.exitCode === 0
|
|
961
|
+
? "auth.md and protected-resource metadata match the generated app contract"
|
|
962
|
+
: (authMdCheck.diagnostics[0]?.message ?? "auth.md is missing or stale"),
|
|
963
|
+
command: "forge authmd generate --json",
|
|
964
|
+
details: {
|
|
965
|
+
path: authMdCheck.path,
|
|
966
|
+
metadataPath: authMdCheck.metadataPath,
|
|
967
|
+
changed: authMdCheck.changed,
|
|
968
|
+
diagnostics: authMdCheck.diagnostics,
|
|
969
|
+
},
|
|
970
|
+
});
|
|
971
|
+
if (hasWorkOSIntegration(options.workspaceRoot)) {
|
|
972
|
+
const workosFgaEnabled = hasWorkOSFgaIntegration(options.workspaceRoot);
|
|
973
|
+
const workosDoctor = await withDeployEnv(options.workspaceRoot, async () =>
|
|
974
|
+
runWorkOSCommand({
|
|
975
|
+
subcommand: "doctor",
|
|
976
|
+
workspaceRoot: options.workspaceRoot,
|
|
977
|
+
json: true,
|
|
978
|
+
yes: false,
|
|
979
|
+
dryRun: true,
|
|
980
|
+
})
|
|
981
|
+
);
|
|
982
|
+
checks.push({
|
|
983
|
+
name: "workos-doctor",
|
|
984
|
+
ok: workosDoctor.exitCode === 0,
|
|
985
|
+
severity: options.production ? "error" : "warning",
|
|
986
|
+
message: workosDoctor.exitCode === 0
|
|
987
|
+
? `WorkOS adapter files, seed, claims, policies, and tenant guards are app-aware${workosFgaEnabled ? " with FGA enabled" : ""}`
|
|
988
|
+
: "WorkOS adapter is incomplete for this app; run forge workos doctor --json",
|
|
989
|
+
command: "forge workos doctor --json",
|
|
990
|
+
details: workosDoctor.checks,
|
|
991
|
+
});
|
|
992
|
+
const workosData = workosDoctor.data && typeof workosDoctor.data === "object"
|
|
993
|
+
? workosDoctor.data as { seedState?: { exists?: boolean; valid?: boolean; matchesSeedHash?: boolean | null; alreadyApplied?: boolean } }
|
|
994
|
+
: {};
|
|
995
|
+
const seedState = workosData.seedState;
|
|
996
|
+
const hostedSeedOk = Boolean(seedState?.exists && seedState.valid && seedState.matchesSeedHash === true);
|
|
997
|
+
checks.push({
|
|
998
|
+
name: "workos-hosted-seed",
|
|
999
|
+
ok: !options.production || hostedSeedOk,
|
|
1000
|
+
severity: options.production ? "error" : "warning",
|
|
1001
|
+
message: hostedSeedOk
|
|
1002
|
+
? `WorkOS hosted seed state matches workos-seed.yml${seedState?.alreadyApplied ? " and records idempotent existing resources" : ""}`
|
|
1003
|
+
: "WorkOS production deploy requires hosted seed evidence matching workos-seed.yml",
|
|
1004
|
+
command: "forge workos prove --real --file workos-seed.yml --json",
|
|
1005
|
+
details: seedState,
|
|
1006
|
+
});
|
|
1007
|
+
if (workosFgaEnabled) {
|
|
1008
|
+
const workosFgaDoctor = await withDeployEnv(options.workspaceRoot, async () =>
|
|
1009
|
+
runWorkOSCommand({
|
|
1010
|
+
subcommand: "fga",
|
|
1011
|
+
fgaAction: "doctor",
|
|
1012
|
+
workspaceRoot: options.workspaceRoot,
|
|
1013
|
+
json: true,
|
|
1014
|
+
yes: false,
|
|
1015
|
+
dryRun: true,
|
|
1016
|
+
real: options.production,
|
|
1017
|
+
})
|
|
1018
|
+
);
|
|
1019
|
+
checks.push({
|
|
1020
|
+
name: "workos-fga-proof",
|
|
1021
|
+
ok: !options.production || workosFgaDoctor.exitCode === 0,
|
|
1022
|
+
severity: options.production ? "error" : "warning",
|
|
1023
|
+
message: workosFgaDoctor.exitCode === 0
|
|
1024
|
+
? "WorkOS FGA resource graph state matches the current app contract and seed"
|
|
1025
|
+
: "WorkOS production deploy requires FGA graph sync/proof evidence matching the current app because FGA helpers are enabled",
|
|
1026
|
+
command: "forge workos fga prove --real --file workos-seed.yml --json",
|
|
1027
|
+
details: workosFgaDoctor.data,
|
|
1028
|
+
});
|
|
1029
|
+
} else {
|
|
1030
|
+
checks.push({
|
|
1031
|
+
name: "workos-fga-optional",
|
|
1032
|
+
ok: true,
|
|
1033
|
+
severity: "warning",
|
|
1034
|
+
message: "WorkOS FGA is not enabled; production deploy is gated by AuthKit, RBAC permission claims, Forge policies, tenant proof, and hosted seed evidence",
|
|
1035
|
+
command: "forge add auth workos --with-fga",
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
const tenantProofRequired = auth.requiresTenant || hasWorkOSIntegration(options.workspaceRoot);
|
|
1040
|
+
const tenantProof = tenantProofRequired
|
|
1041
|
+
? await withDeployEnv(options.workspaceRoot, () =>
|
|
1042
|
+
runAuthCommand({
|
|
1043
|
+
subcommand: "prove",
|
|
1044
|
+
workspaceRoot: options.workspaceRoot,
|
|
1045
|
+
json: true,
|
|
1046
|
+
prod: false,
|
|
1047
|
+
scenario: "multi-tenant",
|
|
1048
|
+
})
|
|
1049
|
+
)
|
|
1050
|
+
: null;
|
|
1051
|
+
checks.push({
|
|
1052
|
+
name: "auth-tenant-proof",
|
|
1053
|
+
ok: tenantProofRequired ? tenantProof?.exitCode === 0 : true,
|
|
1054
|
+
severity: tenantProofRequired && options.production ? "error" : "warning",
|
|
1055
|
+
message: tenantProofRequired
|
|
1056
|
+
? tenantProof?.exitCode === 0
|
|
1057
|
+
? "local multi-tenant auth proof passed"
|
|
1058
|
+
: "local multi-tenant auth proof failed; prove claim mapping, seed coverage, permissions, and auth metadata before production"
|
|
1059
|
+
: "not required; app is not tenant-scoped and no WorkOS adapter was detected",
|
|
1060
|
+
command: tenantProofRequired ? "forge auth prove --scenario multi-tenant --json" : undefined,
|
|
1061
|
+
details: tenantProof?.data,
|
|
1062
|
+
});
|
|
1063
|
+
const fieldReports = fieldTestReportCandidates(options.workspaceRoot);
|
|
1064
|
+
const latestFieldReport = fieldReports[0];
|
|
1065
|
+
const fieldSummary = latestFieldReport ? summarizeFieldTestReport(latestFieldReport.data) : null;
|
|
1066
|
+
const fieldReportComplete = Boolean(fieldSummary?.productionEvidence.readyForDeployCheck);
|
|
1067
|
+
const fieldReportMissing = fieldSummary?.productionEvidence.missing ?? [];
|
|
1068
|
+
checks.push({
|
|
1069
|
+
name: "field-test-report",
|
|
1070
|
+
ok: fieldReportComplete,
|
|
1071
|
+
severity: options.production ? "error" : "warning",
|
|
1072
|
+
message: fieldSummary?.ok
|
|
1073
|
+
? fieldReportComplete
|
|
1074
|
+
? `field-test report ${latestFieldReport?.path} passed with concrete runtime, auth, metadata, UI, and ergonomics evidence`
|
|
1075
|
+
: `field-test report ${latestFieldReport?.path} passed but is missing deploy evidence: ${fieldReportMissing.join(", ")}`
|
|
1076
|
+
: latestFieldReport
|
|
1077
|
+
? `field-test report ${latestFieldReport.path} did not pass`
|
|
1078
|
+
: `no field-test report found; run ${FIELD_TEST_PRODUCTION_COMMAND}`,
|
|
1079
|
+
command: FIELD_TEST_PRODUCTION_COMMAND,
|
|
1080
|
+
details: latestFieldReport ? { path: latestFieldReport.path, summary: fieldSummary } : undefined,
|
|
1081
|
+
});
|
|
1082
|
+
if (fieldSummary?.uiErgonomicsWarnings) {
|
|
1083
|
+
checks.push({
|
|
1084
|
+
name: "field-test-ui-ergonomics",
|
|
1085
|
+
ok: false,
|
|
1086
|
+
severity: "warning",
|
|
1087
|
+
message: `field-test UI ergonomics audit reported ${fieldSummary.uiErgonomicsWarnings} warning(s); fix the product surface before treating the app as polished`,
|
|
1088
|
+
command: "forge inspect ui --ergonomics --json",
|
|
1089
|
+
details: { path: latestFieldReport?.path, summary: fieldSummary },
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
checks.push({
|
|
1093
|
+
name: "frontend-build-script",
|
|
1094
|
+
ok: !readGeneratedJson<FrontendGraph>(options.workspaceRoot, `${GENERATED_DIR}/frontendGraph.json`)?.present || hasScript(options.workspaceRoot, "build") || hasScript(join(options.workspaceRoot, "web"), "build"),
|
|
1095
|
+
severity: "warning",
|
|
1096
|
+
message: "frontend apps should expose a production build script",
|
|
1097
|
+
});
|
|
1098
|
+
checks.push({
|
|
1099
|
+
name: "tenant-claim",
|
|
1100
|
+
ok: !auth.requiresTenant || Boolean(auth.claims.tenantId),
|
|
1101
|
+
severity: "error",
|
|
1102
|
+
message: !auth.requiresTenant || auth.claims.tenantId ? "tenant claim mapping is present or not required" : "tenant-scoped production apps require a tenant claim mapping",
|
|
1103
|
+
});
|
|
1104
|
+
checks.push({
|
|
1105
|
+
name: "live-production",
|
|
1106
|
+
ok: true,
|
|
1107
|
+
severity: "warning",
|
|
1108
|
+
message: "production liveQuery must use durable invalidations; verify with forge inspect live-production --json",
|
|
1109
|
+
command: "forge inspect live-production --json",
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
const errorFree = checks.every((check) => check.ok || check.severity === "warning");
|
|
1113
|
+
const rawFailureActions = unique(checks.flatMap((check) =>
|
|
1114
|
+
!check.ok && check.command ? expandDeployFailureCommand(check.command) : []
|
|
1115
|
+
));
|
|
1116
|
+
const failureActions = prioritizeDeployFailureActions(options, checks, rawFailureActions);
|
|
1117
|
+
const nextActions = errorFree
|
|
1118
|
+
? ["forge deploy verify --production --url https://app.example.com --json"]
|
|
1119
|
+
: failureActions;
|
|
1120
|
+
const readiness = summarizeDeployReadiness(checks, nextActions);
|
|
1121
|
+
return normalizeForgeCliCommandsInValue(options.workspaceRoot, {
|
|
1122
|
+
schemaVersion: "0.1.0",
|
|
1123
|
+
ok: errorFree,
|
|
1124
|
+
kind: "deploy",
|
|
1125
|
+
action: "check",
|
|
1126
|
+
target: options.target,
|
|
1127
|
+
production: options.production,
|
|
1128
|
+
checks,
|
|
1129
|
+
blocking: readiness.blocking,
|
|
1130
|
+
warnings: readiness.warnings,
|
|
1131
|
+
readiness,
|
|
1132
|
+
nextActions,
|
|
1133
|
+
exitCode: errorFree ? 0 : 1,
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function expandDeployFailureCommand(command: string): string[] {
|
|
1138
|
+
if (command.includes("forge workos fga prove")) {
|
|
1139
|
+
return [
|
|
1140
|
+
"forge workos fga plan --file workos-seed.yml --write --json",
|
|
1141
|
+
"forge workos fga sync --real --file workos-seed.yml --json",
|
|
1142
|
+
command,
|
|
1143
|
+
];
|
|
1144
|
+
}
|
|
1145
|
+
return [command];
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function prioritizeDeployFailureActions(
|
|
1149
|
+
options: DeployCommandOptions,
|
|
1150
|
+
checks: DeployCheck[],
|
|
1151
|
+
actions: string[],
|
|
1152
|
+
): string[] {
|
|
1153
|
+
const database = checks.find((check) => check.name === "database-url");
|
|
1154
|
+
if (!database || database.ok || !database.command) {
|
|
1155
|
+
return actions;
|
|
1156
|
+
}
|
|
1157
|
+
const deployEnvMissing = !nodeFileSystem.exists(join(options.workspaceRoot, DEPLOY_ENV_FILE));
|
|
1158
|
+
if (deployEnvMissing && database.command === "cp deploy/.env.production.example deploy/.env.production") {
|
|
1159
|
+
return unique([database.command, ...actions.filter((action) => action !== database.command)]);
|
|
1160
|
+
}
|
|
1161
|
+
return actions;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function renderDocker(options: DeployCommandOptions): DeployCommandResult {
|
|
1165
|
+
const dir = join(options.workspaceRoot, "deploy");
|
|
1166
|
+
nodeFileSystem.mkdirp(dir);
|
|
1167
|
+
const files = [
|
|
1168
|
+
["deploy/docker-compose.yml", renderDockerCompose(options.workspaceRoot)],
|
|
1169
|
+
["deploy/Dockerfile.runtime", renderRuntimeDockerfile(options.workspaceRoot)],
|
|
1170
|
+
["deploy/.env.production.example", renderEnvExample(options.workspaceRoot)],
|
|
1171
|
+
["deploy/README.production.md", renderProductionReadme()],
|
|
1172
|
+
] as const;
|
|
1173
|
+
for (const [file, content] of files) {
|
|
1174
|
+
nodeFileSystem.writeText(join(options.workspaceRoot, file), content);
|
|
1175
|
+
}
|
|
1176
|
+
return normalizeForgeCliCommandsInValue(options.workspaceRoot, {
|
|
1177
|
+
schemaVersion: "0.1.0",
|
|
1178
|
+
ok: true,
|
|
1179
|
+
kind: "deploy",
|
|
1180
|
+
action: options.subcommand,
|
|
1181
|
+
target: options.target,
|
|
1182
|
+
production: options.production,
|
|
1183
|
+
checks: [],
|
|
1184
|
+
files: files.map(([file]) => file),
|
|
1185
|
+
nextActions: [
|
|
1186
|
+
"cp deploy/.env.production.example deploy/.env.production",
|
|
1187
|
+
"forge deploy readiness --production --json",
|
|
1188
|
+
"forge deploy check --production --json",
|
|
1189
|
+
"docker compose -f deploy/docker-compose.yml up --build",
|
|
1190
|
+
],
|
|
1191
|
+
exitCode: 0,
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
async function probe(
|
|
1196
|
+
method: "GET" | "HEAD",
|
|
1197
|
+
url: string,
|
|
1198
|
+
expectation: DeployProbeExpectation = {},
|
|
1199
|
+
): Promise<{ method: string; url: string; ok: boolean; status?: number; contentType?: string; error?: string; jsonValid?: boolean }> {
|
|
1200
|
+
try {
|
|
1201
|
+
const response = await fetch(url, { method });
|
|
1202
|
+
const contentType = response.headers.get("content-type") ?? undefined;
|
|
1203
|
+
const contentTypeOk = !expectation.contentTypeIncludes || contentType?.includes(expectation.contentTypeIncludes) === true;
|
|
1204
|
+
let jsonValid: boolean | undefined;
|
|
1205
|
+
let bodyError: string | undefined;
|
|
1206
|
+
if (expectation.json && method !== "HEAD") {
|
|
1207
|
+
const text = await response.text();
|
|
1208
|
+
try {
|
|
1209
|
+
JSON.parse(text);
|
|
1210
|
+
jsonValid = true;
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
jsonValid = false;
|
|
1213
|
+
bodyError = error instanceof Error ? error.message : String(error);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
const ok = response.ok && contentTypeOk && (jsonValid !== false);
|
|
1217
|
+
return {
|
|
1218
|
+
method,
|
|
1219
|
+
url,
|
|
1220
|
+
ok,
|
|
1221
|
+
status: response.status,
|
|
1222
|
+
contentType,
|
|
1223
|
+
...(jsonValid !== undefined ? { jsonValid } : {}),
|
|
1224
|
+
...(!contentTypeOk
|
|
1225
|
+
? { error: `expected content-type including ${expectation.contentTypeIncludes}; received ${contentType ?? "none"}` }
|
|
1226
|
+
: bodyError
|
|
1227
|
+
? { error: `invalid JSON: ${bodyError}` }
|
|
1228
|
+
: {}),
|
|
1229
|
+
};
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
return {
|
|
1232
|
+
method,
|
|
1233
|
+
url,
|
|
1234
|
+
ok: false,
|
|
1235
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
async function verifyUrl(options: DeployCommandOptions): Promise<DeployCommandResult> {
|
|
1241
|
+
const base = options.url?.replace(/\/$/, "");
|
|
1242
|
+
if (!base) {
|
|
1243
|
+
return {
|
|
1244
|
+
schemaVersion: "0.1.0",
|
|
1245
|
+
ok: false,
|
|
1246
|
+
kind: "deploy",
|
|
1247
|
+
action: "verify",
|
|
1248
|
+
target: options.target,
|
|
1249
|
+
production: options.production,
|
|
1250
|
+
checks: [{ name: "url", ok: false, severity: "error", message: "--url is required" }],
|
|
1251
|
+
nextActions: ["forge deploy verify --url https://app.example.com --json"],
|
|
1252
|
+
exitCode: 1,
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
const probeSpecs: Array<{ method: "GET" | "HEAD"; path: string; expectation?: DeployProbeExpectation }> = [
|
|
1256
|
+
{ method: "GET", path: "/health", expectation: { required: true } },
|
|
1257
|
+
{ method: "HEAD", path: "/auth.md", expectation: { contentTypeIncludes: "text/markdown", required: true } },
|
|
1258
|
+
{ method: "GET", path: "/auth.md", expectation: { contentTypeIncludes: "text/markdown", required: true } },
|
|
1259
|
+
{ method: "HEAD", path: "/.well-known/oauth-protected-resource", expectation: { contentTypeIncludes: "application/json", required: true } },
|
|
1260
|
+
{ method: "GET", path: "/.well-known/oauth-protected-resource", expectation: { contentTypeIncludes: "application/json", json: true, required: true } },
|
|
1261
|
+
{ method: "GET", path: "/ready", expectation: { contentTypeIncludes: "application/json" } },
|
|
1262
|
+
{ method: "GET", path: "/live/status", expectation: { contentTypeIncludes: "application/json" } },
|
|
1263
|
+
{ method: "GET", path: "/outbox/status", expectation: { contentTypeIncludes: "application/json" } },
|
|
1264
|
+
{ method: "HEAD", path: "/webhooks/workos" },
|
|
1265
|
+
];
|
|
1266
|
+
const probes = await Promise.all(probeSpecs.map((item) =>
|
|
1267
|
+
probe(item.method, `${base}${item.path}`, item.expectation ?? {})
|
|
1268
|
+
));
|
|
1269
|
+
const checks = probes.map((item): DeployCheck => ({
|
|
1270
|
+
name: `${item.method} ${item.url.replace(base, "") || "/"}`,
|
|
1271
|
+
ok: item.ok,
|
|
1272
|
+
severity: probeSpecs.find((spec) => `${base}${spec.path}` === item.url && spec.method === item.method)?.expectation?.required || item.url.endsWith("/health")
|
|
1273
|
+
? "error"
|
|
1274
|
+
: "warning",
|
|
1275
|
+
message: item.ok
|
|
1276
|
+
? `HTTP ${item.status}${item.contentType ? ` ${item.contentType}` : ""}`
|
|
1277
|
+
: item.error ?? `HTTP ${item.status ?? "failed"}`,
|
|
1278
|
+
}));
|
|
1279
|
+
const ok = checks.every((check) => check.ok || check.severity === "warning");
|
|
1280
|
+
const readiness = summarizeDeployReadiness(checks, ok ? [] : ["check runtime logs", "forge deploy check --production --json"]);
|
|
1281
|
+
return {
|
|
1282
|
+
schemaVersion: "0.1.0",
|
|
1283
|
+
ok,
|
|
1284
|
+
kind: "deploy",
|
|
1285
|
+
action: "verify",
|
|
1286
|
+
target: options.target,
|
|
1287
|
+
production: options.production,
|
|
1288
|
+
checks,
|
|
1289
|
+
blocking: readiness.blocking,
|
|
1290
|
+
warnings: readiness.warnings,
|
|
1291
|
+
probes,
|
|
1292
|
+
readiness,
|
|
1293
|
+
nextActions: ok
|
|
1294
|
+
? [
|
|
1295
|
+
"forge auth prove --prod --token <jwt> --json",
|
|
1296
|
+
"forge auth prove --scenario multi-tenant --json",
|
|
1297
|
+
"forge handoff --json",
|
|
1298
|
+
]
|
|
1299
|
+
: ["check runtime logs", "forge deploy check --production --json"],
|
|
1300
|
+
exitCode: ok ? 0 : 1,
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
export async function runDeployCommand(options: DeployCommandOptions): Promise<DeployCommandResult> {
|
|
1305
|
+
if (options.subcommand === "plan") return buildPlan(options);
|
|
1306
|
+
if (options.subcommand === "readiness") {
|
|
1307
|
+
const result = await buildChecks({ ...options, subcommand: "check", production: true });
|
|
1308
|
+
return {
|
|
1309
|
+
...result,
|
|
1310
|
+
action: "readiness",
|
|
1311
|
+
nextActions: result.readiness?.answers.nextCommand ? [result.readiness.answers.nextCommand] : result.nextActions,
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
if (options.subcommand === "check") return buildChecks(options);
|
|
1315
|
+
if (options.subcommand === "init" || options.subcommand === "render" || options.subcommand === "package") return renderDocker(options);
|
|
1316
|
+
return verifyUrl(options);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
export function formatDeployJson(result: DeployCommandResult): string {
|
|
1320
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
export function formatDeployHuman(result: DeployCommandResult): string {
|
|
1324
|
+
const lines = [
|
|
1325
|
+
`deploy ${result.action} ${result.ok ? "ok" : "failed"}`,
|
|
1326
|
+
`target: ${result.target}`,
|
|
1327
|
+
...(result.readiness ? [
|
|
1328
|
+
`readiness: ${result.readiness.status} (${result.readiness.score}/100)`,
|
|
1329
|
+
`can publish: ${result.readiness.answers.canPublish ? "yes" : "no"}`,
|
|
1330
|
+
] : []),
|
|
1331
|
+
...result.checks.map((check) => `${check.ok ? "ok" : check.severity === "warning" ? "warn" : "fail"} ${check.name}: ${check.message}`),
|
|
1332
|
+
...(result.blocking?.length ? ["", "Blocking:", ...result.blocking.map((item) => ` ${item}`)] : []),
|
|
1333
|
+
...(result.warnings?.length ? ["", "Warnings:", ...result.warnings.map((item) => ` ${item}`)] : []),
|
|
1334
|
+
...(result.files?.length ? ["", "Files:", ...result.files.map((file) => ` ${file}`)] : []),
|
|
1335
|
+
...(result.plan ? ["", result.plan.summary, "", "Commands:", ...result.plan.commands.map((command) => ` ${command}`)] : []),
|
|
1336
|
+
...(result.nextActions.length ? ["", "Next:", ...result.nextActions.map((action) => ` ${action}`)] : []),
|
|
1337
|
+
];
|
|
1338
|
+
return `${lines.join("\n")}\n`;
|
|
1339
|
+
}
|