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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +46 -2
- package/docs/changelog.md +225 -0
- package/package.json +1 -1
- package/src/forge/_generated/releaseManifest.json +1 -1
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/cli/auth.ts +1 -0
- package/src/forge/cli/commands.ts +13 -0
- package/src/forge/cli/deploy.ts +109 -18
- package/src/forge/cli/dev.ts +176 -10
- package/src/forge/cli/field-test.ts +198 -13
- package/src/forge/cli/main.ts +16 -5
- package/src/forge/cli/new.ts +2 -1
- package/src/forge/cli/output.ts +9 -0
- package/src/forge/cli/parse.ts +95 -12
- package/src/forge/cli/run.ts +1 -0
- package/src/forge/cli/seed.ts +581 -0
- package/src/forge/cli/workos.ts +349 -16
- package/src/forge/compiler/agent-contract/build.ts +58 -0
- package/src/forge/compiler/data-graph/build.ts +29 -1
- package/src/forge/compiler/diagnostics/codes.ts +2 -0
- package/src/forge/compiler/diagnostics/create.ts +7 -0
- package/src/forge/compiler/integration/add.ts +305 -5
- package/src/forge/compiler/integration/templates/workos.ts +51 -2
- package/src/forge/compiler/orchestrator/plan.ts +2 -0
- package/src/forge/compiler/package-manager/version.ts +36 -12
- package/src/forge/make/index.ts +2 -0
- package/src/forge/make/templates.ts +58 -4
- package/src/forge/runtime/external/bridge.ts +14 -0
- package/src/forge/server.ts +59 -9
- package/src/forge/ui/index.ts +643 -25
- package/src/forge/ui/types.ts +2 -1
- package/src/forge/version.ts +1 -1
- package/templates/agent-workroom/web/index.html +1 -0
- package/templates/agent-workroom/web/src/lib/forge.ts +9 -2
- package/templates/agent-workroom/web/vite.config.ts +36 -0
- package/templates/b2b-support-web/web/app/page.tsx +2 -2
- package/templates/b2b-support-web/web/lib/forge.ts +9 -2
- package/templates/b2b-support-web/web/next.config.ts +24 -0
- package/templates/minimal-web/web/index.html +1 -0
- package/templates/minimal-web/web/src/App.tsx +12 -6
- package/templates/minimal-web/web/src/lib/forge.ts +9 -2
- package/templates/minimal-web/web/src/styles.css +34 -0
- package/templates/minimal-web/web/vite.config.ts +36 -0
- package/templates/nuxt-web/web/app.vue +1 -1
- package/templates/vendor-access/.vscode/settings.json +14 -0
- package/templates/vendor-access/README.md +30 -0
- package/templates/vendor-access/forge.config.ts +3 -0
- package/templates/vendor-access/package.json +34 -0
- package/templates/vendor-access/src/commands/addEvidence.ts +42 -0
- package/templates/vendor-access/src/commands/approveAccessRequest.ts +43 -0
- package/templates/vendor-access/src/commands/createAccessRequest.ts +49 -0
- package/templates/vendor-access/src/commands/seedVendorAccessDemo.ts +200 -0
- package/templates/vendor-access/src/forge/schema.ts +74 -0
- package/templates/vendor-access/src/policies.ts +11 -0
- package/templates/vendor-access/src/queries/listVendorAccessDashboard.ts +24 -0
- package/templates/vendor-access/src/queries/liveVendorAccessDashboard.ts +24 -0
- package/templates/vendor-access/tsconfig.json +17 -0
- package/templates/vendor-access/web/index.html +13 -0
- package/templates/vendor-access/web/package.json +21 -0
- package/templates/vendor-access/web/src/App.tsx +573 -0
- package/templates/vendor-access/web/src/lib/forge.ts +20 -0
- package/templates/vendor-access/web/src/main.tsx +205 -0
- package/templates/vendor-access/web/src/styles.css +682 -0
- package/templates/vendor-access/web/tsconfig.json +18 -0
- package/templates/vendor-access/web/vite.config.ts +36 -0
package/src/forge/cli/dev.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 --
|
|
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: [
|
|
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 ?
|
|
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(
|
|
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 --
|
|
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(
|
|
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 --
|
|
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
|
}
|