forgeos 0.1.0-alpha.37 → 0.1.0-alpha.39

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.37 input=055c7d65fca3d6d8b05af2899c233ff69d377edf0f46f719e2d164ee0574309a content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
1
+ // @forge-generated generator=0.1.0-alpha.39 input=1c15364ea76cedba476c98d2f661e485cdee778fa10e7b06f7db1b6f23b1dc4a content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
2
2
  # AGENTS.md
3
3
 
4
4
  <!-- forge-generated:start -->
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # forgeos
2
2
 
3
+ ## 0.1.0-alpha.39
4
+
5
+ ### Patch Changes
6
+
7
+ - Harden real WorkOS seed setup for production-like field tests.
8
+
9
+ - Emit valid YAML for generated `workos-seed.yml` files by skipping the
10
+ JavaScript-style deterministic header on YAML artifacts.
11
+ - Make `forge workos seed --dry-run` the validation-only path and let
12
+ `forge workos seed --file workos-seed.yml --json` delegate to the
13
+ authenticated WorkOS CLI.
14
+ - Treat known WorkOS duplicate-resource seed responses such as permission
15
+ slugs already in use as an already-applied seed instead of a hard failure.
16
+ - Ignore `.workos-seed-state.json` in newly scaffolded apps.
17
+
3
18
  ## 0.1.0-alpha.37
4
19
 
5
20
  ### Patch Changes
package/docs/changelog.md CHANGED
@@ -6,6 +6,43 @@ The canonical source file in the repository is `CHANGELOG.md`.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.1.0-alpha.39
10
+
11
+ - Hardened real WorkOS seed setup: generated `workos-seed.yml` files no longer
12
+ use `// @forge-generated` headers, `forge workos seed --dry-run` is the
13
+ validation-only path, real seed runs handle known duplicate-resource WorkOS
14
+ responses as already-applied state, and scaffolded apps ignore
15
+ `.workos-seed-state.json` by default.
16
+
17
+ ## 0.1.0-alpha.38
18
+
19
+ - Added first-class production deployment commands:
20
+ `forge deploy plan`, `forge deploy render docker`,
21
+ `forge deploy check --production`, and
22
+ `forge deploy verify --production`.
23
+ - Generated Docker deployment files now use the app's detected package manager,
24
+ require the matching lockfile for production checks, read
25
+ `deploy/.env.production`, and avoid hidden hard-coded `DATABASE_URL`
26
+ overrides.
27
+ - Added `forge field-test create`, `forge field-test run`, and
28
+ `forge field-test report`, including runtime/auth probe summaries and
29
+ `productionEvidence` so field-test reports can feed
30
+ `forge deploy check --production`.
31
+ - Split `forge release doctor` into npm publish readiness and production deploy
32
+ readiness, so package publishing is not confused with a production-ready app
33
+ claim.
34
+ - Made `forge handoff --json` include commit-ready files by default, excluding
35
+ generated and operational artifacts while reporting how much noise was
36
+ filtered out.
37
+ - Clarified local auth versus production auth with `forge auth status` posture
38
+ classifications, local `x-forge-*` header reporting, and production proof
39
+ next actions.
40
+ - Added `dbGuide` to `forge doctor runtime` and `forge doctor pglite` so agents
41
+ know when to use `--db memory`, `--db pglite`, stop an active process, or run
42
+ local PGlite repair.
43
+ - Updated production, self-host, field-test, security, troubleshooting, and CLI
44
+ docs to reflect the new deploy/auth/database workflow.
45
+
9
46
  ## 0.1.0-alpha.37
10
47
 
11
48
  - Fixed `forge deps upgrade-plan` for npm alias installs used by ForgeOS apps.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgeos",
3
- "version": "0.1.0-alpha.37",
3
+ "version": "0.1.0-alpha.39",
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.37","releaseId":"forgeos@0.1.0-alpha.37+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.39","releaseId":"forgeos@0.1.0-alpha.39+unknown","schemaVersion":"0.1.0"}
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.37 input=055c7d65fca3d6d8b05af2899c233ff69d377edf0f46f719e2d164ee0574309a content=1e26c5249853569091c1b295aab06bd2b9546462328fe8126eda099222430b61
1
+ // @forge-generated generator=0.1.0-alpha.39 input=1c15364ea76cedba476c98d2f661e485cdee778fa10e7b06f7db1b6f23b1dc4a content=d76bcca47149c3378aca45191d0df75cbc6c25b49548ba94b7e2ac4ecbaa91a8
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.37",
23
- "releaseId": "forgeos@0.1.0-alpha.37+unknown",
22
+ "packageVersion": "0.1.0-alpha.39",
23
+ "releaseId": "forgeos@0.1.0-alpha.39+unknown",
24
24
  "schemaVersion": "0.1.0"
25
25
  } as const;
@@ -12,6 +12,12 @@ import { loadSecretRegistry } from "../runtime/secrets/check.ts";
12
12
  import { existsSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { normalizeForgeCliCommandsInValue } from "../workspace/forge-cli.ts";
15
+ import {
16
+ collectExpectedResourceTypes,
17
+ collectPolicyPermissions,
18
+ missingValues,
19
+ parseSeedFile,
20
+ } from "./workos.ts";
15
21
 
16
22
  export type AuthSubcommand = "check" | "config" | "decode" | "test-token" | "jwks" | "prove" | "status";
17
23
 
@@ -92,14 +98,59 @@ function buildAuthPosture(workspaceRoot: string) {
92
98
  const productionMode = config.mode === "jwt" || config.mode === "oidc";
93
99
  const errors = configErrors(config);
94
100
  const configReady = errors.length === 0;
101
+ const classification = config.mode === "dev-headers"
102
+ ? "local-dev"
103
+ : config.mode === "disabled"
104
+ ? "unauthenticated"
105
+ : configReady
106
+ ? "production-ready"
107
+ : "production-incomplete";
108
+ const reason = productionMode
109
+ ? configReady
110
+ ? "jwt/oidc production auth configuration is present"
111
+ : "jwt/oidc production auth is selected but required settings are missing"
112
+ : config.mode === "disabled"
113
+ ? "auth is disabled; this is not suitable for public production traffic"
114
+ : "dev-headers auth is local-only and is not real production authentication";
95
115
  return normalizeForgeCliCommandsInValue(workspaceRoot, {
96
116
  schemaVersion: "0.1.0",
97
117
  mode: config.mode,
118
+ classification,
98
119
  localOnly: config.mode === "dev-headers",
99
120
  productionReady: productionMode && configReady,
100
121
  requiresTenant: config.requiresTenant,
101
122
  bearerHeader: productionMode ? "Authorization: Bearer <token>" : null,
102
123
  tenantClaim: config.claims.tenantId ?? "tenant_id",
124
+ environment: {
125
+ current: classification,
126
+ localDevelopment: config.mode === "dev-headers",
127
+ publicProduction: productionMode && configReady,
128
+ warning: reason,
129
+ },
130
+ localDevHeaders: config.mode === "dev-headers"
131
+ ? {
132
+ acceptedHeaders: [
133
+ "x-forge-user-id",
134
+ "x-forge-tenant-id",
135
+ "x-forge-role",
136
+ "x-forge-roles",
137
+ "x-forge-permissions",
138
+ ],
139
+ scope: "forge dev, tests, and local agent workflows only",
140
+ neverProduction: true,
141
+ }
142
+ : null,
143
+ productionRequirements: {
144
+ requiredMode: "jwt or oidc",
145
+ bearerHeader: "Authorization: Bearer <token>",
146
+ requiredEnv: [
147
+ "FORGE_AUTH_MODE",
148
+ "FORGE_AUTH_ISSUER",
149
+ "FORGE_AUTH_AUDIENCE",
150
+ config.mode === "oidc" ? "OIDC discovery at FORGE_AUTH_ISSUER" : "FORGE_AUTH_JWKS_URI",
151
+ ],
152
+ proofCommand: "forge auth prove --prod --token <jwt> --json",
153
+ },
103
154
  productionChecklist: [
104
155
  { item: "auth mode is jwt or oidc", ok: productionMode },
105
156
  { item: "FORGE_AUTH_ISSUER configured", ok: Boolean(config.issuer) },
@@ -109,11 +160,7 @@ function buildAuthPosture(workspaceRoot: string) {
109
160
  { item: "permission claim mapped", ok: Boolean(config.claims.permissions) },
110
161
  { item: "dev-headers disabled for public runtime", ok: config.mode !== "dev-headers" },
111
162
  ],
112
- reason: productionMode
113
- ? configReady
114
- ? "jwt/oidc production auth configuration is present"
115
- : "jwt/oidc production auth is selected but required settings are missing"
116
- : "dev-headers auth is local-only and is not real production authentication",
163
+ reason,
117
164
  nextActions: productionMode
118
165
  ? configReady
119
166
  ? ["forge auth prove --prod --token <jwt> --json", "forge serve --json"]
@@ -122,12 +169,26 @@ function buildAuthPosture(workspaceRoot: string) {
122
169
  });
123
170
  }
124
171
 
172
+ function permissionSubsetForRestrictedTenant(permissions: string[]): string[] {
173
+ const readonly = permissions.filter((permission) =>
174
+ /(^|[:._-])(read|list|view|inspect|search)($|[:._-])/.test(permission)
175
+ );
176
+ if (readonly.length > 0) return readonly;
177
+ return permissions.slice(0, Math.max(1, Math.floor(permissions.length / 2)));
178
+ }
179
+
125
180
  function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof detectWorkOS>, requiresTenant: boolean) {
126
181
  const rootSeedPresent = existsSync(join(workspaceRoot, "workos-seed.yml"));
127
182
  const generatedSeedPresent = existsSync(join(workspaceRoot, "src/forge/_generated/integrations/workos/workos-seed.yml"));
128
183
  const authMdPresent = existsSync(join(workspaceRoot, "public/auth.md"));
129
184
  const metadataPresent = existsSync(join(workspaceRoot, "public/.well-known/oauth-protected-resource"));
130
- const permissions = ["onboarding:read", "invitations:create", "tasks:update"];
185
+ const activePermissions = collectPolicyPermissions(workspaceRoot);
186
+ const expectedResourceTypes = collectExpectedResourceTypes(workspaceRoot);
187
+ const seed = parseSeedFile(workspaceRoot);
188
+ const missingSeedPermissions = missingValues(activePermissions, seed.permissions);
189
+ const missingSeedResources = missingValues(expectedResourceTypes, seed.resourceTypes);
190
+ const acmePermissions = activePermissions;
191
+ const globexPermissions = permissionSubsetForRestrictedTenant(activePermissions);
131
192
  const permissionVocabularyPresent = workos.detected && workos.claimStatus.some((claim) => claim.name === "permissions" && claim.ok);
132
193
  const checks = [
133
194
  {
@@ -137,13 +198,35 @@ function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof
137
198
  },
138
199
  {
139
200
  id: "permission-claim",
140
- ok: permissionVocabularyPresent,
141
- evidence: "Forge permissions claim maps to WorkOS permissions.",
201
+ ok: activePermissions.length > 0 && permissionVocabularyPresent,
202
+ evidence: activePermissions.length > 0
203
+ ? `Forge permissions claim maps to WorkOS permissions: ${activePermissions.join(", ")}.`
204
+ : "No active policy permissions were found in the generated policy registry.",
142
205
  },
143
206
  {
144
207
  id: "seed-organizations",
145
208
  ok: rootSeedPresent || generatedSeedPresent,
146
- evidence: rootSeedPresent ? "workos-seed.yml exists at app root." : "generated WorkOS seed exists.",
209
+ evidence: rootSeedPresent
210
+ ? "workos-seed.yml exists at app root."
211
+ : generatedSeedPresent
212
+ ? "generated WorkOS seed exists."
213
+ : "no WorkOS seed file found at app root or in generated integrations.",
214
+ },
215
+ {
216
+ id: "seed-permission-coverage",
217
+ ok: activePermissions.length > 0 && missingSeedPermissions.length === 0,
218
+ evidence: activePermissions.length === 0
219
+ ? "No active policy permissions were found in the generated policy registry."
220
+ : missingSeedPermissions.length === 0
221
+ ? `seed covers active app permission(s): ${activePermissions.join(", ")}`
222
+ : `seed missing active app permission(s): ${missingSeedPermissions.join(", ")}`,
223
+ },
224
+ {
225
+ id: "seed-resource-coverage",
226
+ ok: missingSeedResources.length === 0,
227
+ evidence: missingSeedResources.length === 0
228
+ ? `seed covers expected app resource type(s): ${expectedResourceTypes.join(", ") || "none required"}`
229
+ : `seed missing expected app resource type(s): ${missingSeedResources.join(", ")}`,
147
230
  },
148
231
  {
149
232
  id: "agent-auth-metadata",
@@ -155,14 +238,21 @@ function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof
155
238
  scenario: "multi-tenant",
156
239
  ok: checks.every((check) => check.ok),
157
240
  claims: {
158
- acme: { organization_id: "org_acme", role: "owner", permissions },
159
- globex: { organization_id: "org_globex", role: "member", permissions: ["onboarding:read", "tasks:update"] },
241
+ acme: { organization_id: "org_acme", role: "owner", permissions: acmePermissions },
242
+ globex: { organization_id: "org_globex", role: "member", permissions: globexPermissions },
243
+ },
244
+ appContract: {
245
+ activePermissions,
246
+ expectedResourceTypes,
247
+ seedPath: seed.path,
248
+ seedPermissions: seed.permissions,
249
+ seedResourceTypes: seed.resourceTypes,
160
250
  },
161
251
  invariants: [
162
252
  "Acme and Globex must use different organization_id claim values.",
163
253
  "Tenant-scoped reads must include the active organization_id.",
164
254
  "Tenant-scoped writes must verify the resource tenant before mutation.",
165
- "Role-only UI affordances are not sufficient; policies must use permissions/claims.",
255
+ "Role-only UI affordances are not sufficient; policies must use the app's generated permission vocabulary.",
166
256
  ],
167
257
  checks,
168
258
  nextActions: [
@@ -304,7 +394,31 @@ export async function runAuthCommand(
304
394
  };
305
395
  }
306
396
  if (options.subcommand === "check") {
307
- return validateConfig(options.workspaceRoot);
397
+ const checked = validateConfig(options.workspaceRoot);
398
+ if (!options.prod) {
399
+ return checked;
400
+ }
401
+ const posture = buildAuthPosture(options.workspaceRoot);
402
+ const productionReady = posture.productionReady === true;
403
+ return {
404
+ ok: checked.ok && productionReady,
405
+ mode: checked.mode,
406
+ data: {
407
+ ...(checked.data && typeof checked.data === "object" ? checked.data as Record<string, unknown> : {}),
408
+ production: true,
409
+ authPosture: posture,
410
+ productionCheck: {
411
+ ok: productionReady,
412
+ reason: posture.reason,
413
+ requiredMode: "jwt or oidc",
414
+ localOnlyModeRejected: posture.localOnly === true,
415
+ },
416
+ },
417
+ error: productionReady
418
+ ? checked.error
419
+ : { code: "FORGE_AUTH_MODE_INVALID", message: "production auth requires FORGE_AUTH_MODE=jwt or oidc; dev-headers is local-only" },
420
+ exitCode: checked.ok && productionReady ? 0 : 1,
421
+ };
308
422
  }
309
423
  if (options.subcommand === "prove") {
310
424
  const checked = validateConfig(options.workspaceRoot);
@@ -405,7 +519,51 @@ export function formatAuthJson(result: AuthCommandResult): string {
405
519
  return `${JSON.stringify(result, null, 2)}\n`;
406
520
  }
407
521
 
522
+ function authPostureFromResult(result: AuthCommandResult): Record<string, unknown> | null {
523
+ const data = result.data && typeof result.data === "object" ? result.data as Record<string, unknown> : {};
524
+ const direct = data.schemaVersion === "0.1.0" && "classification" in data ? data : null;
525
+ const nested = data.authPosture && typeof data.authPosture === "object"
526
+ ? data.authPosture as Record<string, unknown>
527
+ : null;
528
+ return direct ?? nested;
529
+ }
530
+
408
531
  export function formatAuthHuman(result: AuthCommandResult): string {
532
+ const posture = authPostureFromResult(result);
533
+ if (posture) {
534
+ const classification = typeof posture.classification === "string" ? posture.classification : result.mode;
535
+ const reason = typeof posture.reason === "string" ? posture.reason : result.error?.message;
536
+ const nextActions = Array.isArray(posture.nextActions) ? posture.nextActions.filter((item): item is string => typeof item === "string") : [];
537
+ const lines = [
538
+ `Auth ${result.mode}: ${result.ok ? "ok" : "failed"} (${classification})`,
539
+ `Production ready: ${posture.productionReady === true ? "yes" : "no"}`,
540
+ ...(reason ? [`Reason: ${reason}`] : []),
541
+ ];
542
+ const localDevHeaders = posture.localDevHeaders && typeof posture.localDevHeaders === "object"
543
+ ? posture.localDevHeaders as Record<string, unknown>
544
+ : null;
545
+ const acceptedHeaders = Array.isArray(localDevHeaders?.acceptedHeaders)
546
+ ? localDevHeaders.acceptedHeaders.filter((item): item is string => typeof item === "string")
547
+ : posture.localOnly === true || result.mode === "dev-headers"
548
+ ? [
549
+ "x-forge-user-id",
550
+ "x-forge-tenant-id",
551
+ "x-forge-role",
552
+ "x-forge-roles",
553
+ "x-forge-permissions",
554
+ ]
555
+ : [];
556
+ if (acceptedHeaders.length > 0) {
557
+ lines.push("Local dev headers:", ...acceptedHeaders.map((header) => ` ${header}`));
558
+ }
559
+ if (!result.ok && result.error) {
560
+ lines.push(`${result.error.code}: ${result.error.message}`);
561
+ }
562
+ if (nextActions.length > 0) {
563
+ lines.push("", "Next:", ...nextActions.map((action) => ` ${action}`));
564
+ }
565
+ return `${lines.join("\n")}\n`;
566
+ }
409
567
  if (!result.ok) {
410
568
  return `Auth ${result.mode}: failed\n${result.error?.code}: ${result.error?.message}\n`;
411
569
  }
@@ -188,8 +188,11 @@ import {
188
188
  formatDoctorJson,
189
189
  formatPgliteDoctorHuman,
190
190
  formatPgliteDoctorJson,
191
+ formatRuntimeDoctorHuman,
192
+ formatRuntimeDoctorJson,
191
193
  runPgliteDoctorCommand,
192
194
  runDoctorCommand,
195
+ runRuntimeDoctorCommand,
193
196
  } from "./doctor.ts";
194
197
  import {
195
198
  formatWindowsDoctorHuman,
@@ -210,6 +213,17 @@ import {
210
213
  formatWorkOSJson,
211
214
  runWorkOSCommand,
212
215
  } from "./workos.ts";
216
+ import {
217
+ formatDeployHuman,
218
+ formatDeployJson,
219
+ type DeployCommandResult,
220
+ runDeployCommand,
221
+ } from "./deploy.ts";
222
+ import {
223
+ formatFieldTestHuman,
224
+ formatFieldTestJson,
225
+ runFieldTestCommand,
226
+ } from "./field-test.ts";
213
227
  import {
214
228
  formatLastHuman,
215
229
  formatLastJson,
@@ -1128,8 +1142,9 @@ interface ReleaseDoctorCheck {
1128
1142
  name: string;
1129
1143
  ok: boolean;
1130
1144
  requiredForPublish: boolean;
1145
+ requiredForProduction?: boolean;
1131
1146
  state?: string;
1132
- result: ReleaseCommandResult | SelfHostCommandResult | DocsCheckResult | PackagePackCheckResult;
1147
+ result: ReleaseCommandResult | SelfHostCommandResult | DocsCheckResult | DeployCommandResult | PackagePackCheckResult;
1133
1148
  }
1134
1149
 
1135
1150
  interface PackagePackCheckResult {
@@ -1150,10 +1165,13 @@ interface ReleaseDoctorResult {
1150
1165
  schemaVersion: "0.1.0";
1151
1166
  ok: boolean;
1152
1167
  readyToPublish: boolean;
1168
+ readyForProductionDeploy: boolean;
1153
1169
  summary: {
1154
1170
  checks: number;
1155
1171
  failed: string[];
1156
1172
  notPrepared: string[];
1173
+ publishBlockers: string[];
1174
+ productionBlockers: string[];
1157
1175
  };
1158
1176
  checks: ReleaseDoctorCheck[];
1159
1177
  nextActions: string[];
@@ -1269,6 +1287,13 @@ export async function runReleaseDoctorCommand(command: Extract<ForgeCommand, { k
1269
1287
  workspaceRoot: command.workspaceRoot,
1270
1288
  json: command.json,
1271
1289
  });
1290
+ const deployProduction = await runDeployCommand({
1291
+ subcommand: "check",
1292
+ workspaceRoot: command.workspaceRoot,
1293
+ json: command.json,
1294
+ target: "docker",
1295
+ production: true,
1296
+ });
1272
1297
  const packagePack = runPackagePackDryRun(command.workspaceRoot);
1273
1298
  const checks: ReleaseDoctorCheck[] = [
1274
1299
  {
@@ -1291,6 +1316,13 @@ export async function runReleaseDoctorCommand(command: Extract<ForgeCommand, { k
1291
1316
  state: selfHost.state,
1292
1317
  result: selfHost,
1293
1318
  },
1319
+ {
1320
+ name: "deploy-production",
1321
+ ok: deployProduction.ok,
1322
+ requiredForPublish: false,
1323
+ requiredForProduction: true,
1324
+ result: deployProduction,
1325
+ },
1294
1326
  {
1295
1327
  name: "docs",
1296
1328
  ok: docs.ok,
@@ -1308,19 +1340,32 @@ export async function runReleaseDoctorCommand(command: Extract<ForgeCommand, { k
1308
1340
  const notPrepared = checks
1309
1341
  .filter((check) => check.state === "missing-prepared-release" || check.state === "not-prepared")
1310
1342
  .map((check) => check.name);
1311
- const readyToPublish = failed.length === 0 && notPrepared.length === 0;
1343
+ const publishBlockers = checks
1344
+ .filter((check) => check.requiredForPublish && !check.ok)
1345
+ .map((check) => check.name);
1346
+ const productionBlockers = checks
1347
+ .filter((check) => check.requiredForProduction && !check.ok)
1348
+ .map((check) => check.name);
1349
+ const notPreparedPublishBlockers = checks
1350
+ .filter((check) => check.requiredForPublish && (check.state === "missing-prepared-release" || check.state === "not-prepared"))
1351
+ .map((check) => check.name);
1352
+ const readyToPublish = publishBlockers.length === 0 && notPreparedPublishBlockers.length === 0;
1353
+ const readyForProductionDeploy = productionBlockers.length === 0;
1312
1354
  return {
1313
1355
  schemaVersion: "0.1.0",
1314
- ok: failed.length === 0,
1356
+ ok: readyToPublish,
1315
1357
  readyToPublish,
1358
+ readyForProductionDeploy,
1316
1359
  summary: {
1317
1360
  checks: checks.length,
1318
1361
  failed,
1319
1362
  notPrepared,
1363
+ publishBlockers,
1364
+ productionBlockers,
1320
1365
  },
1321
1366
  checks,
1322
1367
  nextActions: uniqueNextActions(checks.flatMap((check) => check.result.nextActions ?? [])),
1323
- exitCode: failed.length === 0 ? 0 : 1,
1368
+ exitCode: readyToPublish ? 0 : 1,
1324
1369
  };
1325
1370
  }
1326
1371
 
@@ -1850,6 +1895,11 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
1850
1895
  process.stdout.write(command.json ? formatPgliteDoctorJson(result) : formatPgliteDoctorHuman(result));
1851
1896
  return result.exitCode;
1852
1897
  }
1898
+ if (command.target === "runtime") {
1899
+ const result = await runRuntimeDoctorCommand({ workspaceRoot: command.workspaceRoot });
1900
+ process.stdout.write(command.json ? formatRuntimeDoctorJson(result) : formatRuntimeDoctorHuman(result));
1901
+ return result.exitCode;
1902
+ }
1853
1903
  const result = await runDoctorCommand({ workspaceRoot: command.workspaceRoot });
1854
1904
  if (command.json) {
1855
1905
  process.stdout.write(formatDoctorJson(result));
@@ -1906,6 +1956,16 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
1906
1956
  }
1907
1957
  return result.exitCode;
1908
1958
  }
1959
+ case "deploy": {
1960
+ const result = await runDeployCommand(command);
1961
+ process.stdout.write(command.json ? formatDeployJson(result) : formatDeployHuman(result));
1962
+ return result.exitCode;
1963
+ }
1964
+ case "field-test": {
1965
+ const result = await runFieldTestCommand(command);
1966
+ process.stdout.write(command.json ? formatFieldTestJson(result) : formatFieldTestHuman(result));
1967
+ return result.exitCode;
1968
+ }
1909
1969
  case "rls": {
1910
1970
  const result = await runRlsCommand(command);
1911
1971
  if (command.json) {
@@ -1934,7 +1994,15 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
1934
1994
  [
1935
1995
  `release doctor ${result.ok ? "ok" : "failed"}`,
1936
1996
  `ready to publish: ${result.readyToPublish ? "yes" : "no"}`,
1937
- ...result.checks.map((check) => `${check.ok ? "ok" : "fail"} ${check.name}${check.state ? ` (${check.state})` : ""}`),
1997
+ `ready for production deploy: ${result.readyForProductionDeploy ? "yes" : "no"}`,
1998
+ ...result.checks.map((check) => {
1999
+ const scope = check.requiredForPublish
2000
+ ? "publish"
2001
+ : check.requiredForProduction
2002
+ ? "production"
2003
+ : "optional";
2004
+ return `${check.ok ? "ok" : "fail"} ${check.name} [${scope}]${check.state ? ` (${check.state})` : ""}`;
2005
+ }),
1938
2006
  ...(result.nextActions.length > 0 ? ["", "Next:", ...result.nextActions.map((action) => ` ${action}`)] : []),
1939
2007
  ].join("\n").concat("\n"),
1940
2008
  );