forgeos 0.1.0-alpha.44 → 0.1.0-alpha.45

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.44 input=3c44e5f9f6fd4afe42accec6d73bca4f36f188ed9de5fb11f4a890d3020fe2bc content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
1
+ // @forge-generated generator=0.1.0-alpha.45 input=bd2a19728a6c6eef99a21730384e4e1f16b60d6e561897a0b6f307c136f9555c content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
2
2
  # AGENTS.md
3
3
 
4
4
  <!-- forge-generated:start -->
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # forgeos
2
2
 
3
+ ## 0.1.0-alpha.45
4
+
5
+ ### Patch Changes
6
+
7
+ - Smooth the WorkOS, field-test, and deploy-readiness DX discovered during real
8
+ app field testing.
9
+
10
+ - Accept `--real` through the top-level unknown-option guard so documented
11
+ commands like `forge workos setup --real --file workos-seed.yml --json` and
12
+ `forge workos prove --real --file workos-seed.yml --json` run instead of
13
+ being rejected before parsing.
14
+ - Treat known WorkOS CLI duplicate-resource seed responses as explicit clean
15
+ idempotency: Forge now records `seedAlreadyAppliedReason`, suppresses the
16
+ scary duplicate stderr from the successful JSON result, and writes seed
17
+ state with the already-applied status.
18
+ - Run `forge field-test run` through an async harness process and emit
19
+ periodic stderr heartbeats so long realistic probes no longer look frozen
20
+ while preserving machine-readable JSON output.
21
+ - Add a `deploy-env-sources` check to `forge deploy check --production` that
22
+ reports which env sources were inspected, which keys were present, and which
23
+ production keys are still missing without printing secret values; `.env` and
24
+ `.env.local` are shown as local guidance, not production deploy evidence.
25
+
3
26
  ## 0.1.0-alpha.44
4
27
 
5
28
  ### Patch Changes
package/docs/changelog.md CHANGED
@@ -6,6 +6,21 @@ The canonical source file in the repository is `CHANGELOG.md`.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.1.0-alpha.45
10
+
11
+ - `forge workos setup/prove --real` now passes the top-level unknown-option
12
+ guard, matching the documented no-dashboard WorkOS apply flow.
13
+ - `forge workos seed --file workos-seed.yml --json` now treats known WorkOS
14
+ duplicate-resource responses as explicit idempotency, suppressing duplicate
15
+ stderr in successful JSON output while recording `seedAlreadyAppliedReason`
16
+ and seed state.
17
+ - `forge field-test run` now uses an async harness process with periodic
18
+ progress heartbeats on stderr so realistic probes do not appear stuck.
19
+ - `forge deploy check --production` now reports sanitized env-source evidence,
20
+ including which production keys were found or missing, while keeping `.env`
21
+ and `.env.local` clearly marked as local guidance rather than production
22
+ deploy evidence.
23
+
9
24
  ## 0.1.0-alpha.44
10
25
 
11
26
  - `forge inspect ui --ergonomics` now treats helper-based seed recovery inside
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgeos",
3
- "version": "0.1.0-alpha.44",
3
+ "version": "0.1.0-alpha.45",
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.44","releaseId":"forgeos@0.1.0-alpha.44+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.45","releaseId":"forgeos@0.1.0-alpha.45+unknown","schemaVersion":"0.1.0"}
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.44 input=3c44e5f9f6fd4afe42accec6d73bca4f36f188ed9de5fb11f4a890d3020fe2bc content=3d89cc8170a2affb58421e25ec58882655d6ea2399efe65a822d99e1afa1ddb5
1
+ // @forge-generated generator=0.1.0-alpha.45 input=bd2a19728a6c6eef99a21730384e4e1f16b60d6e561897a0b6f307c136f9555c content=ddd1dcdfbef3bde6b2457bcfc5fd270a97d6d97ec6477305dc9c47c9f85f7c05
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.44",
23
- "releaseId": "forgeos@0.1.0-alpha.44+unknown",
22
+ "packageVersion": "0.1.0-alpha.45",
23
+ "releaseId": "forgeos@0.1.0-alpha.45+unknown",
24
24
  "schemaVersion": "0.1.0"
25
25
  } as const;
@@ -120,6 +120,23 @@ function fieldTestReportCandidates(workspaceRoot: string): Array<{ path: string;
120
120
  }
121
121
 
122
122
  const FIELD_TEST_PRODUCTION_COMMAND = "forge field-test run --realistic --json";
123
+ const DEPLOY_ENV_FILE = "deploy/.env.production";
124
+ const LOCAL_ENV_FILES = [".env", ".env.local"];
125
+ const DEPLOY_ENV_KEYS = [
126
+ "DATABASE_URL",
127
+ "FORGE_AUTH_MODE",
128
+ "FORGE_AUTH_ISSUER",
129
+ "FORGE_AUTH_AUDIENCE",
130
+ "FORGE_AUTH_JWKS_URI",
131
+ "FORGE_AUTH_DISCOVERY_URL",
132
+ "WORKOS_API_KEY",
133
+ "WORKOS_CLIENT_ID",
134
+ "WORKOS_COOKIE_PASSWORD",
135
+ "WORKOS_REDIRECT_URI",
136
+ "WORKOS_POST_LOGIN_REDIRECT_URI",
137
+ "WORKOS_POST_LOGOUT_REDIRECT_URI",
138
+ "WORKOS_WEBHOOK_SECRET",
139
+ ];
123
140
 
124
141
  function summarizeFieldTestReport(data: Record<string, unknown>) {
125
142
  const results = Array.isArray(data.results) ? data.results as Array<Record<string, unknown>> : [];
@@ -428,7 +445,7 @@ function renderEnvExample(workspaceRoot: string): string {
428
445
  }
429
446
 
430
447
  function hasProductionEnvFile(workspaceRoot: string): boolean {
431
- return nodeFileSystem.exists(join(workspaceRoot, "deploy/.env.production"));
448
+ return nodeFileSystem.exists(join(workspaceRoot, DEPLOY_ENV_FILE));
432
449
  }
433
450
 
434
451
  function parseEnvValue(value: string): string {
@@ -442,8 +459,8 @@ function parseEnvValue(value: string): string {
442
459
  return trimmed;
443
460
  }
444
461
 
445
- function readDeployEnvFile(workspaceRoot: string): Record<string, string> {
446
- const absolute = join(workspaceRoot, "deploy/.env.production");
462
+ function readEnvFile(workspaceRoot: string, relative: string): Record<string, string> {
463
+ const absolute = join(workspaceRoot, relative);
447
464
  if (!nodeFileSystem.exists(absolute)) return {};
448
465
  const text = nodeFileSystem.readText(absolute) ?? "";
449
466
  const values: Record<string, string> = {};
@@ -457,6 +474,63 @@ function readDeployEnvFile(workspaceRoot: string): Record<string, string> {
457
474
  return values;
458
475
  }
459
476
 
477
+ function readDeployEnvFile(workspaceRoot: string): Record<string, string> {
478
+ return readEnvFile(workspaceRoot, DEPLOY_ENV_FILE);
479
+ }
480
+
481
+ function envKeys(values: Record<string, string>): string[] {
482
+ return DEPLOY_ENV_KEYS.filter((key) => Boolean(values[key])).sort();
483
+ }
484
+
485
+ function processDeployEnv(): Record<string, string> {
486
+ return Object.fromEntries(
487
+ DEPLOY_ENV_KEYS
488
+ .filter((key) => Boolean(process.env[key]))
489
+ .map((key) => [key, process.env[key]!]),
490
+ );
491
+ }
492
+
493
+ function deployEnvSources(workspaceRoot: string): Record<string, unknown> {
494
+ const processEnv = processDeployEnv();
495
+ const deployEnv = readDeployEnvFile(workspaceRoot);
496
+ const localSources = LOCAL_ENV_FILES.map((path) => {
497
+ const values = readEnvFile(workspaceRoot, path);
498
+ return {
499
+ path,
500
+ present: nodeFileSystem.exists(join(workspaceRoot, path)),
501
+ role: "local-guidance",
502
+ readForProduction: false,
503
+ keys: envKeys(values),
504
+ };
505
+ });
506
+ const productionEvidence = {
507
+ ...deployEnv,
508
+ ...processEnv,
509
+ };
510
+ return {
511
+ readOrder: ["process.env", DEPLOY_ENV_FILE],
512
+ 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.`,
513
+ sources: [
514
+ {
515
+ path: "process.env",
516
+ present: envKeys(processEnv).length > 0,
517
+ role: "production-evidence",
518
+ readForProduction: true,
519
+ keys: envKeys(processEnv),
520
+ },
521
+ {
522
+ path: DEPLOY_ENV_FILE,
523
+ present: hasProductionEnvFile(workspaceRoot),
524
+ role: "production-evidence",
525
+ readForProduction: true,
526
+ keys: envKeys(deployEnv),
527
+ },
528
+ ...localSources,
529
+ ],
530
+ missingProductionKeys: DEPLOY_ENV_KEYS.filter((key) => !productionEvidence[key]),
531
+ };
532
+ }
533
+
460
534
  function deployEnvValue(workspaceRoot: string, name: string): string | undefined {
461
535
  return process.env[name] || readDeployEnvFile(workspaceRoot)[name];
462
536
  }
@@ -494,15 +568,15 @@ function databaseReady(options: DeployCommandOptions): boolean {
494
568
 
495
569
  function databaseReadyMessage(options: DeployCommandOptions): string {
496
570
  if (process.env.DATABASE_URL) return "DATABASE_URL is set in the current environment";
497
- if (readDeployEnvFile(options.workspaceRoot).DATABASE_URL) return "DATABASE_URL is set in deploy/.env.production";
498
- if (hasProductionEnvFile(options.workspaceRoot)) return "deploy/.env.production is present but does not set DATABASE_URL";
571
+ if (readDeployEnvFile(options.workspaceRoot).DATABASE_URL) return `DATABASE_URL is set in ${DEPLOY_ENV_FILE}`;
572
+ if (hasProductionEnvFile(options.workspaceRoot)) return `${DEPLOY_ENV_FILE} is present but does not set DATABASE_URL`;
499
573
  if (hasRenderedProductionEnvExample(options.workspaceRoot)) {
500
574
  return options.production
501
- ? "deploy/.env.production.example is only a template; copy it to deploy/.env.production with DATABASE_URL or set DATABASE_URL"
575
+ ? `deploy/.env.production.example is only a template; copy it to ${DEPLOY_ENV_FILE} with DATABASE_URL or set DATABASE_URL`
502
576
  : "deploy/.env.production.example is present";
503
577
  }
504
578
  return options.production
505
- ? "production deploy requires DATABASE_URL or deploy/.env.production"
579
+ ? `production deploy requires DATABASE_URL or ${DEPLOY_ENV_FILE}`
506
580
  : "deploy readiness requires DATABASE_URL or rendered deploy/.env.production.example";
507
581
  }
508
582
 
@@ -650,6 +724,13 @@ async function buildChecks(options: DeployCommandOptions): Promise<DeployCommand
650
724
  found: lockfiles,
651
725
  },
652
726
  });
727
+ checks.push({
728
+ name: "deploy-env-sources",
729
+ ok: true,
730
+ severity: "warning",
731
+ message: `${DEPLOY_ENV_FILE} and process.env are production deploy evidence; .env/.env.local are local guidance only`,
732
+ details: deployEnvSources(options.workspaceRoot),
733
+ });
653
734
 
654
735
  const auth = await withDeployEnv(options.workspaceRoot, async () => loadAuthConfigFromEnv(options.workspaceRoot));
655
736
  const productionAuth = auth.mode === "jwt" || auth.mode === "oidc";
@@ -1,4 +1,4 @@
1
- import { spawnSync } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { dirname, isAbsolute, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -400,7 +400,56 @@ async function createFieldTestApp(options: FieldTestCommandOptions): Promise<Fie
400
400
  });
401
401
  }
402
402
 
403
- function runHarness(options: FieldTestCommandOptions): FieldTestCommandResult {
403
+ function shouldEmitProgress(): boolean {
404
+ return process.env.FORGE_FIELD_TEST_PROGRESS !== "0";
405
+ }
406
+
407
+ function runHarnessProcess(
408
+ options: FieldTestCommandOptions,
409
+ args: string[],
410
+ ): Promise<{ status: number | null; stdout: string; stderr: string }> {
411
+ return new Promise((resolve) => {
412
+ const startedAt = Date.now();
413
+ const emitProgress = shouldEmitProgress();
414
+ const child = spawn(process.execPath, args, {
415
+ cwd: options.workspaceRoot,
416
+ stdio: ["ignore", "pipe", "pipe"],
417
+ windowsHide: true,
418
+ });
419
+ let stdout = "";
420
+ let stderr = "";
421
+ let settled = false;
422
+ child.stdout?.setEncoding("utf8");
423
+ child.stderr?.setEncoding("utf8");
424
+ child.stdout?.on("data", (chunk) => {
425
+ stdout += chunk;
426
+ });
427
+ child.stderr?.on("data", (chunk) => {
428
+ stderr += chunk;
429
+ });
430
+ const heartbeat = emitProgress
431
+ ? setInterval(() => {
432
+ const elapsed = Math.round((Date.now() - startedAt) / 1000);
433
+ process.stderr.write(`[forge] field-test still running after ${elapsed}s; timeout=${options.timeoutMs}ms\n`);
434
+ }, 30_000)
435
+ : undefined;
436
+ const finish = (status: number | null, extraStderr = "") => {
437
+ if (settled) return;
438
+ settled = true;
439
+ if (heartbeat) clearInterval(heartbeat);
440
+ if (extraStderr) stderr += extraStderr;
441
+ resolve({ status, stdout, stderr });
442
+ };
443
+ child.on("error", (error) => {
444
+ finish(1, `${error.message}\n`);
445
+ });
446
+ child.on("close", (status) => {
447
+ finish(status);
448
+ });
449
+ });
450
+ }
451
+
452
+ async function runHarness(options: FieldTestCommandOptions): Promise<FieldTestCommandResult> {
404
453
  const script = scriptPath(options.workspaceRoot);
405
454
  if (!script) {
406
455
  return {
@@ -433,11 +482,7 @@ function runHarness(options: FieldTestCommandOptions): FieldTestCommandResult {
433
482
  if (options.forgeSpec) args.push("--forge-spec", options.forgeSpec);
434
483
  if (reportPath) args.push("--write-report", reportPath);
435
484
  if (options.json) args.push("--json");
436
- const result = spawnSync(process.execPath, args, {
437
- cwd: options.workspaceRoot,
438
- encoding: "utf8",
439
- windowsHide: true,
440
- });
485
+ const result = await runHarnessProcess(options, args);
441
486
  const reportAbsolute = reportPath ? resolveReportPath(options.workspaceRoot, reportPath) : null;
442
487
  const reportData = reportAbsolute && existsSync(reportAbsolute)
443
488
  ? JSON.parse(readFileSync(reportAbsolute, "utf8")) as unknown
@@ -3069,6 +3069,7 @@ export function hasUnknownOption(argv: string[]): string | null {
3069
3069
  "--auth-probes",
3070
3070
  "--ui-probes",
3071
3071
  "--realistic",
3072
+ "--real",
3072
3073
  "--write-report",
3073
3074
  "--tenant-scoped",
3074
3075
  "--field",
@@ -703,7 +703,9 @@ function seedData(input: {
703
703
  dryRun?: boolean;
704
704
  seedFileSanitized?: boolean;
705
705
  seedAlreadyApplied?: boolean;
706
+ seedAlreadyAppliedReason?: string;
706
707
  seedStateFile?: string;
708
+ workosCli?: Record<string, unknown>;
707
709
  configActions?: WorkOSConfigActionResult[];
708
710
  nextCommand?: string;
709
711
  }): Record<string, unknown> {
@@ -716,7 +718,9 @@ function seedData(input: {
716
718
  dryRun: input.dryRun ?? false,
717
719
  seedFileSanitized: input.seedFileSanitized ?? false,
718
720
  seedAlreadyApplied: input.seedAlreadyApplied ?? false,
721
+ ...(input.seedAlreadyAppliedReason ? { seedAlreadyAppliedReason: input.seedAlreadyAppliedReason } : {}),
719
722
  ...(input.seedStateFile ? { seedStateFile: input.seedStateFile } : {}),
723
+ ...(input.workosCli ? { workosCli: input.workosCli } : {}),
720
724
  configActions: input.configActions ?? [],
721
725
  ...(input.nextCommand ? { nextCommand: input.nextCommand } : {}),
722
726
  };
@@ -1087,6 +1091,9 @@ export function runWorkOSSeedCommand(options: WorkOSCommandOptions): WorkOSComma
1087
1091
  const child = runExternalCommand(delegatedCommand, options);
1088
1092
  const seedAlreadyApplied =
1089
1093
  child.status !== 0 && isWorkOSSeedAlreadyApplied(child.stdout, child.stderr);
1094
+ const seedAlreadyAppliedReason = seedAlreadyApplied
1095
+ ? "workos-cli-existing-resource"
1096
+ : undefined;
1090
1097
  const seedStateFile = child.status === 0 || seedAlreadyApplied
1091
1098
  ? writeWorkOSSeedState({
1092
1099
  workspaceRoot: options.workspaceRoot,
@@ -1113,11 +1120,17 @@ export function runWorkOSSeedCommand(options: WorkOSCommandOptions): WorkOSComma
1113
1120
  seedState: latestSeedState,
1114
1121
  seedFileSanitized: preparedSeed.sanitized,
1115
1122
  seedAlreadyApplied,
1123
+ seedAlreadyAppliedReason,
1116
1124
  seedStateFile,
1125
+ workosCli: {
1126
+ status: child.status,
1127
+ idempotentConflict: seedAlreadyApplied,
1128
+ stderrSuppressed: seedAlreadyApplied && Boolean(child.stderr.trim()),
1129
+ },
1117
1130
  configActions,
1118
1131
  }),
1119
1132
  stdout: child.stdout,
1120
- stderr: child.stderr,
1133
+ stderr: seedAlreadyApplied ? undefined : child.stderr,
1121
1134
  exitCode: child.status === 0 || seedAlreadyApplied ? 0 : 1,
1122
1135
  };
1123
1136
  } finally {
@@ -1,3 +1,3 @@
1
- export const FORGEOS_VERSION = "0.1.0-alpha.44";
1
+ export const FORGEOS_VERSION = "0.1.0-alpha.45";
2
2
  export const GENERATOR_VERSION = FORGEOS_VERSION;
3
3
  export const CLI_VERSION = FORGEOS_VERSION;