forgeos 0.1.0-alpha.33 → 0.1.0-alpha.34

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 (36) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +15 -0
  3. package/docs/changelog.md +27 -0
  4. package/package.json +1 -1
  5. package/src/forge/_generated/releaseManifest.json +1 -1
  6. package/src/forge/_generated/releaseManifest.ts +3 -3
  7. package/src/forge/cli/changed.ts +102 -8
  8. package/src/forge/cli/commands.ts +116 -4
  9. package/src/forge/cli/dev.ts +228 -0
  10. package/src/forge/cli/handoff.ts +17 -0
  11. package/src/forge/cli/main.ts +36 -0
  12. package/src/forge/cli/new.ts +4 -0
  13. package/src/forge/cli/parse.ts +24 -6
  14. package/src/forge/cli/workos.ts +250 -23
  15. package/src/forge/compiler/app-graph/build.ts +44 -1
  16. package/src/forge/compiler/client-sdk/render-client.ts +31 -0
  17. package/src/forge/compiler/data-graph/parse.ts +17 -1
  18. package/src/forge/compiler/data-graph/sql/ddl.ts +24 -13
  19. package/src/forge/compiler/diagnostics/codes.ts +6 -0
  20. package/src/forge/compiler/diagnostics/create.ts +14 -0
  21. package/src/forge/compiler/integration/plan.ts +6 -0
  22. package/src/forge/compiler/integration/render.ts +9 -0
  23. package/src/forge/compiler/integration/templates/render.ts +2 -0
  24. package/src/forge/compiler/integration/templates/types.ts +3 -0
  25. package/src/forge/compiler/integration/templates/workos.ts +165 -99
  26. package/src/forge/compiler/orchestrator/plan.ts +7 -0
  27. package/src/forge/delta/status.ts +10 -1
  28. package/src/forge/impact/index.ts +158 -1
  29. package/src/forge/impact/types.ts +27 -1
  30. package/src/forge/react/index.ts +19 -2
  31. package/src/forge/runtime/auth/resolve.ts +33 -3
  32. package/src/forge/runtime/auth/types.ts +1 -0
  33. package/src/forge/server.ts +5 -1
  34. package/src/forge/version.ts +1 -1
  35. package/src/forge/vue/index.ts +16 -1
  36. package/src/forge/workspace/git-summary.ts +1 -1
package/AGENTS.md CHANGED
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.33 input=618472d83d84f1f735eeaaec13559692c94a492c7f592b20882b5ebac7a1c378 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
1
+ // @forge-generated generator=0.1.0-alpha.34 input=ce9cf8b6c553d7de8113eb2a388d269cb8c90d4bb15c013a7ab907ef207619b9 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
2
2
  # AGENTS.md
3
3
 
4
4
  <!-- forge-generated:start -->
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # forgeos
2
2
 
3
+ ## 0.1.0-alpha.34
4
+
5
+ ### Patch Changes
6
+
7
+ - Harden the ForgeOS app-building DX after the Vendor Access field test.
8
+
9
+ - Make `forge workos doctor` and `forge workos seed --dry-run` app-aware by deriving active permissions and resource types from generated policies, data graph, agent contract, authored WorkOS policies, and `workos-seed.yml` instead of assuming onboarding-specific slugs.
10
+ - Add strong diagnostics for unnamed runtime default exports so `export default command(...)` reports `FORGE_RUNTIME_EXPORT_NAME_REQUIRED` with a named-export fix hint before generated API, frontend bindings, or capability maps drift.
11
+ - Extend local `devAuth` for React, Vue, generated clients, and runtime auth parsing with WorkOS-like `permissions`, `roles`, `claims`, `organizationId`, and `organizationMembershipId`.
12
+ - Add nullable timestamp schema support through `timestamp?` / `nullable("timestamp")` and teach `forge check` to flag empty timestamp literals in commands, queries, and liveQueries.
13
+ - Add `forge dev --detach`, `forge dev status`, `forge dev stop`, and command-specific `forge dev --help` with explicit DB/port examples for agent-run app previews.
14
+ - Add `forge changed --commit-ready` and `forge handoff --commit-ready` so agents can stage exactly authored commit files while excluding generated and operational artifacts.
15
+ - Add `forge test authz` for a cheap generated-contract proof of tenant scope, policy bindings, and capability-map authz coverage.
16
+ - Expose static UI/UX readiness through `forge inspect ui --ergonomics`.
17
+
3
18
  ## 0.1.0-alpha.33
4
19
 
5
20
  ### Patch Changes
package/docs/changelog.md CHANGED
@@ -6,6 +6,33 @@ The canonical source file in the repository is `CHANGELOG.md`.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.1.0-alpha.34
10
+
11
+ - Hardened the ForgeOS app-building DX after the Vendor Access field test.
12
+ - Made `forge workos doctor` and `forge workos seed --dry-run` app-aware by
13
+ deriving active permissions and resource types from generated policies, data
14
+ graph, agent contract, authored WorkOS policies, and `workos-seed.yml`
15
+ instead of assuming onboarding-specific slugs.
16
+ - Added strong diagnostics for unnamed runtime default exports so
17
+ `export default command(...)` reports `FORGE_RUNTIME_EXPORT_NAME_REQUIRED`
18
+ with a named-export fix hint before generated API, frontend bindings, or
19
+ capability maps drift.
20
+ - Extended local `devAuth` for React, Vue, generated clients, and runtime auth
21
+ parsing with WorkOS-like `permissions`, `roles`, `claims`, `organizationId`,
22
+ and `organizationMembershipId`.
23
+ - Added nullable timestamp schema support through `timestamp?` /
24
+ `nullable("timestamp")` and taught `forge check` to flag empty timestamp
25
+ literals in commands, queries, and liveQueries.
26
+ - Added `forge dev --detach`, `forge dev status`, `forge dev stop`, and
27
+ command-specific `forge dev --help` with explicit DB/port examples for
28
+ agent-run app previews.
29
+ - Added `forge changed --commit-ready` and `forge handoff --commit-ready` so
30
+ agents can stage exactly authored commit files while excluding generated and
31
+ operational artifacts.
32
+ - Added `forge test authz` for a cheap generated-contract proof of tenant
33
+ scope, policy bindings, and capability-map authz coverage.
34
+ - Exposed static UI/UX readiness through `forge inspect ui --ergonomics`.
35
+
9
36
  ## 0.1.0-alpha.33
10
37
 
11
38
  - Fixed the Nuxt template smoke by adding the explicit `vue-tsc` dev
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgeos",
3
- "version": "0.1.0-alpha.33",
3
+ "version": "0.1.0-alpha.34",
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.33","releaseId":"forgeos@0.1.0-alpha.33+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.34","releaseId":"forgeos@0.1.0-alpha.34+unknown","schemaVersion":"0.1.0"}
@@ -1,4 +1,4 @@
1
- // @forge-generated generator=0.1.0-alpha.33 input=618472d83d84f1f735eeaaec13559692c94a492c7f592b20882b5ebac7a1c378 content=2cfb24f552edc1fade82dc4abca3f6085ae94a0ac9ae70f5b0cffab11c1c3e7c
1
+ // @forge-generated generator=0.1.0-alpha.34 input=ce9cf8b6c553d7de8113eb2a388d269cb8c90d4bb15c013a7ab907ef207619b9 content=1a3f239bc322349796830461349167bb154a27e57fb1f10868c788194b3be97e
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.33",
23
- "releaseId": "forgeos@0.1.0-alpha.33+unknown",
22
+ "packageVersion": "0.1.0-alpha.34",
23
+ "releaseId": "forgeos@0.1.0-alpha.34+unknown",
24
24
  "schemaVersion": "0.1.0"
25
25
  } as const;
@@ -1,11 +1,18 @@
1
+ import { spawnSync } from "node:child_process";
1
2
  import type { CategorizedFileSummary, DiffPlan } from "../workspace/change-summary.ts";
2
3
  import {
3
4
  buildDiffPlanFromChangeSummary,
5
+ categorizeFiles,
4
6
  filterCategorizedSummary,
5
7
  summarizeChangeTypes,
6
8
  } from "../workspace/change-summary.ts";
7
9
  import { forgeCliCommandsForWorkspace } from "../workspace/forge-cli.ts";
8
- import { buildWorkspaceGitSummary, type WorkspaceGitSummary } from "../workspace/git-summary.ts";
10
+ import {
11
+ buildWorkspaceGitSummary,
12
+ listWorkspaceFiles,
13
+ workspaceChangeClassifier,
14
+ type WorkspaceGitSummary,
15
+ } from "../workspace/git-summary.ts";
9
16
 
10
17
  export interface ChangedCommandResult {
11
18
  ok: boolean;
@@ -148,6 +155,89 @@ function buildRecommendedCommands(git: WorkspaceGitSummary): string[] {
148
155
  ];
149
156
  }
150
157
 
158
+ function parseStatusPath(line: string): string {
159
+ const raw = line.slice(2).trimStart();
160
+ const renamed = raw.split(" -> ");
161
+ return (renamed[renamed.length - 1] ?? raw).replace(/\\/g, "/");
162
+ }
163
+
164
+ function gitStatusFiles(workspaceRoot: string): {
165
+ changed: string[];
166
+ staged: string[];
167
+ unstaged: string[];
168
+ untracked: string[];
169
+ } | null {
170
+ const result = spawnSync("git", ["status", "--porcelain=v1", "-uall"], {
171
+ cwd: workspaceRoot,
172
+ encoding: "utf8",
173
+ windowsHide: true,
174
+ });
175
+ if (result.status !== 0) {
176
+ return null;
177
+ }
178
+ const staged = new Set<string>();
179
+ const unstaged = new Set<string>();
180
+ const untracked = new Set<string>();
181
+ for (const line of result.stdout.split(/\r?\n/).filter(Boolean)) {
182
+ const x = line[0] ?? " ";
183
+ const y = line[1] ?? " ";
184
+ const file = parseStatusPath(line);
185
+ if (x === "?" && y === "?") {
186
+ untracked.add(file);
187
+ continue;
188
+ }
189
+ if (x !== " ") {
190
+ staged.add(file);
191
+ }
192
+ if (y !== " ") {
193
+ unstaged.add(file);
194
+ }
195
+ }
196
+ return {
197
+ changed: [...new Set([...staged, ...unstaged, ...untracked])].sort(),
198
+ staged: [...staged].sort(),
199
+ unstaged: [...unstaged].sort(),
200
+ untracked: [...untracked].sort(),
201
+ };
202
+ }
203
+
204
+ function isCommitReadyPath(file: string): boolean {
205
+ const lower = file.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
206
+ return !(
207
+ lower.startsWith(".forge/") ||
208
+ lower.startsWith(".codex/") ||
209
+ lower.startsWith("src/forge/_generated/") ||
210
+ lower === "forge.lock" ||
211
+ lower.endsWith("/forge.lock")
212
+ );
213
+ }
214
+
215
+ function commitReadySummary(workspaceRoot: string): {
216
+ changed: CategorizedFileSummary;
217
+ staged: CategorizedFileSummary;
218
+ unstaged: CategorizedFileSummary;
219
+ untracked: CategorizedFileSummary;
220
+ files: string[];
221
+ } {
222
+ const classifier = workspaceChangeClassifier(workspaceRoot);
223
+ const status = gitStatusFiles(workspaceRoot) ?? {
224
+ changed: listWorkspaceFiles(workspaceRoot),
225
+ staged: [],
226
+ unstaged: [],
227
+ untracked: listWorkspaceFiles(workspaceRoot),
228
+ };
229
+ const filter = (files: string[]) =>
230
+ files.filter((file) => isCommitReadyPath(file) && !["generated", "operational"].includes(classifier(file)));
231
+ const changed = filter(status.changed);
232
+ return {
233
+ changed: categorizeFiles(changed, 12, classifier),
234
+ staged: categorizeFiles(filter(status.staged), 12, classifier),
235
+ unstaged: categorizeFiles(filter(status.unstaged), 12, classifier),
236
+ untracked: categorizeFiles(filter(status.untracked), 12, classifier),
237
+ files: changed,
238
+ };
239
+ }
240
+
151
241
  function buildReviewFocus(humanChanges: HumanChangeSummary, derivedChanges: DerivedChangeSummary): ReviewFocus {
152
242
  const authoredOrder = ([
153
243
  "source",
@@ -202,7 +292,7 @@ function buildGeneratedChangeExplanation(
202
292
  };
203
293
  }
204
294
 
205
- export function runChangedCommand(workspaceRoot: string, options: { authoredOnly?: boolean; reviewOnly?: boolean } = {}): ChangedCommandResult {
295
+ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly?: boolean; reviewOnly?: boolean; commitReady?: boolean } = {}): ChangedCommandResult {
206
296
  const git = buildWorkspaceGitSummary(workspaceRoot);
207
297
  const changed = git.changeSummary.changed;
208
298
  const humanChanges = selectHumanChangeSummary(changed);
@@ -215,16 +305,19 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
215
305
  const reviewStaged = filterCategorizedSummary(git.changeSummary.staged, [...REVIEW_CHANGE_TYPES]);
216
306
  const reviewUnstaged = filterCategorizedSummary(git.changeSummary.unstaged, [...REVIEW_CHANGE_TYPES]);
217
307
  const reviewUntracked = filterCategorizedSummary(git.changeSummary.untracked, [...REVIEW_CHANGE_TYPES]);
308
+ const commitReady = options.commitReady ? commitReadySummary(workspaceRoot) : null;
218
309
  const viewHumanChanges = options.reviewOnly
219
310
  ? selectHumanChangeSummary(reviewChanged)
311
+ : options.commitReady && commitReady
312
+ ? selectHumanChangeSummary(commitReady.changed)
220
313
  : options.authoredOnly
221
314
  ? selectHumanChangeSummary(authoredChanged)
222
315
  : humanChanges;
223
- const viewChanged = options.reviewOnly ? reviewChanged : options.authoredOnly ? authoredChanged : changed;
224
- const viewStaged = options.reviewOnly ? reviewStaged : options.authoredOnly ? authoredStaged : git.changeSummary.staged;
225
- const viewUnstaged = options.reviewOnly ? reviewUnstaged : options.authoredOnly ? authoredUnstaged : git.changeSummary.unstaged;
226
- const viewUntracked = options.reviewOnly ? reviewUntracked : options.authoredOnly ? authoredUntracked : git.changeSummary.untracked;
227
- const viewDerivedChanges: DerivedChangeSummary = options.authoredOnly || options.reviewOnly
316
+ const viewChanged = options.reviewOnly ? reviewChanged : options.commitReady && commitReady ? commitReady.changed : options.authoredOnly ? authoredChanged : changed;
317
+ const viewStaged = options.reviewOnly ? reviewStaged : options.commitReady && commitReady ? commitReady.staged : options.authoredOnly ? authoredStaged : git.changeSummary.staged;
318
+ const viewUnstaged = options.reviewOnly ? reviewUnstaged : options.commitReady && commitReady ? commitReady.unstaged : options.authoredOnly ? authoredUnstaged : git.changeSummary.unstaged;
319
+ const viewUntracked = options.reviewOnly ? reviewUntracked : options.commitReady && commitReady ? commitReady.untracked : options.authoredOnly ? authoredUntracked : git.changeSummary.untracked;
320
+ const viewDerivedChanges: DerivedChangeSummary = options.authoredOnly || options.reviewOnly || options.commitReady
228
321
  ? { total: 0, generated: { count: 0, sample: [], hidden: 0 } }
229
322
  : derivedChanges;
230
323
  const risks = buildRisks(git);
@@ -245,7 +338,7 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
245
338
  commit: git.commit,
246
339
  workspaceMode: git.workspaceMode ?? (git.available ? "git" : "nonGit"),
247
340
  tracking: git.tracking ?? git.source,
248
- view: options.reviewOnly ? "review" : options.authoredOnly ? "authored" : "all",
341
+ view: options.reviewOnly ? "review" : options.commitReady ? "commit-ready" : options.authoredOnly ? "authored" : "all",
249
342
  changedFiles: viewChanged.total.count,
250
343
  humanFiles: viewHumanChanges.total,
251
344
  generatedFiles: viewDerivedChanges.total,
@@ -274,6 +367,7 @@ export function runChangedCommand(workspaceRoot: string, options: { authoredOnly
274
367
  reviewFocus,
275
368
  generatedExplanation,
276
369
  diffPlan,
370
+ ...(commitReady ? { commitReady: { files: commitReady.files, count: commitReady.files.length } } : {}),
277
371
  risks,
278
372
  advisories,
279
373
  recommendedCommands,
@@ -4,13 +4,18 @@ import { join, resolve } from "node:path";
4
4
  import { stripDeterministicHeader } from "../compiler/primitives/header.ts";
5
5
  import { classify } from "../compiler/classifier/classify.ts";
6
6
  import { buildRuntimeMatrix } from "../compiler/classifier/runtime-matrix.ts";
7
- import { FORGE_RELEASE_PACKAGE_PACK_FAILED } from "../compiler/diagnostics/codes.ts";
7
+ import {
8
+ FORGE_DB_EMPTY_TIMESTAMP_LITERAL,
9
+ FORGE_RELEASE_PACKAGE_PACK_FAILED,
10
+ } from "../compiler/diagnostics/codes.ts";
8
11
  import { createDiagnostic } from "../compiler/diagnostics/create.ts";
9
12
  import { forgeAdd } from "../compiler/integration/add.ts";
10
13
  import { checkImportGuards } from "../compiler/guards/check-import-guards.ts";
11
14
  import { checkDirectProcessEnvUsage } from "../compiler/guards/check-process-env.ts";
12
15
  import { checkAiUsageInApp } from "../compiler/guards/check-ai-usage.ts";
13
16
  import { checkQueryUsageInApp } from "../compiler/guards/check-query-usage.ts";
17
+ import { buildDataGraph } from "../compiler/data-graph/build.ts";
18
+ import { buildSqlPlan } from "../compiler/data-graph/sql/ddl.ts";
14
19
  import { loadSecretRegistry } from "../runtime/secrets/check.ts";
15
20
  import { run } from "../compiler/orchestrator/run.ts";
16
21
  import {
@@ -1003,12 +1008,70 @@ async function loadRuntimeMatrixForCheck(
1003
1008
  return buildRuntimeMatrix(classified);
1004
1009
  }
1005
1010
 
1011
+ function normalizeSchemaType(type: string): string {
1012
+ return type.trim().replace(/\?$/, "").toLowerCase();
1013
+ }
1014
+
1015
+ function timestampFieldNames(dataGraph: DataGraph): Set<string> {
1016
+ const fields = new Set<string>();
1017
+ for (const table of dataGraph.tables) {
1018
+ for (const field of table.fields) {
1019
+ if (["timestamp", "timestamptz"].includes(normalizeSchemaType(field.type))) {
1020
+ fields.add(field.name);
1021
+ }
1022
+ }
1023
+ }
1024
+ return fields;
1025
+ }
1026
+
1027
+ function checkEmptyTimestampLiterals(
1028
+ appGraph: Awaited<ReturnType<typeof buildAppGraphForSession>>,
1029
+ dataGraph: DataGraph,
1030
+ ): ReturnType<typeof createDiagnostic>[] {
1031
+ const fields = timestampFieldNames(dataGraph);
1032
+ if (fields.size === 0) {
1033
+ return [];
1034
+ }
1035
+ const diagnostics: ReturnType<typeof createDiagnostic>[] = [];
1036
+ const runtimeKinds = new Set(["command", "query", "liveQuery", "action", "workflow"]);
1037
+ for (const symbol of appGraph.symbols) {
1038
+ if (!runtimeKinds.has(symbol.kind)) {
1039
+ continue;
1040
+ }
1041
+ const sourceSlice = typeof symbol.meta.sourceSlice === "string" ? symbol.meta.sourceSlice : "";
1042
+ if (!sourceSlice) {
1043
+ continue;
1044
+ }
1045
+ for (const field of fields) {
1046
+ const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1047
+ const pattern = new RegExp(`\\b${escaped}\\s*:\\s*(["']{2})`);
1048
+ const emptyTemplatePattern = new RegExp(`\\b${escaped}\\s*:\\s*\`\``);
1049
+ if (!pattern.test(sourceSlice) && !emptyTemplatePattern.test(sourceSlice)) {
1050
+ continue;
1051
+ }
1052
+ diagnostics.push(
1053
+ createDiagnostic({
1054
+ severity: "error",
1055
+ code: FORGE_DB_EMPTY_TIMESTAMP_LITERAL,
1056
+ message:
1057
+ `Forge timestamp field '${field}' is assigned an empty string in ${symbol.kind} '${symbol.name}'.`,
1058
+ file: symbol.file,
1059
+ span: symbol.span,
1060
+ }),
1061
+ );
1062
+ }
1063
+ }
1064
+ return diagnostics;
1065
+ }
1066
+
1006
1067
  export async function runCheckCommand(
1007
1068
  workspaceRoot: string,
1008
1069
  options?: { strictSecrets?: boolean },
1009
1070
  ): Promise<GenerateResult> {
1010
1071
  const session = getCompileSession(workspaceRoot);
1011
1072
  const appGraph = await buildAppGraphForSession(session);
1073
+ const dataGraph = buildDataGraph(appGraph);
1074
+ const sqlPlan = buildSqlPlan(dataGraph);
1012
1075
 
1013
1076
  const matrix = await loadRuntimeMatrixForCheck(workspaceRoot);
1014
1077
  const guardDiagnostics = checkImportGuards(appGraph.moduleGraph, matrix);
@@ -1029,12 +1092,17 @@ export async function runCheckCommand(
1029
1092
  `${GENERATED_DIR}/capabilityMap.json`,
1030
1093
  )?.diagnostics ?? [];
1031
1094
  const externalDiagnostics = buildExternalServiceGraph(workspaceRoot).diagnostics;
1095
+ const timestampLiteralDiagnostics = checkEmptyTimestampLiterals(appGraph, dataGraph);
1032
1096
 
1033
1097
  const allDiagnostics = [
1098
+ ...appGraph.diagnostics,
1099
+ ...dataGraph.diagnostics,
1100
+ ...sqlPlan.diagnostics,
1034
1101
  ...guardDiagnostics,
1035
1102
  ...processEnvDiagnostics,
1036
1103
  ...aiDiagnostics,
1037
1104
  ...queryDiagnostics,
1105
+ ...timestampLiteralDiagnostics,
1038
1106
  ...frontendDiagnostics,
1039
1107
  ...capabilityDiagnostics,
1040
1108
  ...externalDiagnostics,
@@ -1314,7 +1382,7 @@ function runManifestCommand(command: Extract<ForgeCommand, { kind: "manifest" }>
1314
1382
  export async function runInspectCommand(
1315
1383
  target: InspectTarget,
1316
1384
  workspaceRoot: string,
1317
- options: { full?: boolean; brief?: boolean } = {},
1385
+ options: { full?: boolean; brief?: boolean; ergonomics?: boolean } = {},
1318
1386
  ): Promise<InspectResult> {
1319
1387
  const dataPaths: Partial<Record<InspectTarget, string>> = {
1320
1388
  app: `${GENERATED_DIR}/appGraph.json`,
@@ -1411,6 +1479,47 @@ export async function runInspectCommand(
1411
1479
  };
1412
1480
  }
1413
1481
 
1482
+ if (target === "ui" && options.ergonomics) {
1483
+ const result = await runUiCommand({
1484
+ subcommand: "audit",
1485
+ workspaceRoot,
1486
+ json: true,
1487
+ headed: false,
1488
+ browser: "chromium",
1489
+ trace: "off",
1490
+ screenshot: "off",
1491
+ video: "off",
1492
+ baseUrl: "http://127.0.0.1:3000",
1493
+ runtimeUrl: "http://127.0.0.1:3765",
1494
+ reuseServers: false,
1495
+ startServers: false,
1496
+ all: true,
1497
+ changed: false,
1498
+ ci: false,
1499
+ timeoutMs: 30_000,
1500
+ });
1501
+ return {
1502
+ target,
1503
+ data: {
1504
+ schemaVersion: "0.1.0",
1505
+ mode: "ergonomics",
1506
+ ok: result.ok,
1507
+ manifest: result.manifest,
1508
+ scenarios: result.scenarios ?? [],
1509
+ diagnostics: result.diagnostics,
1510
+ nextActions: [
1511
+ "forge ui audit --json",
1512
+ "forge inspect frontend --json",
1513
+ "forge inspect capabilities --json",
1514
+ ],
1515
+ },
1516
+ warnings: result.diagnostics.filter((diagnostic) => diagnostic.severity === "warning"),
1517
+ errors: result.diagnostics.filter((diagnostic) => diagnostic.severity === "error"),
1518
+ exitCode: result.exitCode,
1519
+ failureKind: result.exitCode === 0 ? undefined : "ui_ergonomics",
1520
+ };
1521
+ }
1522
+
1414
1523
  if (target === "imported") {
1415
1524
  const result = inspectBrownfieldImport(workspaceRoot);
1416
1525
  return {
@@ -2106,6 +2215,7 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
2106
2215
  const result = runChangedCommand(command.workspaceRoot, {
2107
2216
  authoredOnly: command.authoredOnly,
2108
2217
  reviewOnly: command.reviewOnly,
2218
+ commitReady: command.commitReady,
2109
2219
  });
2110
2220
  if (command.json) {
2111
2221
  process.stdout.write(formatJsonResult(result.data));
@@ -2203,8 +2313,8 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
2203
2313
  case "inspect": {
2204
2314
  const result = await runInspectCommand(
2205
2315
  command.target,
2206
- process.cwd(),
2207
- { full: command.full, brief: command.brief },
2316
+ command.workspaceRoot,
2317
+ { full: command.full, brief: command.brief, ergonomics: command.ergonomics },
2208
2318
  );
2209
2319
  if (command.json) {
2210
2320
  process.stdout.write(formatJsonResult(buildInspectJson(result)));
@@ -2346,6 +2456,8 @@ export async function executeCommand(command: ForgeCommand): Promise<number> {
2346
2456
  telemetry: command.telemetry,
2347
2457
  envFile: command.envFile,
2348
2458
  skipStartupConsole: command.skipStartupConsole,
2459
+ detach: command.detach,
2460
+ lifecycle: command.lifecycle,
2349
2461
  });
2350
2462
  return result.exitCode;
2351
2463
  }