forgeos 0.1.0-alpha.4 → 0.1.0-alpha.41
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/.npmignore +10 -0
- package/AGENTS.md +142 -90
- package/CHANGELOG.md +433 -0
- package/LICENSE +21 -0
- package/README.md +67 -8
- 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 +34 -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 +36 -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/cair-protocol.md +103 -0
- package/docs/changelog.md +548 -0
- package/docs/forge-protocol.md +189 -0
- package/examples/go-billing/go.mod +7 -0
- package/examples/go-billing/main.go +120 -0
- package/examples/java-billing/pom.xml +52 -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 +31 -3
- package/schemas/forge-manifest.schema.json +57 -0
- package/src/forge/_generated/releaseManifest.json +1 -2
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/agent-adapters/index.ts +1584 -125
- package/src/forge/agent-adapters/types.ts +224 -1
- package/src/forge/agent-memory/bridge.ts +1333 -0
- package/src/forge/agent-memory/context-pack.ts +277 -0
- package/src/forge/agent-memory/hook-runner.ts +312 -0
- package/src/forge/agent-memory/mcp.ts +224 -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 +273 -0
- package/src/forge/agent-memory/sources/codex.ts +119 -0
- package/src/forge/agent-memory/sources/cursor.ts +35 -0
- package/src/forge/agent-memory/types.ts +204 -0
- package/src/forge/bench.ts +248 -0
- package/src/forge/brownfield-import/index.ts +801 -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 +106 -0
- package/src/forge/cair/query.ts +478 -0
- package/src/forge/cair/snapshot.ts +315 -0
- package/src/forge/cair/types.ts +248 -0
- package/src/forge/cli/ai.ts +339 -21
- package/src/forge/cli/auth.ts +348 -10
- 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 +437 -0
- package/src/forge/cli/codex-app-server.ts +877 -0
- package/src/forge/cli/commands.ts +1620 -20
- package/src/forge/cli/db.ts +209 -2
- package/src/forge/cli/deploy.ts +887 -0
- package/src/forge/cli/deps.ts +82 -13
- package/src/forge/cli/dev.ts +961 -46
- package/src/forge/cli/docs.ts +265 -0
- package/src/forge/cli/doctor.ts +297 -0
- package/src/forge/cli/field-test.ts +329 -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 +107 -3
- package/src/forge/cli/new.ts +152 -13
- package/src/forge/cli/next-actions.ts +23 -0
- package/src/forge/cli/output.ts +389 -6
- package/src/forge/cli/parse.ts +1050 -34
- 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/rls.ts +43 -4
- package/src/forge/cli/run.ts +41 -0
- package/src/forge/cli/security.ts +112 -0
- package/src/forge/cli/self-host.ts +56 -14
- package/src/forge/cli/studio.ts +2218 -0
- package/src/forge/cli/verify.ts +1455 -32
- package/src/forge/cli/windows.ts +23 -0
- package/src/forge/cli/workos.ts +1020 -0
- package/src/forge/compiler/agent-contract/build.ts +670 -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/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/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 +30 -0
- package/src/forge/compiler/diagnostics/create.ts +21 -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 +205 -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 +618 -24
- 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 +1219 -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 +247 -10
- package/src/forge/compiler/orchestrator/profile.ts +65 -0
- package/src/forge/compiler/orchestrator/run.ts +97 -31
- 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-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/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 +75 -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/classifier.ts +52 -0
- package/src/forge/delta/explain.ts +238 -0
- package/src/forge/delta/git-observer.ts +43 -0
- package/src/forge/delta/ids.ts +44 -0
- package/src/forge/delta/index.ts +25 -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 +1202 -0
- package/src/forge/delta/store.ts +3284 -0
- package/src/forge/delta/timeline.ts +178 -0
- package/src/forge/dev/server.ts +808 -17
- package/src/forge/dev/types.ts +15 -1
- package/src/forge/dev/watch.ts +17 -7
- package/src/forge/dev-console/cycle.ts +280 -33
- package/src/forge/dev-console/types.ts +47 -1
- package/src/forge/impact/index.ts +204 -9
- 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 +40 -6
- package/src/forge/make/templates.ts +155 -2
- 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/review/index.ts +158 -12
- package/src/forge/review/types.ts +15 -0
- 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 +263 -2
- package/src/forge/runtime/db/postgres-adapter.ts +6 -3
- package/src/forge/runtime/executor.ts +129 -16
- package/src/forge/runtime/external/bridge.ts +649 -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 +17 -3
- package/src/forge/ui/index.ts +209 -0
- package/src/forge/ui/types.ts +1 -0
- 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 +250 -0
- package/src/forge/workspace/forge-cli.ts +42 -0
- package/src/forge/workspace/git-summary.ts +391 -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 +33 -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 +12 -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 +13 -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/b2b-support-web/package.json +1 -0
- package/templates/b2b-support-web/web/package.json +1 -1
- package/templates/minimal-web/package.json +1 -1
- package/templates/minimal-web/web/package.json +2 -2
- package/templates/nuxt-web/.vscode/settings.json +14 -0
- package/templates/nuxt-web/README.md +30 -0
- package/templates/nuxt-web/forge.config.ts +3 -0
- package/templates/nuxt-web/package.json +33 -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 +18 -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/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 -14919
- 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 -3430
- 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,1020 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { GENERATED_DIR } from "../compiler/emitter/constants.ts";
|
|
6
|
+
import { stripDeterministicHeader } from "../compiler/primitives/header.ts";
|
|
7
|
+
|
|
8
|
+
export type WorkOSSubcommand = "install" | "doctor" | "seed" | "setup";
|
|
9
|
+
|
|
10
|
+
export interface WorkOSCommandOptions {
|
|
11
|
+
subcommand: WorkOSSubcommand;
|
|
12
|
+
workspaceRoot: string;
|
|
13
|
+
json: boolean;
|
|
14
|
+
file?: string;
|
|
15
|
+
yes: boolean;
|
|
16
|
+
dryRun: boolean;
|
|
17
|
+
real?: boolean;
|
|
18
|
+
commandRunner?: WorkOSCommandRunner;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WorkOSCheck {
|
|
22
|
+
name: string;
|
|
23
|
+
ok: boolean;
|
|
24
|
+
detail: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface WorkOSCommandResult {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
kind: "workos-install" | "workos-doctor" | "workos-seed" | "workos-setup";
|
|
30
|
+
checks: WorkOSCheck[];
|
|
31
|
+
command?: string[];
|
|
32
|
+
applied?: boolean;
|
|
33
|
+
data?: unknown;
|
|
34
|
+
stdout?: string;
|
|
35
|
+
stderr?: string;
|
|
36
|
+
exitCode: 0 | 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type WorkOSCommandRunner = (
|
|
40
|
+
command: string,
|
|
41
|
+
args: string[],
|
|
42
|
+
options: {
|
|
43
|
+
cwd: string;
|
|
44
|
+
encoding: "utf8";
|
|
45
|
+
stdio: ["ignore", "pipe", "pipe"];
|
|
46
|
+
},
|
|
47
|
+
) => { status: number | null; stdout: string; stderr: string };
|
|
48
|
+
|
|
49
|
+
const DEFAULT_SEED_FILE = "workos-seed.yml";
|
|
50
|
+
const GENERATED_SEED_FILE = `${GENERATED_DIR}/integrations/workos/workos-seed.yml`;
|
|
51
|
+
|
|
52
|
+
function runExternalCommand(
|
|
53
|
+
command: string[],
|
|
54
|
+
options: WorkOSCommandOptions,
|
|
55
|
+
): { status: number | null; stdout: string; stderr: string } {
|
|
56
|
+
const runner = options.commandRunner ?? spawnSync;
|
|
57
|
+
const result = runner(command[0]!, command.slice(1), {
|
|
58
|
+
cwd: options.workspaceRoot,
|
|
59
|
+
encoding: "utf8",
|
|
60
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
status: result.status,
|
|
64
|
+
stdout: typeof result.stdout === "string" ? result.stdout : result.stdout?.toString("utf8") ?? "",
|
|
65
|
+
stderr: typeof result.stderr === "string" ? result.stderr : result.stderr?.toString("utf8") ?? "",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function exists(root: string, path: string): boolean {
|
|
70
|
+
return existsSync(join(root, path));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readJson(root: string, path: string): unknown | null {
|
|
74
|
+
const absolute = join(root, path);
|
|
75
|
+
if (!existsSync(absolute)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return JSON.parse(stripDeterministicHeader(readFileSync(absolute, "utf8")));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readText(root: string, path: string): string {
|
|
82
|
+
const absolute = join(root, path);
|
|
83
|
+
if (!existsSync(absolute)) {
|
|
84
|
+
return "";
|
|
85
|
+
}
|
|
86
|
+
return stripDeterministicHeader(readFileSync(absolute, "utf8"));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readRawText(root: string, path: string): string {
|
|
90
|
+
const absolute = join(root, path);
|
|
91
|
+
if (!existsSync(absolute)) {
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
return readFileSync(absolute, "utf8");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function includesAll(haystack: string, needles: string[]): boolean {
|
|
98
|
+
return needles.every((needle) => haystack.includes(needle));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function uniqueSorted(values: Iterable<string>): string[] {
|
|
102
|
+
return [...new Set([...values].filter(Boolean))].sort();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function quotedValues(text: string): string[] {
|
|
106
|
+
return [...text.matchAll(/["']([^"']+)["']/g)].map((match) => match[1]!).filter(Boolean);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface WorkOSSeedSummary {
|
|
110
|
+
exists: boolean;
|
|
111
|
+
valid: boolean;
|
|
112
|
+
path: string;
|
|
113
|
+
permissions: string[];
|
|
114
|
+
roles: string[];
|
|
115
|
+
resourceTypes: string[];
|
|
116
|
+
organizations: string[];
|
|
117
|
+
domains: string[];
|
|
118
|
+
redirectUris: string[];
|
|
119
|
+
corsOrigins: string[];
|
|
120
|
+
homepageUrl?: string;
|
|
121
|
+
webhookEndpoints: Array<{ url: string; events: string[] }>;
|
|
122
|
+
diagnostics: string[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function parseSlug(line: string): string | null {
|
|
126
|
+
const match = /(?:^|\s)-?\s*slug:\s*["']?([^"',\]\s]+)["']?/.exec(line);
|
|
127
|
+
return match?.[1] ?? null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseName(line: string): string | null {
|
|
131
|
+
const match = /(?:^|\s)-?\s*name:\s*["']?([^"'\]]+(?:\s[^"'\]]+)*)["']?/.exec(line);
|
|
132
|
+
return match?.[1]?.trim() ?? null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function parseSeedFile(workspaceRoot: string, preferredPath = DEFAULT_SEED_FILE): WorkOSSeedSummary {
|
|
136
|
+
const seedPath = exists(workspaceRoot, preferredPath)
|
|
137
|
+
? preferredPath
|
|
138
|
+
: exists(workspaceRoot, GENERATED_SEED_FILE)
|
|
139
|
+
? GENERATED_SEED_FILE
|
|
140
|
+
: preferredPath;
|
|
141
|
+
const raw = readText(workspaceRoot, seedPath);
|
|
142
|
+
const permissions = new Set<string>();
|
|
143
|
+
const roles = new Set<string>();
|
|
144
|
+
const resourceTypes = new Set<string>();
|
|
145
|
+
const organizations = new Set<string>();
|
|
146
|
+
const domains = new Set<string>();
|
|
147
|
+
const redirectUris = new Set<string>();
|
|
148
|
+
const corsOrigins = new Set<string>();
|
|
149
|
+
let homepageUrl: string | undefined;
|
|
150
|
+
const webhookEndpoints: Array<{ url: string; events: string[] }> = [];
|
|
151
|
+
const diagnostics: string[] = [];
|
|
152
|
+
let section = "";
|
|
153
|
+
let configKey = "";
|
|
154
|
+
let currentWebhook: { url: string; events: string[] } | undefined;
|
|
155
|
+
|
|
156
|
+
if (!raw.trim()) {
|
|
157
|
+
return {
|
|
158
|
+
exists: false,
|
|
159
|
+
valid: false,
|
|
160
|
+
path: seedPath,
|
|
161
|
+
permissions: [],
|
|
162
|
+
roles: [],
|
|
163
|
+
resourceTypes: [],
|
|
164
|
+
organizations: [],
|
|
165
|
+
domains: [],
|
|
166
|
+
redirectUris: [],
|
|
167
|
+
corsOrigins: [],
|
|
168
|
+
webhookEndpoints: [],
|
|
169
|
+
diagnostics: [`${seedPath} is missing or empty`],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
174
|
+
const rootSection = /^([a-zA-Z_][\w-]*):\s*$/.exec(line);
|
|
175
|
+
if (rootSection) {
|
|
176
|
+
section = rootSection[1]!;
|
|
177
|
+
configKey = "";
|
|
178
|
+
currentWebhook = undefined;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (section === "config") {
|
|
183
|
+
const configEntry = /^\s{2}([a-zA-Z_][\w-]*):\s*(.*)$/.exec(line);
|
|
184
|
+
if (configEntry) {
|
|
185
|
+
configKey = configEntry[1]!;
|
|
186
|
+
const rest = configEntry[2] ?? "";
|
|
187
|
+
if (configKey === "redirect_uris") {
|
|
188
|
+
for (const value of quotedValues(rest)) redirectUris.add(value);
|
|
189
|
+
} else if (configKey === "cors_origins") {
|
|
190
|
+
for (const value of quotedValues(rest)) corsOrigins.add(value);
|
|
191
|
+
} else if (configKey === "homepage_url") {
|
|
192
|
+
homepageUrl = quotedValues(rest)[0] ?? (rest.trim().replace(/^["']|["']$/g, "") || homepageUrl);
|
|
193
|
+
} else if (configKey === "webhook_endpoints") {
|
|
194
|
+
currentWebhook = undefined;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const bulletValue = /^\s*-\s*["']?([^"'\]\s]+)["']?\s*$/.exec(line)?.[1];
|
|
198
|
+
if (bulletValue && configKey === "redirect_uris") {
|
|
199
|
+
redirectUris.add(bulletValue);
|
|
200
|
+
} else if (bulletValue && configKey === "cors_origins") {
|
|
201
|
+
corsOrigins.add(bulletValue);
|
|
202
|
+
} else if (bulletValue && configKey === "webhook_events" && currentWebhook) {
|
|
203
|
+
currentWebhook.events.push(bulletValue);
|
|
204
|
+
}
|
|
205
|
+
const webhookUrl = /^\s*-\s*url:\s*["']?([^"']+)["']?\s*$/.exec(line)?.[1] ??
|
|
206
|
+
/^\s{4}url:\s*["']?([^"']+)["']?\s*$/.exec(line)?.[1];
|
|
207
|
+
if (webhookUrl) {
|
|
208
|
+
currentWebhook = { url: webhookUrl.trim(), events: [] };
|
|
209
|
+
webhookEndpoints.push(currentWebhook);
|
|
210
|
+
configKey = "webhook_endpoints";
|
|
211
|
+
}
|
|
212
|
+
if (/^\s{4}events:\s*/.test(line) && currentWebhook) {
|
|
213
|
+
configKey = "webhook_events";
|
|
214
|
+
for (const value of quotedValues(line)) currentWebhook.events.push(value);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const slug = parseSlug(line);
|
|
219
|
+
if (slug) {
|
|
220
|
+
if (section === "permissions") {
|
|
221
|
+
permissions.add(slug);
|
|
222
|
+
} else if (section === "roles") {
|
|
223
|
+
roles.add(slug);
|
|
224
|
+
} else if (section === "resource_types") {
|
|
225
|
+
resourceTypes.add(slug);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (section === "organizations") {
|
|
230
|
+
const name = parseName(line);
|
|
231
|
+
if (name) {
|
|
232
|
+
organizations.add(name);
|
|
233
|
+
}
|
|
234
|
+
for (const value of quotedValues(line)) {
|
|
235
|
+
if (value.includes(".")) {
|
|
236
|
+
domains.add(value);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (const match of line.matchAll(/-\s*["']?([a-zA-Z0-9_.-]+:[a-zA-Z0-9_.-]+)["']?/g)) {
|
|
242
|
+
permissions.add(match[1]!);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (permissions.size === 0) diagnostics.push("no permission slugs found");
|
|
247
|
+
if (roles.size === 0) diagnostics.push("no role slugs found");
|
|
248
|
+
if (resourceTypes.size === 0) diagnostics.push("no resource_types slugs found");
|
|
249
|
+
if (organizations.size === 0) diagnostics.push("no organizations found");
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
exists: true,
|
|
253
|
+
valid: diagnostics.length === 0,
|
|
254
|
+
path: seedPath,
|
|
255
|
+
permissions: uniqueSorted(permissions),
|
|
256
|
+
roles: uniqueSorted(roles),
|
|
257
|
+
resourceTypes: uniqueSorted(resourceTypes),
|
|
258
|
+
organizations: uniqueSorted(organizations),
|
|
259
|
+
domains: uniqueSorted(domains),
|
|
260
|
+
redirectUris: uniqueSorted(redirectUris),
|
|
261
|
+
corsOrigins: uniqueSorted(corsOrigins),
|
|
262
|
+
...(homepageUrl ? { homepageUrl } : {}),
|
|
263
|
+
webhookEndpoints: webhookEndpoints.map((endpoint) => ({
|
|
264
|
+
url: endpoint.url,
|
|
265
|
+
events: uniqueSorted(endpoint.events),
|
|
266
|
+
})),
|
|
267
|
+
diagnostics,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function parseEnvText(text: string): Record<string, string> {
|
|
272
|
+
const values: Record<string, string> = {};
|
|
273
|
+
for (const line of text.split(/\r?\n/)) {
|
|
274
|
+
const match = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/.exec(line);
|
|
275
|
+
if (!match) continue;
|
|
276
|
+
const raw = match[2] ?? "";
|
|
277
|
+
values[match[1]!] = raw.trim().replace(/^["']|["']$/g, "");
|
|
278
|
+
}
|
|
279
|
+
return values;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function readRealEnv(workspaceRoot: string): Record<string, string> {
|
|
283
|
+
return {
|
|
284
|
+
...parseEnvText(readRawText(workspaceRoot, ".env")),
|
|
285
|
+
...parseEnvText(readRawText(workspaceRoot, ".env.local")),
|
|
286
|
+
...Object.fromEntries(
|
|
287
|
+
Object.entries(process.env)
|
|
288
|
+
.filter(([key]) => key.startsWith("FORGE_") || key.startsWith("WORKOS_") || key.startsWith("VITE_WORKOS_"))
|
|
289
|
+
.filter((entry): entry is [string, string] => typeof entry[1] === "string"),
|
|
290
|
+
),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function hasValue(env: Record<string, string>, name: string): boolean {
|
|
295
|
+
return typeof env[name] === "string" && env[name]!.trim().length > 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function workosJwksUri(clientId: string | undefined): string {
|
|
299
|
+
return clientId ? `https://api.workos.com/sso/jwks/${clientId}` : "https://api.workos.com/sso/jwks/<WORKOS_CLIENT_ID>";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function collectPolicyPermissions(workspaceRoot: string): string[] {
|
|
303
|
+
const registry = readJson(workspaceRoot, `${GENERATED_DIR}/policyRegistry.json`) as {
|
|
304
|
+
policies?: Array<{ permissions?: string[] }>;
|
|
305
|
+
} | null;
|
|
306
|
+
const permissions = new Set<string>();
|
|
307
|
+
for (const policy of registry?.policies ?? []) {
|
|
308
|
+
for (const permission of policy.permissions ?? []) {
|
|
309
|
+
permissions.add(permission);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
for (const path of ["src/policies.ts", "src/policies.workos.ts"]) {
|
|
313
|
+
const text = readText(workspaceRoot, path);
|
|
314
|
+
for (const match of text.matchAll(/canPermission\s*\(([^)]*)\)/g)) {
|
|
315
|
+
for (const value of quotedValues(match[1] ?? "")) {
|
|
316
|
+
permissions.add(value);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return uniqueSorted(permissions);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function singularResourceName(name: string): string {
|
|
324
|
+
if (name.endsWith("ies")) {
|
|
325
|
+
return `${name.slice(0, -3)}y`;
|
|
326
|
+
}
|
|
327
|
+
if (name.endsWith("ses")) {
|
|
328
|
+
return name.slice(0, -2);
|
|
329
|
+
}
|
|
330
|
+
if (name.endsWith("s") && name.length > 1) {
|
|
331
|
+
return name.slice(0, -1);
|
|
332
|
+
}
|
|
333
|
+
return name;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function collectExpectedResourceTypes(workspaceRoot: string): string[] {
|
|
337
|
+
const dataGraph = readJson(workspaceRoot, `${GENERATED_DIR}/dataGraph.json`) as {
|
|
338
|
+
tables?: Array<{ name?: string; fields?: Array<{ name?: string }> }>;
|
|
339
|
+
} | null;
|
|
340
|
+
const agentContract = readJson(workspaceRoot, `${GENERATED_DIR}/agentContract.json`) as {
|
|
341
|
+
auth?: { requiresTenant?: boolean };
|
|
342
|
+
} | null;
|
|
343
|
+
const resourceTypes = new Set<string>();
|
|
344
|
+
const tenantScopedTables = (dataGraph?.tables ?? []).filter((table) =>
|
|
345
|
+
(table.fields ?? []).some((field) => field.name === "tenantId")
|
|
346
|
+
);
|
|
347
|
+
if (agentContract?.auth?.requiresTenant || tenantScopedTables.length > 0) {
|
|
348
|
+
resourceTypes.add("organization");
|
|
349
|
+
}
|
|
350
|
+
for (const table of tenantScopedTables) {
|
|
351
|
+
if (!table.name || ["organizations", "organization", "memberships", "membership"].includes(table.name)) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
resourceTypes.add(singularResourceName(table.name));
|
|
355
|
+
}
|
|
356
|
+
return uniqueSorted(resourceTypes);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function missingValues(expected: string[], actual: string[]): string[] {
|
|
360
|
+
const actualSet = new Set(actual);
|
|
361
|
+
return expected.filter((value) => !actualSet.has(value));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function collectWorkOSChecks(workspaceRoot: string): WorkOSCheck[] {
|
|
365
|
+
const packageJson = readJson(workspaceRoot, "package.json") as {
|
|
366
|
+
dependencies?: Record<string, string>;
|
|
367
|
+
devDependencies?: Record<string, string>;
|
|
368
|
+
} | null;
|
|
369
|
+
const authRegistry = readJson(workspaceRoot, `${GENERATED_DIR}/authRegistry.json`) as {
|
|
370
|
+
claims?: Record<string, string | undefined>;
|
|
371
|
+
} | null;
|
|
372
|
+
const secretRegistry = readJson(workspaceRoot, `${GENERATED_DIR}/secretRegistry.json`) as {
|
|
373
|
+
secrets?: Array<{ envVar?: string; name?: string }>;
|
|
374
|
+
} | null;
|
|
375
|
+
const secretNames = new Set((secretRegistry?.secrets ?? []).map((secret) => secret.envVar ?? secret.name));
|
|
376
|
+
const deps = {
|
|
377
|
+
...(packageJson?.dependencies ?? {}),
|
|
378
|
+
...(packageJson?.devDependencies ?? {}),
|
|
379
|
+
};
|
|
380
|
+
const webPackageJson = readJson(workspaceRoot, "web/package.json") as {
|
|
381
|
+
dependencies?: Record<string, string>;
|
|
382
|
+
devDependencies?: Record<string, string>;
|
|
383
|
+
} | null;
|
|
384
|
+
const webDeps = {
|
|
385
|
+
...(webPackageJson?.dependencies ?? {}),
|
|
386
|
+
...(webPackageJson?.devDependencies ?? {}),
|
|
387
|
+
};
|
|
388
|
+
const hasWeb = webPackageJson !== null;
|
|
389
|
+
const seed = parseSeedFile(workspaceRoot);
|
|
390
|
+
const activePermissions = collectPolicyPermissions(workspaceRoot);
|
|
391
|
+
const expectedResourceTypes = collectExpectedResourceTypes(workspaceRoot);
|
|
392
|
+
const missingSeedPermissions = missingValues(activePermissions, seed.permissions);
|
|
393
|
+
const missingSeedResources = missingValues(expectedResourceTypes, seed.resourceTypes);
|
|
394
|
+
const realEnv = readRealEnv(workspaceRoot);
|
|
395
|
+
const authMode = realEnv.FORGE_AUTH_MODE;
|
|
396
|
+
const productionAuthEnabled = authMode === "oidc" || authMode === "jwt";
|
|
397
|
+
const issuerConfigured = hasValue(realEnv, "FORGE_AUTH_ISSUER");
|
|
398
|
+
const jwksConfigured = hasValue(realEnv, "FORGE_AUTH_JWKS_URI");
|
|
399
|
+
const clientId = realEnv.WORKOS_CLIENT_ID || realEnv.VITE_WORKOS_CLIENT_ID;
|
|
400
|
+
const authRoutes = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/auth-routes.ts`);
|
|
401
|
+
const fga = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/fga.ts`);
|
|
402
|
+
const resourceMap = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/resource-map.ts`);
|
|
403
|
+
const httpHandler = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/http-handler.ts`);
|
|
404
|
+
const policies = readText(workspaceRoot, "src/policies.workos.ts");
|
|
405
|
+
const session = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/session.ts`);
|
|
406
|
+
const webhook = readText(workspaceRoot, `${GENERATED_DIR}/integrations/workos/webhook.ts`);
|
|
407
|
+
const generatedFrontendAuthBridge = readText(workspaceRoot, "web/src/lib/workos-auth.tsx");
|
|
408
|
+
const frontendAppShell = [
|
|
409
|
+
readText(workspaceRoot, "web/src/main.tsx"),
|
|
410
|
+
readText(workspaceRoot, "web/src/App.tsx"),
|
|
411
|
+
].join("\n");
|
|
412
|
+
|
|
413
|
+
return [
|
|
414
|
+
{
|
|
415
|
+
name: "package",
|
|
416
|
+
ok: "@workos-inc/node" in deps,
|
|
417
|
+
detail: "@workos-inc/node is present in package dependencies",
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
name: "auth-registry",
|
|
421
|
+
ok: authRegistry?.claims?.tenantId === "organization_id" && authRegistry?.claims?.userId === "sub",
|
|
422
|
+
detail: "authRegistry maps sub and organization_id claims",
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
name: "secrets",
|
|
426
|
+
ok: ["WORKOS_API_KEY", "WORKOS_CLIENT_ID", "WORKOS_COOKIE_PASSWORD"].every((name) => secretNames.has(name)),
|
|
427
|
+
detail: "required WorkOS secret names are registered",
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
name: "env-example",
|
|
431
|
+
ok: exists(workspaceRoot, ".env.example") &&
|
|
432
|
+
includesAll(readRawText(workspaceRoot, ".env.example"), [
|
|
433
|
+
"FORGE_AUTH_MODE=oidc",
|
|
434
|
+
"FORGE_AUTH_ISSUER=https://api.workos.com",
|
|
435
|
+
"FORGE_AUTH_JWKS_URI=",
|
|
436
|
+
"VITE_WORKOS_CLIENT_ID=",
|
|
437
|
+
"VITE_WORKOS_REDIRECT_URI=",
|
|
438
|
+
]),
|
|
439
|
+
detail: ".env.example exists with Forge OIDC and browser AuthKit variables",
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: "production-auth-readiness",
|
|
443
|
+
ok: !productionAuthEnabled || (issuerConfigured && jwksConfigured),
|
|
444
|
+
detail: !productionAuthEnabled
|
|
445
|
+
? "production OIDC/JWT env not enabled in .env/.env.local"
|
|
446
|
+
: issuerConfigured && jwksConfigured
|
|
447
|
+
? "FORGE_AUTH_MODE uses production auth and issuer/JWKS are configured"
|
|
448
|
+
: `FORGE_AUTH_MODE=${authMode} requires FORGE_AUTH_ISSUER=https://api.workos.com and FORGE_AUTH_JWKS_URI=${workosJwksUri(clientId)}`,
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
name: "browser-authkit-env",
|
|
452
|
+
ok: !hasWeb || (hasValue(realEnv, "VITE_WORKOS_CLIENT_ID") || readRawText(workspaceRoot, ".env.example").includes("VITE_WORKOS_CLIENT_ID=")) &&
|
|
453
|
+
(hasValue(realEnv, "VITE_WORKOS_REDIRECT_URI") || readRawText(workspaceRoot, ".env.example").includes("VITE_WORKOS_REDIRECT_URI=")),
|
|
454
|
+
detail: hasWeb
|
|
455
|
+
? "web workspace has VITE_WORKOS_CLIENT_ID and VITE_WORKOS_REDIRECT_URI guidance for AuthKit React"
|
|
456
|
+
: "no web workspace detected",
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
name: "browser-authkit-package",
|
|
460
|
+
ok: !hasWeb || "@workos-inc/authkit-react" in webDeps,
|
|
461
|
+
detail: hasWeb
|
|
462
|
+
? "@workos-inc/authkit-react is present in web/package.json"
|
|
463
|
+
: "no web workspace detected",
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
name: "browser-authkit-bridge",
|
|
467
|
+
ok: !hasWeb || includesAll(generatedFrontendAuthBridge, ["AuthKitProvider", "getAccessToken", "ForgeProvider"]),
|
|
468
|
+
detail: hasWeb
|
|
469
|
+
? "generated web/src/lib/workos-auth.tsx bridge provides AuthKitProvider and ForgeProvider token wiring"
|
|
470
|
+
: "no web workspace detected",
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: "browser-authkit-provider",
|
|
474
|
+
ok: !hasWeb || includesAll(frontendAppShell, ["AuthKitProvider", "getAccessToken", "ForgeProvider"]),
|
|
475
|
+
detail: hasWeb
|
|
476
|
+
? "web app shell mounts AuthKitProvider and forwards getAccessToken to ForgeProvider"
|
|
477
|
+
: "no web workspace detected",
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
name: "seed-file",
|
|
481
|
+
ok: seed.exists,
|
|
482
|
+
detail: seed.exists
|
|
483
|
+
? `${seed.path} exists with ${seed.permissions.length} permission(s), ${seed.roles.length} role(s), ${seed.resourceTypes.length} resource type(s)`
|
|
484
|
+
: `${DEFAULT_SEED_FILE} or ${GENERATED_SEED_FILE} is required`,
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
name: "seed-organizations",
|
|
488
|
+
ok: seed.organizations.length > 0,
|
|
489
|
+
detail: seed.organizations.length > 0
|
|
490
|
+
? `seed file contains organization(s): ${seed.organizations.join(", ")}`
|
|
491
|
+
: "seed file should contain at least one demo organization",
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
name: "seed-roles-permissions",
|
|
495
|
+
ok: seed.roles.length > 0 &&
|
|
496
|
+
(activePermissions.length === 0 ? seed.permissions.length > 0 : missingSeedPermissions.length === 0),
|
|
497
|
+
detail: missingSeedPermissions.length === 0
|
|
498
|
+
? `seed covers ${activePermissions.length} active policy permission(s) with role(s): ${seed.roles.join(", ")}`
|
|
499
|
+
: `seed missing active policy permission(s): ${missingSeedPermissions.join(", ")}`,
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
name: "seed-resource-types",
|
|
503
|
+
ok: seed.resourceTypes.length > 0 &&
|
|
504
|
+
(expectedResourceTypes.length === 0 || missingSeedResources.length === 0),
|
|
505
|
+
detail: missingSeedResources.length === 0
|
|
506
|
+
? `seed resource_types cover app graph: ${expectedResourceTypes.length > 0 ? expectedResourceTypes.join(", ") : seed.resourceTypes.join(", ")}`
|
|
507
|
+
: `seed missing resource_type(s) for app graph: ${missingSeedResources.join(", ")}`,
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
name: "seed-auth-config",
|
|
511
|
+
ok: seed.redirectUris.length > 0 && seed.corsOrigins.length > 0 && Boolean(seed.homepageUrl),
|
|
512
|
+
detail: seed.redirectUris.length > 0 && seed.corsOrigins.length > 0 && Boolean(seed.homepageUrl)
|
|
513
|
+
? `seed config contains ${seed.redirectUris.length} redirect URI(s), ${seed.corsOrigins.length} CORS origin(s), and homepage URL`
|
|
514
|
+
: "seed config should include redirect_uris, cors_origins, and homepage_url for no-dashboard setup",
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
name: "authkit-routes",
|
|
518
|
+
ok: includesAll(authRoutes, ["handleWorkOSAuthRequest", "/login", "/callback", "/logout", "/session"]),
|
|
519
|
+
detail: "AuthKit Request/Response route helper exists for login, callback, logout, and session",
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
name: "authkit-session",
|
|
523
|
+
ok: includesAll(session, ["encodeWorkOSSession", "decodeWorkOSSession", "workOSSessionToClaims"]),
|
|
524
|
+
detail: "AuthKit session helper signs cookies and maps sessions to Forge claims",
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
name: "webhook-helper",
|
|
528
|
+
ok: includesAll(webhook, ["verifyWorkOSWebhook", "handleWorkOSWebhook", 'provider: "workos"']),
|
|
529
|
+
detail: "WorkOS webhook verifier/helper exists and uses WorkOS signature verification",
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
name: "webhook-http-handler",
|
|
533
|
+
ok: includesAll(httpHandler, ["handleWorkOSWebhookRequest", "workosWebhookHttpRoute", "/webhooks/workos"]),
|
|
534
|
+
detail: "WorkOS Request/Response webhook handler exists for POST /webhooks/workos",
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
name: "fga-bridge",
|
|
538
|
+
ok: includesAll(resourceMap, [
|
|
539
|
+
"canWorkOS",
|
|
540
|
+
"assertWorkOSResourceTenant",
|
|
541
|
+
"FORGE_WORKOS_CROSS_TENANT_RESOURCE",
|
|
542
|
+
"syncWorkOSResourceGraph",
|
|
543
|
+
"workOSResourceRecords",
|
|
544
|
+
"ForgeWorkOSFgaDecisionCache",
|
|
545
|
+
"permissionSlug",
|
|
546
|
+
"resourceExternalId",
|
|
547
|
+
]) &&
|
|
548
|
+
includesAll(fga, ["forgeWorkOSResourceTypes"]),
|
|
549
|
+
detail: "WorkOS FGA resource-map bridge exists with sync, cache, telemetry, official check shape, and cross-tenant guard",
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
name: "policies",
|
|
553
|
+
ok: exists(workspaceRoot, "src/policies.workos.ts") &&
|
|
554
|
+
policies.includes("canPermission") &&
|
|
555
|
+
missingValues(activePermissions, quotedValues(policies)).length === 0,
|
|
556
|
+
detail: activePermissions.length === 0
|
|
557
|
+
? "WorkOS-derived Forge policy template exists and is permission-first"
|
|
558
|
+
: `WorkOS-derived policy template covers active permission(s): ${activePermissions.join(", ")}`,
|
|
559
|
+
},
|
|
560
|
+
];
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function prepareSeedFileForWorkOSCli(
|
|
564
|
+
workspaceRoot: string,
|
|
565
|
+
file: string,
|
|
566
|
+
): { file: string; sanitized: boolean; cleanup: () => void } {
|
|
567
|
+
const raw = readRawText(workspaceRoot, file);
|
|
568
|
+
const stripped = stripDeterministicHeader(raw);
|
|
569
|
+
if (!raw || raw === stripped) {
|
|
570
|
+
return { file, sanitized: false, cleanup: () => undefined };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const tempDir = mkdtempSync(join(tmpdir(), "forge-workos-seed-"));
|
|
574
|
+
const preparedFile = join(tempDir, basename(file));
|
|
575
|
+
writeFileSync(preparedFile, stripped, "utf8");
|
|
576
|
+
return {
|
|
577
|
+
file: preparedFile,
|
|
578
|
+
sanitized: true,
|
|
579
|
+
cleanup: () => rmSync(tempDir, { recursive: true, force: true }),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function isWorkOSSeedAlreadyApplied(stdout: string, stderr: string): boolean {
|
|
584
|
+
const output = `${stdout}\n${stderr}`;
|
|
585
|
+
return /(?:permission|role|resource type|organization|slug|domain)[^\n]*(?:already in use|already exists|exists already)|already in use/i
|
|
586
|
+
.test(output);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function seedData(input: {
|
|
590
|
+
seed: WorkOSSeedSummary;
|
|
591
|
+
activePermissions: string[];
|
|
592
|
+
expectedResourceTypes: string[];
|
|
593
|
+
unusedSeedPermissions: string[];
|
|
594
|
+
dryRun?: boolean;
|
|
595
|
+
seedFileSanitized?: boolean;
|
|
596
|
+
seedAlreadyApplied?: boolean;
|
|
597
|
+
configActions?: WorkOSConfigActionResult[];
|
|
598
|
+
nextCommand?: string;
|
|
599
|
+
}): Record<string, unknown> {
|
|
600
|
+
return {
|
|
601
|
+
seed: input.seed,
|
|
602
|
+
activePermissions: input.activePermissions,
|
|
603
|
+
expectedResourceTypes: input.expectedResourceTypes,
|
|
604
|
+
unusedSeedPermissions: input.unusedSeedPermissions,
|
|
605
|
+
dryRun: input.dryRun ?? false,
|
|
606
|
+
seedFileSanitized: input.seedFileSanitized ?? false,
|
|
607
|
+
seedAlreadyApplied: input.seedAlreadyApplied ?? false,
|
|
608
|
+
configActions: input.configActions ?? [],
|
|
609
|
+
...(input.nextCommand ? { nextCommand: input.nextCommand } : {}),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export interface WorkOSConfigActionResult {
|
|
614
|
+
name: string;
|
|
615
|
+
command?: string[];
|
|
616
|
+
ok: boolean;
|
|
617
|
+
skipped: boolean;
|
|
618
|
+
reason?: string;
|
|
619
|
+
stdout?: string;
|
|
620
|
+
stderr?: string;
|
|
621
|
+
status?: number | null;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function configActionsForSeed(seed: WorkOSSeedSummary): Array<{ name: string; command: string[]; skipReason?: string }> {
|
|
625
|
+
const actions: Array<{ name: string; command: string[]; skipReason?: string }> = [];
|
|
626
|
+
for (const uri of seed.redirectUris) {
|
|
627
|
+
actions.push({
|
|
628
|
+
name: "redirect-uri",
|
|
629
|
+
command: ["npx", "--yes", "workos@latest", "config", "redirect", "add", uri],
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
for (const origin of seed.corsOrigins) {
|
|
633
|
+
actions.push({
|
|
634
|
+
name: "cors-origin",
|
|
635
|
+
command: ["npx", "--yes", "workos@latest", "config", "cors", "add", origin],
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
if (seed.homepageUrl) {
|
|
639
|
+
actions.push({
|
|
640
|
+
name: "homepage-url",
|
|
641
|
+
command: ["npx", "--yes", "workos@latest", "config", "homepage-url", "set", seed.homepageUrl],
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
for (const endpoint of seed.webhookEndpoints) {
|
|
645
|
+
const events = endpoint.events.length > 0 ? endpoint.events.join(",") : "user.created,organization_membership.updated";
|
|
646
|
+
actions.push({
|
|
647
|
+
name: "webhook",
|
|
648
|
+
command: ["npx", "--yes", "workos@latest", "webhook", "create", "--url", endpoint.url, "--events", events],
|
|
649
|
+
skipReason: endpoint.url.startsWith("https://")
|
|
650
|
+
? undefined
|
|
651
|
+
: "WorkOS hosted webhook endpoints require HTTPS; use a tunnel or production URL for this endpoint.",
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
return actions;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function runWorkOSConfigActions(
|
|
658
|
+
seed: WorkOSSeedSummary,
|
|
659
|
+
options: WorkOSCommandOptions,
|
|
660
|
+
dryRun: boolean,
|
|
661
|
+
): WorkOSConfigActionResult[] {
|
|
662
|
+
return configActionsForSeed(seed).map((action) => {
|
|
663
|
+
if (action.skipReason) {
|
|
664
|
+
return {
|
|
665
|
+
name: action.name,
|
|
666
|
+
command: action.command,
|
|
667
|
+
ok: true,
|
|
668
|
+
skipped: true,
|
|
669
|
+
reason: action.skipReason,
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
if (dryRun) {
|
|
673
|
+
return {
|
|
674
|
+
name: action.name,
|
|
675
|
+
command: action.command,
|
|
676
|
+
ok: true,
|
|
677
|
+
skipped: true,
|
|
678
|
+
reason: "dry-run",
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
const child = runExternalCommand(action.command, options);
|
|
682
|
+
return {
|
|
683
|
+
name: action.name,
|
|
684
|
+
command: action.command,
|
|
685
|
+
ok: child.status === 0 || isWorkOSSeedAlreadyApplied(child.stdout, child.stderr),
|
|
686
|
+
skipped: false,
|
|
687
|
+
stdout: child.stdout,
|
|
688
|
+
stderr: child.stderr,
|
|
689
|
+
status: child.status,
|
|
690
|
+
...(child.status !== 0 && isWorkOSSeedAlreadyApplied(child.stdout, child.stderr)
|
|
691
|
+
? { reason: "already-applied" }
|
|
692
|
+
: {}),
|
|
693
|
+
};
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export function runWorkOSDoctorCommand(options: WorkOSCommandOptions): WorkOSCommandResult {
|
|
698
|
+
const checks = collectWorkOSChecks(options.workspaceRoot);
|
|
699
|
+
const ok = checks.every((check) => check.ok);
|
|
700
|
+
const command = ["npx", "--yes", "workos@latest", "doctor"];
|
|
701
|
+
if (!ok) {
|
|
702
|
+
return {
|
|
703
|
+
ok: false,
|
|
704
|
+
kind: "workos-doctor",
|
|
705
|
+
checks,
|
|
706
|
+
command,
|
|
707
|
+
applied: false,
|
|
708
|
+
exitCode: 1,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
if (!options.yes || options.dryRun) {
|
|
712
|
+
return {
|
|
713
|
+
ok: true,
|
|
714
|
+
kind: "workos-doctor",
|
|
715
|
+
checks,
|
|
716
|
+
command,
|
|
717
|
+
applied: false,
|
|
718
|
+
exitCode: 0,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const child = runExternalCommand(command, options);
|
|
722
|
+
return {
|
|
723
|
+
ok: child.status === 0,
|
|
724
|
+
kind: "workos-doctor",
|
|
725
|
+
checks,
|
|
726
|
+
command,
|
|
727
|
+
applied: child.status === 0,
|
|
728
|
+
stdout: child.stdout,
|
|
729
|
+
stderr: child.stderr,
|
|
730
|
+
exitCode: child.status === 0 ? 0 : 1,
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export function runWorkOSInstallCommand(options: WorkOSCommandOptions): WorkOSCommandResult {
|
|
735
|
+
const checks = collectWorkOSChecks(options.workspaceRoot);
|
|
736
|
+
const ok = checks.every((check) => check.ok);
|
|
737
|
+
const command = ["npx", "--yes", "workos@latest", "install"];
|
|
738
|
+
if (!ok) {
|
|
739
|
+
return {
|
|
740
|
+
ok: false,
|
|
741
|
+
kind: "workos-install",
|
|
742
|
+
checks,
|
|
743
|
+
command,
|
|
744
|
+
applied: false,
|
|
745
|
+
exitCode: 1,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
if (!options.yes || options.dryRun) {
|
|
749
|
+
return {
|
|
750
|
+
ok: true,
|
|
751
|
+
kind: "workos-install",
|
|
752
|
+
checks,
|
|
753
|
+
command,
|
|
754
|
+
applied: false,
|
|
755
|
+
exitCode: 0,
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
const child = runExternalCommand(command, options);
|
|
759
|
+
return {
|
|
760
|
+
ok: child.status === 0,
|
|
761
|
+
kind: "workos-install",
|
|
762
|
+
checks,
|
|
763
|
+
command,
|
|
764
|
+
applied: child.status === 0,
|
|
765
|
+
stdout: child.stdout,
|
|
766
|
+
stderr: child.stderr,
|
|
767
|
+
exitCode: child.status === 0 ? 0 : 1,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
export function runWorkOSSeedCommand(options: WorkOSCommandOptions): WorkOSCommandResult {
|
|
772
|
+
const file = options.file ?? DEFAULT_SEED_FILE;
|
|
773
|
+
const seed = parseSeedFile(options.workspaceRoot, file);
|
|
774
|
+
const activePermissions = collectPolicyPermissions(options.workspaceRoot);
|
|
775
|
+
const expectedResourceTypes = collectExpectedResourceTypes(options.workspaceRoot);
|
|
776
|
+
const missingSeedPermissions = missingValues(activePermissions, seed.permissions);
|
|
777
|
+
const missingSeedResources = missingValues(expectedResourceTypes, seed.resourceTypes);
|
|
778
|
+
const unusedSeedPermissions = seed.permissions.filter((permission) => !activePermissions.includes(permission));
|
|
779
|
+
const checks = [
|
|
780
|
+
{
|
|
781
|
+
name: "seed-file",
|
|
782
|
+
ok: seed.exists,
|
|
783
|
+
detail: seed.exists ? `${seed.path} exists` : `${file} exists`,
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
name: "seed-yaml-shape",
|
|
787
|
+
ok: seed.valid,
|
|
788
|
+
detail: seed.valid ? "seed contains permissions, roles, resource_types, and organizations" : seed.diagnostics.join("; "),
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
name: "seed-policy-coverage",
|
|
792
|
+
ok: missingSeedPermissions.length === 0,
|
|
793
|
+
detail: missingSeedPermissions.length === 0
|
|
794
|
+
? `seed covers ${activePermissions.length} active permission(s)`
|
|
795
|
+
: `seed missing active permission(s): ${missingSeedPermissions.join(", ")}`,
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
name: "seed-resource-coverage",
|
|
799
|
+
ok: missingSeedResources.length === 0,
|
|
800
|
+
detail: missingSeedResources.length === 0
|
|
801
|
+
? `seed covers app resource type(s): ${expectedResourceTypes.join(", ") || "none required"}`
|
|
802
|
+
: `seed missing resource type(s): ${missingSeedResources.join(", ")}`,
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
name: "seed-hosted-config",
|
|
806
|
+
ok: seed.redirectUris.length > 0 && seed.corsOrigins.length > 0 && Boolean(seed.homepageUrl),
|
|
807
|
+
detail: seed.redirectUris.length > 0 && seed.corsOrigins.length > 0 && Boolean(seed.homepageUrl)
|
|
808
|
+
? `seed includes hosted config: ${seed.redirectUris.length} redirect URI(s), ${seed.corsOrigins.length} CORS origin(s), homepage ${seed.homepageUrl}`
|
|
809
|
+
: "seed should include config.redirect_uris, config.cors_origins, and config.homepage_url",
|
|
810
|
+
},
|
|
811
|
+
];
|
|
812
|
+
const command = ["npx", "--yes", "workos@latest", "seed", "--file", file];
|
|
813
|
+
if (!checks.every((check) => check.ok)) {
|
|
814
|
+
return {
|
|
815
|
+
ok: false,
|
|
816
|
+
kind: "workos-seed",
|
|
817
|
+
checks,
|
|
818
|
+
command,
|
|
819
|
+
applied: false,
|
|
820
|
+
data: seedData({ seed, activePermissions, expectedResourceTypes, unusedSeedPermissions }),
|
|
821
|
+
exitCode: 1,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
if (options.dryRun) {
|
|
825
|
+
const configActions = runWorkOSConfigActions(seed, options, true);
|
|
826
|
+
return {
|
|
827
|
+
ok: true,
|
|
828
|
+
kind: "workos-seed",
|
|
829
|
+
checks,
|
|
830
|
+
command,
|
|
831
|
+
applied: false,
|
|
832
|
+
data: seedData({
|
|
833
|
+
seed,
|
|
834
|
+
activePermissions,
|
|
835
|
+
expectedResourceTypes,
|
|
836
|
+
unusedSeedPermissions,
|
|
837
|
+
dryRun: true,
|
|
838
|
+
configActions,
|
|
839
|
+
nextCommand: `forge workos seed --file ${file} --json`,
|
|
840
|
+
}),
|
|
841
|
+
exitCode: 0,
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
const preparedSeed = prepareSeedFileForWorkOSCli(options.workspaceRoot, seed.path);
|
|
845
|
+
const delegatedCommand = [
|
|
846
|
+
"npx",
|
|
847
|
+
"--yes",
|
|
848
|
+
"workos@latest",
|
|
849
|
+
"seed",
|
|
850
|
+
"--file",
|
|
851
|
+
preparedSeed.file,
|
|
852
|
+
];
|
|
853
|
+
try {
|
|
854
|
+
const configActions = runWorkOSConfigActions(seed, options, false);
|
|
855
|
+
const configOk = configActions.every((action) => action.ok);
|
|
856
|
+
if (!configOk) {
|
|
857
|
+
return {
|
|
858
|
+
ok: false,
|
|
859
|
+
kind: "workos-seed",
|
|
860
|
+
checks,
|
|
861
|
+
command: configActions.find((action) => !action.ok)?.command ?? delegatedCommand,
|
|
862
|
+
applied: false,
|
|
863
|
+
data: seedData({
|
|
864
|
+
seed,
|
|
865
|
+
activePermissions,
|
|
866
|
+
expectedResourceTypes,
|
|
867
|
+
unusedSeedPermissions,
|
|
868
|
+
seedFileSanitized: preparedSeed.sanitized,
|
|
869
|
+
configActions,
|
|
870
|
+
}),
|
|
871
|
+
exitCode: 1,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
const child = runExternalCommand(delegatedCommand, options);
|
|
875
|
+
const seedAlreadyApplied =
|
|
876
|
+
child.status !== 0 && isWorkOSSeedAlreadyApplied(child.stdout, child.stderr);
|
|
877
|
+
return {
|
|
878
|
+
ok: child.status === 0 || seedAlreadyApplied,
|
|
879
|
+
kind: "workos-seed",
|
|
880
|
+
checks,
|
|
881
|
+
command: delegatedCommand,
|
|
882
|
+
applied: child.status === 0,
|
|
883
|
+
data: seedData({
|
|
884
|
+
seed,
|
|
885
|
+
activePermissions,
|
|
886
|
+
expectedResourceTypes,
|
|
887
|
+
unusedSeedPermissions,
|
|
888
|
+
seedFileSanitized: preparedSeed.sanitized,
|
|
889
|
+
seedAlreadyApplied,
|
|
890
|
+
configActions,
|
|
891
|
+
}),
|
|
892
|
+
stdout: child.stdout,
|
|
893
|
+
stderr: child.stderr,
|
|
894
|
+
exitCode: child.status === 0 || seedAlreadyApplied ? 0 : 1,
|
|
895
|
+
};
|
|
896
|
+
} finally {
|
|
897
|
+
preparedSeed.cleanup();
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
export function runWorkOSSetupCommand(options: WorkOSCommandOptions): WorkOSCommandResult {
|
|
902
|
+
const file = options.file ?? DEFAULT_SEED_FILE;
|
|
903
|
+
const checks = collectWorkOSChecks(options.workspaceRoot);
|
|
904
|
+
const localOk = checks.every((check) => check.ok);
|
|
905
|
+
const seed = parseSeedFile(options.workspaceRoot, file);
|
|
906
|
+
const setupDryRun = options.dryRun || !options.real;
|
|
907
|
+
const configActions = runWorkOSConfigActions(seed, options, true);
|
|
908
|
+
const command = ["npx", "--yes", "workos@latest", "seed", "--file", file];
|
|
909
|
+
if (!localOk) {
|
|
910
|
+
return {
|
|
911
|
+
ok: false,
|
|
912
|
+
kind: "workos-setup",
|
|
913
|
+
checks,
|
|
914
|
+
command,
|
|
915
|
+
applied: false,
|
|
916
|
+
data: {
|
|
917
|
+
dryRun: setupDryRun,
|
|
918
|
+
real: options.real ?? false,
|
|
919
|
+
seed,
|
|
920
|
+
configActions,
|
|
921
|
+
nextCommand: "forge workos doctor --json",
|
|
922
|
+
},
|
|
923
|
+
exitCode: 1,
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
if (setupDryRun) {
|
|
927
|
+
return {
|
|
928
|
+
ok: true,
|
|
929
|
+
kind: "workos-setup",
|
|
930
|
+
checks,
|
|
931
|
+
command,
|
|
932
|
+
applied: false,
|
|
933
|
+
data: {
|
|
934
|
+
dryRun: true,
|
|
935
|
+
real: false,
|
|
936
|
+
seed,
|
|
937
|
+
configActions,
|
|
938
|
+
nextCommand: `forge workos setup --real --file ${file} --json`,
|
|
939
|
+
},
|
|
940
|
+
exitCode: 0,
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
const seedResult = runWorkOSSeedCommand({
|
|
944
|
+
...options,
|
|
945
|
+
subcommand: "seed",
|
|
946
|
+
dryRun: false,
|
|
947
|
+
file,
|
|
948
|
+
});
|
|
949
|
+
return {
|
|
950
|
+
ok: seedResult.ok,
|
|
951
|
+
kind: "workos-setup",
|
|
952
|
+
checks: seedResult.checks,
|
|
953
|
+
command: seedResult.command,
|
|
954
|
+
applied: seedResult.ok,
|
|
955
|
+
data: {
|
|
956
|
+
real: true,
|
|
957
|
+
seed,
|
|
958
|
+
seedResult: seedResult.data,
|
|
959
|
+
nextCommand: "forge workos doctor --json",
|
|
960
|
+
},
|
|
961
|
+
stdout: seedResult.stdout,
|
|
962
|
+
stderr: seedResult.stderr,
|
|
963
|
+
exitCode: seedResult.exitCode,
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
export function runWorkOSCommand(options: WorkOSCommandOptions): WorkOSCommandResult {
|
|
968
|
+
if (options.subcommand === "install") {
|
|
969
|
+
return runWorkOSInstallCommand(options);
|
|
970
|
+
}
|
|
971
|
+
if (options.subcommand === "setup") {
|
|
972
|
+
return runWorkOSSetupCommand(options);
|
|
973
|
+
}
|
|
974
|
+
return options.subcommand === "doctor"
|
|
975
|
+
? runWorkOSDoctorCommand(options)
|
|
976
|
+
: runWorkOSSeedCommand(options);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
export function formatWorkOSJson(result: WorkOSCommandResult): string {
|
|
980
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
export function formatWorkOSHuman(result: WorkOSCommandResult): string {
|
|
984
|
+
const lines = [
|
|
985
|
+
result.ok ? "WorkOS: ok" : "WorkOS: needs attention",
|
|
986
|
+
...result.checks.map((check) => `${check.ok ? "ok" : "fail"} ${check.name}: ${check.detail}`),
|
|
987
|
+
];
|
|
988
|
+
if (result.command) {
|
|
989
|
+
lines.push(`command: ${result.command.join(" ")}`);
|
|
990
|
+
}
|
|
991
|
+
if (result.kind === "workos-install" && !result.applied) {
|
|
992
|
+
lines.push("AuthKit install not applied; pass --yes to execute the WorkOS CLI command");
|
|
993
|
+
}
|
|
994
|
+
if (result.kind === "workos-doctor" && !result.applied) {
|
|
995
|
+
lines.push("external WorkOS doctor not run; pass --yes to execute the WorkOS CLI command");
|
|
996
|
+
}
|
|
997
|
+
if (result.kind === "workos-seed" && !result.applied) {
|
|
998
|
+
const data = result.data && typeof result.data === "object"
|
|
999
|
+
? result.data as { dryRun?: boolean; seedAlreadyApplied?: boolean; nextCommand?: string }
|
|
1000
|
+
: {};
|
|
1001
|
+
if (data.seedAlreadyApplied) {
|
|
1002
|
+
lines.push("seed already appears applied; WorkOS reported existing resources");
|
|
1003
|
+
} else if (data.dryRun) {
|
|
1004
|
+
lines.push(`seed dry-run only; run ${data.nextCommand ?? "forge workos seed --file workos-seed.yml --json"} to execute the WorkOS CLI command`);
|
|
1005
|
+
} else {
|
|
1006
|
+
lines.push("seed not applied; inspect stdout/stderr from the WorkOS CLI command");
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (result.kind === "workos-setup" && !result.applied) {
|
|
1010
|
+
const data = result.data && typeof result.data === "object"
|
|
1011
|
+
? result.data as { dryRun?: boolean; nextCommand?: string }
|
|
1012
|
+
: {};
|
|
1013
|
+
if (data.dryRun) {
|
|
1014
|
+
lines.push(`setup dry-run only; run ${data.nextCommand ?? "forge workos setup --real --file workos-seed.yml --json"} to apply hosted config`);
|
|
1015
|
+
} else {
|
|
1016
|
+
lines.push("setup not applied; inspect WorkOS checks and seed/config action output");
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
return `${lines.join("\n")}\n`;
|
|
1020
|
+
}
|