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 +1 @@
|
|
|
1
|
-
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.
|
|
1
|
+
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.43","releaseId":"forgeos@0.1.0-alpha.43+unknown","schemaVersion":"0.1.0"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.43 input=f15e1d5ecb88338643677cb7fb622a7ccdcf347f2b5c020ea5ce16b33f7d1f56 content=26804428ac690374e29343ac563ce217391f23b1039aad88e6ae0a1b05aa2036
|
|
2
2
|
export const releaseManifest = {
|
|
3
3
|
"defaultProvider": "local",
|
|
4
4
|
"diagnostics": [],
|
|
@@ -19,7 +19,7 @@ export const releaseManifest = {
|
|
|
19
19
|
"custom"
|
|
20
20
|
],
|
|
21
21
|
"packageName": "forgeos",
|
|
22
|
-
"packageVersion": "0.1.0-alpha.
|
|
23
|
-
"releaseId": "forgeos@0.1.0-alpha.
|
|
22
|
+
"packageVersion": "0.1.0-alpha.43",
|
|
23
|
+
"releaseId": "forgeos@0.1.0-alpha.43+unknown",
|
|
24
24
|
"schemaVersion": "0.1.0"
|
|
25
25
|
} as const;
|
package/src/forge/cli/auth.ts
CHANGED
|
@@ -258,6 +258,7 @@ function buildMultiTenantProof(workspaceRoot: string, workos: ReturnType<typeof
|
|
|
258
258
|
nextActions: [
|
|
259
259
|
"forge workos doctor --json",
|
|
260
260
|
"forge workos seed --file workos-seed.yml --dry-run --json",
|
|
261
|
+
"forge workos prove --file workos-seed.yml --json",
|
|
261
262
|
"run an HTTP E2E with Acme and Globex tokens before production",
|
|
262
263
|
],
|
|
263
264
|
});
|
|
@@ -224,6 +224,11 @@ import {
|
|
|
224
224
|
formatFieldTestJson,
|
|
225
225
|
runFieldTestCommand,
|
|
226
226
|
} from "./field-test.ts";
|
|
227
|
+
import {
|
|
228
|
+
formatSeedHuman,
|
|
229
|
+
formatSeedJson,
|
|
230
|
+
runSeedCommand,
|
|
231
|
+
} from "./seed.ts";
|
|
227
232
|
import {
|
|
228
233
|
formatLastHuman,
|
|
229
234
|
formatLastJson,
|
|
@@ -1966,6 +1971,11 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
|
|
|
1966
1971
|
process.stdout.write(command.json ? formatFieldTestJson(result) : formatFieldTestHuman(result));
|
|
1967
1972
|
return result.exitCode;
|
|
1968
1973
|
}
|
|
1974
|
+
case "seed": {
|
|
1975
|
+
const result = await runSeedCommand(command);
|
|
1976
|
+
process.stdout.write(command.json ? formatSeedJson(result) : formatSeedHuman(result));
|
|
1977
|
+
return result.exitCode;
|
|
1978
|
+
}
|
|
1969
1979
|
case "rls": {
|
|
1970
1980
|
const result = await runRlsCommand(command);
|
|
1971
1981
|
if (command.json) {
|
|
@@ -2525,6 +2535,9 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
|
|
|
2525
2535
|
envFile: command.envFile,
|
|
2526
2536
|
skipStartupConsole: command.skipStartupConsole,
|
|
2527
2537
|
detach: command.detach,
|
|
2538
|
+
seed: command.seed,
|
|
2539
|
+
seedCommand: command.seedCommand,
|
|
2540
|
+
seedAllTenants: command.seedAllTenants,
|
|
2528
2541
|
lifecycle: command.lifecycle,
|
|
2529
2542
|
});
|
|
2530
2543
|
return result.exitCode;
|
package/src/forge/cli/deploy.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { runAuthCommand } from "./auth.ts";
|
|
|
13
13
|
import { runAuthMdCommand } from "./authmd.ts";
|
|
14
14
|
import { runWorkOSCommand } from "./workos.ts";
|
|
15
15
|
|
|
16
|
-
export type DeploySubcommand = "plan" | "check" | "render" | "verify";
|
|
16
|
+
export type DeploySubcommand = "plan" | "check" | "render" | "package" | "verify";
|
|
17
17
|
export type DeployTarget = "docker" | "forge-cloud";
|
|
18
18
|
|
|
19
19
|
export interface DeployCommandOptions {
|
|
@@ -119,25 +119,104 @@ function fieldTestReportCandidates(workspaceRoot: string): Array<{ path: string;
|
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
const FIELD_TEST_PRODUCTION_COMMAND = "forge field-test run --realistic --json";
|
|
123
|
+
|
|
122
124
|
function summarizeFieldTestReport(data: Record<string, unknown>) {
|
|
123
125
|
const results = Array.isArray(data.results) ? data.results as Array<Record<string, unknown>> : [];
|
|
124
126
|
const failed = results.filter((result) => result.ok === false && result.skipped !== true);
|
|
125
127
|
const skipped = results.filter((result) => result.skipped === true);
|
|
128
|
+
const executed = results.filter((result) => result.skipped !== true);
|
|
129
|
+
const topLevelSteps = results.flatMap((result) =>
|
|
130
|
+
Array.isArray(result.steps) ? result.steps as Array<Record<string, unknown>> : [],
|
|
131
|
+
);
|
|
126
132
|
const runtimeSteps = results.flatMap((result) => {
|
|
127
133
|
const runtime = result.runtime && typeof result.runtime === "object"
|
|
128
134
|
? result.runtime as Record<string, unknown>
|
|
129
135
|
: {};
|
|
130
136
|
return Array.isArray(runtime.steps) ? runtime.steps as Array<Record<string, unknown>> : [];
|
|
131
137
|
});
|
|
138
|
+
const commandOf = (step: unknown): string =>
|
|
139
|
+
step && typeof step === "object" && typeof (step as { command?: unknown }).command === "string"
|
|
140
|
+
? (step as { command: string }).command
|
|
141
|
+
: "";
|
|
142
|
+
const okStep = (step: unknown): boolean =>
|
|
143
|
+
Boolean(step && typeof step === "object" && (step as { ok?: unknown }).ok === true);
|
|
144
|
+
const hasOkRuntimeCommand = (pattern: RegExp): boolean =>
|
|
145
|
+
runtimeSteps.some((step) => okStep(step) && pattern.test(commandOf(step)));
|
|
146
|
+
const hasOkTopLevelCommand = (pattern: RegExp): boolean =>
|
|
147
|
+
topLevelSteps.some((step) => okStep(step) && pattern.test(commandOf(step)));
|
|
148
|
+
const authSetupProbeSteps = topLevelSteps.filter((step) =>
|
|
149
|
+
/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)),
|
|
150
|
+
);
|
|
151
|
+
const authMetadataProbeSteps = runtimeSteps.filter((step) =>
|
|
152
|
+
/^(HEAD|GET)\s+https?:\/\/[^/]+\/(auth\.md|\.well-known\/oauth-protected-resource)\b/i.test(commandOf(step)),
|
|
153
|
+
);
|
|
154
|
+
const uiErgonomicsResults = results
|
|
155
|
+
.map((result) => result.uiErgonomics)
|
|
156
|
+
.filter((value): value is Record<string, unknown> => Boolean(value && typeof value === "object"));
|
|
157
|
+
const uiErgonomicsWarnings = uiErgonomicsResults.reduce((total, result) =>
|
|
158
|
+
total + (typeof result.warnings === "number" ? result.warnings : 0), 0);
|
|
159
|
+
const uiErgonomicsErrors = uiErgonomicsResults.reduce((total, result) =>
|
|
160
|
+
total + (typeof result.errors === "number" ? result.errors : 0), 0);
|
|
161
|
+
const runtimeProbes = data.runtimeProbes === true;
|
|
162
|
+
const authProbes = data.authProbes === true;
|
|
163
|
+
const uiProbes = data.uiProbes === true;
|
|
164
|
+
const uiProbeSteps = runtimeSteps.filter((step) =>
|
|
165
|
+
/^GET\s+https?:\/\/[^/]+\/$/i.test(commandOf(step)),
|
|
166
|
+
);
|
|
167
|
+
const uiErgonomics = uiProbes !== true || (executed.length > 0 && uiErgonomicsResults.length === executed.length);
|
|
168
|
+
const runtimeHealthEvidence = !runtimeProbes || hasOkRuntimeCommand(/\bGET\s+.*\/health\b/i);
|
|
169
|
+
const runtimeEntriesEvidence = !runtimeProbes || hasOkRuntimeCommand(/\bGET\s+.*\/entries\b/i);
|
|
170
|
+
const authSetupEvidence = !authProbes || [
|
|
171
|
+
/forge\s+(?:--\s+)?add\s+auth\s+workos\b/,
|
|
172
|
+
/forge\s+(?:--\s+)?authmd\s+generate\b/,
|
|
173
|
+
/forge\s+(?:--\s+)?authmd\s+check\b/,
|
|
174
|
+
/forge\s+(?:--\s+)?workos\s+doctor\b/,
|
|
175
|
+
/forge\s+(?:--\s+)?workos\s+seed\b/,
|
|
176
|
+
/forge\s+(?:--\s+)?workos\s+prove\b/,
|
|
177
|
+
/forge\s+(?:--\s+)?auth\s+prove\b/,
|
|
178
|
+
].every((pattern) => hasOkTopLevelCommand(pattern));
|
|
179
|
+
const authMetadataEvidence = !authProbes || [
|
|
180
|
+
/^HEAD\s+.*\/auth\.md\b/i,
|
|
181
|
+
/^GET\s+.*\/auth\.md\b/i,
|
|
182
|
+
/^HEAD\s+.*\/\.well-known\/oauth-protected-resource\b/i,
|
|
183
|
+
/^GET\s+.*\/\.well-known\/oauth-protected-resource\b/i,
|
|
184
|
+
].every((pattern) => hasOkRuntimeCommand(pattern));
|
|
185
|
+
const uiProbeEvidence = !uiProbes || uiProbeSteps.some((step) => okStep(step));
|
|
186
|
+
const productionEvidenceMissing = [
|
|
187
|
+
...(data.ok !== true ? ["passing field-test report"] : []),
|
|
188
|
+
...(!runtimeProbes ? ["runtime probes"] : []),
|
|
189
|
+
...(!authProbes ? ["auth probes"] : []),
|
|
190
|
+
...(!uiProbes ? ["ui probes"] : []),
|
|
191
|
+
...(!runtimeHealthEvidence ? ["runtime health probe"] : []),
|
|
192
|
+
...(!runtimeEntriesEvidence ? ["runtime entries probe"] : []),
|
|
193
|
+
...(!authSetupEvidence ? ["auth setup probes"] : []),
|
|
194
|
+
...(!authMetadataEvidence ? ["auth metadata endpoint probes"] : []),
|
|
195
|
+
...(!uiProbeEvidence ? ["web UI probe"] : []),
|
|
196
|
+
...(!uiErgonomics ? ["UI ergonomics audit"] : []),
|
|
197
|
+
...(uiErgonomicsErrors > 0 ? ["zero UI ergonomics errors"] : []),
|
|
198
|
+
...(failed.length > 0 ? ["zero failed cases"] : []),
|
|
199
|
+
];
|
|
132
200
|
return {
|
|
133
201
|
ok: data.ok === true,
|
|
134
202
|
cases: results.length,
|
|
135
203
|
passed: results.filter((result) => result.ok === true && result.skipped !== true).length,
|
|
136
204
|
failed: failed.length,
|
|
137
205
|
skipped: skipped.length,
|
|
138
|
-
runtimeProbes
|
|
139
|
-
authProbes
|
|
206
|
+
runtimeProbes,
|
|
207
|
+
authProbes,
|
|
208
|
+
uiProbes,
|
|
209
|
+
uiErgonomics,
|
|
210
|
+
uiErgonomicsWarnings,
|
|
211
|
+
uiErgonomicsErrors,
|
|
140
212
|
runtimeProbeSteps: runtimeSteps.length,
|
|
213
|
+
authSetupProbeSteps: authSetupProbeSteps.length,
|
|
214
|
+
authMetadataProbeSteps: authMetadataProbeSteps.length,
|
|
215
|
+
uiProbeSteps: uiProbeSteps.length,
|
|
216
|
+
productionEvidence: {
|
|
217
|
+
readyForDeployCheck: productionEvidenceMissing.length === 0,
|
|
218
|
+
missing: productionEvidenceMissing,
|
|
219
|
+
},
|
|
141
220
|
};
|
|
142
221
|
}
|
|
143
222
|
|
|
@@ -431,13 +510,13 @@ function databaseReadyCommand(options: DeployCommandOptions): string {
|
|
|
431
510
|
if (hasRenderedProductionEnvExample(options.workspaceRoot)) {
|
|
432
511
|
return "cp deploy/.env.production.example deploy/.env.production";
|
|
433
512
|
}
|
|
434
|
-
return "forge deploy
|
|
513
|
+
return "forge deploy package --target docker";
|
|
435
514
|
}
|
|
436
515
|
|
|
437
516
|
function renderProductionReadme(): string {
|
|
438
517
|
return `# ForgeOS Production Deploy
|
|
439
518
|
|
|
440
|
-
This directory is generated by \`forge deploy
|
|
519
|
+
This directory is generated by \`forge deploy package --target docker\`.
|
|
441
520
|
|
|
442
521
|
## 1. Prepare production env
|
|
443
522
|
|
|
@@ -463,7 +542,7 @@ forge generate --check --json
|
|
|
463
542
|
forge check --json
|
|
464
543
|
forge authmd generate --json
|
|
465
544
|
forge auth prove --scenario multi-tenant --json
|
|
466
|
-
forge field-test run --
|
|
545
|
+
forge field-test run --realistic --json
|
|
467
546
|
forge deploy check --production --json
|
|
468
547
|
\`\`\`
|
|
469
548
|
|
|
@@ -492,15 +571,15 @@ function buildPlan(options: DeployCommandOptions): DeployCommandResult {
|
|
|
492
571
|
"forge deploy check --production --json",
|
|
493
572
|
"forge auth check --production --json",
|
|
494
573
|
"forge authmd check --json",
|
|
495
|
-
|
|
574
|
+
FIELD_TEST_PRODUCTION_COMMAND,
|
|
496
575
|
"forge field-test report --json",
|
|
497
576
|
"forge deploy verify --production --url https://<your-forge-cloud-app> --json",
|
|
498
577
|
]
|
|
499
578
|
: [
|
|
500
|
-
"forge deploy
|
|
579
|
+
"forge deploy package --target docker",
|
|
501
580
|
"cp deploy/.env.production.example deploy/.env.production",
|
|
502
581
|
"forge deploy check --production --json",
|
|
503
|
-
|
|
582
|
+
FIELD_TEST_PRODUCTION_COMMAND,
|
|
504
583
|
"docker compose -f deploy/docker-compose.yml up --build",
|
|
505
584
|
"forge deploy verify --production --url https://app.example.com --json",
|
|
506
585
|
];
|
|
@@ -524,7 +603,7 @@ function buildPlan(options: DeployCommandOptions): DeployCommandResult {
|
|
|
524
603
|
"production database env evidence is present",
|
|
525
604
|
"required secret names are present",
|
|
526
605
|
"auth.md and protected-resource metadata are published",
|
|
527
|
-
"field-test report exists with runtime/auth probes",
|
|
606
|
+
"field-test report exists with runtime/auth/UI probes",
|
|
528
607
|
"runtime /health responds",
|
|
529
608
|
"tenant and policy proof is run before public traffic",
|
|
530
609
|
],
|
|
@@ -693,20 +772,32 @@ async function buildChecks(options: DeployCommandOptions): Promise<DeployCommand
|
|
|
693
772
|
const fieldReports = fieldTestReportCandidates(options.workspaceRoot);
|
|
694
773
|
const latestFieldReport = fieldReports[0];
|
|
695
774
|
const fieldSummary = latestFieldReport ? summarizeFieldTestReport(latestFieldReport.data) : null;
|
|
775
|
+
const fieldReportComplete = Boolean(fieldSummary?.productionEvidence.readyForDeployCheck);
|
|
776
|
+
const fieldReportMissing = fieldSummary?.productionEvidence.missing ?? [];
|
|
696
777
|
checks.push({
|
|
697
778
|
name: "field-test-report",
|
|
698
|
-
ok:
|
|
779
|
+
ok: fieldReportComplete,
|
|
699
780
|
severity: options.production ? "error" : "warning",
|
|
700
781
|
message: fieldSummary?.ok
|
|
701
|
-
?
|
|
702
|
-
? `field-test report ${latestFieldReport?.path} passed with runtime and
|
|
703
|
-
: `field-test report ${latestFieldReport?.path} passed but is missing
|
|
782
|
+
? fieldReportComplete
|
|
783
|
+
? `field-test report ${latestFieldReport?.path} passed with concrete runtime, auth, metadata, UI, and ergonomics evidence`
|
|
784
|
+
: `field-test report ${latestFieldReport?.path} passed but is missing deploy evidence: ${fieldReportMissing.join(", ")}`
|
|
704
785
|
: latestFieldReport
|
|
705
786
|
? `field-test report ${latestFieldReport.path} did not pass`
|
|
706
|
-
|
|
707
|
-
command:
|
|
787
|
+
: `no field-test report found; run ${FIELD_TEST_PRODUCTION_COMMAND}`,
|
|
788
|
+
command: FIELD_TEST_PRODUCTION_COMMAND,
|
|
708
789
|
details: latestFieldReport ? { path: latestFieldReport.path, summary: fieldSummary } : undefined,
|
|
709
790
|
});
|
|
791
|
+
if (fieldSummary?.uiErgonomicsWarnings) {
|
|
792
|
+
checks.push({
|
|
793
|
+
name: "field-test-ui-ergonomics",
|
|
794
|
+
ok: false,
|
|
795
|
+
severity: "warning",
|
|
796
|
+
message: `field-test UI ergonomics audit reported ${fieldSummary.uiErgonomicsWarnings} warning(s); fix the product surface before treating the app as polished`,
|
|
797
|
+
command: "forge inspect ui --ergonomics --json",
|
|
798
|
+
details: { path: latestFieldReport?.path, summary: fieldSummary },
|
|
799
|
+
});
|
|
800
|
+
}
|
|
710
801
|
checks.push({
|
|
711
802
|
name: "frontend-build-script",
|
|
712
803
|
ok: !readGeneratedJson<FrontendGraph>(options.workspaceRoot, `${GENERATED_DIR}/frontendGraph.json`)?.present || hasScript(options.workspaceRoot, "build") || hasScript(join(options.workspaceRoot, "web"), "build"),
|
|
@@ -759,7 +850,7 @@ function renderDocker(options: DeployCommandOptions): DeployCommandResult {
|
|
|
759
850
|
schemaVersion: "0.1.0",
|
|
760
851
|
ok: true,
|
|
761
852
|
kind: "deploy",
|
|
762
|
-
action:
|
|
853
|
+
action: options.subcommand,
|
|
763
854
|
target: options.target,
|
|
764
855
|
production: options.production,
|
|
765
856
|
checks: [],
|
|
@@ -866,7 +957,7 @@ async function verifyUrl(options: DeployCommandOptions): Promise<DeployCommandRe
|
|
|
866
957
|
export async function runDeployCommand(options: DeployCommandOptions): Promise<DeployCommandResult> {
|
|
867
958
|
if (options.subcommand === "plan") return buildPlan(options);
|
|
868
959
|
if (options.subcommand === "check") return buildChecks(options);
|
|
869
|
-
if (options.subcommand === "render") return renderDocker(options);
|
|
960
|
+
if (options.subcommand === "render" || options.subcommand === "package") return renderDocker(options);
|
|
870
961
|
return verifyUrl(options);
|
|
871
962
|
}
|
|
872
963
|
|
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);
|