forgeos 0.1.0-alpha.60 → 0.1.0-alpha.61

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.60 input=dd73ff0861775ded0c4b42b55e3261a44043d4c09aa3c9f5641d3c23b17e2e6c content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
1
+ // @forge-generated generator=0.1.0-alpha.61 input=6dd290dbb57f6f75a802f741028ac6dbd672aa09d598ba2639ffcec7fa7feb7d content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
2
2
  # AGENTS.md
3
3
 
4
4
  <!-- forge-generated:start -->
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # forgeos
2
2
 
3
+ ## 0.1.0-alpha.61
4
+
5
+ ### Patch Changes
6
+
7
+ - Harden the WorkOS golden path and production deploy gate.
8
+
9
+ - Add the ForgeOS golden path status/plan flow for the official
10
+ `create -> auth -> field-test -> deploy` path.
11
+ - Add WorkOS real-auth proof helpers and env synchronization for app and web
12
+ workspaces, including backend-owned AuthKit session bridge guidance.
13
+ - Block production readiness when `deploy/.env.production` still contains
14
+ placeholder or dummy values, preventing local/demo WorkOS and database
15
+ settings from being mistaken for publishable production evidence.
16
+ - Expand field-test, deploy, env doctor, WorkOS, and golden-path regression
17
+ coverage, plus docs for the production-shaped WorkOS vendor-access flow.
18
+
3
19
  ## 0.1.0-alpha.60
4
20
 
5
21
  ### Patch Changes
package/README.md CHANGED
@@ -75,6 +75,53 @@ npm run dev -- --open
75
75
  Template apps ignore `src/forge/_generated/`, `forge.lock`, and operational `.forge/**` work dirs by default so a freshly created app does not flood git or the editor with generated files. Run `forge generate` after checkout or before verification to recreate the agent contract, client SDK, frontend graph, capability map, and runtime manifests.
76
76
  Templates also include workspace editor excludes for generated/runtime directories so source files stay visually prominent.
77
77
 
78
+ ## Production-Shaped Golden Path
79
+
80
+ For a real multi-tenant app with WorkOS/AuthKit, start with the golden-path
81
+ planner instead of piecing together commands from docs:
82
+
83
+ ```bash
84
+ forge golden-path plan --auth workos --target docker --real --production --json
85
+ ```
86
+
87
+ The canonical path is:
88
+
89
+ ```bash
90
+ forge field-test create vendor-access --auth workos --template vendor-access --package-manager npm --install --git --json
91
+ cd vendor-access
92
+ npm run forge -- workos env --client-id client_... --write --json
93
+ npm run forge -- workos setup --real --file workos-seed.yml --json
94
+ npm run forge -- auth prove --provider workos --real --client-id client_... --file workos-seed.yml --json
95
+ npm run forge -- field-test run --realistic --json
96
+ npm run forge -- deploy readiness --production --json
97
+ npm run forge -- deploy package --target docker
98
+ ```
99
+
100
+ For an existing app, ask ForgeOS for the current blocking stage:
101
+
102
+ ```bash
103
+ forge golden-path status --real --production --json
104
+ ```
105
+
106
+ It reports whether the app can publish, which stage blocks publication, and the
107
+ next command to run. WorkOS FGA is optional; ordinary B2B apps can ship with
108
+ AuthKit, RBAC permission claims, Forge policies, and tenant isolation.
109
+
110
+ If the AuthKit client id is already known, pass it to the status command so the
111
+ next action is copy/paste exact instead of using `client_...` placeholders:
112
+
113
+ ```bash
114
+ forge golden-path status --real --production --client-id client_... --json
115
+ ```
116
+
117
+ When the WorkOS CLI can confirm a client id exists but cannot expose the value,
118
+ `golden-path status` asks for `forge workos env --client-id client_... --write
119
+ --json`; ForgeOS then writes the OIDC/JWKS and web public env values without
120
+ dashboard clicks or shell-specific env syntax. The higher-level
121
+ `forge auth prove --provider workos --real --client-id client_... --file
122
+ workos-seed.yml --json` wrapper performs the same env preparation before it
123
+ proves hosted WorkOS setup.
124
+
78
125
  For release or external smoke testing, choose the Forge package source explicitly:
79
126
 
80
127
  ```bash
package/docs/changelog.md CHANGED
@@ -4,7 +4,63 @@ Release history for the `forgeos` npm package.
4
4
 
5
5
  The canonical source file in the repository is `CHANGELOG.md`.
6
6
 
7
- ## Unreleased
7
+ ## 0.1.0-alpha.61
8
+
9
+ - Added `forge golden-path plan/status` as the canonical
10
+ create -> WorkOS auth -> field-test -> Docker deploy production path for
11
+ agents and users.
12
+ - `forge golden-path plan/status` now accepts `--forge-spec`, so maintainers can
13
+ run the same P0 path against `npm:forgeos@alpha`, a packed tarball, or a local
14
+ checkout before publishing.
15
+ - `forge golden-path status --real --production --json` now wraps WorkOS
16
+ posture, field-test evidence, and deploy readiness into one answer with the
17
+ current stage, blockers, and next command.
18
+ - `forge golden-path plan/status` now accepts `--client-id client_...`, so
19
+ agents that already know the AuthKit client id get exact WorkOS env/proof
20
+ commands instead of placeholder next actions.
21
+ - `forge golden-path status` now includes the first blocking stage in
22
+ `summary.blockers` (for example `workos-real-seed` or `workos-doctor`) before
23
+ lower-level deploy readiness blockers, so agents do not have to infer the
24
+ real next action.
25
+ - Added `forge auth prove --provider workos --real --client-id client_...
26
+ --file workos-seed.yml --json` as the provider-aware auth proof wrapper
27
+ around hosted WorkOS setup, seed evidence, public AuthKit env preparation, and
28
+ CLI login instructions.
29
+ - `forge auth prove --provider workos --real --client-id ...` now reads the
30
+ `.env.local` values it just prepared, so a successful real WorkOS proof reports
31
+ `mode: "oidc"`/`productionReady: true` instead of falling back to local
32
+ `dev-headers` in the same process.
33
+ - Added `forge workos env --client-id client_... --write --json` to prepare `.env.local` and
34
+ `web/.env.local` for real WorkOS/AuthKit proof, including audience/JWKS values
35
+ when `WORKOS_CLIENT_ID` is supplied and an explicit next action when the CLI
36
+ reports a client id exists but cannot expose the value.
37
+ - `forge golden-path plan/status --real` now includes the WorkOS env preparation
38
+ gate before hosted setup/seed proof, so agents fix missing client/JWKS envs
39
+ before retrying AuthKit login or deploy readiness.
40
+ - In `--real` mode, `forge golden-path status` now treats missing or stale
41
+ `.workos-seed-state.json` evidence as an auth-stage blocker and points agents
42
+ to `forge auth prove --provider workos --real --file workos-seed.yml --json`
43
+ after env is ready, or the `--client-id client_...` form when agents need the
44
+ proof command to prepare WorkOS env in the same step.
45
+ - `forge golden-path status` now returns compact field-test/deploy summaries
46
+ instead of embedding full command stdout, keeping the JSON safe for agents to
47
+ parse after realistic field-test runs.
48
+ - `forge deploy plan` and generated Docker production README output now point
49
+ back to `forge golden-path status --real --production --json` as the first
50
+ app-to-production triage command.
51
+ - `forge deploy readiness/check` now prioritizes the deploy env action that
52
+ actually unblocks production, such as copying
53
+ `deploy/.env.production.example` to `deploy/.env.production` before rerunning
54
+ auth diagnostics.
55
+ - `forge env doctor --target production` and `forge deploy check --production`
56
+ now reject known placeholder/dummy production env values without printing the
57
+ secret value, preventing copied examples like `client_test_local` or
58
+ `sk_test_dummy_do_not_use` from being treated as deploy-ready evidence.
59
+ - Docs now surface the WorkOS real + Docker deploy golden path from the CLI,
60
+ golden-path, field-testing, production-readiness pages, and README.
61
+ - Added a redacted WorkOS real vendor-access field report documenting hosted
62
+ setup, idempotent seed, AuthKit session bridge, tenant normalization,
63
+ policy denial, field-test evidence, and production deploy blockers.
8
64
 
9
65
  ## 0.1.0-alpha.60
10
66
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgeos",
3
- "version": "0.1.0-alpha.60",
3
+ "version": "0.1.0-alpha.61",
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.60","releaseId":"forgeos@0.1.0-alpha.60+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.61","releaseId":"forgeos@0.1.0-alpha.61+unknown","schemaVersion":"0.1.0"}
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.60 input=dd73ff0861775ded0c4b42b55e3261a44043d4c09aa3c9f5641d3c23b17e2e6c content=185c42e22dbf3b1cc6e329af86d71f513f19cde067ec0e4ea0a228df020c30f9
1
+ // @forge-generated generator=0.1.0-alpha.61 input=6dd290dbb57f6f75a802f741028ac6dbd672aa09d598ba2639ffcec7fa7feb7d content=82fdee65e1219bbb3fa9572a4c904c7e1e4061b00430769528426ece3b3a4067
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.60",
23
- "releaseId": "forgeos@0.1.0-alpha.60+unknown",
22
+ "packageVersion": "0.1.0-alpha.61",
23
+ "releaseId": "forgeos@0.1.0-alpha.61+unknown",
24
24
  "schemaVersion": "0.1.0"
25
25
  } as const;
@@ -4,11 +4,12 @@ import {
4
4
  FORGE_AUTH_INVALID_AUDIENCE,
5
5
  FORGE_AUTH_JWKS_FAILED,
6
6
  } from "../compiler/diagnostics/codes.ts";
7
- import { loadAuthConfigFromEnv, type AuthClaimsMapping } from "../runtime/auth/config.ts";
7
+ import { AUTH_ENV, loadAuthConfigFromEnv, type AuthClaimsMapping } from "../runtime/auth/config.ts";
8
8
  import { mapClaimsToAuthContext } from "../runtime/auth/claims.ts";
9
9
  import { ForgeAuthError } from "../runtime/auth/errors.ts";
10
10
  import { verifyJwtToken } from "../runtime/auth/verifier.ts";
11
11
  import { loadSecretRegistry } from "../runtime/secrets/check.ts";
12
+ import { loadEnvFiles } from "../runtime/secrets/env-loader.ts";
12
13
  import { existsSync } from "node:fs";
13
14
  import { join } from "node:path";
14
15
  import { normalizeForgeCliCommandsInValue } from "../workspace/forge-cli.ts";
@@ -17,6 +18,9 @@ import {
17
18
  collectPolicyPermissions,
18
19
  missingValues,
19
20
  parseSeedFile,
21
+ runWorkOSCommand,
22
+ type WorkOSCommandResult,
23
+ type WorkOSCommandRunner,
20
24
  } from "./workos.ts";
21
25
 
22
26
  export type AuthSubcommand = "check" | "config" | "decode" | "test-token" | "jwks" | "prove" | "status";
@@ -28,6 +32,11 @@ export interface AuthCommandOptions {
28
32
  token?: string;
29
33
  prod?: boolean;
30
34
  scenario?: string;
35
+ provider?: "workos";
36
+ real?: boolean;
37
+ file?: string;
38
+ clientId?: string;
39
+ commandRunner?: WorkOSCommandRunner;
31
40
  }
32
41
 
33
42
  export interface AuthCommandResult {
@@ -38,6 +47,42 @@ export interface AuthCommandResult {
38
47
  exitCode: 0 | 1;
39
48
  }
40
49
 
50
+ const AUTH_CLI_ENV_KEYS = Object.values(AUTH_ENV);
51
+
52
+ function withCliAuthEnv<T>(workspaceRoot: string, fn: () => T): T {
53
+ const { store } = loadEnvFiles({ workspaceRoot });
54
+ const previous = AUTH_CLI_ENV_KEYS.map((key) => ({
55
+ key,
56
+ had: Object.prototype.hasOwnProperty.call(process.env, key),
57
+ value: process.env[key],
58
+ }));
59
+
60
+ for (const key of AUTH_CLI_ENV_KEYS) {
61
+ if (process.env[key] === undefined) {
62
+ const value = store.resolve(key);
63
+ if (value !== undefined) {
64
+ process.env[key] = value;
65
+ }
66
+ }
67
+ }
68
+
69
+ try {
70
+ return fn();
71
+ } finally {
72
+ for (const entry of previous) {
73
+ if (entry.had) {
74
+ process.env[entry.key] = entry.value;
75
+ } else {
76
+ delete process.env[entry.key];
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ function loadAuthConfigForCli(workspaceRoot: string) {
83
+ return withCliAuthEnv(workspaceRoot, () => loadAuthConfigFromEnv(workspaceRoot));
84
+ }
85
+
41
86
  function detectWorkOS(workspaceRoot: string, claims: AuthClaimsMapping) {
42
87
  const secretRegistry = loadSecretRegistry(workspaceRoot);
43
88
  const secretNames = new Set((secretRegistry?.secrets ?? []).map((secret) => secret.name));
@@ -94,7 +139,7 @@ function configErrors(config: ReturnType<typeof loadAuthConfigFromEnv>): { code:
94
139
  }
95
140
 
96
141
  function buildAuthPosture(workspaceRoot: string) {
97
- const config = loadAuthConfigFromEnv(workspaceRoot);
142
+ const config = loadAuthConfigForCli(workspaceRoot);
98
143
  const productionMode = config.mode === "jwt" || config.mode === "oidc";
99
144
  const errors = configErrors(config);
100
145
  const configReady = errors.length === 0;
@@ -272,8 +317,41 @@ function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof
272
317
  });
273
318
  }
274
319
 
320
+ function runProviderProof(options: AuthCommandOptions): WorkOSCommandResult | null {
321
+ if (options.provider !== "workos") return null;
322
+ const file = options.file ?? "workos-seed.yml";
323
+ if (options.real && options.clientId) {
324
+ const env = runWorkOSCommand({
325
+ subcommand: "env",
326
+ workspaceRoot: options.workspaceRoot,
327
+ json: true,
328
+ file,
329
+ yes: false,
330
+ dryRun: false,
331
+ real: true,
332
+ write: true,
333
+ clientId: options.clientId,
334
+ commandRunner: options.commandRunner,
335
+ });
336
+ if (!env.ok) {
337
+ return env;
338
+ }
339
+ }
340
+ return runWorkOSCommand({
341
+ subcommand: "prove",
342
+ workspaceRoot: options.workspaceRoot,
343
+ json: true,
344
+ file,
345
+ yes: false,
346
+ dryRun: !options.real,
347
+ real: options.real ?? false,
348
+ clientId: options.clientId,
349
+ commandRunner: options.commandRunner,
350
+ });
351
+ }
352
+
275
353
  function validateConfig(workspaceRoot: string): AuthCommandResult {
276
- const config = loadAuthConfigFromEnv(workspaceRoot);
354
+ const config = loadAuthConfigForCli(workspaceRoot);
277
355
  const errors = configErrors(config);
278
356
  const workos = detectWorkOS(workspaceRoot, config.claims);
279
357
 
@@ -298,7 +376,7 @@ function validateConfig(workspaceRoot: string): AuthCommandResult {
298
376
  }
299
377
 
300
378
  function publicConfig(workspaceRoot: string): AuthCommandResult {
301
- const config = loadAuthConfigFromEnv(workspaceRoot);
379
+ const config = loadAuthConfigForCli(workspaceRoot);
302
380
  const workos = detectWorkOS(workspaceRoot, config.claims);
303
381
  return {
304
382
  ok: true,
@@ -319,7 +397,7 @@ function publicConfig(workspaceRoot: string): AuthCommandResult {
319
397
  }
320
398
 
321
399
  function decodeToken(workspaceRoot: string, token: string | undefined): AuthCommandResult {
322
- const config = loadAuthConfigFromEnv(workspaceRoot);
400
+ const config = loadAuthConfigForCli(workspaceRoot);
323
401
  if (!token) {
324
402
  return {
325
403
  ok: false,
@@ -354,7 +432,7 @@ async function testToken(
354
432
  workspaceRoot: string,
355
433
  token: string | undefined,
356
434
  ): Promise<AuthCommandResult> {
357
- const config = loadAuthConfigFromEnv(workspaceRoot);
435
+ const config = loadAuthConfigForCli(workspaceRoot);
358
436
  if (!token) {
359
437
  return {
360
438
  ok: false,
@@ -430,8 +508,9 @@ export async function runAuthCommand(
430
508
  };
431
509
  }
432
510
  if (options.subcommand === "prove") {
511
+ const providerProof = runProviderProof(options);
433
512
  const checked = validateConfig(options.workspaceRoot);
434
- const config = loadAuthConfigFromEnv(options.workspaceRoot);
513
+ const config = loadAuthConfigForCli(options.workspaceRoot);
435
514
  const workos = detectWorkOS(options.workspaceRoot, config.claims);
436
515
  const productionMode = config.mode === "jwt" || config.mode === "oidc";
437
516
  const workosClaimsOk = workos.claimStatus.every((claim) => claim.ok);
@@ -444,7 +523,9 @@ export async function runAuthCommand(
444
523
  : options.prod && tokenProof && !tokenProof.ok
445
524
  ? tokenProof.error
446
525
  : undefined;
447
- const proofOk = checked.ok && (!options.prod || (productionMode && tokenProof?.ok === true));
526
+ const proofOk = checked.ok &&
527
+ (!options.prod || (productionMode && tokenProof?.ok === true)) &&
528
+ (!providerProof || providerProof.ok);
448
529
  const multiTenantProof = options.scenario === "multi-tenant"
449
530
  ? buildMultiTenantProof(options.workspaceRoot, workos, config.requiresTenant)
450
531
  : null;
@@ -460,8 +541,11 @@ export async function runAuthCommand(
460
541
  productionReady: productionMode && checked.ok,
461
542
  prod: options.prod === true,
462
543
  scenario: options.scenario ?? null,
544
+ provider: options.provider ?? null,
545
+ real: options.real === true,
463
546
  authPosture: posture,
464
547
  ...(tokenProof ? { tokenProof: tokenProof.ok ? tokenProof.data : tokenProof.error } : {}),
548
+ ...(providerProof ? { providerProof } : {}),
465
549
  ...(multiTenantProof ? { multiTenantProof } : {}),
466
550
  invariants: [
467
551
  {
@@ -495,9 +579,19 @@ export async function runAuthCommand(
495
579
  },
496
580
  ],
497
581
  workos,
582
+ nextActions: [
583
+ ...(providerProof && !providerProof.ok
584
+ ? providerProof.data && typeof providerProof.data === "object" && "nextCommand" in providerProof.data
585
+ ? [String((providerProof.data as { nextCommand?: unknown }).nextCommand)]
586
+ : ["forge workos prove --real --file workos-seed.yml --json"]
587
+ : []),
588
+ ...(options.provider === "workos" && providerProof?.ok
589
+ ? ["forge auth prove --scenario multi-tenant --json", "forge deploy readiness --production --json"]
590
+ : []),
591
+ ],
498
592
  checkedAt: "deterministic",
499
593
  },
500
- error: prodError ?? checked.error,
594
+ error: prodError ?? (!providerProof || providerProof.ok ? checked.error : { code: "FORGE_AUTH_PROVIDER_PROOF_FAILED", message: "WorkOS provider proof failed" }),
501
595
  exitCode: proofOk && scenarioOk ? 0 : 1,
502
596
  };
503
597
  }
@@ -511,7 +605,7 @@ export async function runAuthCommand(
511
605
  return testToken(options.workspaceRoot, options.token);
512
606
  }
513
607
 
514
- const config = loadAuthConfigFromEnv(options.workspaceRoot);
608
+ const config = loadAuthConfigForCli(options.workspaceRoot);
515
609
  return {
516
610
  ok: true,
517
611
  mode: config.mode,
@@ -540,9 +634,12 @@ function authPostureFromResult(result: AuthCommandResult): Record<string, unknow
540
634
  export function formatAuthHuman(result: AuthCommandResult): string {
541
635
  const posture = authPostureFromResult(result);
542
636
  if (posture) {
637
+ const data = result.data && typeof result.data === "object" ? result.data as Record<string, unknown> : {};
543
638
  const classification = typeof posture.classification === "string" ? posture.classification : result.mode;
544
639
  const reason = typeof posture.reason === "string" ? posture.reason : result.error?.message;
545
- const nextActions = Array.isArray(posture.nextActions) ? posture.nextActions.filter((item): item is string => typeof item === "string") : [];
640
+ const postureNextActions = Array.isArray(posture.nextActions) ? posture.nextActions.filter((item): item is string => typeof item === "string") : [];
641
+ const proofNextActions = Array.isArray(data.nextActions) ? data.nextActions.filter((item): item is string => typeof item === "string") : [];
642
+ const nextActions = Array.from(new Set([...proofNextActions, ...postureNextActions]));
546
643
  const lines = [
547
644
  `Auth ${result.mode}: ${result.ok ? "ok" : "failed"} (${classification})`,
548
645
  `Production ready: ${posture.productionReady === true ? "yes" : "no"}`,
@@ -226,6 +226,11 @@ import {
226
226
  formatFieldTestJson,
227
227
  runFieldTestCommand,
228
228
  } from "./field-test.ts";
229
+ import {
230
+ formatGoldenPathHuman,
231
+ formatGoldenPathJson,
232
+ runGoldenPathCommand,
233
+ } from "./golden-path.ts";
229
234
  import {
230
235
  formatSeedHuman,
231
236
  formatSeedJson,
@@ -2144,6 +2149,11 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
2144
2149
  process.stdout.write(command.json ? formatDeployJson(result) : formatDeployHuman(result));
2145
2150
  return result.exitCode;
2146
2151
  }
2152
+ case "golden-path": {
2153
+ const result = await runGoldenPathCommand(command);
2154
+ process.stdout.write(command.json ? formatGoldenPathJson(result) : formatGoldenPathHuman(result));
2155
+ return result.exitCode;
2156
+ }
2147
2157
  case "field-test": {
2148
2158
  const result = await runFieldTestCommand(command);
2149
2159
  process.stdout.write(command.json ? formatFieldTestJson(result) : formatFieldTestHuman(result));
@@ -12,6 +12,7 @@ import { join } from "node:path";
12
12
  import { runAuthCommand } from "./auth.ts";
13
13
  import { runAuthMdCommand } from "./authmd.ts";
14
14
  import { runWorkOSCommand } from "./workos.ts";
15
+ import { productionPlaceholderEnvProblems } from "./env-placeholders.ts";
15
16
 
16
17
  export type DeploySubcommand = "plan" | "init" | "check" | "readiness" | "render" | "package" | "verify";
17
18
  export type DeployTarget = "docker" | "forge-cloud";
@@ -125,7 +126,7 @@ function deployStageForCheck(name: string): string {
125
126
  if (name.startsWith("auth-md") || name === "oauth-protected-resource") return "auth metadata";
126
127
  if (name.startsWith("field-test")) return "field test";
127
128
  if (name === "live-production") return "live/runtime";
128
- if (name === "deploy-env-sources") return "environment";
129
+ if (name === "deploy-env-sources" || name === "production-env-values") return "environment";
129
130
  return "production";
130
131
  }
131
132
 
@@ -588,6 +589,13 @@ function processDeployEnv(): Record<string, string> {
588
589
  );
589
590
  }
590
591
 
592
+ function productionEnvValues(workspaceRoot: string): Record<string, string> {
593
+ return {
594
+ ...readDeployEnvFile(workspaceRoot),
595
+ ...processDeployEnv(),
596
+ };
597
+ }
598
+
591
599
  function deployEnvSources(workspaceRoot: string): Record<string, unknown> {
592
600
  const processEnv = processDeployEnv();
593
601
  const deployEnv = readDeployEnvFile(workspaceRoot);
@@ -601,10 +609,7 @@ function deployEnvSources(workspaceRoot: string): Record<string, unknown> {
601
609
  keys: envKeys(values),
602
610
  };
603
611
  });
604
- const productionEvidence = {
605
- ...deployEnv,
606
- ...processEnv,
607
- };
612
+ const productionEvidence = productionEnvValues(workspaceRoot);
608
613
  return {
609
614
  readOrder: ["process.env", DEPLOY_ENV_FILE],
610
615
  note: `${DEPLOY_ENV_FILE} and current process env are production deploy evidence; .env/.env.local are reported only as local guidance and are not used to pass production gates.`,
@@ -690,6 +695,16 @@ function renderProductionReadme(): string {
690
695
 
691
696
  This directory is generated by \`forge deploy init --target docker\` or \`forge deploy package --target docker\`.
692
697
 
698
+ Start with the golden-path status gate when you are unsure what comes next:
699
+
700
+ \`\`\`bash
701
+ forge golden-path status --real --production --json
702
+ \`\`\`
703
+
704
+ It reads WorkOS hosted setup evidence, field-test evidence, and deploy readiness
705
+ without hiding the lower-level commands. If it cannot publish, it returns the
706
+ current blocking stage and the next command to run.
707
+
693
708
  ## 1. Prepare production env
694
709
 
695
710
  \`\`\`bash
@@ -760,6 +775,7 @@ forge deploy verify --production --url https://app.example.com --json
760
775
  function buildPlan(options: DeployCommandOptions): DeployCommandResult {
761
776
  const commands = options.target === "forge-cloud"
762
777
  ? [
778
+ "forge golden-path status --real --production --json",
763
779
  "forge deploy check --production --json",
764
780
  "forge auth check --production --json",
765
781
  "forge authmd check --json",
@@ -768,6 +784,7 @@ function buildPlan(options: DeployCommandOptions): DeployCommandResult {
768
784
  "forge deploy verify --production --url https://<your-forge-cloud-app> --json",
769
785
  ]
770
786
  : [
787
+ "forge golden-path status --real --production --json",
771
788
  "forge deploy init --target docker",
772
789
  "cp deploy/.env.production.example deploy/.env.production",
773
790
  "forge authmd generate --json",
@@ -810,6 +827,7 @@ function buildPlan(options: DeployCommandOptions): DeployCommandResult {
810
827
  notes: [
811
828
  "dev-headers auth is local-only and must not be enabled for public runtime.",
812
829
  "Use forge workos doctor/seed when WorkOS is configured.",
830
+ "Use forge golden-path status --real --production --json when the next blocking stage is unclear.",
813
831
  "WorkOS FGA is optional; ordinary WorkOS apps can be production-ready with AuthKit/RBAC permission claims, Forge policies, and tenant isolation.",
814
832
  "Use forge test authz or HTTP probes to prove cross-tenant denial.",
815
833
  ],
@@ -858,6 +876,22 @@ async function buildChecks(options: DeployCommandOptions): Promise<DeployCommand
858
876
  message: `${DEPLOY_ENV_FILE} and process.env are production deploy evidence; .env/.env.local are local guidance only`,
859
877
  details: deployEnvSources(options.workspaceRoot),
860
878
  });
879
+ const placeholderEnvProblems = options.production
880
+ ? productionPlaceholderEnvProblems(productionEnvValues(options.workspaceRoot), DEPLOY_ENV_KEYS as unknown as string[])
881
+ : [];
882
+ checks.push({
883
+ name: "production-env-values",
884
+ ok: placeholderEnvProblems.length === 0,
885
+ severity: "error",
886
+ message: placeholderEnvProblems.length === 0
887
+ ? "production env values do not contain known placeholders"
888
+ : `production env contains placeholder value(s): ${placeholderEnvProblems.map((problem) => problem.key).join(", ")}`,
889
+ command: "edit deploy/.env.production with real production values",
890
+ details: {
891
+ placeholders: placeholderEnvProblems,
892
+ valuesRedacted: true,
893
+ },
894
+ });
861
895
 
862
896
  const auth = await withDeployEnv(options.workspaceRoot, async () => loadAuthConfigFromEnv(options.workspaceRoot));
863
897
  const productionAuth = auth.mode === "jwt" || auth.mode === "oidc";
@@ -1076,9 +1110,10 @@ async function buildChecks(options: DeployCommandOptions): Promise<DeployCommand
1076
1110
  });
1077
1111
 
1078
1112
  const errorFree = checks.every((check) => check.ok || check.severity === "warning");
1079
- const failureActions = unique(checks.flatMap((check) =>
1113
+ const rawFailureActions = unique(checks.flatMap((check) =>
1080
1114
  !check.ok && check.command ? expandDeployFailureCommand(check.command) : []
1081
1115
  ));
1116
+ const failureActions = prioritizeDeployFailureActions(options, checks, rawFailureActions);
1082
1117
  const nextActions = errorFree
1083
1118
  ? ["forge deploy verify --production --url https://app.example.com --json"]
1084
1119
  : failureActions;
@@ -1110,6 +1145,22 @@ function expandDeployFailureCommand(command: string): string[] {
1110
1145
  return [command];
1111
1146
  }
1112
1147
 
1148
+ function prioritizeDeployFailureActions(
1149
+ options: DeployCommandOptions,
1150
+ checks: DeployCheck[],
1151
+ actions: string[],
1152
+ ): string[] {
1153
+ const database = checks.find((check) => check.name === "database-url");
1154
+ if (!database || database.ok || !database.command) {
1155
+ return actions;
1156
+ }
1157
+ const deployEnvMissing = !nodeFileSystem.exists(join(options.workspaceRoot, DEPLOY_ENV_FILE));
1158
+ if (deployEnvMissing && database.command === "cp deploy/.env.production.example deploy/.env.production") {
1159
+ return unique([database.command, ...actions.filter((action) => action !== database.command)]);
1160
+ }
1161
+ return actions;
1162
+ }
1163
+
1113
1164
  function renderDocker(options: DeployCommandOptions): DeployCommandResult {
1114
1165
  const dir = join(options.workspaceRoot, "deploy");
1115
1166
  nodeFileSystem.mkdirp(dir);
@@ -0,0 +1,62 @@
1
+ export interface PlaceholderEnvProblem {
2
+ key: string;
3
+ reason: string;
4
+ }
5
+
6
+ const GENERIC_PLACEHOLDER_PATTERNS: Array<{ pattern: RegExp; reason: string }> = [
7
+ { pattern: /\bexample\.(com|test|org|net)\b/, reason: "example domain" },
8
+ { pattern: /\bdummy\b/, reason: "dummy marker" },
9
+ { pattern: /\bplaceholder\b/, reason: "placeholder marker" },
10
+ { pattern: /\bchange[-_]?me\b/, reason: "change-me marker" },
11
+ { pattern: /\bdo[-_]?not[-_]?use\b/, reason: "do-not-use marker" },
12
+ { pattern: /^<.*>$/, reason: "angle-bracket placeholder" },
13
+ { pattern: /^your[-_]/, reason: "your-value placeholder" },
14
+ ];
15
+
16
+ const KEY_PLACEHOLDER_PATTERNS: Record<string, Array<{ pattern: RegExp; reason: string }>> = {
17
+ DATABASE_URL: [
18
+ { pattern: /^postgres:\/\/forge:forge@postgres:5432\/forge_app$/i, reason: "generated Docker example database URL" },
19
+ { pattern: /^postgres:\/\/forge:forge@db:5432\/forge$/i, reason: "local dummy database URL" },
20
+ { pattern: /^postgres:\/\/local-only\b/i, reason: "local-only database URL" },
21
+ ],
22
+ WORKOS_API_KEY: [
23
+ { pattern: /^sk_test_dummy/i, reason: "dummy WorkOS API key" },
24
+ { pattern: /^sk_test_?\.\.\.$/i, reason: "placeholder WorkOS API key" },
25
+ ],
26
+ WORKOS_CLIENT_ID: [
27
+ { pattern: /^client_?\.\.\.$/i, reason: "placeholder WorkOS client id" },
28
+ { pattern: /^client_test_local$/i, reason: "local fake WorkOS client id" },
29
+ ],
30
+ WORKOS_COOKIE_PASSWORD: [
31
+ { pattern: /dummy/i, reason: "dummy cookie password" },
32
+ { pattern: /change[-_]?me/i, reason: "change-me cookie password" },
33
+ ],
34
+ FORGE_AUTH_AUDIENCE: [
35
+ { pattern: /^client_test_local$/i, reason: "local fake auth audience" },
36
+ ],
37
+ FORGE_AUTH_JWKS_URI: [
38
+ { pattern: /\/client_test_local$/i, reason: "local fake JWKS client id" },
39
+ ],
40
+ };
41
+
42
+ export function placeholderEnvProblem(key: string, value: string | undefined): PlaceholderEnvProblem | null {
43
+ const trimmed = value?.trim() ?? "";
44
+ if (!trimmed) return null;
45
+ const normalized = trimmed.toLowerCase();
46
+ for (const { pattern, reason } of KEY_PLACEHOLDER_PATTERNS[key] ?? []) {
47
+ if (pattern.test(trimmed)) return { key, reason };
48
+ }
49
+ for (const { pattern, reason } of GENERIC_PLACEHOLDER_PATTERNS) {
50
+ if (pattern.test(normalized)) return { key, reason };
51
+ }
52
+ return null;
53
+ }
54
+
55
+ export function productionPlaceholderEnvProblems(
56
+ values: Record<string, string>,
57
+ keys = Object.keys(values),
58
+ ): PlaceholderEnvProblem[] {
59
+ return keys
60
+ .map((key) => placeholderEnvProblem(key, values[key]))
61
+ .filter((problem): problem is PlaceholderEnvProblem => Boolean(problem));
62
+ }
@@ -317,6 +317,7 @@ async function createFieldTestApp(options: FieldTestCommandOptions): Promise<Fie
317
317
  };
318
318
  }
319
319
  if (options.dryRun) {
320
+ const forgeSpecFlag = options.forgeSpec ? ` --forge-spec ${options.forgeSpec}` : "";
320
321
  return normalizeForgeCliCommandsInValue(options.workspaceRoot, {
321
322
  schemaVersion: "0.1.0",
322
323
  ok: true,
@@ -328,11 +329,12 @@ async function createFieldTestApp(options: FieldTestCommandOptions): Promise<Fie
328
329
  template: options.template,
329
330
  packageManager: options.packageManager,
330
331
  auth: options.auth,
332
+ forgeSpec: options.forgeSpec,
331
333
  install: true,
332
334
  git: true,
333
- command: `forge new ${options.name} --template ${options.template} --package-manager ${options.packageManager} --field-test --install`,
335
+ command: `forge new ${options.name} --template ${options.template} --package-manager ${options.packageManager}${forgeSpecFlag} --field-test --install`,
334
336
  goldenPath: [
335
- `forge field-test create ${options.name} --auth ${options.auth ?? "none"} --template ${options.template} --package-manager ${options.packageManager} --install --git --json`,
337
+ `forge field-test create ${options.name} --auth ${options.auth ?? "none"} --template ${options.template} --package-manager ${options.packageManager}${forgeSpecFlag} --install --git --json`,
336
338
  `cd ${options.name}`,
337
339
  "forge field-test run --realistic --json",
338
340
  "forge field-test report --json",
@@ -341,7 +343,7 @@ async function createFieldTestApp(options: FieldTestCommandOptions): Promise<Fie
341
343
  ],
342
344
  },
343
345
  nextActions: [
344
- `forge field-test create ${options.name} --auth ${options.auth ?? "none"} --template ${options.template} --package-manager ${options.packageManager} --install --git --json`,
346
+ `forge field-test create ${options.name} --auth ${options.auth ?? "none"} --template ${options.template} --package-manager ${options.packageManager}${forgeSpecFlag} --install --git --json`,
345
347
  ],
346
348
  exitCode: 0,
347
349
  });