forgeos 0.1.0-alpha.55 → 0.1.0-alpha.57

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.md CHANGED
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.55 input=bbd63535f88ad4d382f6da1f26a2898e49fa00849042fcd683e316054e3b3959 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
1
+ // @forge-generated generator=0.1.0-alpha.57 input=7128597ce3f6cd158bf4361cebb1eef8678c19ff9b2cb60c454a0876356d37c6 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
2
2
  # AGENTS.md
3
3
 
4
4
  <!-- forge-generated:start -->
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # forgeos
2
2
 
3
+ ## 0.1.0-alpha.57
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix the WorkOS AuthKit local and tunneled development flow.
8
+
9
+ - Add `response_type=code` to Forge-owned WorkOS authorization URLs and avoid sending a default `organization_id` during login so AuthKit can present normal sign-in and organization selection.
10
+ - Resolve WorkOS organization and membership claims during callback when AuthKit returns only the user/token, then expose Forge-normalized session claims through `/session`.
11
+ - Add `forge dev --public-api-url` for browser-facing API URLs behind tunnels and update the WorkOS React bridge to use backend-owned `/login`, `/session`, and `/logout` routes through same-origin/proxy-aware URL resolution.
12
+ - Sync public WorkOS browser env into `web/.env.local` during `forge add auth workos` and `forge workos setup --real`, and update WorkOS doctor/UI audit checks for the backend-owned AuthKit session flow.
13
+
14
+ ## 0.1.0-alpha.56
15
+
16
+ ### Patch Changes
17
+
18
+ - Add the production deploy golden path as an explicit CLI workflow.
19
+
20
+ - `forge deploy init --target docker` now acts as the canonical Docker
21
+ production bootstrap command for runtime files, `deploy/.env.production`
22
+ guidance, and production handoff docs.
23
+ - `forge deploy readiness --production --json` now summarizes whether an app
24
+ can publish, grouped blockers/warnings, readiness score, and the next
25
+ command an agent should run.
26
+ - `forge env doctor --target local|staging|production --json` now reports the
27
+ effective auth mode, database posture, provider detection, missing env
28
+ names, and safe source metadata without printing secret values.
29
+ - `forge deploy check --production --json` now includes blocking/warning
30
+ summaries and the readiness object used by the deploy gate.
31
+ - `forge deploy verify --production --url ... --json` now validates required
32
+ public auth metadata and probes optional runtime endpoints such as
33
+ `/ready`, `/live/status`, `/outbox/status`, and `/webhooks/workos`.
34
+ - Production docs now present the canonical flow:
35
+ `deploy init -> env doctor -> auth/auth.md/WorkOS proof -> field-test ->
36
+ readiness -> check -> package -> verify`, while keeping WorkOS FGA
37
+ explicitly optional unless the app opts into FGA artifacts.
38
+
3
39
  ## 0.1.0-alpha.55
4
40
 
5
41
  ### Patch Changes
@@ -90,7 +126,7 @@
90
126
  - Tighten the no-dashboard WorkOS real setup/proof handoff.
91
127
 
92
128
  - WorkOS hosted setup now records explicit `WORKOS_MODE=agent npx --yes
93
- workos@latest auth login --json` shell guidance when CLI login is required.
129
+ workos@latest auth login --json` shell guidance when CLI login is required.
94
130
  - Failed `forge workos setup --real` login attempts now tell the operator to
95
131
  rerun the same setup command after completing OAuth/device-code login,
96
132
  instead of pointing generically at `prove --real`.
@@ -251,7 +287,7 @@
251
287
  `forge dev --seed --all-tenants`, and seed readiness evidence in dev and
252
288
  field-test summaries.
253
289
  - Update templates, docs, and tests around the new `create app -> add auth ->
254
- field-test -> deploy` path.
290
+ field-test -> deploy` path.
255
291
 
256
292
  ## 0.1.0-alpha.41
257
293
 
package/docs/changelog.md CHANGED
@@ -6,6 +6,38 @@ The canonical source file in the repository is `CHANGELOG.md`.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.1.0-alpha.57
10
+
11
+ - Fixed the WorkOS/AuthKit login path used by local and tunneled development:
12
+ Forge-owned authorization URLs now include `response_type=code` and no longer
13
+ send a default `organization_id` unless explicitly requested.
14
+ - The WorkOS callback now resolves organization and membership claims when
15
+ AuthKit returns only the user/token, then exposes Forge-normalized session
16
+ claims through `/session`.
17
+ - Added `forge dev --public-api-url` so browser-facing API URLs can be injected
18
+ cleanly for SSH tunnels, Docker Desktop, and remote dev hosts.
19
+ - The generated WorkOS React bridge now uses backend-owned `/login`, `/session`,
20
+ and `/logout` routes through same-origin/proxy-aware URL resolution.
21
+ - `forge add auth workos`, `forge workos setup --real`, `workos doctor`, and
22
+ `inspect ui --ergonomics` now understand the backend-owned AuthKit session
23
+ flow and sync public browser env into `web/.env.local`.
24
+
25
+ ## 0.1.0-alpha.56
26
+
27
+ - Added `forge deploy init --target docker` as the canonical production Docker
28
+ bootstrap command.
29
+ - Added `forge deploy readiness --production --json` to answer whether an app
30
+ can publish, what blocks production, and which command should run next.
31
+ - Added `forge env doctor --target local|staging|production --json` for safe,
32
+ target-specific env source reporting without exposing secret values.
33
+ - `forge deploy check --production --json` now includes grouped
34
+ blockers/warnings and a readiness summary for agent handoffs.
35
+ - `forge deploy verify --production --url ... --json` now validates required
36
+ public auth metadata and optional operational endpoints.
37
+ - Production docs now describe the canonical flow from deploy init through
38
+ field-test, readiness, package, and verify, with WorkOS FGA kept optional
39
+ unless the app opts into FGA artifacts.
40
+
9
41
  ## 0.1.0-alpha.55
10
42
 
11
43
  - `forge add auth workos` now defaults to AuthKit/RBAC, permission claims,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgeos",
3
- "version": "0.1.0-alpha.55",
3
+ "version": "0.1.0-alpha.57",
4
4
  "description": "Agent-native application framework and compiler for building Forge apps without a mandatory dashboard.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +1 @@
1
- {"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.55","releaseId":"forgeos@0.1.0-alpha.55+unknown","schemaVersion":"0.1.0"}
1
+ {"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.57","releaseId":"forgeos@0.1.0-alpha.57+unknown","schemaVersion":"0.1.0"}
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.55 input=bbd63535f88ad4d382f6da1f26a2898e49fa00849042fcd683e316054e3b3959 content=c6fc38ddd436db8a660ed2e37713bc2d76c3d305cd1ebcb207de25ac1830b307
1
+ // @forge-generated generator=0.1.0-alpha.57 input=7128597ce3f6cd158bf4361cebb1eef8678c19ff9b2cb60c454a0876356d37c6 content=40c7132e14bfeffd8018f04de93db9ad294cfca4e190b21106d86bb80bc3bc03
2
2
  export const releaseManifest = {
3
3
  "defaultProvider": "local",
4
4
  "diagnostics": [],
@@ -19,7 +19,7 @@ export const releaseManifest = {
19
19
  "custom"
20
20
  ],
21
21
  "packageName": "forgeos",
22
- "packageVersion": "0.1.0-alpha.55",
23
- "releaseId": "forgeos@0.1.0-alpha.55+unknown",
22
+ "packageVersion": "0.1.0-alpha.57",
23
+ "releaseId": "forgeos@0.1.0-alpha.57+unknown",
24
24
  "schemaVersion": "0.1.0"
25
25
  } as const;
@@ -180,11 +180,16 @@ function permissionSubsetForRestrictedTenant(permissions: string[]): string[] {
180
180
  function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof detectWorkOS>, requiresTenant: boolean) {
181
181
  const rootSeedPresent = existsSync(join(workspaceRoot, "workos-seed.yml"));
182
182
  const generatedSeedPresent = existsSync(join(workspaceRoot, "src/forge/_generated/integrations/workos/workos-seed.yml"));
183
+ const fgaArtifactsPresent =
184
+ existsSync(join(workspaceRoot, "src/forge/_generated/integrations/workos/fga.ts")) ||
185
+ existsSync(join(workspaceRoot, "src/forge/_generated/integrations/workos/resource-map.ts")) ||
186
+ existsSync(join(workspaceRoot, ".workos-fga-state.json"));
183
187
  const authMdPresent = existsSync(join(workspaceRoot, "public/auth.md"));
184
188
  const metadataPresent = existsSync(join(workspaceRoot, "public/.well-known/oauth-protected-resource"));
185
189
  const activePermissions = collectPolicyPermissions(workspaceRoot);
186
190
  const expectedResourceTypes = collectExpectedResourceTypes(workspaceRoot);
187
191
  const seed = parseSeedFile(workspaceRoot);
192
+ const fgaEnabled = fgaArtifactsPresent || seed.resourceTypes.length > 0;
188
193
  const missingSeedPermissions = missingValues(activePermissions, seed.permissions);
189
194
  const missingSeedResources = missingValues(expectedResourceTypes, seed.resourceTypes);
190
195
  const acmePermissions = activePermissions;
@@ -223,8 +228,10 @@ function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof
223
228
  },
224
229
  {
225
230
  id: "seed-resource-coverage",
226
- ok: missingSeedResources.length === 0,
227
- evidence: missingSeedResources.length === 0
231
+ ok: !fgaEnabled || missingSeedResources.length === 0,
232
+ evidence: !fgaEnabled
233
+ ? "FGA is not enabled; seed resource type coverage is not required for AuthKit/RBAC tenant isolation."
234
+ : missingSeedResources.length === 0
228
235
  ? `seed covers expected app resource type(s): ${expectedResourceTypes.join(", ") || "none required"}`
229
236
  : `seed missing expected app resource type(s): ${missingSeedResources.join(", ")}`,
230
237
  },
@@ -244,6 +251,7 @@ function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof
244
251
  appContract: {
245
252
  activePermissions,
246
253
  expectedResourceTypes,
254
+ fgaEnabled,
247
255
  seedPath: seed.path,
248
256
  seedPermissions: seed.permissions,
249
257
  seedResourceTypes: seed.resourceTypes,
@@ -2709,6 +2709,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
2709
2709
  webOnly: command.webOnly,
2710
2710
  open: command.open,
2711
2711
  webPort: command.webPort,
2712
+ publicApiUrl: command.publicApiUrl,
2712
2713
  telemetry: command.telemetry,
2713
2714
  envFile: command.envFile,
2714
2715
  skipStartupConsole: command.skipStartupConsole,
@@ -2863,6 +2864,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
2863
2864
  workspaceRoot: command.workspaceRoot,
2864
2865
  json: command.json,
2865
2866
  redacted: command.redacted,
2867
+ target: command.target,
2866
2868
  });
2867
2869
 
2868
2870
  if (command.json) {
@@ -13,7 +13,7 @@ import { runAuthCommand } from "./auth.ts";
13
13
  import { runAuthMdCommand } from "./authmd.ts";
14
14
  import { runWorkOSCommand } from "./workos.ts";
15
15
 
16
- export type DeploySubcommand = "plan" | "check" | "render" | "package" | "verify";
16
+ export type DeploySubcommand = "plan" | "init" | "check" | "readiness" | "render" | "package" | "verify";
17
17
  export type DeployTarget = "docker" | "forge-cloud";
18
18
 
19
19
  export interface DeployCommandOptions {
@@ -42,6 +42,8 @@ export interface DeployCommandResult {
42
42
  target: DeployTarget;
43
43
  production: boolean;
44
44
  checks: DeployCheck[];
45
+ blocking?: string[];
46
+ warnings?: string[];
45
47
  files?: string[];
46
48
  plan?: {
47
49
  summary: string;
@@ -50,6 +52,7 @@ export interface DeployCommandResult {
50
52
  notes: string[];
51
53
  };
52
54
  probes?: Array<{ method: string; url: string; ok: boolean; status?: number; contentType?: string; error?: string; jsonValid?: boolean }>;
55
+ readiness?: DeployReadinessSummary;
53
56
  nextActions: string[];
54
57
  exitCode: 0 | 1;
55
58
  }
@@ -57,6 +60,27 @@ export interface DeployCommandResult {
57
60
  interface DeployProbeExpectation {
58
61
  contentTypeIncludes?: string;
59
62
  json?: boolean;
63
+ required?: boolean;
64
+ }
65
+
66
+ export interface DeployReadinessSummary {
67
+ score: number;
68
+ status: "ready" | "blocked" | "warning";
69
+ blocking: string[];
70
+ warnings: string[];
71
+ stages: Array<{
72
+ name: string;
73
+ ok: boolean;
74
+ score: number;
75
+ checks: string[];
76
+ blocking: string[];
77
+ warnings: string[];
78
+ }>;
79
+ answers: {
80
+ canPublish: boolean;
81
+ ifNotWhatIsMissing: string[];
82
+ nextCommand: string | null;
83
+ };
60
84
  }
61
85
 
62
86
  function readGeneratedJson<T>(workspaceRoot: string, relative: string): T | null {
@@ -93,6 +117,74 @@ function unique(values: string[]): string[] {
93
117
  return [...new Set(values)];
94
118
  }
95
119
 
120
+ function deployStageForCheck(name: string): string {
121
+ if (["generated", "package-lockfile", "frontend-build-script"].includes(name)) return "build";
122
+ if (["database-url"].includes(name)) return "database";
123
+ if (name.startsWith("auth-") || ["production-auth-mode", "auth-issuer", "auth-audience", "auth-jwks", "tenant-claim"].includes(name)) return "auth";
124
+ if (name.startsWith("workos")) return "workos";
125
+ if (name.startsWith("auth-md") || name === "oauth-protected-resource") return "auth metadata";
126
+ if (name.startsWith("field-test")) return "field test";
127
+ if (name === "live-production") return "live/runtime";
128
+ if (name === "deploy-env-sources") return "environment";
129
+ return "production";
130
+ }
131
+
132
+ function summarizeDeployReadiness(checks: DeployCheck[], nextActions: string[]): DeployReadinessSummary {
133
+ const blocking = checks
134
+ .filter((check) => !check.ok && check.severity === "error")
135
+ .map((check) => `${check.name}: ${check.message}`);
136
+ const warnings = checks
137
+ .filter((check) => !check.ok && check.severity === "warning")
138
+ .map((check) => `${check.name}: ${check.message}`);
139
+ const stageNames = [
140
+ "build",
141
+ "environment",
142
+ "database",
143
+ "auth",
144
+ "workos",
145
+ "auth metadata",
146
+ "field test",
147
+ "live/runtime",
148
+ "production",
149
+ ];
150
+ const stages = stageNames
151
+ .map((stage) => {
152
+ const stageChecks = checks.filter((check) => deployStageForCheck(check.name) === stage);
153
+ const stageBlocking = stageChecks
154
+ .filter((check) => !check.ok && check.severity === "error")
155
+ .map((check) => `${check.name}: ${check.message}`);
156
+ const stageWarnings = stageChecks
157
+ .filter((check) => !check.ok && check.severity === "warning")
158
+ .map((check) => `${check.name}: ${check.message}`);
159
+ const passed = stageChecks.filter((check) => check.ok || check.severity === "warning").length;
160
+ const score = stageChecks.length === 0 ? 100 : Math.round((passed / stageChecks.length) * 100);
161
+ return {
162
+ name: stage,
163
+ ok: stageBlocking.length === 0,
164
+ score,
165
+ checks: stageChecks.map((check) => check.name),
166
+ blocking: stageBlocking,
167
+ warnings: stageWarnings,
168
+ };
169
+ })
170
+ .filter((stage) => stage.checks.length > 0);
171
+ const passed = checks.filter((check) => check.ok || check.severity === "warning").length;
172
+ const score = checks.length === 0 ? 100 : Math.round((passed / checks.length) * 100);
173
+ const canPublish = blocking.length === 0;
174
+ return {
175
+ score,
176
+ status: blocking.length > 0 ? "blocked" : warnings.length > 0 ? "warning" : "ready",
177
+ blocking,
178
+ warnings,
179
+ stages,
180
+ answers: {
181
+ canPublish,
182
+ ifNotWhatIsMissing: blocking,
183
+ nextCommand: canPublish ? "forge deploy verify --production --url https://app.example.com --json" : nextActions[0] ?? null,
184
+ },
185
+ };
186
+ }
187
+
96
188
  function hasWorkOSIntegration(workspaceRoot: string): boolean {
97
189
  const secrets = new Set(requiredSecretNames(workspaceRoot));
98
190
  return secrets.has("WORKOS_API_KEY") ||
@@ -596,7 +688,7 @@ function databaseReadyCommand(options: DeployCommandOptions): string {
596
688
  function renderProductionReadme(): string {
597
689
  return `# ForgeOS Production Deploy
598
690
 
599
- This directory is generated by \`forge deploy package --target docker\`.
691
+ This directory is generated by \`forge deploy init --target docker\` or \`forge deploy package --target docker\`.
600
692
 
601
693
  ## 1. Prepare production env
602
694
 
@@ -619,13 +711,17 @@ For WorkOS-backed apps, local dry-runs are not enough for production deploy
619
711
  readiness. Run:
620
712
 
621
713
  \`\`\`bash
714
+ forge workos setup --real --file workos-seed.yml --json
622
715
  forge workos prove --real --file workos-seed.yml --json
623
716
  \`\`\`
624
717
 
625
- This applies or confirms hosted WorkOS config/seed through the WorkOS CLI and
626
- writes \`.workos-seed-state.json\`. \`forge deploy check --production\` requires
627
- that state to match the current \`workos-seed.yml\` before treating WorkOS
628
- posture as production evidence.
718
+ This applies or confirms hosted redirect/CORS/webhook config and seed data
719
+ through the WorkOS CLI and writes \`.workos-seed-state.json\`. \`forge deploy
720
+ check --production\` requires that state to match the current \`workos-seed.yml\`
721
+ before treating WorkOS posture as production evidence. WorkOS FGA is optional:
722
+ most apps can ship with AuthKit, RBAC permission claims, Forge policies, and
723
+ tenant isolation; use \`forge add auth workos --with-fga\` only when the app needs
724
+ resource-level authorization outside normal role/permission checks.
629
725
 
630
726
  ## 2. Prove the app before traffic
631
727
 
@@ -633,9 +729,12 @@ posture as production evidence.
633
729
  forge generate --check --json
634
730
  forge check --json
635
731
  forge authmd generate --json
732
+ forge env doctor --target production --json
636
733
  forge auth prove --scenario multi-tenant --json
734
+ forge workos setup --real --file workos-seed.yml --json
637
735
  forge workos prove --real --file workos-seed.yml --json
638
736
  forge field-test run --realistic --json
737
+ forge deploy readiness --production --json
639
738
  forge deploy check --production --json
640
739
  \`\`\`
641
740
 
@@ -654,7 +753,7 @@ forge auth prove --prod --token <jwt> --json
654
753
  forge deploy verify --production --url https://app.example.com --json
655
754
  \`\`\`
656
755
 
657
- \`forge auth prove --prod\` verifies a real JWT/OIDC token against the configured issuer/audience/JWKS. \`forge deploy verify --production\` probes \`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.
756
+ \`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.
658
757
  `;
659
758
  }
660
759
 
@@ -669,10 +768,17 @@ function buildPlan(options: DeployCommandOptions): DeployCommandResult {
669
768
  "forge deploy verify --production --url https://<your-forge-cloud-app> --json",
670
769
  ]
671
770
  : [
672
- "forge deploy package --target docker",
771
+ "forge deploy init --target docker",
673
772
  "cp deploy/.env.production.example deploy/.env.production",
674
- "forge deploy check --production --json",
773
+ "forge authmd generate --json",
774
+ "forge env doctor --target production --json",
775
+ "forge auth prove --scenario multi-tenant --json",
776
+ "forge workos setup --real --file workos-seed.yml --json",
777
+ "forge workos prove --real --file workos-seed.yml --json",
675
778
  FIELD_TEST_PRODUCTION_COMMAND,
779
+ "forge deploy readiness --production --json",
780
+ "forge deploy check --production --json",
781
+ "forge deploy package --target docker",
676
782
  "docker compose -f deploy/docker-compose.yml up --build",
677
783
  "forge deploy verify --production --url https://app.example.com --json",
678
784
  ];
@@ -699,10 +805,12 @@ function buildPlan(options: DeployCommandOptions): DeployCommandResult {
699
805
  "field-test report exists with runtime/auth/UI probes",
700
806
  "runtime /health responds",
701
807
  "tenant and policy proof is run before public traffic",
808
+ "readiness score has no blocking items",
702
809
  ],
703
810
  notes: [
704
811
  "dev-headers auth is local-only and must not be enabled for public runtime.",
705
812
  "Use forge workos doctor/seed when WorkOS is configured.",
813
+ "WorkOS FGA is optional; ordinary WorkOS apps can be production-ready with AuthKit/RBAC permission claims, Forge policies, and tenant isolation.",
706
814
  "Use forge test authz or HTTP probes to prove cross-tenant denial.",
707
815
  ],
708
816
  },
@@ -971,6 +1079,10 @@ async function buildChecks(options: DeployCommandOptions): Promise<DeployCommand
971
1079
  const failureActions = unique(checks.flatMap((check) =>
972
1080
  !check.ok && check.command ? expandDeployFailureCommand(check.command) : []
973
1081
  ));
1082
+ const nextActions = errorFree
1083
+ ? ["forge deploy verify --production --url https://app.example.com --json"]
1084
+ : failureActions;
1085
+ const readiness = summarizeDeployReadiness(checks, nextActions);
974
1086
  return normalizeForgeCliCommandsInValue(options.workspaceRoot, {
975
1087
  schemaVersion: "0.1.0",
976
1088
  ok: errorFree,
@@ -979,9 +1091,10 @@ async function buildChecks(options: DeployCommandOptions): Promise<DeployCommand
979
1091
  target: options.target,
980
1092
  production: options.production,
981
1093
  checks,
982
- nextActions: errorFree
983
- ? ["forge deploy verify --production --url https://app.example.com --json"]
984
- : failureActions,
1094
+ blocking: readiness.blocking,
1095
+ warnings: readiness.warnings,
1096
+ readiness,
1097
+ nextActions,
985
1098
  exitCode: errorFree ? 0 : 1,
986
1099
  });
987
1100
  }
@@ -1020,6 +1133,7 @@ function renderDocker(options: DeployCommandOptions): DeployCommandResult {
1020
1133
  files: files.map(([file]) => file),
1021
1134
  nextActions: [
1022
1135
  "cp deploy/.env.production.example deploy/.env.production",
1136
+ "forge deploy readiness --production --json",
1023
1137
  "forge deploy check --production --json",
1024
1138
  "docker compose -f deploy/docker-compose.yml up --build",
1025
1139
  ],
@@ -1087,22 +1201,32 @@ async function verifyUrl(options: DeployCommandOptions): Promise<DeployCommandRe
1087
1201
  exitCode: 1,
1088
1202
  };
1089
1203
  }
1090
- const probes = await Promise.all([
1091
- probe("GET", `${base}/health`),
1092
- probe("HEAD", `${base}/auth.md`, { contentTypeIncludes: "text/markdown" }),
1093
- probe("GET", `${base}/auth.md`, { contentTypeIncludes: "text/markdown" }),
1094
- probe("HEAD", `${base}/.well-known/oauth-protected-resource`, { contentTypeIncludes: "application/json" }),
1095
- probe("GET", `${base}/.well-known/oauth-protected-resource`, { contentTypeIncludes: "application/json", json: true }),
1096
- ]);
1204
+ const probeSpecs: Array<{ method: "GET" | "HEAD"; path: string; expectation?: DeployProbeExpectation }> = [
1205
+ { method: "GET", path: "/health", expectation: { required: true } },
1206
+ { method: "HEAD", path: "/auth.md", expectation: { contentTypeIncludes: "text/markdown", required: true } },
1207
+ { method: "GET", path: "/auth.md", expectation: { contentTypeIncludes: "text/markdown", required: true } },
1208
+ { method: "HEAD", path: "/.well-known/oauth-protected-resource", expectation: { contentTypeIncludes: "application/json", required: true } },
1209
+ { method: "GET", path: "/.well-known/oauth-protected-resource", expectation: { contentTypeIncludes: "application/json", json: true, required: true } },
1210
+ { method: "GET", path: "/ready", expectation: { contentTypeIncludes: "application/json" } },
1211
+ { method: "GET", path: "/live/status", expectation: { contentTypeIncludes: "application/json" } },
1212
+ { method: "GET", path: "/outbox/status", expectation: { contentTypeIncludes: "application/json" } },
1213
+ { method: "HEAD", path: "/webhooks/workos" },
1214
+ ];
1215
+ const probes = await Promise.all(probeSpecs.map((item) =>
1216
+ probe(item.method, `${base}${item.path}`, item.expectation ?? {})
1217
+ ));
1097
1218
  const checks = probes.map((item): DeployCheck => ({
1098
1219
  name: `${item.method} ${item.url.replace(base, "") || "/"}`,
1099
1220
  ok: item.ok,
1100
- severity: item.url.endsWith("/health") || options.production ? "error" : "warning",
1221
+ severity: probeSpecs.find((spec) => `${base}${spec.path}` === item.url && spec.method === item.method)?.expectation?.required || item.url.endsWith("/health")
1222
+ ? "error"
1223
+ : "warning",
1101
1224
  message: item.ok
1102
1225
  ? `HTTP ${item.status}${item.contentType ? ` ${item.contentType}` : ""}`
1103
1226
  : item.error ?? `HTTP ${item.status ?? "failed"}`,
1104
1227
  }));
1105
1228
  const ok = checks.every((check) => check.ok || check.severity === "warning");
1229
+ const readiness = summarizeDeployReadiness(checks, ok ? [] : ["check runtime logs", "forge deploy check --production --json"]);
1106
1230
  return {
1107
1231
  schemaVersion: "0.1.0",
1108
1232
  ok,
@@ -1111,16 +1235,33 @@ async function verifyUrl(options: DeployCommandOptions): Promise<DeployCommandRe
1111
1235
  target: options.target,
1112
1236
  production: options.production,
1113
1237
  checks,
1238
+ blocking: readiness.blocking,
1239
+ warnings: readiness.warnings,
1114
1240
  probes,
1115
- nextActions: ok ? ["forge handoff --json"] : ["check runtime logs", "forge deploy check --production --json"],
1241
+ readiness,
1242
+ nextActions: ok
1243
+ ? [
1244
+ "forge auth prove --prod --token <jwt> --json",
1245
+ "forge auth prove --scenario multi-tenant --json",
1246
+ "forge handoff --json",
1247
+ ]
1248
+ : ["check runtime logs", "forge deploy check --production --json"],
1116
1249
  exitCode: ok ? 0 : 1,
1117
1250
  };
1118
1251
  }
1119
1252
 
1120
1253
  export async function runDeployCommand(options: DeployCommandOptions): Promise<DeployCommandResult> {
1121
1254
  if (options.subcommand === "plan") return buildPlan(options);
1255
+ if (options.subcommand === "readiness") {
1256
+ const result = await buildChecks({ ...options, subcommand: "check", production: true });
1257
+ return {
1258
+ ...result,
1259
+ action: "readiness",
1260
+ nextActions: result.readiness?.answers.nextCommand ? [result.readiness.answers.nextCommand] : result.nextActions,
1261
+ };
1262
+ }
1122
1263
  if (options.subcommand === "check") return buildChecks(options);
1123
- if (options.subcommand === "render" || options.subcommand === "package") return renderDocker(options);
1264
+ if (options.subcommand === "init" || options.subcommand === "render" || options.subcommand === "package") return renderDocker(options);
1124
1265
  return verifyUrl(options);
1125
1266
  }
1126
1267
 
@@ -1132,7 +1273,13 @@ export function formatDeployHuman(result: DeployCommandResult): string {
1132
1273
  const lines = [
1133
1274
  `deploy ${result.action} ${result.ok ? "ok" : "failed"}`,
1134
1275
  `target: ${result.target}`,
1276
+ ...(result.readiness ? [
1277
+ `readiness: ${result.readiness.status} (${result.readiness.score}/100)`,
1278
+ `can publish: ${result.readiness.answers.canPublish ? "yes" : "no"}`,
1279
+ ] : []),
1135
1280
  ...result.checks.map((check) => `${check.ok ? "ok" : check.severity === "warning" ? "warn" : "fail"} ${check.name}: ${check.message}`),
1281
+ ...(result.blocking?.length ? ["", "Blocking:", ...result.blocking.map((item) => ` ${item}`)] : []),
1282
+ ...(result.warnings?.length ? ["", "Warnings:", ...result.warnings.map((item) => ` ${item}`)] : []),
1136
1283
  ...(result.files?.length ? ["", "Files:", ...result.files.map((file) => ` ${file}`)] : []),
1137
1284
  ...(result.plan ? ["", result.plan.summary, "", "Commands:", ...result.plan.commands.map((command) => ` ${command}`)] : []),
1138
1285
  ...(result.nextActions.length ? ["", "Next:", ...result.nextActions.map((action) => ` ${action}`)] : []),
@@ -53,6 +53,7 @@ export interface DevCommandOptions {
53
53
  webOnly?: boolean;
54
54
  open?: boolean;
55
55
  webPort?: number;
56
+ publicApiUrl?: string;
56
57
  telemetry: string[];
57
58
  envFile?: string;
58
59
  mode?: "dev" | "serve";
@@ -305,6 +306,7 @@ function buildDetachedDevArgs(options: DevCommandOptions): string[] {
305
306
  if (options.webOnly) args.push("--web-only");
306
307
  if (options.open) args.push("--open");
307
308
  if (options.webPort !== undefined) args.push("--web-port", String(options.webPort));
309
+ if (options.publicApiUrl) args.push("--public-api-url", options.publicApiUrl);
308
310
  if (options.telemetry.length > 0) args.push("--telemetry", options.telemetry.join(","));
309
311
  if (options.envFile) args.push("--env-file", options.envFile);
310
312
  if (options.seed) args.push("--seed");
@@ -1343,13 +1345,14 @@ export async function runDevCommand(
1343
1345
 
1344
1346
  if (options.webOnly) {
1345
1347
  const apiUrl = `http://${host}:${port}`;
1348
+ const publicApiUrl = options.publicApiUrl ?? apiUrl;
1346
1349
  const webHandle = startWebDevServer({
1347
1350
  workspaceRoot,
1348
1351
  host,
1349
1352
  port: webPort,
1350
1353
  requestedPort: webPortSelection.requestedPort,
1351
1354
  autoPortSelected: webPortSelection.autoPortSelected,
1352
- apiUrl,
1355
+ apiUrl: publicApiUrl,
1353
1356
  json: options.json,
1354
1357
  });
1355
1358
  if (!webHandle) {
@@ -1451,7 +1454,7 @@ export async function runDevCommand(
1451
1454
  port: webPort,
1452
1455
  requestedPort: webPortSelection.requestedPort,
1453
1456
  autoPortSelected: webPortSelection.autoPortSelected,
1454
- apiUrl: handle.url,
1457
+ apiUrl: options.publicApiUrl ?? handle.url,
1455
1458
  json: options.json,
1456
1459
  });
1457
1460
 
@@ -20,6 +20,9 @@ function formatHelp(): string {
20
20
  " forge field-test create vendor-access --auth workos --install --git --json Create a real field-test app",
21
21
  " forge field-test run --realistic --templates vendor-access --package-managers npm --json",
22
22
  " forge field-test report --json Summarize the machine-readable field-test report",
23
+ " forge deploy init --target docker --json Generate Docker production files, env template, and deploy README",
24
+ " forge deploy readiness --production --json Answer whether the app can publish, what blocks it, and the next command",
25
+ " forge env doctor --target production --json Show production env sources, missing keys, auth mode, database, and provider",
23
26
  " forge seed status --json Discover app seed commands from the generated runtime graph",
24
27
  " forge seed dev --json Run the app seed command against the local Forge dev runtime",
25
28
  " forge seed dev --all-tenants --json Seed every discovered local tenant/persona profile",
@@ -51,7 +54,7 @@ function formatHelp(): string {
51
54
  " forge deploy plan --target docker --json Explain production deploy gates and commands",
52
55
  " forge deploy check --production --json Gate auth, DB, metadata, generated artifacts, and liveQuery readiness",
53
56
  " forge deploy package --target docker Write Docker production deploy files under deploy/",
54
- " forge deploy verify --production --url https://app.example.com --json Probe /health and validate public auth metadata",
57
+ " forge deploy verify --production --url https://app.example.com --json Probe health, public auth metadata, and optional runtime endpoints",
55
58
  " forge release check --allow-missing-local-release --json Gate release readiness without failing on unprepared local artifacts",
56
59
  " forge self-host check --prepared-only --json Report compose readiness without creating deploy files",
57
60
  " forge delta status --verbose --json Include Delta schema, lock, and aggregate count details",
@@ -107,6 +110,7 @@ function formatDevHelp(): string {
107
110
  " --db memory|pglite|postgres|none Choose the development database adapter",
108
111
  " --port <port> API runtime port; use 0 for an ephemeral port",
109
112
  " --web-port <port> Web dev server port; use 0 for an ephemeral port",
113
+ " --public-api-url <url> Browser-facing API URL injected into web env, useful for tunnels",
110
114
  " --host <host> Bind host, default 127.0.0.1",
111
115
  " --no-web Start API/runtime only",
112
116
  " --api-only Start API/runtime only",
@@ -121,6 +125,7 @@ function formatDevHelp(): string {
121
125
  "",
122
126
  "Examples:",
123
127
  " forge dev --db memory --port 3777 --web-port 5174",
128
+ " forge dev --public-api-url http://localhost:3765",
124
129
  " forge dev --db memory --port 0 --web-port 0",
125
130
  " forge dev --seed --db pglite",
126
131
  " forge dev --seed --all-tenants --db pglite",