forgeos 0.1.0-alpha.55 → 0.1.0-alpha.56

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.56 input=a013874c7856b533aae64f467111c45bd707703fd9db6f0b8115898afc97af53 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
2
2
  # AGENTS.md
3
3
 
4
4
  <!-- forge-generated:start -->
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # forgeos
2
2
 
3
+ ## 0.1.0-alpha.56
4
+
5
+ ### Patch Changes
6
+
7
+ - Add the production deploy golden path as an explicit CLI workflow.
8
+
9
+ - `forge deploy init --target docker` now acts as the canonical Docker
10
+ production bootstrap command for runtime files, `deploy/.env.production`
11
+ guidance, and production handoff docs.
12
+ - `forge deploy readiness --production --json` now summarizes whether an app
13
+ can publish, grouped blockers/warnings, readiness score, and the next
14
+ command an agent should run.
15
+ - `forge env doctor --target local|staging|production --json` now reports the
16
+ effective auth mode, database posture, provider detection, missing env
17
+ names, and safe source metadata without printing secret values.
18
+ - `forge deploy check --production --json` now includes blocking/warning
19
+ summaries and the readiness object used by the deploy gate.
20
+ - `forge deploy verify --production --url ... --json` now validates required
21
+ public auth metadata and probes optional runtime endpoints such as
22
+ `/ready`, `/live/status`, `/outbox/status`, and `/webhooks/workos`.
23
+ - Production docs now present the canonical flow:
24
+ `deploy init -> env doctor -> auth/auth.md/WorkOS proof -> field-test ->
25
+ readiness -> check -> package -> verify`, while keeping WorkOS FGA
26
+ explicitly optional unless the app opts into FGA artifacts.
27
+
3
28
  ## 0.1.0-alpha.55
4
29
 
5
30
  ### Patch Changes
package/docs/changelog.md CHANGED
@@ -6,6 +6,22 @@ The canonical source file in the repository is `CHANGELOG.md`.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.1.0-alpha.56
10
+
11
+ - Added `forge deploy init --target docker` as the canonical production Docker
12
+ bootstrap command.
13
+ - Added `forge deploy readiness --production --json` to answer whether an app
14
+ can publish, what blocks production, and which command should run next.
15
+ - Added `forge env doctor --target local|staging|production --json` for safe,
16
+ target-specific env source reporting without exposing secret values.
17
+ - `forge deploy check --production --json` now includes grouped
18
+ blockers/warnings and a readiness summary for agent handoffs.
19
+ - `forge deploy verify --production --url ... --json` now validates required
20
+ public auth metadata and optional operational endpoints.
21
+ - Production docs now describe the canonical flow from deploy init through
22
+ field-test, readiness, package, and verify, with WorkOS FGA kept optional
23
+ unless the app opts into FGA artifacts.
24
+
9
25
  ## 0.1.0-alpha.55
10
26
 
11
27
  - `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.56",
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.56","releaseId":"forgeos@0.1.0-alpha.56+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.56 input=a013874c7856b533aae64f467111c45bd707703fd9db6f0b8115898afc97af53 content=a9688092f903ceb0263f9581e3ab71ecc968284ffd530d9112a5467ea10baf63
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.56",
23
+ "releaseId": "forgeos@0.1.0-alpha.56+unknown",
24
24
  "schemaVersion": "0.1.0"
25
25
  } as const;
@@ -2863,6 +2863,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
2863
2863
  workspaceRoot: command.workspaceRoot,
2864
2864
  json: command.json,
2865
2865
  redacted: command.redacted,
2866
+ target: command.target,
2866
2867
  });
2867
2868
 
2868
2869
  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}`)] : []),
@@ -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",
@@ -418,6 +418,7 @@ export type ForgeCommand =
418
418
  subcommand: EnvSubcommand;
419
419
  json: boolean;
420
420
  redacted: boolean;
421
+ target?: "local" | "staging" | "production";
421
422
  workspaceRoot: string;
422
423
  }
423
424
  | {
@@ -602,7 +603,7 @@ const BASELINE_SUBCOMMANDS: BaselineSubcommand[] = ["create", "status"];
602
603
  const AUTHMD_SUBCOMMANDS: AuthMdSubcommand[] = ["generate", "check"];
603
604
  const WORKOS_SUBCOMMANDS: WorkOSSubcommand[] = ["install", "doctor", "seed", "setup", "prove", "fga"];
604
605
  const WORKOS_FGA_ACTIONS: WorkOSFgaAction[] = ["plan", "sync", "prove", "doctor"];
605
- const DEPLOY_SUBCOMMANDS: DeploySubcommand[] = ["plan", "check", "render", "package", "verify"];
606
+ const DEPLOY_SUBCOMMANDS: DeploySubcommand[] = ["plan", "init", "check", "readiness", "render", "package", "verify"];
606
607
  const FIELD_TEST_SUBCOMMANDS: FieldTestSubcommand[] = ["create", "run", "report"];
607
608
  const SECURITY_SUBCOMMANDS: SecuritySubcommand[] = ["prove"];
608
609
  const RLS_SUBCOMMANDS: RlsSubcommand[] = ["generate", "check", "apply", "test", "mutate-test"];
@@ -1484,10 +1485,10 @@ export function parseCli(argv: string[]): ParsedCli {
1484
1485
  case "deploy": {
1485
1486
  const subcommand = rest[0] as DeploySubcommand | undefined;
1486
1487
  if (!subcommand || !DEPLOY_SUBCOMMANDS.includes(subcommand)) {
1487
- errors.push("forge deploy requires subcommand: plan, check, render, package, or verify");
1488
+ errors.push("forge deploy requires subcommand: plan, init, check, readiness, render, package, or verify");
1488
1489
  return { command: null, workspaceRoot, errors };
1489
1490
  }
1490
- const targetRaw = parseOptionValue(argv, "--target") ?? (subcommand === "render" || subcommand === "package" ? rest[1] : undefined) ?? "docker";
1491
+ const targetRaw = parseOptionValue(argv, "--target") ?? (subcommand === "init" || subcommand === "render" || subcommand === "package" ? rest[1] : undefined) ?? "docker";
1491
1492
  if (targetRaw !== "docker" && targetRaw !== "forge-cloud") {
1492
1493
  errors.push("forge deploy --target must be docker or forge-cloud");
1493
1494
  }
@@ -2974,10 +2975,14 @@ export function parseCli(argv: string[]): ParsedCli {
2974
2975
  }
2975
2976
  case "env": {
2976
2977
  const subcommand = rest[0] as EnvSubcommand | undefined;
2977
- if (!subcommand || !["list", "check", "print"].includes(subcommand)) {
2978
- errors.push("forge env requires subcommand: list, check, or print");
2978
+ if (!subcommand || !["list", "check", "print", "doctor"].includes(subcommand)) {
2979
+ errors.push("forge env requires subcommand: list, check, print, or doctor");
2979
2980
  return { command: null, workspaceRoot, errors };
2980
2981
  }
2982
+ const targetRaw = parseOptionValue(argv, "--target") ?? "local";
2983
+ if (!["local", "staging", "production"].includes(targetRaw)) {
2984
+ errors.push("forge env --target must be local, staging, or production");
2985
+ }
2981
2986
 
2982
2987
  return {
2983
2988
  command: {
@@ -2985,6 +2990,7 @@ export function parseCli(argv: string[]): ParsedCli {
2985
2990
  subcommand,
2986
2991
  json: parseFlag(argv, "--json"),
2987
2992
  redacted: parseFlag(argv, "--redacted"),
2993
+ target: targetRaw as "local" | "staging" | "production",
2988
2994
  workspaceRoot,
2989
2995
  },
2990
2996
  workspaceRoot,
@@ -219,13 +219,141 @@ export function formatSecretsJson(result: SecretsCommandResult): string {
219
219
  return `${JSON.stringify(result, null, 2)}\n`;
220
220
  }
221
221
 
222
- export type EnvSubcommand = "list" | "check" | "print";
222
+ export type EnvSubcommand = "list" | "check" | "print" | "doctor";
223
+ export type EnvDoctorTarget = "local" | "staging" | "production";
223
224
 
224
225
  export interface EnvCommandOptions {
225
226
  subcommand: EnvSubcommand;
226
227
  workspaceRoot: string;
227
228
  json: boolean;
228
229
  redacted?: boolean;
230
+ target?: EnvDoctorTarget;
231
+ }
232
+
233
+ const ENV_DOCTOR_KEYS = [
234
+ "DATABASE_URL",
235
+ "FORGE_AUTH_MODE",
236
+ "FORGE_AUTH_ISSUER",
237
+ "FORGE_AUTH_AUDIENCE",
238
+ "FORGE_AUTH_JWKS_URI",
239
+ "FORGE_AUTH_DISCOVERY_URL",
240
+ "WORKOS_API_KEY",
241
+ "WORKOS_CLIENT_ID",
242
+ "WORKOS_COOKIE_PASSWORD",
243
+ "WORKOS_REDIRECT_URI",
244
+ "WORKOS_POST_LOGIN_REDIRECT_URI",
245
+ "WORKOS_POST_LOGOUT_REDIRECT_URI",
246
+ "WORKOS_WEBHOOK_SECRET",
247
+ ] as const;
248
+
249
+ function parseEnvFile(workspaceRoot: string, relative: string): Record<string, string> {
250
+ const path = join(workspaceRoot, relative);
251
+ if (!nodeFileSystem.exists(path)) return {};
252
+ const values: Record<string, string> = {};
253
+ for (const rawLine of (nodeFileSystem.readText(path) ?? "").split(/\r?\n/)) {
254
+ const line = rawLine.trim();
255
+ if (!line || line.startsWith("#")) continue;
256
+ const eq = line.indexOf("=");
257
+ if (eq <= 0) continue;
258
+ const rawValue = line.slice(eq + 1).trim();
259
+ values[line.slice(0, eq).trim()] = rawValue.replace(/^["']|["']$/g, "");
260
+ }
261
+ return values;
262
+ }
263
+
264
+ function envDoctorFiles(target: EnvDoctorTarget): string[] {
265
+ if (target === "production") return ["deploy/.env.production"];
266
+ if (target === "staging") return ["deploy/.env.staging", ".env.staging"];
267
+ return [".env", ".env.local"];
268
+ }
269
+
270
+ function hasWorkOSAppArtifacts(workspaceRoot: string): boolean {
271
+ return nodeFileSystem.exists(join(workspaceRoot, "workos-seed.yml")) ||
272
+ nodeFileSystem.exists(join(workspaceRoot, "src/policies.workos.ts")) ||
273
+ nodeFileSystem.exists(join(workspaceRoot, "src/forge/_generated/integrations/workos/auth-routes.ts"));
274
+ }
275
+
276
+ function envDoctor(options: EnvCommandOptions): SecretsCommandResult {
277
+ const target = options.target ?? "local";
278
+ const files = envDoctorFiles(target).map((path) => {
279
+ const values = parseEnvFile(options.workspaceRoot, path);
280
+ return {
281
+ path,
282
+ present: nodeFileSystem.exists(join(options.workspaceRoot, path)),
283
+ keys: Object.keys(values).filter((key) => ENV_DOCTOR_KEYS.includes(key as (typeof ENV_DOCTOR_KEYS)[number])).sort(),
284
+ values,
285
+ };
286
+ });
287
+ const processValues = Object.fromEntries(
288
+ ENV_DOCTOR_KEYS.filter((key) => Boolean(process.env[key])).map((key) => [key, process.env[key]!]),
289
+ );
290
+ const effective = {
291
+ ...Object.assign({}, ...files.map((file) => file.values)),
292
+ ...processValues,
293
+ } as Record<string, string>;
294
+ const required = target === "production"
295
+ ? ["DATABASE_URL", "FORGE_AUTH_MODE", "FORGE_AUTH_ISSUER", "FORGE_AUTH_AUDIENCE"]
296
+ : ["FORGE_AUTH_MODE"];
297
+ const missing = required.filter((key) => !effective[key]);
298
+ const authMode = effective.FORGE_AUTH_MODE ?? "dev-headers";
299
+ const productionAuth = authMode === "jwt" || authMode === "oidc";
300
+ const database = effective.DATABASE_URL
301
+ ? "postgres"
302
+ : target === "production"
303
+ ? "missing"
304
+ : "local-dev";
305
+ const workosDetected = hasWorkOSAppArtifacts(options.workspaceRoot) ||
306
+ Boolean(effective.WORKOS_CLIENT_ID || effective.WORKOS_API_KEY);
307
+ const provider = workosDetected ? "workos" : "none";
308
+ const hasJwtSource = Boolean(effective.FORGE_AUTH_JWKS_URI || effective.FORGE_AUTH_DISCOVERY_URL);
309
+ const blockers = [
310
+ ...missing.filter((key) => key !== "DATABASE_URL").map((key) => `${key} missing`),
311
+ ...(target === "production" && !productionAuth ? [`FORGE_AUTH_MODE=${authMode} is not production auth`] : []),
312
+ ...(target === "production" && database === "missing" ? ["DATABASE_URL missing"] : []),
313
+ ...(target === "production" && productionAuth && !hasJwtSource
314
+ ? ["FORGE_AUTH_JWKS_URI or FORGE_AUTH_DISCOVERY_URL missing"]
315
+ : []),
316
+ ...(target === "production" && workosDetected && !effective.WORKOS_CLIENT_ID ? ["WORKOS_CLIENT_ID missing"] : []),
317
+ ...(target === "production" && workosDetected && !effective.WORKOS_API_KEY ? ["WORKOS_API_KEY missing"] : []),
318
+ ];
319
+ const warnings = [
320
+ ...(target === "production" && files.every((file) => !file.present)
321
+ ? ["deploy/.env.production not found; process.env alone may be fine in CI but is easy for agents to miss"]
322
+ : []),
323
+ ...(workosDetected && !effective.WORKOS_COOKIE_PASSWORD ? ["WORKOS_COOKIE_PASSWORD missing"] : []),
324
+ ...(workosDetected && !effective.WORKOS_REDIRECT_URI ? ["WORKOS_REDIRECT_URI missing"] : []),
325
+ ];
326
+ return {
327
+ exitCode: blockers.length === 0 ? 0 : 1,
328
+ data: {
329
+ schemaVersion: "0.1.0",
330
+ kind: "env-doctor",
331
+ ok: blockers.length === 0,
332
+ target,
333
+ authMode,
334
+ productionAuth,
335
+ database,
336
+ provider,
337
+ sources: [
338
+ {
339
+ path: "process.env",
340
+ present: Object.keys(processValues).length > 0,
341
+ keys: Object.keys(processValues).sort(),
342
+ values: undefined,
343
+ },
344
+ ...files.map(({ path, present, keys }) => ({ path, present, keys })),
345
+ ],
346
+ present: ENV_DOCTOR_KEYS.filter((key) => Boolean(effective[key])).sort(),
347
+ missing,
348
+ blockers,
349
+ warnings,
350
+ nextActions: blockers.length === 0
351
+ ? ["forge deploy readiness --production --json"]
352
+ : target === "production"
353
+ ? ["cp deploy/.env.production.example deploy/.env.production", "forge env doctor --target production --json"]
354
+ : ["forge env doctor --target local --json"],
355
+ },
356
+ };
229
357
  }
230
358
 
231
359
  export async function runEnvCommand(options: EnvCommandOptions): Promise<SecretsCommandResult> {
@@ -233,6 +361,8 @@ export async function runEnvCommand(options: EnvCommandOptions): Promise<Secrets
233
361
  const schema = loadEnvSchema(options.workspaceRoot);
234
362
 
235
363
  switch (options.subcommand) {
364
+ case "doctor":
365
+ return envDoctor(options);
236
366
  case "list":
237
367
  if (!schema) {
238
368
  return {
@@ -1,3 +1,3 @@
1
- export const FORGEOS_VERSION = "0.1.0-alpha.55";
1
+ export const FORGEOS_VERSION = "0.1.0-alpha.56";
2
2
  export const GENERATOR_VERSION = FORGEOS_VERSION;
3
3
  export const CLI_VERSION = FORGEOS_VERSION;