forgeos 0.1.0-alpha.41 → 0.1.0-alpha.43
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 +222 -0
- package/package.json +2 -1
- package/scripts/field-test-forgeos.mjs +1012 -0
- 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 +210 -18
- 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/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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { runNewCommand, type NewPackageManager, type NewTemplateName } from "./new.ts";
|
|
5
6
|
import { normalizeForgeCliCommandsInValue } from "../workspace/forge-cli.ts";
|
|
6
7
|
|
|
@@ -21,6 +22,8 @@ export interface FieldTestCommandOptions {
|
|
|
21
22
|
keep: boolean;
|
|
22
23
|
runtimeProbes: boolean;
|
|
23
24
|
authProbes: boolean;
|
|
25
|
+
uiProbes: boolean;
|
|
26
|
+
realistic: boolean;
|
|
24
27
|
timeoutMs: number;
|
|
25
28
|
writeReport?: string;
|
|
26
29
|
}
|
|
@@ -40,8 +43,14 @@ export interface FieldTestCommandResult {
|
|
|
40
43
|
exitCode: 0 | 1;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
function
|
|
44
|
-
|
|
46
|
+
function scriptCandidates(workspaceRoot: string): string[] {
|
|
47
|
+
const localScript = join(workspaceRoot, "scripts", "field-test-forgeos.mjs");
|
|
48
|
+
const packageScript = join(dirname(fileURLToPath(import.meta.url)), "../../../scripts/field-test-forgeos.mjs");
|
|
49
|
+
return Array.from(new Set([localScript, packageScript]));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function scriptPath(workspaceRoot: string): string | null {
|
|
53
|
+
return scriptCandidates(workspaceRoot).find((candidate) => existsSync(candidate)) ?? null;
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
function compact(text: string, limit = 12000): string {
|
|
@@ -54,6 +63,10 @@ function defaultReportPath(): string {
|
|
|
54
63
|
return ".forge/field-test-report.json";
|
|
55
64
|
}
|
|
56
65
|
|
|
66
|
+
function resolveReportPath(workspaceRoot: string, reportPath: string): string {
|
|
67
|
+
return isAbsolute(reportPath) ? reportPath : join(workspaceRoot, reportPath);
|
|
68
|
+
}
|
|
69
|
+
|
|
57
70
|
function parseJsonOutput(text: string): unknown | null {
|
|
58
71
|
const trimmed = text.trim();
|
|
59
72
|
if (!trimmed) return null;
|
|
@@ -77,19 +90,156 @@ function summarizeReport(data: unknown) {
|
|
|
77
90
|
const plannedCases = Array.isArray(root.cases) ? root.cases as Array<Record<string, unknown>> : [];
|
|
78
91
|
const failed = results.filter((result) => result.ok === false && result.skipped !== true);
|
|
79
92
|
const skipped = results.filter((result) => result.skipped === true);
|
|
93
|
+
const topLevelSteps = results.flatMap((result) =>
|
|
94
|
+
Array.isArray(result.steps) ? result.steps as unknown[] : [],
|
|
95
|
+
);
|
|
80
96
|
const runtimeProbeSteps = results.flatMap((result) => {
|
|
81
97
|
const runtime = result.runtime && typeof result.runtime === "object"
|
|
82
98
|
? result.runtime as Record<string, unknown>
|
|
83
99
|
: {};
|
|
84
100
|
return Array.isArray(runtime.steps) ? runtime.steps as unknown[] : [];
|
|
85
101
|
});
|
|
102
|
+
const hasVendorAccessCase = results.some((result) => result.template === "vendor-access");
|
|
103
|
+
const commandOf = (step: unknown): string =>
|
|
104
|
+
step && typeof step === "object" && typeof (step as { command?: unknown }).command === "string"
|
|
105
|
+
? (step as { command: string }).command
|
|
106
|
+
: "";
|
|
107
|
+
const stdoutOf = (step: unknown): string =>
|
|
108
|
+
step && typeof step === "object" && typeof (step as { stdout?: unknown }).stdout === "string"
|
|
109
|
+
? (step as { stdout: string }).stdout
|
|
110
|
+
: "";
|
|
111
|
+
const okStep = (step: unknown): boolean =>
|
|
112
|
+
Boolean(step && typeof step === "object" && (step as { ok?: unknown }).ok === true);
|
|
113
|
+
const jsonOfStep = (step: unknown): Record<string, unknown> | null => {
|
|
114
|
+
const stdout = stdoutOf(step).trim();
|
|
115
|
+
if (!stdout) return null;
|
|
116
|
+
const parsed = parseJsonOutput(stdout);
|
|
117
|
+
return parsed && typeof parsed === "object" ? parsed as Record<string, unknown> : null;
|
|
118
|
+
};
|
|
119
|
+
const hasOkRuntimeCommand = (pattern: RegExp): boolean =>
|
|
120
|
+
runtimeProbeSteps.some((step) => okStep(step) && pattern.test(commandOf(step)));
|
|
121
|
+
const hasOkTopLevelCommand = (pattern: RegExp): boolean =>
|
|
122
|
+
topLevelSteps.some((step) => okStep(step) && pattern.test(commandOf(step)));
|
|
123
|
+
const seedProbeSteps = runtimeProbeSteps.filter((step) => commandOf(step).includes("seed-"));
|
|
124
|
+
const seedReadinessSteps = runtimeProbeSteps.filter((step) => commandOf(step).includes("seed-status"));
|
|
125
|
+
const seedReadinessResults = seedReadinessSteps
|
|
126
|
+
.filter((step) => okStep(step))
|
|
127
|
+
.map((step) => {
|
|
128
|
+
const payload = jsonOfStep(step);
|
|
129
|
+
const readiness = payload?.readiness && typeof payload.readiness === "object"
|
|
130
|
+
? payload.readiness as Record<string, unknown>
|
|
131
|
+
: {};
|
|
132
|
+
const recovery = Array.isArray(readiness.emptyWorkspaceRecovery)
|
|
133
|
+
? readiness.emptyWorkspaceRecovery.filter((item) => typeof item === "string")
|
|
134
|
+
: [];
|
|
135
|
+
return {
|
|
136
|
+
ready: readiness.ready === true,
|
|
137
|
+
autoSeedOnDev: readiness.autoSeedOnDev === true,
|
|
138
|
+
autoSeedAllTenantsOnDev: readiness.autoSeedAllTenantsOnDev === true,
|
|
139
|
+
autoSeedMode: typeof readiness.autoSeedMode === "string" ? readiness.autoSeedMode : undefined,
|
|
140
|
+
selectedCommand: typeof readiness.selectedCommand === "string" ? readiness.selectedCommand : undefined,
|
|
141
|
+
recoveryCommands: recovery.length,
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
const seedReadinessEvidence = !hasVendorAccessCase || seedReadinessResults.some((result) =>
|
|
145
|
+
result.ready &&
|
|
146
|
+
result.selectedCommand === "seedVendorAccessDemo" &&
|
|
147
|
+
result.recoveryCommands >= 2,
|
|
148
|
+
);
|
|
149
|
+
const seedAllTenantsAutoSeedEvidence = !hasVendorAccessCase || seedReadinessResults.some((result) =>
|
|
150
|
+
result.ready &&
|
|
151
|
+
result.selectedCommand === "seedVendorAccessDemo" &&
|
|
152
|
+
(result.autoSeedMode === "all-tenants" || result.autoSeedAllTenantsOnDev),
|
|
153
|
+
);
|
|
154
|
+
const vendorAccessProbeSteps = runtimeProbeSteps.filter((step) => commandOf(step).includes("vendor-access-"));
|
|
155
|
+
const uiProbeSteps = runtimeProbeSteps.filter((step) => /^GET\s+https?:\/\/[^/]+\/$/i.test(commandOf(step)));
|
|
156
|
+
const authMetadataProbeSteps = runtimeProbeSteps.filter((step) =>
|
|
157
|
+
/^(HEAD|GET)\s+https?:\/\/[^/]+\/(auth\.md|\.well-known\/oauth-protected-resource)\b/i.test(commandOf(step)),
|
|
158
|
+
);
|
|
159
|
+
const authSetupProbeSteps = topLevelSteps.filter((step) =>
|
|
160
|
+
/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)),
|
|
161
|
+
);
|
|
162
|
+
const uiErgonomicsResults = results
|
|
163
|
+
.map((result) => result.uiErgonomics)
|
|
164
|
+
.filter((value): value is Record<string, unknown> => Boolean(value && typeof value === "object"));
|
|
165
|
+
const uiErgonomicsWarnings = uiErgonomicsResults.reduce((total, result) =>
|
|
166
|
+
total + (typeof result.warnings === "number" ? result.warnings : 0), 0);
|
|
167
|
+
const uiErgonomicsErrors = uiErgonomicsResults.reduce((total, result) =>
|
|
168
|
+
total + (typeof result.errors === "number" ? result.errors : 0), 0);
|
|
169
|
+
const uiErgonomicsWarningCodes = Array.from(new Set(uiErgonomicsResults.flatMap((result) =>
|
|
170
|
+
Array.isArray(result.diagnosticCodes)
|
|
171
|
+
? result.diagnosticCodes.filter((code): code is string => typeof code === "string")
|
|
172
|
+
: [],
|
|
173
|
+
))).sort();
|
|
174
|
+
const uiScenarioNames = Array.from(new Set(uiErgonomicsResults.flatMap((result) =>
|
|
175
|
+
Array.isArray(result.scenarioNames)
|
|
176
|
+
? result.scenarioNames.filter((name): name is string => typeof name === "string")
|
|
177
|
+
: [],
|
|
178
|
+
))).sort();
|
|
179
|
+
const vendorAccessRequiredUiScenarios = [
|
|
180
|
+
"vendor-access-autoseed-data-visible",
|
|
181
|
+
"vendor-access-local-login",
|
|
182
|
+
"vendor-access-requester-denied-visible",
|
|
183
|
+
"vendor-access-seed-control-visible",
|
|
184
|
+
];
|
|
86
185
|
const ok = root.ok === true;
|
|
87
186
|
const runtimeProbes = root.runtimeProbes === true;
|
|
88
187
|
const authProbes = root.authProbes === true;
|
|
188
|
+
const uiProbes = root.uiProbes === true;
|
|
189
|
+
const uiErgonomics = !uiProbes || (results.length > 0 && uiErgonomicsResults.length === results.filter((result) => result.skipped !== true).length);
|
|
190
|
+
const runtimeHealthEvidence = !runtimeProbes || hasOkRuntimeCommand(/\bGET\s+.*\/health\b/i);
|
|
191
|
+
const runtimeEntriesEvidence = !runtimeProbes || hasOkRuntimeCommand(/\bGET\s+.*\/entries\b/i);
|
|
192
|
+
const authSetupEvidence = !authProbes || [
|
|
193
|
+
/forge\s+(?:--\s+)?add\s+auth\s+workos\b/,
|
|
194
|
+
/forge\s+(?:--\s+)?authmd\s+generate\b/,
|
|
195
|
+
/forge\s+(?:--\s+)?authmd\s+check\b/,
|
|
196
|
+
/forge\s+(?:--\s+)?workos\s+doctor\b/,
|
|
197
|
+
/forge\s+(?:--\s+)?workos\s+seed\b/,
|
|
198
|
+
/forge\s+(?:--\s+)?workos\s+prove\b/,
|
|
199
|
+
/forge\s+(?:--\s+)?auth\s+prove\b/,
|
|
200
|
+
].every((pattern) => hasOkTopLevelCommand(pattern));
|
|
201
|
+
const authMetadataEvidence = !authProbes || [
|
|
202
|
+
/^HEAD\s+.*\/auth\.md\b/i,
|
|
203
|
+
/^GET\s+.*\/auth\.md\b/i,
|
|
204
|
+
/^HEAD\s+.*\/\.well-known\/oauth-protected-resource\b/i,
|
|
205
|
+
/^GET\s+.*\/\.well-known\/oauth-protected-resource\b/i,
|
|
206
|
+
].every((pattern) => hasOkRuntimeCommand(pattern));
|
|
207
|
+
const uiProbeEvidence = !uiProbes || uiProbeSteps.some((step) => okStep(step));
|
|
208
|
+
const vendorAccessSeedEvidence = !hasVendorAccessCase ||
|
|
209
|
+
hasOkRuntimeCommand(/vendor-access-seed-all-tenants:/) ||
|
|
210
|
+
[
|
|
211
|
+
/vendor-access-seed-acme:/,
|
|
212
|
+
/vendor-access-seed-globex:/,
|
|
213
|
+
].every((pattern) => hasOkRuntimeCommand(pattern));
|
|
214
|
+
const vendorAccessEvidence = !hasVendorAccessCase || (
|
|
215
|
+
vendorAccessSeedEvidence &&
|
|
216
|
+
[
|
|
217
|
+
/vendor-access-query-acme:/,
|
|
218
|
+
/vendor-access-query-globex:/,
|
|
219
|
+
/vendor-access-owner-approve:/,
|
|
220
|
+
/vendor-access-requester-approve-denied:/,
|
|
221
|
+
/vendor-access-cross-tenant-approve-denied:/,
|
|
222
|
+
].every((pattern) => hasOkRuntimeCommand(pattern))
|
|
223
|
+
);
|
|
224
|
+
const vendorAccessUiScenarioEvidence = !hasVendorAccessCase || !uiProbes ||
|
|
225
|
+
vendorAccessRequiredUiScenarios.every((scenario) => uiScenarioNames.includes(scenario));
|
|
89
226
|
const productionEvidenceMissing = [
|
|
90
227
|
...(!ok ? ["passing field-test report"] : []),
|
|
91
228
|
...(!runtimeProbes ? ["runtime probes"] : []),
|
|
92
229
|
...(!authProbes ? ["auth probes"] : []),
|
|
230
|
+
...(!uiProbes ? ["ui probes"] : []),
|
|
231
|
+
...(!runtimeHealthEvidence ? ["runtime health probe"] : []),
|
|
232
|
+
...(!runtimeEntriesEvidence ? ["runtime entries probe"] : []),
|
|
233
|
+
...(!authSetupEvidence ? ["auth setup probes"] : []),
|
|
234
|
+
...(!authMetadataEvidence ? ["auth metadata endpoint probes"] : []),
|
|
235
|
+
...(!uiProbeEvidence ? ["web UI probe"] : []),
|
|
236
|
+
...(!seedReadinessEvidence ? ["seed readiness evidence"] : []),
|
|
237
|
+
...(!seedAllTenantsAutoSeedEvidence ? ["seed readiness all-tenants auto-seed evidence"] : []),
|
|
238
|
+
...(!vendorAccessEvidence ? ["vendor-access multi-tenant domain probes"] : []),
|
|
239
|
+
...(!vendorAccessUiScenarioEvidence ? ["vendor-access UI scenarios"] : []),
|
|
240
|
+
...(!uiErgonomics ? ["UI ergonomics audit"] : []),
|
|
241
|
+
...(uiErgonomicsWarnings > 0 ? ["zero UI ergonomics warnings"] : []),
|
|
242
|
+
...(uiErgonomicsErrors > 0 ? ["zero UI ergonomics errors"] : []),
|
|
93
243
|
...(failed.length > 0 ? ["zero failed cases"] : []),
|
|
94
244
|
];
|
|
95
245
|
return {
|
|
@@ -102,7 +252,35 @@ function summarizeReport(data: unknown) {
|
|
|
102
252
|
skipped: skipped.length,
|
|
103
253
|
runtimeProbes,
|
|
104
254
|
authProbes,
|
|
255
|
+
uiProbes,
|
|
256
|
+
uiErgonomics,
|
|
257
|
+
uiErgonomicsWarnings,
|
|
258
|
+
uiErgonomicsErrors,
|
|
259
|
+
uiErgonomicsWarningCodes,
|
|
260
|
+
uiScenarios: {
|
|
261
|
+
names: uiScenarioNames,
|
|
262
|
+
vendorAccessReady: vendorAccessUiScenarioEvidence,
|
|
263
|
+
requiredVendorAccess: hasVendorAccessCase ? vendorAccessRequiredUiScenarios : [],
|
|
264
|
+
},
|
|
105
265
|
runtimeProbeSteps: runtimeProbeSteps.length,
|
|
266
|
+
seedProbeSteps: seedProbeSteps.length,
|
|
267
|
+
seedReadiness: {
|
|
268
|
+
ready: seedReadinessEvidence,
|
|
269
|
+
steps: seedReadinessSteps.length,
|
|
270
|
+
autoSeedOnDev: seedReadinessResults.some((result) => result.autoSeedOnDev),
|
|
271
|
+
autoSeedAllTenantsOnDev: seedReadinessResults.some((result) => result.autoSeedAllTenantsOnDev),
|
|
272
|
+
allTenantsAutoSeedReady: seedAllTenantsAutoSeedEvidence,
|
|
273
|
+
autoSeedModes: Array.from(new Set(seedReadinessResults
|
|
274
|
+
.map((result) => result.autoSeedMode)
|
|
275
|
+
.filter((value): value is string => Boolean(value)))),
|
|
276
|
+
selectedCommands: Array.from(new Set(seedReadinessResults
|
|
277
|
+
.map((result) => result.selectedCommand)
|
|
278
|
+
.filter((value): value is string => Boolean(value)))),
|
|
279
|
+
},
|
|
280
|
+
authSetupProbeSteps: authSetupProbeSteps.length,
|
|
281
|
+
authMetadataProbeSteps: authMetadataProbeSteps.length,
|
|
282
|
+
uiProbeSteps: uiProbeSteps.length,
|
|
283
|
+
vendorAccessProbeSteps: vendorAccessProbeSteps.length,
|
|
106
284
|
productionEvidence: {
|
|
107
285
|
readyForDeployCheck: productionEvidenceMissing.length === 0,
|
|
108
286
|
missing: productionEvidenceMissing,
|
|
@@ -119,11 +297,9 @@ function summarizeReport(data: unknown) {
|
|
|
119
297
|
|
|
120
298
|
function runCommandForOptions(templates: NewTemplateName[], packageManagers: NewPackageManager[]): string {
|
|
121
299
|
return [
|
|
122
|
-
"forge field-test run",
|
|
300
|
+
"forge field-test run --realistic",
|
|
123
301
|
`--templates ${templates.join(",")}`,
|
|
124
302
|
`--package-managers ${packageManagers.join(",")}`,
|
|
125
|
-
"--runtime-probes",
|
|
126
|
-
"--auth-probes",
|
|
127
303
|
"--json",
|
|
128
304
|
].join(" ");
|
|
129
305
|
}
|
|
@@ -136,7 +312,7 @@ async function createFieldTestApp(options: FieldTestCommandOptions): Promise<Fie
|
|
|
136
312
|
kind: "field-test",
|
|
137
313
|
action: "create",
|
|
138
314
|
data: { error: "forge field-test create requires an app name" },
|
|
139
|
-
nextActions: ["forge field-test create vendor-access --auth workos --
|
|
315
|
+
nextActions: ["forge field-test create vendor-access --auth workos --json"],
|
|
140
316
|
exitCode: 1,
|
|
141
317
|
};
|
|
142
318
|
}
|
|
@@ -152,9 +328,21 @@ async function createFieldTestApp(options: FieldTestCommandOptions): Promise<Fie
|
|
|
152
328
|
template: options.template,
|
|
153
329
|
packageManager: options.packageManager,
|
|
154
330
|
auth: options.auth,
|
|
331
|
+
install: true,
|
|
332
|
+
git: true,
|
|
155
333
|
command: `forge new ${options.name} --template ${options.template} --package-manager ${options.packageManager} --field-test --install`,
|
|
334
|
+
goldenPath: [
|
|
335
|
+
`forge field-test create ${options.name} --auth ${options.auth ?? "none"} --template ${options.template} --package-manager ${options.packageManager} --install --git --json`,
|
|
336
|
+
`cd ${options.name}`,
|
|
337
|
+
"forge field-test run --realistic --json",
|
|
338
|
+
"forge field-test report --json",
|
|
339
|
+
"forge deploy plan --target docker --json",
|
|
340
|
+
"forge deploy check --production --json",
|
|
341
|
+
],
|
|
156
342
|
},
|
|
157
|
-
nextActions: [
|
|
343
|
+
nextActions: [
|
|
344
|
+
`forge field-test create ${options.name} --auth ${options.auth ?? "none"} --template ${options.template} --package-manager ${options.packageManager} --install --git --json`,
|
|
345
|
+
],
|
|
158
346
|
exitCode: 0,
|
|
159
347
|
});
|
|
160
348
|
}
|
|
@@ -198,9 +386,12 @@ async function createFieldTestApp(options: FieldTestCommandOptions): Promise<Fie
|
|
|
198
386
|
`${options.packageManager} run forge -- authmd check --json`,
|
|
199
387
|
`${options.packageManager} run forge -- workos doctor --json`,
|
|
200
388
|
`${options.packageManager} run forge -- workos seed --file workos-seed.yml --dry-run --json`,
|
|
389
|
+
`${options.packageManager} run forge -- workos prove --file workos-seed.yml --json`,
|
|
201
390
|
`${options.packageManager} run forge -- workos setup --file workos-seed.yml --json`,
|
|
202
391
|
]
|
|
203
392
|
: []),
|
|
393
|
+
`${options.packageManager} run forge -- field-test run --realistic --json`,
|
|
394
|
+
`${options.packageManager} run forge -- field-test report --json`,
|
|
204
395
|
`${options.packageManager} run forge -- dev --once --json`,
|
|
205
396
|
`${options.packageManager} run forge -- verify --smoke --json`,
|
|
206
397
|
`${options.packageManager} run forge -- handoff --json`,
|
|
@@ -211,14 +402,14 @@ async function createFieldTestApp(options: FieldTestCommandOptions): Promise<Fie
|
|
|
211
402
|
|
|
212
403
|
function runHarness(options: FieldTestCommandOptions): FieldTestCommandResult {
|
|
213
404
|
const script = scriptPath(options.workspaceRoot);
|
|
214
|
-
if (!
|
|
405
|
+
if (!script) {
|
|
215
406
|
return {
|
|
216
407
|
schemaVersion: "0.1.0",
|
|
217
408
|
ok: false,
|
|
218
409
|
kind: "field-test",
|
|
219
410
|
action: "run",
|
|
220
|
-
data: { error: "field-test harness script not found",
|
|
221
|
-
nextActions: ["run from the ForgeOS framework checkout
|
|
411
|
+
data: { error: "field-test harness script not found", searched: scriptCandidates(options.workspaceRoot) },
|
|
412
|
+
nextActions: ["upgrade forgeos to a version that includes scripts/field-test-forgeos.mjs", "run from the ForgeOS framework checkout with npm run field:test"],
|
|
222
413
|
exitCode: 1,
|
|
223
414
|
};
|
|
224
415
|
}
|
|
@@ -236,8 +427,9 @@ function runHarness(options: FieldTestCommandOptions): FieldTestCommandResult {
|
|
|
236
427
|
];
|
|
237
428
|
if (options.dryRun) args.push("--dry-run");
|
|
238
429
|
if (options.keep) args.push("--keep");
|
|
239
|
-
if (options.runtimeProbes) args.push("--runtime-probes");
|
|
240
|
-
if (options.authProbes) args.push("--auth-probes");
|
|
430
|
+
if (options.runtimeProbes || options.realistic) args.push("--runtime-probes");
|
|
431
|
+
if (options.authProbes || options.realistic) args.push("--auth-probes");
|
|
432
|
+
if (options.uiProbes || options.realistic) args.push("--ui-probes");
|
|
241
433
|
if (options.forgeSpec) args.push("--forge-spec", options.forgeSpec);
|
|
242
434
|
if (reportPath) args.push("--write-report", reportPath);
|
|
243
435
|
if (options.json) args.push("--json");
|
|
@@ -246,7 +438,7 @@ function runHarness(options: FieldTestCommandOptions): FieldTestCommandResult {
|
|
|
246
438
|
encoding: "utf8",
|
|
247
439
|
windowsHide: true,
|
|
248
440
|
});
|
|
249
|
-
const reportAbsolute = reportPath ?
|
|
441
|
+
const reportAbsolute = reportPath ? resolveReportPath(options.workspaceRoot, reportPath) : null;
|
|
250
442
|
const reportData = reportAbsolute && existsSync(reportAbsolute)
|
|
251
443
|
? JSON.parse(readFileSync(reportAbsolute, "utf8")) as unknown
|
|
252
444
|
: parseJsonOutput(result.stdout ?? "");
|
|
@@ -276,7 +468,7 @@ function readReport(options: FieldTestCommandOptions): FieldTestCommandResult {
|
|
|
276
468
|
".forge/field-test-report.json",
|
|
277
469
|
"field-reports/full-alpha.json",
|
|
278
470
|
].filter(Boolean) as string[];
|
|
279
|
-
const report = candidates.find((candidate) => existsSync(
|
|
471
|
+
const report = candidates.find((candidate) => existsSync(resolveReportPath(options.workspaceRoot, candidate)));
|
|
280
472
|
if (!report) {
|
|
281
473
|
return {
|
|
282
474
|
schemaVersion: "0.1.0",
|
|
@@ -284,11 +476,11 @@ function readReport(options: FieldTestCommandOptions): FieldTestCommandResult {
|
|
|
284
476
|
kind: "field-test",
|
|
285
477
|
action: "report",
|
|
286
478
|
data: { searched: candidates },
|
|
287
|
-
nextActions: ["forge field-test run --
|
|
479
|
+
nextActions: ["forge field-test run --realistic --write-report .forge/field-test-report.json --json"],
|
|
288
480
|
exitCode: 1,
|
|
289
481
|
};
|
|
290
482
|
}
|
|
291
|
-
const data = JSON.parse(readFileSync(
|
|
483
|
+
const data = JSON.parse(readFileSync(resolveReportPath(options.workspaceRoot, report), "utf8")) as unknown;
|
|
292
484
|
const summary = summarizeReport(data);
|
|
293
485
|
const productionEvidence = (summary as { productionEvidence?: { readyForDeployCheck?: boolean } }).productionEvidence;
|
|
294
486
|
return {
|
|
@@ -301,7 +493,7 @@ function readReport(options: FieldTestCommandOptions): FieldTestCommandResult {
|
|
|
301
493
|
data,
|
|
302
494
|
nextActions: productionEvidence?.readyForDeployCheck
|
|
303
495
|
? ["forge deploy check --production --json"]
|
|
304
|
-
: ["forge field-test run --
|
|
496
|
+
: ["forge field-test run --realistic --write-report .forge/field-test-report.json --json"],
|
|
305
497
|
exitCode: (data && typeof data === "object" && "ok" in data && (data as { ok?: unknown }).ok !== true) ? 1 : 0,
|
|
306
498
|
};
|
|
307
499
|
}
|
package/src/forge/cli/main.ts
CHANGED
|
@@ -16,10 +16,14 @@ function formatHelp(): string {
|
|
|
16
16
|
" forge changed --authored --json Show only authored changed files, excluding generated artifacts",
|
|
17
17
|
" forge changed --review --json Show review-focused app/config/docs changes, excluding local agent/browser artifacts",
|
|
18
18
|
" forge changed --commit-ready --json Show files suitable for git add, excluding generated and operational artifacts",
|
|
19
|
-
" forge new my-app --template
|
|
20
|
-
" forge field-test create vendor-access --auth workos --
|
|
21
|
-
" forge field-test run --templates
|
|
19
|
+
" forge new my-app --template vendor-access --field-test Create an installed WorkOS/auth.md field-test app",
|
|
20
|
+
" forge field-test create vendor-access --auth workos --install --git --json Create a real field-test app",
|
|
21
|
+
" forge field-test run --realistic --templates vendor-access --package-managers npm --json",
|
|
22
22
|
" forge field-test report --json Summarize the machine-readable field-test report",
|
|
23
|
+
" forge seed status --json Discover app seed commands from the generated runtime graph",
|
|
24
|
+
" forge seed dev --json Run the app seed command against the local Forge dev runtime",
|
|
25
|
+
" forge seed dev --all-tenants --json Seed every discovered local tenant/persona profile",
|
|
26
|
+
" forge seed reset --json Run the seed command with args.reset=true against the local dev runtime",
|
|
23
27
|
" forge diff authored Run the authored-only git diff pathspec",
|
|
24
28
|
" forge handoff --json Compact work handoff for the next external code agent",
|
|
25
29
|
" forge agent onboard --target codex --json Prepare adapter, hooks, memory, and dev snapshot",
|
|
@@ -37,10 +41,11 @@ function formatHelp(): string {
|
|
|
37
41
|
" forge workos doctor --json Check WorkOS AuthKit/FGA files, claims, seed, webhook, and tenant guards",
|
|
38
42
|
" forge workos doctor --yes --json Run local checks, then delegate to npx --yes workos@latest doctor",
|
|
39
43
|
" forge workos seed --file workos-seed.yml --dry-run --json Validate WorkOS seed without hosted changes",
|
|
44
|
+
" forge workos prove --file workos-seed.yml --json Prove local WorkOS/AuthKit/FGA/seed readiness without dashboard changes",
|
|
40
45
|
" forge workos setup --real --file workos-seed.yml --json Apply redirect/CORS/homepage config and seed WorkOS without dashboard",
|
|
41
46
|
" forge deploy plan --target docker --json Explain production deploy gates and commands",
|
|
42
47
|
" forge deploy check --production --json Gate auth, DB, metadata, generated artifacts, and liveQuery readiness",
|
|
43
|
-
" forge deploy
|
|
48
|
+
" forge deploy package --target docker Write Docker production deploy files under deploy/",
|
|
44
49
|
" forge deploy verify --production --url https://app.example.com --json Probe /health and validate public auth metadata",
|
|
45
50
|
" forge release check --allow-missing-local-release --json Gate release readiness without failing on unprepared local artifacts",
|
|
46
51
|
" forge self-host check --prepared-only --json Report compose readiness without creating deploy files",
|
|
@@ -96,18 +101,24 @@ function formatDevHelp(): string {
|
|
|
96
101
|
"Options:",
|
|
97
102
|
" --db memory|pglite|postgres|none Choose the development database adapter",
|
|
98
103
|
" --port <port> API runtime port; use 0 for an ephemeral port",
|
|
99
|
-
" --web-port <port> Web dev server port",
|
|
104
|
+
" --web-port <port> Web dev server port; use 0 for an ephemeral port",
|
|
100
105
|
" --host <host> Bind host, default 127.0.0.1",
|
|
101
106
|
" --no-web Start API/runtime only",
|
|
102
107
|
" --api-only Start API/runtime only",
|
|
103
108
|
" --web-only Start web server only, expecting an API runtime",
|
|
104
109
|
" --no-worker Disable local worker",
|
|
105
110
|
" --no-watch Disable file watching",
|
|
111
|
+
" --seed Run the discovered seed command after the API runtime starts",
|
|
112
|
+
" --seed-command <name> Select a specific seed command for --seed",
|
|
113
|
+
" --all-tenants With --seed, seed every discovered local tenant/persona",
|
|
106
114
|
" --once --json Run one-shot diagnostics without starting servers",
|
|
107
115
|
" --detach --json Start dev in the background with .forge/dev/dev.pid and .forge/dev/dev.log",
|
|
108
116
|
"",
|
|
109
117
|
"Examples:",
|
|
110
118
|
" forge dev --db memory --port 3777 --web-port 5174",
|
|
119
|
+
" forge dev --db memory --port 0 --web-port 0",
|
|
120
|
+
" forge dev --seed --db pglite",
|
|
121
|
+
" forge dev --seed --all-tenants --db pglite",
|
|
111
122
|
" forge dev --db pglite --once --json",
|
|
112
123
|
" forge dev --detach --db memory --port 0 --json",
|
|
113
124
|
" forge dev status --json",
|
package/src/forge/cli/new.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { run as runGenerate } from "../compiler/orchestrator/run.ts";
|
|
|
8
8
|
import { resolvePackageManagerArgv } from "../compiler/package-manager/executor.ts";
|
|
9
9
|
import { moduleDir } from "../platform/module.ts";
|
|
10
10
|
|
|
11
|
-
export type NewTemplateName = "agent-workroom" | "b2b-support-web" | "minimal-web" | "nuxt-web";
|
|
11
|
+
export type NewTemplateName = "agent-workroom" | "b2b-support-web" | "minimal-web" | "nuxt-web" | "vendor-access";
|
|
12
12
|
export type NewPackageManager = "bun" | "npm" | "pnpm" | "yarn";
|
|
13
13
|
|
|
14
14
|
export interface NewCommandOptions {
|
|
@@ -79,6 +79,7 @@ const TEXT_EXTENSIONS = new Set([
|
|
|
79
79
|
".css",
|
|
80
80
|
".env",
|
|
81
81
|
".example",
|
|
82
|
+
".html",
|
|
82
83
|
".json",
|
|
83
84
|
".md",
|
|
84
85
|
".ts",
|
package/src/forge/cli/output.ts
CHANGED
|
@@ -118,6 +118,8 @@ export function buildGenerateJson(
|
|
|
118
118
|
? {
|
|
119
119
|
kind: "generated-drift",
|
|
120
120
|
message: `${result.changed.length} generated artifact(s) would change; run forge generate before handoff or verification.`,
|
|
121
|
+
concurrencyNote:
|
|
122
|
+
"Do not run forge generate, forge generate --check, forge dev --once, or forge verify concurrently in the same workspace. If a generate command was active or just started, wait for it to finish and rerun the check once.",
|
|
121
123
|
changedGroups: summarizeGeneratedArtifacts(result.changed),
|
|
122
124
|
sampleChanged: changed.sample,
|
|
123
125
|
hiddenChanged: changed.hidden,
|
|
@@ -127,6 +129,12 @@ export function buildGenerateJson(
|
|
|
127
129
|
checkCommand: options.workspaceRoot
|
|
128
130
|
? forgeCliCommandsForWorkspace(options.workspaceRoot, ["forge generate --check --json"])[0]
|
|
129
131
|
: "forge generate --check --json",
|
|
132
|
+
sequentialCommands: options.workspaceRoot
|
|
133
|
+
? forgeCliCommandsForWorkspace(options.workspaceRoot, [
|
|
134
|
+
"forge generate --json",
|
|
135
|
+
"forge generate --check --json",
|
|
136
|
+
])
|
|
137
|
+
: ["forge generate --json", "forge generate --check --json"],
|
|
130
138
|
}
|
|
131
139
|
: null;
|
|
132
140
|
return {
|
|
@@ -234,6 +242,7 @@ export function buildAddJson(
|
|
|
234
242
|
"forge workos doctor --yes --json",
|
|
235
243
|
"forge workos seed --file workos-seed.yml --dry-run --json",
|
|
236
244
|
"forge workos seed --file workos-seed.yml --json",
|
|
245
|
+
"forge workos prove --file workos-seed.yml --json",
|
|
237
246
|
"forge auth check --json",
|
|
238
247
|
"forge auth prove --json",
|
|
239
248
|
]
|