forgeos 0.1.0-alpha.40 → 0.1.0-alpha.42

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.
Files changed (66) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +46 -2
  3. package/docs/changelog.md +225 -0
  4. package/package.json +1 -1
  5. package/src/forge/_generated/releaseManifest.json +1 -1
  6. package/src/forge/_generated/releaseManifest.ts +3 -3
  7. package/src/forge/cli/auth.ts +1 -0
  8. package/src/forge/cli/commands.ts +13 -0
  9. package/src/forge/cli/deploy.ts +109 -18
  10. package/src/forge/cli/dev.ts +176 -10
  11. package/src/forge/cli/field-test.ts +198 -13
  12. package/src/forge/cli/main.ts +16 -5
  13. package/src/forge/cli/new.ts +2 -1
  14. package/src/forge/cli/output.ts +9 -0
  15. package/src/forge/cli/parse.ts +95 -12
  16. package/src/forge/cli/run.ts +1 -0
  17. package/src/forge/cli/seed.ts +581 -0
  18. package/src/forge/cli/workos.ts +349 -16
  19. package/src/forge/compiler/agent-contract/build.ts +58 -0
  20. package/src/forge/compiler/data-graph/build.ts +29 -1
  21. package/src/forge/compiler/diagnostics/codes.ts +2 -0
  22. package/src/forge/compiler/diagnostics/create.ts +7 -0
  23. package/src/forge/compiler/integration/add.ts +305 -5
  24. package/src/forge/compiler/integration/templates/workos.ts +51 -2
  25. package/src/forge/compiler/orchestrator/plan.ts +2 -0
  26. package/src/forge/compiler/package-manager/version.ts +36 -12
  27. package/src/forge/make/index.ts +2 -0
  28. package/src/forge/make/templates.ts +58 -4
  29. package/src/forge/runtime/external/bridge.ts +14 -0
  30. package/src/forge/server.ts +59 -9
  31. package/src/forge/ui/index.ts +643 -25
  32. package/src/forge/ui/types.ts +2 -1
  33. package/src/forge/version.ts +1 -1
  34. package/templates/agent-workroom/web/index.html +1 -0
  35. package/templates/agent-workroom/web/src/lib/forge.ts +9 -2
  36. package/templates/agent-workroom/web/vite.config.ts +36 -0
  37. package/templates/b2b-support-web/web/app/page.tsx +2 -2
  38. package/templates/b2b-support-web/web/lib/forge.ts +9 -2
  39. package/templates/b2b-support-web/web/next.config.ts +24 -0
  40. package/templates/minimal-web/web/index.html +1 -0
  41. package/templates/minimal-web/web/src/App.tsx +12 -6
  42. package/templates/minimal-web/web/src/lib/forge.ts +9 -2
  43. package/templates/minimal-web/web/src/styles.css +34 -0
  44. package/templates/minimal-web/web/vite.config.ts +36 -0
  45. package/templates/nuxt-web/web/app.vue +1 -1
  46. package/templates/vendor-access/.vscode/settings.json +14 -0
  47. package/templates/vendor-access/README.md +30 -0
  48. package/templates/vendor-access/forge.config.ts +3 -0
  49. package/templates/vendor-access/package.json +34 -0
  50. package/templates/vendor-access/src/commands/addEvidence.ts +42 -0
  51. package/templates/vendor-access/src/commands/approveAccessRequest.ts +43 -0
  52. package/templates/vendor-access/src/commands/createAccessRequest.ts +49 -0
  53. package/templates/vendor-access/src/commands/seedVendorAccessDemo.ts +200 -0
  54. package/templates/vendor-access/src/forge/schema.ts +74 -0
  55. package/templates/vendor-access/src/policies.ts +11 -0
  56. package/templates/vendor-access/src/queries/listVendorAccessDashboard.ts +24 -0
  57. package/templates/vendor-access/src/queries/liveVendorAccessDashboard.ts +24 -0
  58. package/templates/vendor-access/tsconfig.json +17 -0
  59. package/templates/vendor-access/web/index.html +13 -0
  60. package/templates/vendor-access/web/package.json +21 -0
  61. package/templates/vendor-access/web/src/App.tsx +573 -0
  62. package/templates/vendor-access/web/src/lib/forge.ts +20 -0
  63. package/templates/vendor-access/web/src/main.tsx +205 -0
  64. package/templates/vendor-access/web/src/styles.css +682 -0
  65. package/templates/vendor-access/web/tsconfig.json +18 -0
  66. package/templates/vendor-access/web/vite.config.ts +36 -0
@@ -34,6 +34,7 @@ import { FORGE_PGLITE_STORE_ABORTED } from "../compiler/diagnostics/codes.ts";
34
34
  import { isPgliteAbortMessage } from "../runtime/db/pglite-adapter.ts";
35
35
  import { forgeCliCommandForWorkspace, forgeCliCommandsForWorkspace } from "../workspace/forge-cli.ts";
36
36
  import { writeLastRunRecord } from "./last-run.ts";
37
+ import { runSeedCommand, type SeedCommandResult } from "./seed.ts";
37
38
 
38
39
  export interface DevCommandOptions {
39
40
  workspaceRoot: string;
@@ -58,6 +59,9 @@ export interface DevCommandOptions {
58
59
  allowDevAuth?: boolean;
59
60
  skipStartupConsole?: boolean;
60
61
  detach?: boolean;
62
+ seed?: boolean;
63
+ seedCommand?: string;
64
+ seedAllTenants?: boolean;
61
65
  lifecycle?: "status" | "stop";
62
66
  }
63
67
 
@@ -75,7 +79,7 @@ export interface DevGenerateResult {
75
79
  exitCode: 0 | 1;
76
80
  }
77
81
 
78
- interface DevStartupGeneratedEvidence {
82
+ export interface DevStartupGeneratedEvidence {
79
83
  ok: boolean;
80
84
  state: "fresh" | "regenerated" | "stale-risk";
81
85
  changedFiles: number;
@@ -86,7 +90,7 @@ interface DevStartupGeneratedEvidence {
86
90
  checkCommand: string;
87
91
  }
88
92
 
89
- interface WebDevServerHandle {
93
+ export interface WebDevServerHandle {
90
94
  url: string;
91
95
  port: number;
92
96
  requestedPort?: number;
@@ -95,7 +99,7 @@ interface WebDevServerHandle {
95
99
  stop: () => void;
96
100
  }
97
101
 
98
- interface DevStartupSummary {
102
+ export interface DevStartupSummary {
99
103
  schemaVersion: "0.1.0";
100
104
  ok: true;
101
105
  mode: "dev";
@@ -155,11 +159,31 @@ interface DevStartupSummary {
155
159
  checkCommand: string;
156
160
  message: string;
157
161
  };
162
+ seed: {
163
+ requested: boolean;
164
+ available: boolean;
165
+ commands: string[];
166
+ selectedCommand?: string;
167
+ readiness?: SeedCommandResult["readiness"];
168
+ ran: boolean;
169
+ ok?: boolean;
170
+ responseStatus?: number;
171
+ tenantRuns?: Array<{
172
+ tenantId: string;
173
+ label?: string;
174
+ organizationName?: string;
175
+ ok: boolean;
176
+ responseStatus?: number;
177
+ }>;
178
+ error?: string;
179
+ nextActions: string[];
180
+ };
158
181
  next: {
159
182
  browserUrl: string;
160
183
  apiIndex: string;
161
184
  inspect: string;
162
185
  verify: string;
186
+ changed: string;
163
187
  };
164
188
  pid: number;
165
189
  }
@@ -283,6 +307,9 @@ function buildDetachedDevArgs(options: DevCommandOptions): string[] {
283
307
  if (options.webPort !== undefined) args.push("--web-port", String(options.webPort));
284
308
  if (options.telemetry.length > 0) args.push("--telemetry", options.telemetry.join(","));
285
309
  if (options.envFile) args.push("--env-file", options.envFile);
310
+ if (options.seed) args.push("--seed");
311
+ if (options.seedCommand) args.push("--seed-command", options.seedCommand);
312
+ if (options.seedAllTenants) args.push("--all-tenants");
286
313
  return args;
287
314
  }
288
315
 
@@ -486,6 +513,27 @@ async function isPortAvailable(host: string, port: number): Promise<boolean> {
486
513
  });
487
514
  }
488
515
 
516
+ async function reserveEphemeralPort(host: string): Promise<number> {
517
+ const server = createNetServer();
518
+ return new Promise<number>((resolve, reject) => {
519
+ const cleanup = () => {
520
+ server.removeAllListeners();
521
+ };
522
+ server.once("error", (error) => {
523
+ cleanup();
524
+ reject(error);
525
+ });
526
+ server.listen(0, host, () => {
527
+ const address = server.address();
528
+ const port = typeof address === "object" && address !== null ? address.port : 0;
529
+ server.close(() => {
530
+ cleanup();
531
+ resolve(port);
532
+ });
533
+ });
534
+ });
535
+ }
536
+
489
537
  function classifyDevStartFailure(input: {
490
538
  rawMessage: string;
491
539
  host: string;
@@ -602,7 +650,11 @@ export async function resolveAvailableWebPort(input: {
602
650
  const attempts = input.maxAttempts ?? 20;
603
651
  const start = Math.max(0, Math.floor(input.preferredPort));
604
652
  if (start === 0) {
605
- return { port: 0, autoPortSelected: false };
653
+ return {
654
+ port: await reserveEphemeralPort(input.host),
655
+ requestedPort: 0,
656
+ autoPortSelected: true,
657
+ };
606
658
  }
607
659
 
608
660
  for (let offset = 0; offset < attempts; offset += 1) {
@@ -787,12 +839,27 @@ function startWebDevServer(input: {
787
839
  };
788
840
  }
789
841
 
790
- function buildStartupSummary(input: {
842
+ function seedReadinessForWorkspace(
843
+ workspaceRoot: string,
844
+ readiness: SeedCommandResult["readiness"],
845
+ ): SeedCommandResult["readiness"] {
846
+ return {
847
+ ...readiness,
848
+ emptyWorkspaceRecovery: forgeCliCommandsForWorkspace(
849
+ workspaceRoot,
850
+ readiness.emptyWorkspaceRecovery,
851
+ ),
852
+ };
853
+ }
854
+
855
+ export function buildStartupSummary(input: {
791
856
  workspaceRoot: string;
792
857
  handle: DevServerHandle;
793
858
  web?: WebDevServerHandle | null;
794
859
  watch: boolean;
795
860
  generated?: DevStartupGeneratedEvidence;
861
+ seed?: SeedCommandResult;
862
+ seedRequested?: boolean;
796
863
  }): DevStartupSummary {
797
864
  const frontend = readGeneratedJson<FrontendGraph>(
798
865
  input.workspaceRoot,
@@ -819,6 +886,22 @@ function buildStartupSummary(input: {
819
886
  },
820
887
  ]
821
888
  : [];
889
+ if (input.seed && input.seed.subcommand !== "status" && input.seed.ok === false) {
890
+ warnings.push({
891
+ code: input.seed.diagnostics[0]?.code ?? "FORGE_SEED_FAILED",
892
+ message: input.seed.diagnostics[0]?.message ?? "Seed command failed.",
893
+ nextAction: forgeCliCommandForWorkspace(input.workspaceRoot, "forge seed status --json"),
894
+ });
895
+ }
896
+ for (const diagnostic of input.seed?.diagnostics.filter((item) => item.severity === "warning") ?? []) {
897
+ warnings.push({
898
+ code: diagnostic.code,
899
+ message: diagnostic.message,
900
+ nextAction: diagnostic.suggestedCommands?.[0]
901
+ ? forgeCliCommandForWorkspace(input.workspaceRoot, diagnostic.suggestedCommands[0])
902
+ : forgeCliCommandForWorkspace(input.workspaceRoot, "forge seed status --json"),
903
+ });
904
+ }
822
905
  const preview = previewSummaryFor({
823
906
  workspaceRoot: input.workspaceRoot,
824
907
  host: input.handle.host,
@@ -884,11 +967,34 @@ function buildStartupSummary(input: {
884
967
  checkCommand: input.generated?.checkCommand ?? forgeCliCommandForWorkspace(input.workspaceRoot, "forge generate --check --json"),
885
968
  message: input.generated?.message ?? (buildInfoRaw ? "generated artifacts are loaded" : "generated build info is missing"),
886
969
  },
970
+ seed: {
971
+ requested: input.seedRequested === true,
972
+ available: (input.seed?.commands.length ?? 0) > 0,
973
+ commands: input.seed?.commands.map((command) => command.name) ?? [],
974
+ ...(input.seed?.selectedCommand ? { selectedCommand: input.seed.selectedCommand } : {}),
975
+ ...(input.seed?.readiness ? { readiness: seedReadinessForWorkspace(input.workspaceRoot, input.seed.readiness) } : {}),
976
+ ran: input.seedRequested === true && input.seed?.subcommand !== "status",
977
+ ...(input.seed && input.seed.subcommand !== "status" ? { ok: input.seed.ok } : {}),
978
+ ...(input.seed?.response ? { responseStatus: input.seed.response.status } : {}),
979
+ ...(input.seed?.tenantRuns ? {
980
+ tenantRuns: input.seed.tenantRuns.map((run) => ({
981
+ tenantId: run.tenantId,
982
+ ...(run.label ? { label: run.label } : {}),
983
+ ...(run.organizationName ? { organizationName: run.organizationName } : {}),
984
+ ok: run.ok,
985
+ ...(run.response ? { responseStatus: run.response.status } : {}),
986
+ })),
987
+ } : {}),
988
+ ...(!input.seed?.ok && input.seed?.diagnostics[0]?.message ? { error: input.seed.diagnostics[0].message } : {}),
989
+ nextActions: input.seed?.nextActions.map((action) => forgeCliCommandForWorkspace(input.workspaceRoot, action)) ??
990
+ [forgeCliCommandForWorkspace(input.workspaceRoot, "forge seed status --json")],
991
+ },
887
992
  next: {
888
993
  browserUrl,
889
994
  apiIndex: input.handle.url,
890
995
  inspect: forgeCliCommandForWorkspace(input.workspaceRoot, "forge inspect summary --json"),
891
996
  verify: forgeCliCommandForWorkspace(input.workspaceRoot, "forge dev --once --json"),
997
+ changed: forgeCliCommandForWorkspace(input.workspaceRoot, "forge changed --json"),
892
998
  },
893
999
  pid: process.pid,
894
1000
  };
@@ -898,7 +1004,7 @@ function printStartupJson(summary: DevStartupSummary): void {
898
1004
  process.stdout.write(`${JSON.stringify(summary)}\n`);
899
1005
  }
900
1006
 
901
- function printStartupHuman(summary: DevStartupSummary): void {
1007
+ export function formatStartupHuman(summary: DevStartupSummary): string {
902
1008
  const lines = ["Forge Dev", ""];
903
1009
  lines.push("API runtime");
904
1010
  lines.push(` URL: ${summary.api.url}`);
@@ -909,6 +1015,39 @@ function printStartupHuman(summary: DevStartupSummary): void {
909
1015
  lines.push(` Generated: ${summary.generated.state}${summary.generated.changedFiles > 0 ? ` (${summary.generated.changedFiles} changed)` : ""}${summary.generated.runtimeStaleRisk ? " (stale risk)" : ""}`);
910
1016
  lines.push(` Generated note: ${summary.generated.message}`);
911
1017
  lines.push(` Generated check: ${summary.generated.checkCommand}`);
1018
+ if (summary.seed.available || summary.seed.requested) {
1019
+ lines.push(` Seed: ${summary.seed.available ? summary.seed.commands.join(", ") : "none detected"}`);
1020
+ if (summary.seed.readiness) {
1021
+ const autoSeedNote = summary.seed.readiness.autoSeedMode !== "none"
1022
+ ? summary.seed.readiness.autoSeedMode === "all-tenants"
1023
+ ? " (npm run dev auto-seeds all local tenants)"
1024
+ : " (npm run dev auto-seeds)"
1025
+ : "";
1026
+ lines.push(` Seed readiness: ${summary.seed.readiness.ready ? "ready" : summary.seed.readiness.reason}${autoSeedNote}`);
1027
+ if (summary.seed.readiness.emptyWorkspaceRecovery.length > 0) {
1028
+ lines.push(" Seed recovery:");
1029
+ for (const action of summary.seed.readiness.emptyWorkspaceRecovery) {
1030
+ lines.push(` - ${action}`);
1031
+ }
1032
+ }
1033
+ }
1034
+ if (summary.seed.requested) {
1035
+ lines.push(` Seed run: ${summary.seed.ok ? "ok" : "failed"}${summary.seed.selectedCommand ? ` (${summary.seed.selectedCommand})` : ""}${summary.seed.responseStatus ? ` HTTP ${summary.seed.responseStatus}` : ""}`);
1036
+ if (summary.seed.tenantRuns && summary.seed.tenantRuns.length > 0) {
1037
+ const okCount = summary.seed.tenantRuns.filter((run) => run.ok).length;
1038
+ lines.push(` Seed tenants: ${okCount}/${summary.seed.tenantRuns.length} ok`);
1039
+ for (const run of summary.seed.tenantRuns) {
1040
+ const label = run.organizationName ?? run.label ?? run.tenantId;
1041
+ lines.push(` - ${run.ok ? "ok" : "failed"} ${label} (${run.tenantId})${run.responseStatus ? ` HTTP ${run.responseStatus}` : ""}`);
1042
+ }
1043
+ }
1044
+ if (summary.seed.error) {
1045
+ lines.push(` Seed error: ${summary.seed.error}`);
1046
+ }
1047
+ } else if (summary.seed.available) {
1048
+ lines.push(` Seed next: ${summary.seed.nextActions[0] ?? "forge seed dev --json"}`);
1049
+ }
1050
+ }
912
1051
  for (const warning of summary.warnings) {
913
1052
  lines.push(` Warning ${warning.code}: ${warning.message}`);
914
1053
  if (warning.nextAction) {
@@ -920,8 +1059,9 @@ function printStartupHuman(summary: DevStartupSummary): void {
920
1059
  lines.push("Web app");
921
1060
  if (summary.web) {
922
1061
  lines.push(` URL: ${summary.web.url}`);
923
- if (summary.web.autoPortSelected && summary.web.requestedPort) {
924
- lines.push(` Requested port: ${summary.web.requestedPort} (busy; selected ${summary.web.port})`);
1062
+ if (summary.web.autoPortSelected && summary.web.requestedPort !== undefined) {
1063
+ const reason = summary.web.requestedPort === 0 ? "ephemeral" : "busy";
1064
+ lines.push(` Requested port: ${summary.web.requestedPort} (${reason}; selected ${summary.web.port})`);
925
1065
  }
926
1066
  lines.push(` Framework: ${summary.web.framework}`);
927
1067
  lines.push(` API env: ${summary.web.apiUrlEnv ?? "unknown"}=${summary.api.url}`);
@@ -948,7 +1088,7 @@ function printStartupHuman(summary: DevStartupSummary): void {
948
1088
  lines.push("Agent checks");
949
1089
  lines.push(` ${summary.next.verify}`);
950
1090
  lines.push(` ${summary.next.inspect}`);
951
- lines.push(" forge changed --json");
1091
+ lines.push(` ${summary.next.changed}`);
952
1092
  lines.push("");
953
1093
 
954
1094
  lines.push("Preview");
@@ -959,7 +1099,11 @@ function printStartupHuman(summary: DevStartupSummary): void {
959
1099
  lines.push(` Note: ${summary.preview.note}`);
960
1100
  lines.push("");
961
1101
 
962
- process.stdout.write(`${lines.join("\n")}\n`);
1102
+ return `${lines.join("\n")}\n`;
1103
+ }
1104
+
1105
+ function printStartupHuman(summary: DevStartupSummary): void {
1106
+ process.stdout.write(formatStartupHuman(summary));
963
1107
  }
964
1108
 
965
1109
  function openBrowser(url: string): void {
@@ -1311,12 +1455,34 @@ export async function runDevCommand(
1311
1455
  json: options.json,
1312
1456
  });
1313
1457
 
1458
+ const seedStatus = await runSeedCommand({
1459
+ subcommand: "status",
1460
+ command: options.seedCommand,
1461
+ args: {},
1462
+ url: handle.url,
1463
+ json: true,
1464
+ workspaceRoot,
1465
+ });
1466
+ const seedResult = options.seed
1467
+ ? await runSeedCommand({
1468
+ subcommand: "dev",
1469
+ command: options.seedCommand ?? seedStatus.selectedCommand,
1470
+ args: {},
1471
+ url: handle.url,
1472
+ allTenants: options.seedAllTenants,
1473
+ json: true,
1474
+ workspaceRoot,
1475
+ })
1476
+ : seedStatus;
1477
+
1314
1478
  const startupSummary = buildStartupSummary({
1315
1479
  workspaceRoot,
1316
1480
  handle,
1317
1481
  web: webHandle,
1318
1482
  watch: options.watch,
1319
1483
  generated: startupGenerated,
1484
+ seed: seedResult,
1485
+ seedRequested: options.seed,
1320
1486
  });
1321
1487
  if (options.json) {
1322
1488
  printStartupJson(startupSummary);
@@ -1,6 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
- import { join } from "node:path";
3
+ import { isAbsolute, join } from "node:path";
4
4
  import { runNewCommand, type NewPackageManager, type NewTemplateName } from "./new.ts";
5
5
  import { normalizeForgeCliCommandsInValue } from "../workspace/forge-cli.ts";
6
6
 
@@ -21,6 +21,8 @@ export interface FieldTestCommandOptions {
21
21
  keep: boolean;
22
22
  runtimeProbes: boolean;
23
23
  authProbes: boolean;
24
+ uiProbes: boolean;
25
+ realistic: boolean;
24
26
  timeoutMs: number;
25
27
  writeReport?: string;
26
28
  }
@@ -54,6 +56,10 @@ function defaultReportPath(): string {
54
56
  return ".forge/field-test-report.json";
55
57
  }
56
58
 
59
+ function resolveReportPath(workspaceRoot: string, reportPath: string): string {
60
+ return isAbsolute(reportPath) ? reportPath : join(workspaceRoot, reportPath);
61
+ }
62
+
57
63
  function parseJsonOutput(text: string): unknown | null {
58
64
  const trimmed = text.trim();
59
65
  if (!trimmed) return null;
@@ -77,19 +83,156 @@ function summarizeReport(data: unknown) {
77
83
  const plannedCases = Array.isArray(root.cases) ? root.cases as Array<Record<string, unknown>> : [];
78
84
  const failed = results.filter((result) => result.ok === false && result.skipped !== true);
79
85
  const skipped = results.filter((result) => result.skipped === true);
86
+ const topLevelSteps = results.flatMap((result) =>
87
+ Array.isArray(result.steps) ? result.steps as unknown[] : [],
88
+ );
80
89
  const runtimeProbeSteps = results.flatMap((result) => {
81
90
  const runtime = result.runtime && typeof result.runtime === "object"
82
91
  ? result.runtime as Record<string, unknown>
83
92
  : {};
84
93
  return Array.isArray(runtime.steps) ? runtime.steps as unknown[] : [];
85
94
  });
95
+ const hasVendorAccessCase = results.some((result) => result.template === "vendor-access");
96
+ const commandOf = (step: unknown): string =>
97
+ step && typeof step === "object" && typeof (step as { command?: unknown }).command === "string"
98
+ ? (step as { command: string }).command
99
+ : "";
100
+ const stdoutOf = (step: unknown): string =>
101
+ step && typeof step === "object" && typeof (step as { stdout?: unknown }).stdout === "string"
102
+ ? (step as { stdout: string }).stdout
103
+ : "";
104
+ const okStep = (step: unknown): boolean =>
105
+ Boolean(step && typeof step === "object" && (step as { ok?: unknown }).ok === true);
106
+ const jsonOfStep = (step: unknown): Record<string, unknown> | null => {
107
+ const stdout = stdoutOf(step).trim();
108
+ if (!stdout) return null;
109
+ const parsed = parseJsonOutput(stdout);
110
+ return parsed && typeof parsed === "object" ? parsed as Record<string, unknown> : null;
111
+ };
112
+ const hasOkRuntimeCommand = (pattern: RegExp): boolean =>
113
+ runtimeProbeSteps.some((step) => okStep(step) && pattern.test(commandOf(step)));
114
+ const hasOkTopLevelCommand = (pattern: RegExp): boolean =>
115
+ topLevelSteps.some((step) => okStep(step) && pattern.test(commandOf(step)));
116
+ const seedProbeSteps = runtimeProbeSteps.filter((step) => commandOf(step).includes("seed-"));
117
+ const seedReadinessSteps = runtimeProbeSteps.filter((step) => commandOf(step).includes("seed-status"));
118
+ const seedReadinessResults = seedReadinessSteps
119
+ .filter((step) => okStep(step))
120
+ .map((step) => {
121
+ const payload = jsonOfStep(step);
122
+ const readiness = payload?.readiness && typeof payload.readiness === "object"
123
+ ? payload.readiness as Record<string, unknown>
124
+ : {};
125
+ const recovery = Array.isArray(readiness.emptyWorkspaceRecovery)
126
+ ? readiness.emptyWorkspaceRecovery.filter((item) => typeof item === "string")
127
+ : [];
128
+ return {
129
+ ready: readiness.ready === true,
130
+ autoSeedOnDev: readiness.autoSeedOnDev === true,
131
+ autoSeedAllTenantsOnDev: readiness.autoSeedAllTenantsOnDev === true,
132
+ autoSeedMode: typeof readiness.autoSeedMode === "string" ? readiness.autoSeedMode : undefined,
133
+ selectedCommand: typeof readiness.selectedCommand === "string" ? readiness.selectedCommand : undefined,
134
+ recoveryCommands: recovery.length,
135
+ };
136
+ });
137
+ const seedReadinessEvidence = !hasVendorAccessCase || seedReadinessResults.some((result) =>
138
+ result.ready &&
139
+ result.selectedCommand === "seedVendorAccessDemo" &&
140
+ result.recoveryCommands >= 2,
141
+ );
142
+ const seedAllTenantsAutoSeedEvidence = !hasVendorAccessCase || seedReadinessResults.some((result) =>
143
+ result.ready &&
144
+ result.selectedCommand === "seedVendorAccessDemo" &&
145
+ (result.autoSeedMode === "all-tenants" || result.autoSeedAllTenantsOnDev),
146
+ );
147
+ const vendorAccessProbeSteps = runtimeProbeSteps.filter((step) => commandOf(step).includes("vendor-access-"));
148
+ const uiProbeSteps = runtimeProbeSteps.filter((step) => /^GET\s+https?:\/\/[^/]+\/$/i.test(commandOf(step)));
149
+ const authMetadataProbeSteps = runtimeProbeSteps.filter((step) =>
150
+ /^(HEAD|GET)\s+https?:\/\/[^/]+\/(auth\.md|\.well-known\/oauth-protected-resource)\b/i.test(commandOf(step)),
151
+ );
152
+ const authSetupProbeSteps = topLevelSteps.filter((step) =>
153
+ /forge\s+(?:--\s+)?(add\s+auth\s+workos|authmd\s+generate|authmd\s+check|workos\s+doctor|workos\s+seed|workos\s+prove|auth\s+prove)/.test(commandOf(step)),
154
+ );
155
+ const uiErgonomicsResults = results
156
+ .map((result) => result.uiErgonomics)
157
+ .filter((value): value is Record<string, unknown> => Boolean(value && typeof value === "object"));
158
+ const uiErgonomicsWarnings = uiErgonomicsResults.reduce((total, result) =>
159
+ total + (typeof result.warnings === "number" ? result.warnings : 0), 0);
160
+ const uiErgonomicsErrors = uiErgonomicsResults.reduce((total, result) =>
161
+ total + (typeof result.errors === "number" ? result.errors : 0), 0);
162
+ const uiErgonomicsWarningCodes = Array.from(new Set(uiErgonomicsResults.flatMap((result) =>
163
+ Array.isArray(result.diagnosticCodes)
164
+ ? result.diagnosticCodes.filter((code): code is string => typeof code === "string")
165
+ : [],
166
+ ))).sort();
167
+ const uiScenarioNames = Array.from(new Set(uiErgonomicsResults.flatMap((result) =>
168
+ Array.isArray(result.scenarioNames)
169
+ ? result.scenarioNames.filter((name): name is string => typeof name === "string")
170
+ : [],
171
+ ))).sort();
172
+ const vendorAccessRequiredUiScenarios = [
173
+ "vendor-access-autoseed-data-visible",
174
+ "vendor-access-local-login",
175
+ "vendor-access-requester-denied-visible",
176
+ "vendor-access-seed-control-visible",
177
+ ];
86
178
  const ok = root.ok === true;
87
179
  const runtimeProbes = root.runtimeProbes === true;
88
180
  const authProbes = root.authProbes === true;
181
+ const uiProbes = root.uiProbes === true;
182
+ const uiErgonomics = !uiProbes || (results.length > 0 && uiErgonomicsResults.length === results.filter((result) => result.skipped !== true).length);
183
+ const runtimeHealthEvidence = !runtimeProbes || hasOkRuntimeCommand(/\bGET\s+.*\/health\b/i);
184
+ const runtimeEntriesEvidence = !runtimeProbes || hasOkRuntimeCommand(/\bGET\s+.*\/entries\b/i);
185
+ const authSetupEvidence = !authProbes || [
186
+ /forge\s+(?:--\s+)?add\s+auth\s+workos\b/,
187
+ /forge\s+(?:--\s+)?authmd\s+generate\b/,
188
+ /forge\s+(?:--\s+)?authmd\s+check\b/,
189
+ /forge\s+(?:--\s+)?workos\s+doctor\b/,
190
+ /forge\s+(?:--\s+)?workos\s+seed\b/,
191
+ /forge\s+(?:--\s+)?workos\s+prove\b/,
192
+ /forge\s+(?:--\s+)?auth\s+prove\b/,
193
+ ].every((pattern) => hasOkTopLevelCommand(pattern));
194
+ const authMetadataEvidence = !authProbes || [
195
+ /^HEAD\s+.*\/auth\.md\b/i,
196
+ /^GET\s+.*\/auth\.md\b/i,
197
+ /^HEAD\s+.*\/\.well-known\/oauth-protected-resource\b/i,
198
+ /^GET\s+.*\/\.well-known\/oauth-protected-resource\b/i,
199
+ ].every((pattern) => hasOkRuntimeCommand(pattern));
200
+ const uiProbeEvidence = !uiProbes || uiProbeSteps.some((step) => okStep(step));
201
+ const vendorAccessSeedEvidence = !hasVendorAccessCase ||
202
+ hasOkRuntimeCommand(/vendor-access-seed-all-tenants:/) ||
203
+ [
204
+ /vendor-access-seed-acme:/,
205
+ /vendor-access-seed-globex:/,
206
+ ].every((pattern) => hasOkRuntimeCommand(pattern));
207
+ const vendorAccessEvidence = !hasVendorAccessCase || (
208
+ vendorAccessSeedEvidence &&
209
+ [
210
+ /vendor-access-query-acme:/,
211
+ /vendor-access-query-globex:/,
212
+ /vendor-access-owner-approve:/,
213
+ /vendor-access-requester-approve-denied:/,
214
+ /vendor-access-cross-tenant-approve-denied:/,
215
+ ].every((pattern) => hasOkRuntimeCommand(pattern))
216
+ );
217
+ const vendorAccessUiScenarioEvidence = !hasVendorAccessCase || !uiProbes ||
218
+ vendorAccessRequiredUiScenarios.every((scenario) => uiScenarioNames.includes(scenario));
89
219
  const productionEvidenceMissing = [
90
220
  ...(!ok ? ["passing field-test report"] : []),
91
221
  ...(!runtimeProbes ? ["runtime probes"] : []),
92
222
  ...(!authProbes ? ["auth probes"] : []),
223
+ ...(!uiProbes ? ["ui probes"] : []),
224
+ ...(!runtimeHealthEvidence ? ["runtime health probe"] : []),
225
+ ...(!runtimeEntriesEvidence ? ["runtime entries probe"] : []),
226
+ ...(!authSetupEvidence ? ["auth setup probes"] : []),
227
+ ...(!authMetadataEvidence ? ["auth metadata endpoint probes"] : []),
228
+ ...(!uiProbeEvidence ? ["web UI probe"] : []),
229
+ ...(!seedReadinessEvidence ? ["seed readiness evidence"] : []),
230
+ ...(!seedAllTenantsAutoSeedEvidence ? ["seed readiness all-tenants auto-seed evidence"] : []),
231
+ ...(!vendorAccessEvidence ? ["vendor-access multi-tenant domain probes"] : []),
232
+ ...(!vendorAccessUiScenarioEvidence ? ["vendor-access UI scenarios"] : []),
233
+ ...(!uiErgonomics ? ["UI ergonomics audit"] : []),
234
+ ...(uiErgonomicsWarnings > 0 ? ["zero UI ergonomics warnings"] : []),
235
+ ...(uiErgonomicsErrors > 0 ? ["zero UI ergonomics errors"] : []),
93
236
  ...(failed.length > 0 ? ["zero failed cases"] : []),
94
237
  ];
95
238
  return {
@@ -102,7 +245,35 @@ function summarizeReport(data: unknown) {
102
245
  skipped: skipped.length,
103
246
  runtimeProbes,
104
247
  authProbes,
248
+ uiProbes,
249
+ uiErgonomics,
250
+ uiErgonomicsWarnings,
251
+ uiErgonomicsErrors,
252
+ uiErgonomicsWarningCodes,
253
+ uiScenarios: {
254
+ names: uiScenarioNames,
255
+ vendorAccessReady: vendorAccessUiScenarioEvidence,
256
+ requiredVendorAccess: hasVendorAccessCase ? vendorAccessRequiredUiScenarios : [],
257
+ },
105
258
  runtimeProbeSteps: runtimeProbeSteps.length,
259
+ seedProbeSteps: seedProbeSteps.length,
260
+ seedReadiness: {
261
+ ready: seedReadinessEvidence,
262
+ steps: seedReadinessSteps.length,
263
+ autoSeedOnDev: seedReadinessResults.some((result) => result.autoSeedOnDev),
264
+ autoSeedAllTenantsOnDev: seedReadinessResults.some((result) => result.autoSeedAllTenantsOnDev),
265
+ allTenantsAutoSeedReady: seedAllTenantsAutoSeedEvidence,
266
+ autoSeedModes: Array.from(new Set(seedReadinessResults
267
+ .map((result) => result.autoSeedMode)
268
+ .filter((value): value is string => Boolean(value)))),
269
+ selectedCommands: Array.from(new Set(seedReadinessResults
270
+ .map((result) => result.selectedCommand)
271
+ .filter((value): value is string => Boolean(value)))),
272
+ },
273
+ authSetupProbeSteps: authSetupProbeSteps.length,
274
+ authMetadataProbeSteps: authMetadataProbeSteps.length,
275
+ uiProbeSteps: uiProbeSteps.length,
276
+ vendorAccessProbeSteps: vendorAccessProbeSteps.length,
106
277
  productionEvidence: {
107
278
  readyForDeployCheck: productionEvidenceMissing.length === 0,
108
279
  missing: productionEvidenceMissing,
@@ -119,11 +290,9 @@ function summarizeReport(data: unknown) {
119
290
 
120
291
  function runCommandForOptions(templates: NewTemplateName[], packageManagers: NewPackageManager[]): string {
121
292
  return [
122
- "forge field-test run",
293
+ "forge field-test run --realistic",
123
294
  `--templates ${templates.join(",")}`,
124
295
  `--package-managers ${packageManagers.join(",")}`,
125
- "--runtime-probes",
126
- "--auth-probes",
127
296
  "--json",
128
297
  ].join(" ");
129
298
  }
@@ -136,7 +305,7 @@ async function createFieldTestApp(options: FieldTestCommandOptions): Promise<Fie
136
305
  kind: "field-test",
137
306
  action: "create",
138
307
  data: { error: "forge field-test create requires an app name" },
139
- nextActions: ["forge field-test create vendor-access --auth workos --template minimal-web --json"],
308
+ nextActions: ["forge field-test create vendor-access --auth workos --json"],
140
309
  exitCode: 1,
141
310
  };
142
311
  }
@@ -152,9 +321,21 @@ async function createFieldTestApp(options: FieldTestCommandOptions): Promise<Fie
152
321
  template: options.template,
153
322
  packageManager: options.packageManager,
154
323
  auth: options.auth,
324
+ install: true,
325
+ git: true,
155
326
  command: `forge new ${options.name} --template ${options.template} --package-manager ${options.packageManager} --field-test --install`,
327
+ goldenPath: [
328
+ `forge field-test create ${options.name} --auth ${options.auth ?? "none"} --template ${options.template} --package-manager ${options.packageManager} --install --git --json`,
329
+ `cd ${options.name}`,
330
+ "forge field-test run --realistic --json",
331
+ "forge field-test report --json",
332
+ "forge deploy plan --target docker --json",
333
+ "forge deploy check --production --json",
334
+ ],
156
335
  },
157
- nextActions: [`forge new ${options.name} --template ${options.template} --package-manager ${options.packageManager} --field-test --install`],
336
+ nextActions: [
337
+ `forge field-test create ${options.name} --auth ${options.auth ?? "none"} --template ${options.template} --package-manager ${options.packageManager} --install --git --json`,
338
+ ],
158
339
  exitCode: 0,
159
340
  });
160
341
  }
@@ -198,9 +379,12 @@ async function createFieldTestApp(options: FieldTestCommandOptions): Promise<Fie
198
379
  `${options.packageManager} run forge -- authmd check --json`,
199
380
  `${options.packageManager} run forge -- workos doctor --json`,
200
381
  `${options.packageManager} run forge -- workos seed --file workos-seed.yml --dry-run --json`,
382
+ `${options.packageManager} run forge -- workos prove --file workos-seed.yml --json`,
201
383
  `${options.packageManager} run forge -- workos setup --file workos-seed.yml --json`,
202
384
  ]
203
385
  : []),
386
+ `${options.packageManager} run forge -- field-test run --realistic --json`,
387
+ `${options.packageManager} run forge -- field-test report --json`,
204
388
  `${options.packageManager} run forge -- dev --once --json`,
205
389
  `${options.packageManager} run forge -- verify --smoke --json`,
206
390
  `${options.packageManager} run forge -- handoff --json`,
@@ -236,8 +420,9 @@ function runHarness(options: FieldTestCommandOptions): FieldTestCommandResult {
236
420
  ];
237
421
  if (options.dryRun) args.push("--dry-run");
238
422
  if (options.keep) args.push("--keep");
239
- if (options.runtimeProbes) args.push("--runtime-probes");
240
- if (options.authProbes) args.push("--auth-probes");
423
+ if (options.runtimeProbes || options.realistic) args.push("--runtime-probes");
424
+ if (options.authProbes || options.realistic) args.push("--auth-probes");
425
+ if (options.uiProbes || options.realistic) args.push("--ui-probes");
241
426
  if (options.forgeSpec) args.push("--forge-spec", options.forgeSpec);
242
427
  if (reportPath) args.push("--write-report", reportPath);
243
428
  if (options.json) args.push("--json");
@@ -246,7 +431,7 @@ function runHarness(options: FieldTestCommandOptions): FieldTestCommandResult {
246
431
  encoding: "utf8",
247
432
  windowsHide: true,
248
433
  });
249
- const reportAbsolute = reportPath ? join(options.workspaceRoot, reportPath) : null;
434
+ const reportAbsolute = reportPath ? resolveReportPath(options.workspaceRoot, reportPath) : null;
250
435
  const reportData = reportAbsolute && existsSync(reportAbsolute)
251
436
  ? JSON.parse(readFileSync(reportAbsolute, "utf8")) as unknown
252
437
  : parseJsonOutput(result.stdout ?? "");
@@ -276,7 +461,7 @@ function readReport(options: FieldTestCommandOptions): FieldTestCommandResult {
276
461
  ".forge/field-test-report.json",
277
462
  "field-reports/full-alpha.json",
278
463
  ].filter(Boolean) as string[];
279
- const report = candidates.find((candidate) => existsSync(join(options.workspaceRoot, candidate)));
464
+ const report = candidates.find((candidate) => existsSync(resolveReportPath(options.workspaceRoot, candidate)));
280
465
  if (!report) {
281
466
  return {
282
467
  schemaVersion: "0.1.0",
@@ -284,11 +469,11 @@ function readReport(options: FieldTestCommandOptions): FieldTestCommandResult {
284
469
  kind: "field-test",
285
470
  action: "report",
286
471
  data: { searched: candidates },
287
- nextActions: ["forge field-test run --runtime-probes --auth-probes --write-report .forge/field-test-report.json --json"],
472
+ nextActions: ["forge field-test run --realistic --write-report .forge/field-test-report.json --json"],
288
473
  exitCode: 1,
289
474
  };
290
475
  }
291
- const data = JSON.parse(readFileSync(join(options.workspaceRoot, report), "utf8")) as unknown;
476
+ const data = JSON.parse(readFileSync(resolveReportPath(options.workspaceRoot, report), "utf8")) as unknown;
292
477
  const summary = summarizeReport(data);
293
478
  const productionEvidence = (summary as { productionEvidence?: { readyForDeployCheck?: boolean } }).productionEvidence;
294
479
  return {
@@ -301,7 +486,7 @@ function readReport(options: FieldTestCommandOptions): FieldTestCommandResult {
301
486
  data,
302
487
  nextActions: productionEvidence?.readyForDeployCheck
303
488
  ? ["forge deploy check --production --json"]
304
- : ["forge field-test run --runtime-probes --auth-probes --write-report .forge/field-test-report.json --json"],
489
+ : ["forge field-test run --realistic --write-report .forge/field-test-report.json --json"],
305
490
  exitCode: (data && typeof data === "object" && "ok" in data && (data as { ok?: unknown }).ok !== true) ? 1 : 0,
306
491
  };
307
492
  }