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.
Files changed (66) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +46 -2
  3. package/docs/changelog.md +222 -0
  4. package/package.json +2 -1
  5. package/scripts/field-test-forgeos.mjs +1012 -0
  6. package/src/forge/_generated/releaseManifest.json +1 -1
  7. package/src/forge/_generated/releaseManifest.ts +3 -3
  8. package/src/forge/cli/auth.ts +1 -0
  9. package/src/forge/cli/commands.ts +13 -0
  10. package/src/forge/cli/deploy.ts +109 -18
  11. package/src/forge/cli/dev.ts +176 -10
  12. package/src/forge/cli/field-test.ts +210 -18
  13. package/src/forge/cli/main.ts +16 -5
  14. package/src/forge/cli/new.ts +2 -1
  15. package/src/forge/cli/output.ts +9 -0
  16. package/src/forge/cli/parse.ts +95 -12
  17. package/src/forge/cli/run.ts +1 -0
  18. package/src/forge/cli/seed.ts +581 -0
  19. package/src/forge/cli/workos.ts +349 -16
  20. package/src/forge/compiler/agent-contract/build.ts +58 -0
  21. package/src/forge/compiler/data-graph/build.ts +29 -1
  22. package/src/forge/compiler/diagnostics/codes.ts +2 -0
  23. package/src/forge/compiler/diagnostics/create.ts +7 -0
  24. package/src/forge/compiler/integration/add.ts +305 -5
  25. package/src/forge/compiler/integration/templates/workos.ts +51 -2
  26. package/src/forge/compiler/orchestrator/plan.ts +2 -0
  27. package/src/forge/make/index.ts +2 -0
  28. package/src/forge/make/templates.ts +58 -4
  29. package/src/forge/runtime/external/bridge.ts +14 -0
  30. package/src/forge/server.ts +59 -9
  31. package/src/forge/ui/index.ts +643 -25
  32. package/src/forge/ui/types.ts +2 -1
  33. package/src/forge/version.ts +1 -1
  34. package/templates/agent-workroom/web/index.html +1 -0
  35. package/templates/agent-workroom/web/src/lib/forge.ts +9 -2
  36. package/templates/agent-workroom/web/vite.config.ts +36 -0
  37. package/templates/b2b-support-web/web/app/page.tsx +2 -2
  38. package/templates/b2b-support-web/web/lib/forge.ts +9 -2
  39. package/templates/b2b-support-web/web/next.config.ts +24 -0
  40. package/templates/minimal-web/web/index.html +1 -0
  41. package/templates/minimal-web/web/src/App.tsx +12 -6
  42. package/templates/minimal-web/web/src/lib/forge.ts +9 -2
  43. package/templates/minimal-web/web/src/styles.css +34 -0
  44. package/templates/minimal-web/web/vite.config.ts +36 -0
  45. package/templates/nuxt-web/web/app.vue +1 -1
  46. package/templates/vendor-access/.vscode/settings.json +14 -0
  47. package/templates/vendor-access/README.md +30 -0
  48. package/templates/vendor-access/forge.config.ts +3 -0
  49. package/templates/vendor-access/package.json +34 -0
  50. package/templates/vendor-access/src/commands/addEvidence.ts +42 -0
  51. package/templates/vendor-access/src/commands/approveAccessRequest.ts +43 -0
  52. package/templates/vendor-access/src/commands/createAccessRequest.ts +49 -0
  53. package/templates/vendor-access/src/commands/seedVendorAccessDemo.ts +200 -0
  54. package/templates/vendor-access/src/forge/schema.ts +74 -0
  55. package/templates/vendor-access/src/policies.ts +11 -0
  56. package/templates/vendor-access/src/queries/listVendorAccessDashboard.ts +24 -0
  57. package/templates/vendor-access/src/queries/liveVendorAccessDashboard.ts +24 -0
  58. package/templates/vendor-access/tsconfig.json +17 -0
  59. package/templates/vendor-access/web/index.html +13 -0
  60. package/templates/vendor-access/web/package.json +21 -0
  61. package/templates/vendor-access/web/src/App.tsx +573 -0
  62. package/templates/vendor-access/web/src/lib/forge.ts +20 -0
  63. package/templates/vendor-access/web/src/main.tsx +205 -0
  64. package/templates/vendor-access/web/src/styles.css +682 -0
  65. package/templates/vendor-access/web/tsconfig.json +18 -0
  66. package/templates/vendor-access/web/vite.config.ts +36 -0
@@ -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.41","releaseId":"forgeos@0.1.0-alpha.41+unknown","schemaVersion":"0.1.0"}
1
+ {"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.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.41 input=472a35507fac4f297d64b66cb443aa712ceb113986ac03317278c6464c17cdd4 content=ba31c1269d2cbbe1fdb5c6dc20de16dabe1afbe97d1af2a72346806ad2d17e16
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.41",
23
- "releaseId": "forgeos@0.1.0-alpha.41+unknown",
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;
@@ -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;
@@ -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: data.runtimeProbes === true,
139
- authProbes: data.authProbes === true,
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 render docker";
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 render docker\`.
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 --runtime-probes --auth-probes --json
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
- "forge field-test run --runtime-probes --auth-probes --json",
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 render docker",
579
+ "forge deploy package --target docker",
501
580
  "cp deploy/.env.production.example deploy/.env.production",
502
581
  "forge deploy check --production --json",
503
- "forge field-test run --runtime-probes --auth-probes --json",
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: Boolean(fieldSummary?.ok && fieldSummary.runtimeProbes && fieldSummary.authProbes),
779
+ ok: fieldReportComplete,
699
780
  severity: options.production ? "error" : "warning",
700
781
  message: fieldSummary?.ok
701
- ? fieldSummary.runtimeProbes && fieldSummary.authProbes
702
- ? `field-test report ${latestFieldReport?.path} passed with runtime and auth probes`
703
- : `field-test report ${latestFieldReport?.path} passed but is missing runtime/auth probes`
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
- : "no field-test report found; run forge field-test run --runtime-probes --auth-probes --json",
707
- command: "forge field-test run --runtime-probes --auth-probes --json",
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: "render",
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
 
@@ -34,6 +34,7 @@ import { FORGE_PGLITE_STORE_ABORTED } from "../compiler/diagnostics/codes.ts";
34
34
  import { isPgliteAbortMessage } from "../runtime/db/pglite-adapter.ts";
35
35
  import { forgeCliCommandForWorkspace, forgeCliCommandsForWorkspace } from "../workspace/forge-cli.ts";
36
36
  import { writeLastRunRecord } from "./last-run.ts";
37
+ import { runSeedCommand, type SeedCommandResult } from "./seed.ts";
37
38
 
38
39
  export interface DevCommandOptions {
39
40
  workspaceRoot: string;
@@ -58,6 +59,9 @@ export interface DevCommandOptions {
58
59
  allowDevAuth?: boolean;
59
60
  skipStartupConsole?: boolean;
60
61
  detach?: boolean;
62
+ seed?: boolean;
63
+ seedCommand?: string;
64
+ seedAllTenants?: boolean;
61
65
  lifecycle?: "status" | "stop";
62
66
  }
63
67
 
@@ -75,7 +79,7 @@ export interface DevGenerateResult {
75
79
  exitCode: 0 | 1;
76
80
  }
77
81
 
78
- interface DevStartupGeneratedEvidence {
82
+ export interface DevStartupGeneratedEvidence {
79
83
  ok: boolean;
80
84
  state: "fresh" | "regenerated" | "stale-risk";
81
85
  changedFiles: number;
@@ -86,7 +90,7 @@ interface DevStartupGeneratedEvidence {
86
90
  checkCommand: string;
87
91
  }
88
92
 
89
- interface WebDevServerHandle {
93
+ export interface WebDevServerHandle {
90
94
  url: string;
91
95
  port: number;
92
96
  requestedPort?: number;
@@ -95,7 +99,7 @@ interface WebDevServerHandle {
95
99
  stop: () => void;
96
100
  }
97
101
 
98
- interface DevStartupSummary {
102
+ export interface DevStartupSummary {
99
103
  schemaVersion: "0.1.0";
100
104
  ok: true;
101
105
  mode: "dev";
@@ -155,11 +159,31 @@ interface DevStartupSummary {
155
159
  checkCommand: string;
156
160
  message: string;
157
161
  };
162
+ seed: {
163
+ requested: boolean;
164
+ available: boolean;
165
+ commands: string[];
166
+ selectedCommand?: string;
167
+ readiness?: SeedCommandResult["readiness"];
168
+ ran: boolean;
169
+ ok?: boolean;
170
+ responseStatus?: number;
171
+ tenantRuns?: Array<{
172
+ tenantId: string;
173
+ label?: string;
174
+ organizationName?: string;
175
+ ok: boolean;
176
+ responseStatus?: number;
177
+ }>;
178
+ error?: string;
179
+ nextActions: string[];
180
+ };
158
181
  next: {
159
182
  browserUrl: string;
160
183
  apiIndex: string;
161
184
  inspect: string;
162
185
  verify: string;
186
+ changed: string;
163
187
  };
164
188
  pid: number;
165
189
  }
@@ -283,6 +307,9 @@ function buildDetachedDevArgs(options: DevCommandOptions): string[] {
283
307
  if (options.webPort !== undefined) args.push("--web-port", String(options.webPort));
284
308
  if (options.telemetry.length > 0) args.push("--telemetry", options.telemetry.join(","));
285
309
  if (options.envFile) args.push("--env-file", options.envFile);
310
+ if (options.seed) args.push("--seed");
311
+ if (options.seedCommand) args.push("--seed-command", options.seedCommand);
312
+ if (options.seedAllTenants) args.push("--all-tenants");
286
313
  return args;
287
314
  }
288
315
 
@@ -486,6 +513,27 @@ async function isPortAvailable(host: string, port: number): Promise<boolean> {
486
513
  });
487
514
  }
488
515
 
516
+ async function reserveEphemeralPort(host: string): Promise<number> {
517
+ const server = createNetServer();
518
+ return new Promise<number>((resolve, reject) => {
519
+ const cleanup = () => {
520
+ server.removeAllListeners();
521
+ };
522
+ server.once("error", (error) => {
523
+ cleanup();
524
+ reject(error);
525
+ });
526
+ server.listen(0, host, () => {
527
+ const address = server.address();
528
+ const port = typeof address === "object" && address !== null ? address.port : 0;
529
+ server.close(() => {
530
+ cleanup();
531
+ resolve(port);
532
+ });
533
+ });
534
+ });
535
+ }
536
+
489
537
  function classifyDevStartFailure(input: {
490
538
  rawMessage: string;
491
539
  host: string;
@@ -602,7 +650,11 @@ export async function resolveAvailableWebPort(input: {
602
650
  const attempts = input.maxAttempts ?? 20;
603
651
  const start = Math.max(0, Math.floor(input.preferredPort));
604
652
  if (start === 0) {
605
- return { port: 0, autoPortSelected: false };
653
+ return {
654
+ port: await reserveEphemeralPort(input.host),
655
+ requestedPort: 0,
656
+ autoPortSelected: true,
657
+ };
606
658
  }
607
659
 
608
660
  for (let offset = 0; offset < attempts; offset += 1) {
@@ -787,12 +839,27 @@ function startWebDevServer(input: {
787
839
  };
788
840
  }
789
841
 
790
- function buildStartupSummary(input: {
842
+ function seedReadinessForWorkspace(
843
+ workspaceRoot: string,
844
+ readiness: SeedCommandResult["readiness"],
845
+ ): SeedCommandResult["readiness"] {
846
+ return {
847
+ ...readiness,
848
+ emptyWorkspaceRecovery: forgeCliCommandsForWorkspace(
849
+ workspaceRoot,
850
+ readiness.emptyWorkspaceRecovery,
851
+ ),
852
+ };
853
+ }
854
+
855
+ export function buildStartupSummary(input: {
791
856
  workspaceRoot: string;
792
857
  handle: DevServerHandle;
793
858
  web?: WebDevServerHandle | null;
794
859
  watch: boolean;
795
860
  generated?: DevStartupGeneratedEvidence;
861
+ seed?: SeedCommandResult;
862
+ seedRequested?: boolean;
796
863
  }): DevStartupSummary {
797
864
  const frontend = readGeneratedJson<FrontendGraph>(
798
865
  input.workspaceRoot,
@@ -819,6 +886,22 @@ function buildStartupSummary(input: {
819
886
  },
820
887
  ]
821
888
  : [];
889
+ if (input.seed && input.seed.subcommand !== "status" && input.seed.ok === false) {
890
+ warnings.push({
891
+ code: input.seed.diagnostics[0]?.code ?? "FORGE_SEED_FAILED",
892
+ message: input.seed.diagnostics[0]?.message ?? "Seed command failed.",
893
+ nextAction: forgeCliCommandForWorkspace(input.workspaceRoot, "forge seed status --json"),
894
+ });
895
+ }
896
+ for (const diagnostic of input.seed?.diagnostics.filter((item) => item.severity === "warning") ?? []) {
897
+ warnings.push({
898
+ code: diagnostic.code,
899
+ message: diagnostic.message,
900
+ nextAction: diagnostic.suggestedCommands?.[0]
901
+ ? forgeCliCommandForWorkspace(input.workspaceRoot, diagnostic.suggestedCommands[0])
902
+ : forgeCliCommandForWorkspace(input.workspaceRoot, "forge seed status --json"),
903
+ });
904
+ }
822
905
  const preview = previewSummaryFor({
823
906
  workspaceRoot: input.workspaceRoot,
824
907
  host: input.handle.host,
@@ -884,11 +967,34 @@ function buildStartupSummary(input: {
884
967
  checkCommand: input.generated?.checkCommand ?? forgeCliCommandForWorkspace(input.workspaceRoot, "forge generate --check --json"),
885
968
  message: input.generated?.message ?? (buildInfoRaw ? "generated artifacts are loaded" : "generated build info is missing"),
886
969
  },
970
+ seed: {
971
+ requested: input.seedRequested === true,
972
+ available: (input.seed?.commands.length ?? 0) > 0,
973
+ commands: input.seed?.commands.map((command) => command.name) ?? [],
974
+ ...(input.seed?.selectedCommand ? { selectedCommand: input.seed.selectedCommand } : {}),
975
+ ...(input.seed?.readiness ? { readiness: seedReadinessForWorkspace(input.workspaceRoot, input.seed.readiness) } : {}),
976
+ ran: input.seedRequested === true && input.seed?.subcommand !== "status",
977
+ ...(input.seed && input.seed.subcommand !== "status" ? { ok: input.seed.ok } : {}),
978
+ ...(input.seed?.response ? { responseStatus: input.seed.response.status } : {}),
979
+ ...(input.seed?.tenantRuns ? {
980
+ tenantRuns: input.seed.tenantRuns.map((run) => ({
981
+ tenantId: run.tenantId,
982
+ ...(run.label ? { label: run.label } : {}),
983
+ ...(run.organizationName ? { organizationName: run.organizationName } : {}),
984
+ ok: run.ok,
985
+ ...(run.response ? { responseStatus: run.response.status } : {}),
986
+ })),
987
+ } : {}),
988
+ ...(!input.seed?.ok && input.seed?.diagnostics[0]?.message ? { error: input.seed.diagnostics[0].message } : {}),
989
+ nextActions: input.seed?.nextActions.map((action) => forgeCliCommandForWorkspace(input.workspaceRoot, action)) ??
990
+ [forgeCliCommandForWorkspace(input.workspaceRoot, "forge seed status --json")],
991
+ },
887
992
  next: {
888
993
  browserUrl,
889
994
  apiIndex: input.handle.url,
890
995
  inspect: forgeCliCommandForWorkspace(input.workspaceRoot, "forge inspect summary --json"),
891
996
  verify: forgeCliCommandForWorkspace(input.workspaceRoot, "forge dev --once --json"),
997
+ changed: forgeCliCommandForWorkspace(input.workspaceRoot, "forge changed --json"),
892
998
  },
893
999
  pid: process.pid,
894
1000
  };
@@ -898,7 +1004,7 @@ function printStartupJson(summary: DevStartupSummary): void {
898
1004
  process.stdout.write(`${JSON.stringify(summary)}\n`);
899
1005
  }
900
1006
 
901
- function printStartupHuman(summary: DevStartupSummary): void {
1007
+ export function formatStartupHuman(summary: DevStartupSummary): string {
902
1008
  const lines = ["Forge Dev", ""];
903
1009
  lines.push("API runtime");
904
1010
  lines.push(` URL: ${summary.api.url}`);
@@ -909,6 +1015,39 @@ function printStartupHuman(summary: DevStartupSummary): void {
909
1015
  lines.push(` Generated: ${summary.generated.state}${summary.generated.changedFiles > 0 ? ` (${summary.generated.changedFiles} changed)` : ""}${summary.generated.runtimeStaleRisk ? " (stale risk)" : ""}`);
910
1016
  lines.push(` Generated note: ${summary.generated.message}`);
911
1017
  lines.push(` Generated check: ${summary.generated.checkCommand}`);
1018
+ if (summary.seed.available || summary.seed.requested) {
1019
+ lines.push(` Seed: ${summary.seed.available ? summary.seed.commands.join(", ") : "none detected"}`);
1020
+ if (summary.seed.readiness) {
1021
+ const autoSeedNote = summary.seed.readiness.autoSeedMode !== "none"
1022
+ ? summary.seed.readiness.autoSeedMode === "all-tenants"
1023
+ ? " (npm run dev auto-seeds all local tenants)"
1024
+ : " (npm run dev auto-seeds)"
1025
+ : "";
1026
+ lines.push(` Seed readiness: ${summary.seed.readiness.ready ? "ready" : summary.seed.readiness.reason}${autoSeedNote}`);
1027
+ if (summary.seed.readiness.emptyWorkspaceRecovery.length > 0) {
1028
+ lines.push(" Seed recovery:");
1029
+ for (const action of summary.seed.readiness.emptyWorkspaceRecovery) {
1030
+ lines.push(` - ${action}`);
1031
+ }
1032
+ }
1033
+ }
1034
+ if (summary.seed.requested) {
1035
+ lines.push(` Seed run: ${summary.seed.ok ? "ok" : "failed"}${summary.seed.selectedCommand ? ` (${summary.seed.selectedCommand})` : ""}${summary.seed.responseStatus ? ` HTTP ${summary.seed.responseStatus}` : ""}`);
1036
+ if (summary.seed.tenantRuns && summary.seed.tenantRuns.length > 0) {
1037
+ const okCount = summary.seed.tenantRuns.filter((run) => run.ok).length;
1038
+ lines.push(` Seed tenants: ${okCount}/${summary.seed.tenantRuns.length} ok`);
1039
+ for (const run of summary.seed.tenantRuns) {
1040
+ const label = run.organizationName ?? run.label ?? run.tenantId;
1041
+ lines.push(` - ${run.ok ? "ok" : "failed"} ${label} (${run.tenantId})${run.responseStatus ? ` HTTP ${run.responseStatus}` : ""}`);
1042
+ }
1043
+ }
1044
+ if (summary.seed.error) {
1045
+ lines.push(` Seed error: ${summary.seed.error}`);
1046
+ }
1047
+ } else if (summary.seed.available) {
1048
+ lines.push(` Seed next: ${summary.seed.nextActions[0] ?? "forge seed dev --json"}`);
1049
+ }
1050
+ }
912
1051
  for (const warning of summary.warnings) {
913
1052
  lines.push(` Warning ${warning.code}: ${warning.message}`);
914
1053
  if (warning.nextAction) {
@@ -920,8 +1059,9 @@ function printStartupHuman(summary: DevStartupSummary): void {
920
1059
  lines.push("Web app");
921
1060
  if (summary.web) {
922
1061
  lines.push(` URL: ${summary.web.url}`);
923
- if (summary.web.autoPortSelected && summary.web.requestedPort) {
924
- lines.push(` Requested port: ${summary.web.requestedPort} (busy; selected ${summary.web.port})`);
1062
+ if (summary.web.autoPortSelected && summary.web.requestedPort !== undefined) {
1063
+ const reason = summary.web.requestedPort === 0 ? "ephemeral" : "busy";
1064
+ lines.push(` Requested port: ${summary.web.requestedPort} (${reason}; selected ${summary.web.port})`);
925
1065
  }
926
1066
  lines.push(` Framework: ${summary.web.framework}`);
927
1067
  lines.push(` API env: ${summary.web.apiUrlEnv ?? "unknown"}=${summary.api.url}`);
@@ -948,7 +1088,7 @@ function printStartupHuman(summary: DevStartupSummary): void {
948
1088
  lines.push("Agent checks");
949
1089
  lines.push(` ${summary.next.verify}`);
950
1090
  lines.push(` ${summary.next.inspect}`);
951
- lines.push(" forge changed --json");
1091
+ lines.push(` ${summary.next.changed}`);
952
1092
  lines.push("");
953
1093
 
954
1094
  lines.push("Preview");
@@ -959,7 +1099,11 @@ function printStartupHuman(summary: DevStartupSummary): void {
959
1099
  lines.push(` Note: ${summary.preview.note}`);
960
1100
  lines.push("");
961
1101
 
962
- process.stdout.write(`${lines.join("\n")}\n`);
1102
+ return `${lines.join("\n")}\n`;
1103
+ }
1104
+
1105
+ function printStartupHuman(summary: DevStartupSummary): void {
1106
+ process.stdout.write(formatStartupHuman(summary));
963
1107
  }
964
1108
 
965
1109
  function openBrowser(url: string): void {
@@ -1311,12 +1455,34 @@ export async function runDevCommand(
1311
1455
  json: options.json,
1312
1456
  });
1313
1457
 
1458
+ const seedStatus = await runSeedCommand({
1459
+ subcommand: "status",
1460
+ command: options.seedCommand,
1461
+ args: {},
1462
+ url: handle.url,
1463
+ json: true,
1464
+ workspaceRoot,
1465
+ });
1466
+ const seedResult = options.seed
1467
+ ? await runSeedCommand({
1468
+ subcommand: "dev",
1469
+ command: options.seedCommand ?? seedStatus.selectedCommand,
1470
+ args: {},
1471
+ url: handle.url,
1472
+ allTenants: options.seedAllTenants,
1473
+ json: true,
1474
+ workspaceRoot,
1475
+ })
1476
+ : seedStatus;
1477
+
1314
1478
  const startupSummary = buildStartupSummary({
1315
1479
  workspaceRoot,
1316
1480
  handle,
1317
1481
  web: webHandle,
1318
1482
  watch: options.watch,
1319
1483
  generated: startupGenerated,
1484
+ seed: seedResult,
1485
+ seedRequested: options.seed,
1320
1486
  });
1321
1487
  if (options.json) {
1322
1488
  printStartupJson(startupSummary);