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 +1 -1
- package/CHANGELOG.md +11 -0
- package/docs/changelog.md +16 -0
- package/package.json +1 -1
- package/src/forge/_generated/releaseManifest.json +1 -1
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/cli/auth.ts +64 -9
- package/src/forge/cli/baseline.ts +112 -0
- package/src/forge/cli/changed.ts +14 -4
- package/src/forge/cli/commands.ts +46 -6
- package/src/forge/cli/db.ts +89 -2
- package/src/forge/cli/dev.ts +125 -21
- package/src/forge/cli/doctor.ts +80 -0
- package/src/forge/cli/handoff.ts +13 -2
- package/src/forge/cli/last-run.ts +84 -0
- package/src/forge/cli/parse.ts +59 -9
- package/src/forge/cli/progress.ts +51 -0
- package/src/forge/cli/verify.ts +31 -0
- package/src/forge/cli/windows.ts +22 -0
- package/src/forge/compiler/diagnostics/codes.ts +4 -0
- package/src/forge/runtime/db/factory.ts +1 -3
- package/src/forge/runtime/db/pglite-adapter.ts +188 -2
- package/src/forge/ui/index.ts +45 -0
- package/src/forge/ui/types.ts +1 -0
- package/src/forge/version.ts +1 -1
- package/src/forge/workspace/baseline.ts +112 -0
- package/src/forge/workspace/change-summary.ts +1 -0
- package/src/forge/workspace/git-summary.ts +65 -2
package/AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
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 +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.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.
|
|
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.
|
|
23
|
-
"releaseId": "forgeos@0.1.0-alpha.
|
|
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;
|
package/src/forge/cli/auth.ts
CHANGED
|
@@ -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
|
|
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:
|
|
262
|
+
ok: proofOk,
|
|
211
263
|
mode: config.mode,
|
|
212
264
|
data: {
|
|
213
265
|
schemaVersion: "0.1.0",
|
|
214
266
|
kind: "auth-proof",
|
|
215
|
-
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:
|
|
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
|
+
}
|
package/src/forge/cli/changed.ts
CHANGED
|
@@ -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
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
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
|
|
package/src/forge/cli/db.ts
CHANGED
|
@@ -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
|
|