forgeos 0.1.0-alpha.30 → 0.1.0-alpha.31

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 CHANGED
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.30 input=5d12d4ad2a959d692b306eb18ba706c52f22f2d148c6e4ed541bd67fd8222c84 content=0d493cf0e41b71cb652d5e0e1b0c1f83d2a1281b748321f0b00f0773ba93074e
1
+ // @forge-generated generator=0.1.0-alpha.31 input=ffdf0fea34e4880fa5215aaef174878f8b3e824def735dec6040c8a7502c4ae4 content=0d493cf0e41b71cb652d5e0e1b0c1f83d2a1281b748321f0b00f0773ba93074e
2
2
  # AGENTS.md
3
3
 
4
4
  <!-- forge-generated:start -->
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # forgeos
2
2
 
3
+ ## 0.1.0-alpha.31
4
+
5
+ ### Patch Changes
6
+
7
+ - Smooth the ForgeOS field-demo DX after the alpha.30 app-server and WorkOS exercises.
8
+
9
+ - Add `forge baseline create` and `forge baseline status` so non-git template workspaces can establish a local baseline and get useful `forge changed` / `forge handoff` diffs instead of noisy filesystem inventories.
10
+ - Add `forge auth status` and production-focused `forge auth prove --prod` output so `dev-headers` is clearly labeled as local-only while JWT/OIDC proofs show production auth posture.
11
+ - Expand `forge doctor windows` with local PGlite store posture and cleanup guidance, including safer PGlite abort inspection that does not poison the surrounding process exit code.
12
+ - Add `forge ui audit` and run it during `forge verify --smoke` when a web app is present, catching missing UI scenarios, missing stable Forge test IDs, and missing policy-denied coverage for sensitive routes.
13
+
3
14
  ## 0.1.0-alpha.30
4
15
 
5
16
  ### Patch Changes
package/docs/changelog.md CHANGED
@@ -6,6 +6,22 @@ The canonical source file in the repository is `CHANGELOG.md`.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.1.0-alpha.31
10
+
11
+ - Added Forge workspace baselines for non-git app directories:
12
+ `forge baseline create` records a local baseline and `forge changed` /
13
+ `forge handoff` can now report baseline diffs instead of noisy full
14
+ filesystem inventories.
15
+ - Added `forge auth status` and production-aware `forge auth prove --prod`
16
+ posture checks so local `dev-headers` auth is clearly distinguished from
17
+ JWT/OIDC production authentication.
18
+ - Expanded `forge doctor windows` with local PGlite store posture and cleanup
19
+ guidance, and made PGlite abort inspection preserve the surrounding process
20
+ exit code.
21
+ - Added `forge ui audit` and wired it into `forge verify --smoke` for web apps
22
+ to catch missing route scenarios, missing stable Forge test IDs, and missing
23
+ policy-denied coverage for sensitive flows.
24
+
9
25
  ## 0.1.0-alpha.30
10
26
 
11
27
  - Hardened the WorkOS/AuthKit adapter and dev telemetry after the alpha.29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgeos",
3
- "version": "0.1.0-alpha.30",
3
+ "version": "0.1.0-alpha.31",
4
4
  "description": "Agent-native application framework and compiler for building Forge apps without a mandatory dashboard.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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.30","releaseId":"forgeos@0.1.0-alpha.30+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.31","releaseId":"forgeos@0.1.0-alpha.31+unknown","schemaVersion":"0.1.0"}
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.30 input=5d12d4ad2a959d692b306eb18ba706c52f22f2d148c6e4ed541bd67fd8222c84 content=065153708d210de0b5893a6e8c8ecbfc8bab3b011c8f1e576ae54d9c59bff838
1
+ // @forge-generated generator=0.1.0-alpha.31 input=ffdf0fea34e4880fa5215aaef174878f8b3e824def735dec6040c8a7502c4ae4 content=806e4e7f20264628107bf964331f13335175f065d6a8826b5defc2027d8047cd
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.30",
23
- "releaseId": "forgeos@0.1.0-alpha.30+unknown",
22
+ "packageVersion": "0.1.0-alpha.31",
23
+ "releaseId": "forgeos@0.1.0-alpha.31+unknown",
24
24
  "schemaVersion": "0.1.0"
25
25
  } as const;
@@ -10,13 +10,14 @@ import { ForgeAuthError } from "../runtime/auth/errors.ts";
10
10
  import { verifyJwtToken } from "../runtime/auth/verifier.ts";
11
11
  import { loadSecretRegistry } from "../runtime/secrets/check.ts";
12
12
 
13
- export type AuthSubcommand = "check" | "config" | "decode" | "test-token" | "jwks" | "prove";
13
+ export type AuthSubcommand = "check" | "config" | "decode" | "test-token" | "jwks" | "prove" | "status";
14
14
 
15
15
  export interface AuthCommandOptions {
16
16
  subcommand: AuthSubcommand;
17
17
  workspaceRoot: string;
18
18
  json: boolean;
19
19
  token?: string;
20
+ prod?: boolean;
20
21
  }
21
22
 
22
23
  export interface AuthCommandResult {
@@ -59,11 +60,8 @@ function detectWorkOS(workspaceRoot: string, claims: AuthClaimsMapping) {
59
60
  };
60
61
  }
61
62
 
62
- function validateConfig(workspaceRoot: string): AuthCommandResult {
63
- const config = loadAuthConfigFromEnv(workspaceRoot);
63
+ function configErrors(config: ReturnType<typeof loadAuthConfigFromEnv>): { code: string; message: string }[] {
64
64
  const errors: { code: string; message: string }[] = [];
65
- const workos = detectWorkOS(workspaceRoot, config.claims);
66
-
67
65
  if ((config.mode === "jwt" || config.mode === "oidc") && !config.issuer) {
68
66
  errors.push({
69
67
  code: FORGE_AUTH_INVALID_ISSUER,
@@ -82,6 +80,39 @@ function validateConfig(workspaceRoot: string): AuthCommandResult {
82
80
  message: "FORGE_AUTH_JWKS_URI is required for jwt auth",
83
81
  });
84
82
  }
83
+ return errors;
84
+ }
85
+
86
+ function buildAuthPosture(workspaceRoot: string) {
87
+ const config = loadAuthConfigFromEnv(workspaceRoot);
88
+ const productionMode = config.mode === "jwt" || config.mode === "oidc";
89
+ const errors = configErrors(config);
90
+ const configReady = errors.length === 0;
91
+ return {
92
+ schemaVersion: "0.1.0",
93
+ mode: config.mode,
94
+ localOnly: config.mode === "dev-headers",
95
+ productionReady: productionMode && configReady,
96
+ requiresTenant: config.requiresTenant,
97
+ bearerHeader: productionMode ? "Authorization: Bearer <token>" : null,
98
+ tenantClaim: config.claims.tenantId ?? "tenant_id",
99
+ reason: productionMode
100
+ ? configReady
101
+ ? "jwt/oidc production auth configuration is present"
102
+ : "jwt/oidc production auth is selected but required settings are missing"
103
+ : "dev-headers auth is local-only and is not real production authentication",
104
+ nextActions: productionMode
105
+ ? configReady
106
+ ? ["forge auth prove --prod --token <jwt> --json", "forge serve --json"]
107
+ : ["forge auth check --json", "configure FORGE_AUTH_ISSUER, FORGE_AUTH_AUDIENCE, and FORGE_AUTH_JWKS_URI or OIDC issuer"]
108
+ : ["set FORGE_AUTH_MODE=jwt or oidc for production", "forge auth prove --prod --token <jwt> --json"],
109
+ };
110
+ }
111
+
112
+ function validateConfig(workspaceRoot: string): AuthCommandResult {
113
+ const config = loadAuthConfigFromEnv(workspaceRoot);
114
+ const errors = configErrors(config);
115
+ const workos = detectWorkOS(workspaceRoot, config.claims);
85
116
 
86
117
  return {
87
118
  ok: errors.length === 0,
@@ -94,6 +125,7 @@ function validateConfig(workspaceRoot: string): AuthCommandResult {
94
125
  algorithms: config.algorithms,
95
126
  claims: config.claims,
96
127
  requiresTenant: config.requiresTenant,
128
+ authPosture: buildAuthPosture(workspaceRoot),
97
129
  workos,
98
130
  errors,
99
131
  },
@@ -116,6 +148,7 @@ function publicConfig(workspaceRoot: string): AuthCommandResult {
116
148
  algorithms: config.algorithms,
117
149
  claims: config.claims,
118
150
  requiresTenant: config.requiresTenant,
151
+ authPosture: buildAuthPosture(workspaceRoot),
119
152
  workos,
120
153
  },
121
154
  exitCode: 0,
@@ -197,6 +230,15 @@ async function testToken(
197
230
  export async function runAuthCommand(
198
231
  options: AuthCommandOptions,
199
232
  ): Promise<AuthCommandResult> {
233
+ if (options.subcommand === "status") {
234
+ const posture = buildAuthPosture(options.workspaceRoot);
235
+ return {
236
+ ok: true,
237
+ mode: posture.mode,
238
+ data: posture,
239
+ exitCode: 0,
240
+ };
241
+ }
200
242
  if (options.subcommand === "check") {
201
243
  return validateConfig(options.workspaceRoot);
202
244
  }
@@ -206,15 +248,28 @@ export async function runAuthCommand(
206
248
  const workos = detectWorkOS(options.workspaceRoot, config.claims);
207
249
  const productionMode = config.mode === "jwt" || config.mode === "oidc";
208
250
  const workosClaimsOk = workos.claimStatus.every((claim) => claim.ok);
251
+ const posture = buildAuthPosture(options.workspaceRoot);
252
+ const tokenProof = options.token ? await testToken(options.workspaceRoot, options.token) : null;
253
+ const prodError = options.prod && !productionMode
254
+ ? { code: "FORGE_AUTH_MODE_INVALID", message: "forge auth prove --prod requires FORGE_AUTH_MODE=jwt or oidc" }
255
+ : options.prod && !options.token
256
+ ? { code: "FORGE_AUTH_MISSING_TOKEN", message: "forge auth prove --prod requires --token" }
257
+ : options.prod && tokenProof && !tokenProof.ok
258
+ ? tokenProof.error
259
+ : undefined;
260
+ const proofOk = checked.ok && (!options.prod || (productionMode && tokenProof?.ok === true));
209
261
  return {
210
- ok: checked.ok,
262
+ ok: proofOk,
211
263
  mode: config.mode,
212
264
  data: {
213
265
  schemaVersion: "0.1.0",
214
266
  kind: "auth-proof",
215
- ok: checked.ok,
267
+ ok: proofOk,
216
268
  mode: config.mode,
217
269
  productionReady: productionMode && checked.ok,
270
+ prod: options.prod === true,
271
+ authPosture: posture,
272
+ ...(tokenProof ? { tokenProof: tokenProof.ok ? tokenProof.data : tokenProof.error } : {}),
218
273
  invariants: [
219
274
  {
220
275
  id: "INV-001",
@@ -249,8 +304,8 @@ export async function runAuthCommand(
249
304
  workos,
250
305
  checkedAt: "deterministic",
251
306
  },
252
- error: checked.error,
253
- exitCode: checked.exitCode,
307
+ error: prodError ?? checked.error,
308
+ exitCode: proofOk ? 0 : 1,
254
309
  };
255
310
  }
256
311
  if (options.subcommand === "config") {
@@ -0,0 +1,112 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createWorkspaceBaseline, readWorkspaceBaseline, writeWorkspaceBaseline, WORKSPACE_BASELINE_PATH } from "../workspace/baseline.ts";
3
+ import { listWorkspaceFiles } from "../workspace/git-summary.ts";
4
+
5
+ export type BaselineSubcommand = "create" | "status";
6
+
7
+ export interface BaselineCommandOptions {
8
+ subcommand: BaselineSubcommand;
9
+ workspaceRoot: string;
10
+ json: boolean;
11
+ reason?: string;
12
+ }
13
+
14
+ export interface BaselineCommandResult {
15
+ ok: boolean;
16
+ path: string;
17
+ required: boolean;
18
+ created?: boolean;
19
+ baseline?: ReturnType<typeof readWorkspaceBaseline>;
20
+ summary: {
21
+ files: number;
22
+ reason?: string;
23
+ tracking: "git" | "forge-baseline" | "missing";
24
+ };
25
+ nextActions: string[];
26
+ exitCode: 0 | 1;
27
+ }
28
+
29
+ function gitAvailable(workspaceRoot: string): boolean {
30
+ const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
31
+ cwd: workspaceRoot,
32
+ encoding: "utf8",
33
+ windowsHide: true,
34
+ });
35
+ return result.status === 0;
36
+ }
37
+
38
+ export function runBaselineCommand(options: BaselineCommandOptions): BaselineCommandResult {
39
+ if (options.subcommand === "create") {
40
+ const files = listWorkspaceFiles(options.workspaceRoot);
41
+ const baseline = createWorkspaceBaseline({
42
+ workspaceRoot: options.workspaceRoot,
43
+ files,
44
+ reason: options.reason,
45
+ });
46
+ writeWorkspaceBaseline(options.workspaceRoot, baseline);
47
+ return {
48
+ ok: true,
49
+ path: WORKSPACE_BASELINE_PATH,
50
+ required: false,
51
+ created: true,
52
+ baseline,
53
+ summary: {
54
+ files: Object.keys(baseline.files).length,
55
+ tracking: "forge-baseline",
56
+ ...(baseline.reason ? { reason: baseline.reason } : {}),
57
+ },
58
+ nextActions: ["forge changed --json", "forge handoff --json"],
59
+ exitCode: 0,
60
+ };
61
+ }
62
+
63
+ const baseline = readWorkspaceBaseline(options.workspaceRoot);
64
+ if (!baseline && gitAvailable(options.workspaceRoot)) {
65
+ return {
66
+ ok: true,
67
+ path: WORKSPACE_BASELINE_PATH,
68
+ required: false,
69
+ baseline: null,
70
+ summary: {
71
+ files: 0,
72
+ tracking: "git",
73
+ },
74
+ nextActions: ["forge changed --json", "git status --short"],
75
+ exitCode: 0,
76
+ };
77
+ }
78
+ return {
79
+ ok: baseline !== null,
80
+ path: WORKSPACE_BASELINE_PATH,
81
+ required: baseline === null,
82
+ baseline,
83
+ summary: {
84
+ files: baseline ? Object.keys(baseline.files).length : 0,
85
+ tracking: baseline ? "forge-baseline" : "missing",
86
+ ...(baseline?.reason ? { reason: baseline.reason } : {}),
87
+ },
88
+ nextActions: baseline
89
+ ? ["forge changed --json", "forge baseline create --reason refresh --json"]
90
+ : ["forge baseline create --reason initial-scaffold --json", "git init"],
91
+ exitCode: baseline ? 0 : 1,
92
+ };
93
+ }
94
+
95
+ export function formatBaselineJson(result: BaselineCommandResult): string {
96
+ return `${JSON.stringify(result, null, 2)}\n`;
97
+ }
98
+
99
+ export function formatBaselineHuman(result: BaselineCommandResult): string {
100
+ const label = result.required ? "missing" : result.summary.tracking === "git" ? "not required (git workspace)" : "ready";
101
+ const lines = [
102
+ `Forge baseline: ${label}`,
103
+ `Path: ${result.path}`,
104
+ `Tracking: ${result.summary.tracking}`,
105
+ `Files: ${result.summary.files}`,
106
+ ];
107
+ if (result.summary.reason) {
108
+ lines.push(`Reason: ${result.summary.reason}`);
109
+ }
110
+ lines.push("", "Next:", ...result.nextActions.map((action) => ` ${action}`));
111
+ return `${lines.join("\n")}\n`;
112
+ }
@@ -87,10 +87,12 @@ function selectDerivedChangeSummary(summary: CategorizedFileSummary): DerivedCha
87
87
  function buildRisks(git: WorkspaceGitSummary): string[] {
88
88
  const risks: string[] = [];
89
89
  const changed = git.changeSummary.changed;
90
- if (!git.available) {
90
+ if (!git.available && git.source === "forge-baseline") {
91
+ risks.push("git status is unavailable; using Forge workspace baseline for non-git change tracking");
92
+ } else if (!git.available) {
91
93
  risks.push("git status is unavailable; using filesystem inventory as untracked-file analysis");
92
94
  }
93
- if (git.untracked.count > 0) {
95
+ if (git.untracked.count > 0 && git.source !== "forge-baseline") {
94
96
  risks.push(`${git.untracked.count} untracked file(s) are not in git history`);
95
97
  }
96
98
  if (changed.byType.other.count > 0) {
@@ -107,8 +109,11 @@ function buildRisks(git: WorkspaceGitSummary): string[] {
107
109
  }
108
110
 
109
111
  function buildRecommendedCommands(git: WorkspaceGitSummary): string[] {
112
+ if (!git.available && git.source === "forge-baseline") {
113
+ return ["forge handoff --json", "forge test plan --changed --json", "forge verify --changed", "git init"];
114
+ }
110
115
  if (!git.available) {
111
- return ["forge status --json", "forge handoff --json", "git init"];
116
+ return ["forge baseline create --reason initial-scaffold --json", "forge status --json", "forge handoff --json", "git init"];
112
117
  }
113
118
  if (git.changeSummary.changed.total.count === 0) {
114
119
  return ["forge status --json", "forge dev --once --json"];
@@ -206,7 +211,7 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
206
211
  const reviewFocus = buildReviewFocus(viewHumanChanges, viewDerivedChanges);
207
212
  const generatedExplanation = buildGeneratedChangeExplanation(viewHumanChanges, viewDerivedChanges);
208
213
  const diffPlan: DiffPlan = buildDiffPlanFromChangeSummary(viewChanged);
209
- const ok = git.available || git.source === "filesystem";
214
+ const ok = git.available || git.source === "filesystem" || git.source === "forge-baseline";
210
215
 
211
216
  return {
212
217
  ok,
@@ -216,6 +221,8 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
216
221
  summary: {
217
222
  branch: git.branch,
218
223
  commit: git.commit,
224
+ workspaceMode: git.workspaceMode ?? (git.available ? "git" : "nonGit"),
225
+ tracking: git.tracking ?? git.source,
219
226
  view: options.authoredOnly ? "authored" : "all",
220
227
  changedFiles: viewChanged.total.count,
221
228
  humanFiles: viewHumanChanges.total,
@@ -229,6 +236,9 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
229
236
  git: {
230
237
  available: git.available,
231
238
  source: git.source,
239
+ workspaceMode: git.workspaceMode,
240
+ tracking: git.tracking,
241
+ baseline: git.baseline,
232
242
  ...(git.error ? { error: git.error } : {}),
233
243
  branch: git.branch,
234
244
  commit: git.commit,
@@ -181,6 +181,9 @@ import {
181
181
  import {
182
182
  formatDoctorHuman,
183
183
  formatDoctorJson,
184
+ formatPgliteDoctorHuman,
185
+ formatPgliteDoctorJson,
186
+ runPgliteDoctorCommand,
184
187
  runDoctorCommand,
185
188
  } from "./doctor.ts";
186
189
  import {
@@ -202,6 +205,16 @@ import {
202
205
  formatWorkOSJson,
203
206
  runWorkOSCommand,
204
207
  } from "./workos.ts";
208
+ import {
209
+ formatLastHuman,
210
+ formatLastJson,
211
+ runLastCommand,
212
+ } from "./last-run.ts";
213
+ import {
214
+ formatBaselineHuman,
215
+ formatBaselineJson,
216
+ runBaselineCommand,
217
+ } from "./baseline.ts";
205
218
  import { formatRlsHuman, formatRlsJson, runRlsCommand } from "./rls.ts";
206
219
  import {
207
220
  formatSecurityHuman,
@@ -272,6 +285,7 @@ import { getActiveDbAdapter } from "../runtime/executor.ts";
272
285
  import { CLI_VERSION, FORGEOS_VERSION } from "../version.ts";
273
286
  import type { CategorizedFileSummary } from "../workspace/change-summary.ts";
274
287
  import { buildWorkspaceGitSummary } from "../workspace/git-summary.ts";
288
+ import { startCommandHeartbeat } from "./progress.ts";
275
289
 
276
290
  function readGeneratedJson<T>(workspaceRoot: string, relative: string): T | null {
277
291
  const absolute = join(workspaceRoot, relative);
@@ -1558,6 +1572,16 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
1558
1572
  }
1559
1573
  return 0;
1560
1574
  }
1575
+ case "last": {
1576
+ const result = runLastCommand({ workspaceRoot: command.workspaceRoot });
1577
+ process.stdout.write(command.json ? formatLastJson(result) : formatLastHuman(result));
1578
+ return result.exitCode;
1579
+ }
1580
+ case "baseline": {
1581
+ const result = runBaselineCommand(command);
1582
+ process.stdout.write(command.json ? formatBaselineJson(result) : formatBaselineHuman(result));
1583
+ return result.exitCode;
1584
+ }
1561
1585
  case "new": {
1562
1586
  const result = await runNewCommand({
1563
1587
  name: command.name,
@@ -1668,6 +1692,11 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
1668
1692
  process.stdout.write(command.json ? formatDeltaDoctorJson(result) : formatDeltaDoctorHuman(result));
1669
1693
  return result.exitCode;
1670
1694
  }
1695
+ if (command.target === "pglite") {
1696
+ const result = await runPgliteDoctorCommand({ workspaceRoot: command.workspaceRoot });
1697
+ process.stdout.write(command.json ? formatPgliteDoctorJson(result) : formatPgliteDoctorHuman(result));
1698
+ return result.exitCode;
1699
+ }
1671
1700
  const result = await runDoctorCommand({ workspaceRoot: command.workspaceRoot });
1672
1701
  if (command.json) {
1673
1702
  process.stdout.write(formatDoctorJson(result));
@@ -1930,13 +1959,23 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
1930
1959
  return result.exitCode;
1931
1960
  }
1932
1961
  case "agent": {
1933
- const result = await runAgentCommand(command.options);
1934
- if (command.options.json) {
1935
- process.stdout.write(formatAgentJson(result));
1936
- } else {
1937
- process.stdout.write(formatAgentHuman(result));
1962
+ const heartbeat = command.options.subcommand === "onboard"
1963
+ ? startCommandHeartbeat({
1964
+ label: `agent onboard ${command.options.target ?? "codex"}`,
1965
+ initialPhase: "prepare-hooks-memory-and-dev-snapshot",
1966
+ })
1967
+ : null;
1968
+ try {
1969
+ const result = await runAgentCommand(command.options);
1970
+ if (command.options.json) {
1971
+ process.stdout.write(formatAgentJson(result));
1972
+ } else {
1973
+ process.stdout.write(formatAgentHuman(result));
1974
+ }
1975
+ return result.exitCode;
1976
+ } finally {
1977
+ heartbeat?.stop();
1938
1978
  }
1939
- return result.exitCode;
1940
1979
  }
1941
1980
  case "mcp": {
1942
1981
  return runMcpServe(command.workspaceRoot);
@@ -2286,6 +2325,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
2286
2325
  workspaceRoot: command.workspaceRoot,
2287
2326
  db: command.db,
2288
2327
  databaseUrl: command.databaseUrl,
2328
+ local: command.local,
2289
2329
  json: command.json,
2290
2330
  });
2291
2331
 
@@ -7,6 +7,9 @@ import { buildAppGraph } from "../compiler/app-graph/build.ts";
7
7
  import { createDiagnostic } from "../compiler/diagnostics/create.ts";
8
8
  import {
9
9
  FORGE_RLS_APPLY_FAILED,
10
+ FORGE_DB_ADAPTER_UNAVAILABLE,
11
+ FORGE_PGLITE_STORE_ABORTED,
12
+ FORGE_PGLITE_STORE_ACTIVE,
10
13
  FORGE_RUNTIME_NOT_FOUND,
11
14
  } from "../compiler/diagnostics/codes.ts";
12
15
  import { GENERATED_DIR } from "../compiler/emitter/constants.ts";
@@ -23,14 +26,19 @@ import {
23
26
  resetDatabase,
24
27
  } from "../runtime/db/migrate.ts";
25
28
  import type { DbAdapterKind } from "../runtime/db/adapter.ts";
29
+ import {
30
+ DEFAULT_PGLITE_DIR,
31
+ repairLocalPgliteStore,
32
+ } from "../runtime/db/pglite-adapter.ts";
26
33
 
27
- export type DbSubcommand = "diff" | "migrate" | "reset" | "status" | "doctor" | "rls-check";
34
+ export type DbSubcommand = "diff" | "migrate" | "reset" | "status" | "doctor" | "repair" | "rls-check";
28
35
 
29
36
  export interface DbCommandOptions {
30
37
  subcommand: DbSubcommand;
31
38
  workspaceRoot: string;
32
39
  db: DbAdapterKind;
33
40
  databaseUrl?: string;
41
+ local?: boolean;
34
42
  json: boolean;
35
43
  }
36
44
 
@@ -285,7 +293,86 @@ async function runDbDoctor(
285
293
  }
286
294
  }
287
295
 
296
+ async function runDbRepair(options: DbCommandOptions): Promise<DbCommandResult> {
297
+ if (!options.local) {
298
+ return {
299
+ ok: false,
300
+ data: {
301
+ schemaVersion: "0.1.0",
302
+ repaired: false,
303
+ adapter: options.db,
304
+ local: false,
305
+ nextActions: ["forge db repair --local --adapter pglite --json"],
306
+ },
307
+ diagnostics: [
308
+ createDiagnostic({
309
+ severity: "error",
310
+ code: "FORGE_CLI_USAGE",
311
+ message: "local database repair requires --local",
312
+ fixHint: "Pass --local to confirm you want Forge to repair the local development database store.",
313
+ suggestedCommands: ["forge db repair --local --adapter pglite --json"],
314
+ }),
315
+ ],
316
+ exitCode: 1,
317
+ };
318
+ }
319
+
320
+ if (options.db !== "pglite") {
321
+ return {
322
+ ok: false,
323
+ data: {
324
+ schemaVersion: "0.1.0",
325
+ repaired: false,
326
+ adapter: options.db,
327
+ nextActions: ["forge db repair --local --adapter pglite --json"],
328
+ },
329
+ diagnostics: [
330
+ createDiagnostic({
331
+ severity: "error",
332
+ code: FORGE_DB_ADAPTER_UNAVAILABLE,
333
+ message: "local repair currently supports only the pglite adapter",
334
+ fixHint: "Use --adapter pglite for the local Forge dev database store.",
335
+ suggestedCommands: ["forge db repair --local --adapter pglite --json"],
336
+ }),
337
+ ],
338
+ exitCode: 1,
339
+ };
340
+ }
341
+
342
+ const dataDir = join(options.workspaceRoot, DEFAULT_PGLITE_DIR);
343
+ const result = await repairLocalPgliteStore(dataDir);
344
+ const diagnostics = result.ok
345
+ ? []
346
+ : [
347
+ createDiagnostic({
348
+ severity: "error",
349
+ code: result.before.state === "active" ? FORGE_PGLITE_STORE_ACTIVE : FORGE_PGLITE_STORE_ABORTED,
350
+ message: result.message,
351
+ fixHint: result.before.state === "active"
352
+ ? "Stop the running forge dev process before repairing the PGlite store."
353
+ : "Archive the local PGlite store and retry forge dev, or use --db memory for non-persistent validation.",
354
+ suggestedCommands: result.nextActions,
355
+ }),
356
+ ];
357
+
358
+ return {
359
+ ok: result.ok,
360
+ data: {
361
+ schemaVersion: "0.1.0",
362
+ adapter: "pglite",
363
+ local: true,
364
+ ...result,
365
+ },
366
+ diagnostics,
367
+ exitCode: result.ok ? 0 : 1,
368
+ };
369
+ }
370
+
288
371
  export async function runDbCommand(options: DbCommandOptions): Promise<DbCommandResult> {
372
+ if (options.subcommand === "repair") {
373
+ return runDbRepair(options);
374
+ }
375
+
289
376
  const { plan, diagnostics: planDiagnostics } = await loadSqlPlan(options.workspaceRoot);
290
377
 
291
378
  if (!plan) {
@@ -427,7 +514,7 @@ export function formatDbHuman(subcommand: DbSubcommand, result: DbCommandResult)
427
514
  return "database reset complete\n";
428
515
  }
429
516
 
430
- if (subcommand === "status" || subcommand === "diff" || subcommand === "doctor") {
517
+ if (subcommand === "status" || subcommand === "diff" || subcommand === "doctor" || subcommand === "repair") {
431
518
  return `${JSON.stringify(result.data, null, 2)}\n`;
432
519
  }
433
520