forgeos 0.1.0-alpha.30 → 0.1.0-alpha.32

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 (49) hide show
  1. package/AGENTS.md +3 -3
  2. package/CHANGELOG.md +30 -0
  3. package/docs/changelog.md +55 -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/agent-adapters/index.ts +28 -2
  8. package/src/forge/cli/auth.ts +134 -9
  9. package/src/forge/cli/baseline.ts +112 -0
  10. package/src/forge/cli/changed.ts +52 -15
  11. package/src/forge/cli/commands.ts +117 -31
  12. package/src/forge/cli/db.ts +92 -4
  13. package/src/forge/cli/dev.ts +229 -44
  14. package/src/forge/cli/doctor.ts +81 -0
  15. package/src/forge/cli/handoff.ts +63 -5
  16. package/src/forge/cli/last-run.ts +84 -0
  17. package/src/forge/cli/main.ts +3 -1
  18. package/src/forge/cli/new.ts +130 -12
  19. package/src/forge/cli/output.ts +94 -12
  20. package/src/forge/cli/parse.ts +72 -10
  21. package/src/forge/cli/progress.ts +51 -0
  22. package/src/forge/cli/studio.ts +15 -14
  23. package/src/forge/cli/verify.ts +31 -0
  24. package/src/forge/cli/windows.ts +23 -0
  25. package/src/forge/cli/workos.ts +5 -4
  26. package/src/forge/compiler/agent-contract/build.ts +3 -3
  27. package/src/forge/compiler/data-graph/sql/ddl.ts +11 -1
  28. package/src/forge/compiler/diagnostics/codes.ts +4 -0
  29. package/src/forge/compiler/integration/templates/render.ts +1 -0
  30. package/src/forge/compiler/integration/templates/workos.ts +5 -5
  31. package/src/forge/compiler/make-registry/build.ts +2 -2
  32. package/src/forge/compiler/orchestrator/generate-lock.ts +14 -3
  33. package/src/forge/compiler/recipes/definitions.ts +1 -1
  34. package/src/forge/delta/status.ts +79 -12
  35. package/src/forge/dev/server.ts +4 -4
  36. package/src/forge/dev-console/cycle.ts +55 -20
  37. package/src/forge/dev-console/types.ts +1 -0
  38. package/src/forge/make/fields.ts +26 -0
  39. package/src/forge/make/index.ts +6 -1
  40. package/src/forge/runtime/db/factory.ts +1 -3
  41. package/src/forge/runtime/db/memory-adapter.ts +139 -32
  42. package/src/forge/runtime/db/pglite-adapter.ts +188 -2
  43. package/src/forge/ui/index.ts +174 -0
  44. package/src/forge/ui/types.ts +1 -0
  45. package/src/forge/version.ts +1 -1
  46. package/src/forge/workspace/baseline.ts +112 -0
  47. package/src/forge/workspace/change-summary.ts +8 -1
  48. package/src/forge/workspace/forge-cli.ts +29 -1
  49. package/src/forge/workspace/git-summary.ts +65 -2
@@ -0,0 +1,84 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ export interface LastRunRecord {
5
+ schemaVersion: "0.1.0";
6
+ command: string;
7
+ ok: boolean;
8
+ startedAt: string;
9
+ finishedAt: string;
10
+ durationMs: number;
11
+ code?: string;
12
+ failureKind?: string;
13
+ message?: string;
14
+ nextActions: string[];
15
+ details?: Record<string, unknown>;
16
+ }
17
+
18
+ export interface LastRunCommandResult {
19
+ ok: boolean;
20
+ record: LastRunRecord | null;
21
+ path: string;
22
+ exitCode: 0 | 1;
23
+ }
24
+
25
+ function lastRunPath(workspaceRoot: string): string {
26
+ return join(workspaceRoot, ".forge", "last-run.json");
27
+ }
28
+
29
+ export function writeLastRunRecord(workspaceRoot: string, record: LastRunRecord): void {
30
+ const file = lastRunPath(workspaceRoot);
31
+ mkdirSync(join(workspaceRoot, ".forge"), { recursive: true });
32
+ writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`, "utf8");
33
+ }
34
+
35
+ export function readLastRunRecord(workspaceRoot: string): LastRunRecord | null {
36
+ const file = lastRunPath(workspaceRoot);
37
+ if (!existsSync(file)) {
38
+ return null;
39
+ }
40
+ return JSON.parse(readFileSync(file, "utf8")) as LastRunRecord;
41
+ }
42
+
43
+ export function runLastCommand(options: { workspaceRoot: string }): LastRunCommandResult {
44
+ const path = lastRunPath(options.workspaceRoot);
45
+ const record = readLastRunRecord(options.workspaceRoot);
46
+ return {
47
+ ok: record !== null,
48
+ record,
49
+ path,
50
+ exitCode: record ? 0 : 1,
51
+ };
52
+ }
53
+
54
+ export function formatLastJson(result: LastRunCommandResult): string {
55
+ return `${JSON.stringify(result, null, 2)}\n`;
56
+ }
57
+
58
+ export function formatLastHuman(result: LastRunCommandResult): string {
59
+ if (!result.record) {
60
+ return `No last run record found at ${result.path}\n`;
61
+ }
62
+ const lines = [
63
+ `Last Forge run: ${result.record.command}`,
64
+ `OK: ${result.record.ok}`,
65
+ `Finished: ${result.record.finishedAt}`,
66
+ `Duration: ${result.record.durationMs}ms`,
67
+ ];
68
+ if (result.record.code) {
69
+ lines.push(`Code: ${result.record.code}`);
70
+ }
71
+ if (result.record.failureKind) {
72
+ lines.push(`Failure: ${result.record.failureKind}`);
73
+ }
74
+ if (result.record.message) {
75
+ lines.push(`Message: ${result.record.message}`);
76
+ }
77
+ if (result.record.nextActions.length > 0) {
78
+ lines.push("Next actions:");
79
+ for (const action of result.record.nextActions) {
80
+ lines.push(` ${action}`);
81
+ }
82
+ }
83
+ return `${lines.join("\n")}\n`;
84
+ }
@@ -14,6 +14,8 @@ function formatHelp(): string {
14
14
  " forge status --json Compact project health, handoff state, and next actions",
15
15
  " forge changed --json Group changed files into human, generated, and risk buckets",
16
16
  " forge changed --authored --json Show only authored changed files, excluding generated artifacts",
17
+ " forge changed --review --json Show review-focused app/config/docs changes, excluding local agent/browser artifacts",
18
+ " forge new my-app --template minimal-web --field-test Create an installed WorkOS/auth.md field-test app",
17
19
  " forge diff authored Run the authored-only git diff pathspec",
18
20
  " forge handoff --json Compact work handoff for the next external code agent",
19
21
  " forge agent onboard --target codex --json Prepare adapter, hooks, memory, and dev snapshot",
@@ -28,7 +30,7 @@ function formatHelp(): string {
28
30
  " forge workos install --yes --json Delegate AuthKit setup to npx --yes workos@latest install",
29
31
  " forge workos doctor --json Check WorkOS AuthKit/FGA files, claims, seed, webhook, and tenant guards",
30
32
  " forge workos doctor --yes --json Run local checks, then delegate to npx --yes workos@latest doctor",
31
- " forge workos seed --file src/forge/_generated/integrations/workos/workos-seed.yml --json Plan WorkOS CLI seed",
33
+ " forge workos seed --file workos-seed.yml --json Plan WorkOS CLI seed",
32
34
  " forge release check --allow-missing-local-release --json Gate release readiness without failing on unprepared local artifacts",
33
35
  " forge self-host check --prepared-only --json Report compose readiness without creating deploy files",
34
36
  " forge delta status --verbose --json Include Delta schema, lock, and aggregate count details",
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { cpSync, statSync } from "node:fs";
3
- import { dirname, join, parse, relative, resolve } from "node:path";
3
+ import { basename, dirname, join, parse, relative, resolve } from "node:path";
4
4
  import { setTimeout as sleep } from "node:timers/promises";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { nodeFileSystem } from "../compiler/fs/index.ts";
@@ -17,6 +17,7 @@ export interface NewCommandOptions {
17
17
  packageManager: NewPackageManager;
18
18
  install: boolean;
19
19
  git: boolean;
20
+ fieldTest?: boolean;
20
21
  forgePackageSpec?: string;
21
22
  localForge?: boolean;
22
23
  workspaceRoot: string;
@@ -29,6 +30,11 @@ export interface NewCommandResult {
29
30
  packageManager: NewPackageManager;
30
31
  installed: boolean;
31
32
  gitInitialized: boolean;
33
+ fieldTest: {
34
+ requested: boolean;
35
+ ok: boolean;
36
+ steps: Array<{ name: string; ok: boolean; command: string }>;
37
+ };
32
38
  generated: boolean;
33
39
  gitHygiene: {
34
40
  ok: boolean;
@@ -195,12 +201,49 @@ function ensureProjectName(name: string): string | null {
195
201
  if (!name.trim()) {
196
202
  return "forge new requires a project name";
197
203
  }
198
- if (name.includes("/") || name.includes("\\") || name === "." || name === "..") {
204
+ if (name === ".") {
205
+ return null;
206
+ }
207
+ if (name.includes("/") || name.includes("\\") || name === "..") {
199
208
  return "project name must be a directory name, not a path";
200
209
  }
201
210
  return null;
202
211
  }
203
212
 
213
+ function isCurrentDirectoryTarget(name: string): boolean {
214
+ return name === ".";
215
+ }
216
+
217
+ function targetDirectoryFor(options: NewCommandOptions): string {
218
+ return isCurrentDirectoryTarget(options.name)
219
+ ? resolve(options.workspaceRoot)
220
+ : resolve(options.workspaceRoot, options.name);
221
+ }
222
+
223
+ function sanitizePackageName(name: string): string {
224
+ return name
225
+ .trim()
226
+ .toLowerCase()
227
+ .replace(/[^a-z0-9._-]+/g, "-")
228
+ .replace(/-+/g, "-")
229
+ .replace(/^[._-]+|[._-]+$/g, "") || "forge-app";
230
+ }
231
+
232
+ function projectNameForTarget(options: NewCommandOptions, targetDir: string): string {
233
+ return isCurrentDirectoryTarget(options.name)
234
+ ? sanitizePackageName(basename(targetDir))
235
+ : options.name;
236
+ }
237
+
238
+ function blockingExistingEntries(targetDir: string): string[] {
239
+ const allowed = new Set([".git", ".DS_Store", "Thumbs.db"]);
240
+ return nodeFileSystem
241
+ .readDir(targetDir)
242
+ .map((entry) => entry.name)
243
+ .filter((name) => !allowed.has(name))
244
+ .sort();
245
+ }
246
+
204
247
  function analyzeGitHygiene(targetDir: string): NewCommandResult["gitHygiene"] {
205
248
  const gitignorePath = join(targetDir, ".gitignore");
206
249
  const gitignore = nodeFileSystem.exists(gitignorePath)
@@ -380,6 +423,7 @@ export async function runNewCommand(options: NewCommandOptions): Promise<NewComm
380
423
  packageManager: options.packageManager,
381
424
  installed: false,
382
425
  gitInitialized: false,
426
+ fieldTest: { requested: Boolean(options.fieldTest), ok: false, steps: [] },
383
427
  generated: false,
384
428
  gitHygiene: { ok: false, ignoredPaths: [], missingPaths: [] },
385
429
  exitCode: 1,
@@ -397,6 +441,7 @@ export async function runNewCommand(options: NewCommandOptions): Promise<NewComm
397
441
  packageManager: options.packageManager,
398
442
  installed: false,
399
443
  gitInitialized: false,
444
+ fieldTest: { requested: Boolean(options.fieldTest), ok: false, steps: [] },
400
445
  generated: false,
401
446
  gitHygiene: { ok: false, ignoredPaths: [], missingPaths: [] },
402
447
  exitCode: 1,
@@ -405,15 +450,18 @@ export async function runNewCommand(options: NewCommandOptions): Promise<NewComm
405
450
  };
406
451
  }
407
452
 
408
- const targetDir = resolve(options.workspaceRoot, options.name);
409
- if (nodeFileSystem.exists(targetDir)) {
453
+ const targetDir = targetDirectoryFor(options);
454
+ const appName = projectNameForTarget(options, targetDir);
455
+ const currentDirectoryTarget = isCurrentDirectoryTarget(options.name);
456
+ if (nodeFileSystem.exists(targetDir) && !currentDirectoryTarget) {
410
457
  return {
411
- name: options.name,
458
+ name: appName,
412
459
  template: options.template,
413
460
  targetDir,
414
461
  packageManager: options.packageManager,
415
462
  installed: false,
416
463
  gitInitialized: false,
464
+ fieldTest: { requested: Boolean(options.fieldTest), ok: false, steps: [] },
417
465
  generated: false,
418
466
  gitHygiene: { ok: false, ignoredPaths: [], missingPaths: [] },
419
467
  exitCode: 1,
@@ -421,13 +469,32 @@ export async function runNewCommand(options: NewCommandOptions): Promise<NewComm
421
469
  nextSteps: [],
422
470
  };
423
471
  }
472
+ if (nodeFileSystem.exists(targetDir) && currentDirectoryTarget) {
473
+ const blocking = blockingExistingEntries(targetDir);
474
+ if (blocking.length > 0) {
475
+ return {
476
+ name: appName,
477
+ template: options.template,
478
+ targetDir,
479
+ packageManager: options.packageManager,
480
+ installed: false,
481
+ gitInitialized: false,
482
+ fieldTest: { requested: Boolean(options.fieldTest), ok: false, steps: [] },
483
+ generated: false,
484
+ gitHygiene: { ok: false, ignoredPaths: [], missingPaths: [] },
485
+ exitCode: 1,
486
+ message: `current directory is not empty: ${blocking.slice(0, 6).join(", ")}${blocking.length > 6 ? ` (+${blocking.length - 6})` : ""}`,
487
+ nextSteps: [],
488
+ };
489
+ }
490
+ }
424
491
 
425
492
  nodeFileSystem.mkdirp(targetDir);
426
493
  cpSync(source, targetDir, { recursive: true, force: true });
427
494
  ensureGitignore(targetDir);
428
495
  replaceTokens(
429
496
  targetDir,
430
- options.name,
497
+ appName,
431
498
  options.packageManager,
432
499
  forgePackageSpec(targetDir, options),
433
500
  );
@@ -438,12 +505,13 @@ export async function runNewCommand(options: NewCommandOptions): Promise<NewComm
438
505
  installed = installCode === 0;
439
506
  if (!installed) {
440
507
  return {
441
- name: options.name,
508
+ name: appName,
442
509
  template: options.template,
443
510
  targetDir,
444
511
  packageManager: options.packageManager,
445
512
  installed,
446
513
  gitInitialized: false,
514
+ fieldTest: { requested: Boolean(options.fieldTest), ok: false, steps: [] },
447
515
  generated: false,
448
516
  gitHygiene: analyzeGitHygiene(targetDir),
449
517
  exitCode: 1,
@@ -472,12 +540,13 @@ export async function runNewCommand(options: NewCommandOptions): Promise<NewComm
472
540
  }
473
541
  if (!generated) {
474
542
  return {
475
- name: options.name,
543
+ name: appName,
476
544
  template: options.template,
477
545
  targetDir,
478
546
  packageManager: options.packageManager,
479
547
  installed,
480
548
  gitInitialized: false,
549
+ fieldTest: { requested: Boolean(options.fieldTest), ok: false, steps: [] },
481
550
  generated,
482
551
  gitHygiene: analyzeGitHygiene(targetDir),
483
552
  exitCode: 1,
@@ -493,26 +562,68 @@ export async function runNewCommand(options: NewCommandOptions): Promise<NewComm
493
562
  gitInitialized = gitCode === 0;
494
563
  }
495
564
 
565
+ const fieldTestSteps: NewCommandResult["fieldTest"]["steps"] = [];
566
+ if (options.fieldTest && installed) {
567
+ const steps: Array<{ name: string; command: string; args: string[] }> = [
568
+ { name: "workos", command: options.packageManager, args: ["run", "forge", "--", "add", "auth", "workos", "--json"] },
569
+ { name: "authmd", command: options.packageManager, args: ["run", "forge", "--", "authmd", "generate", "--json"] },
570
+ { name: "check", command: options.packageManager, args: ["run", "forge", "--", "check", "--json"] },
571
+ ];
572
+ for (const step of steps) {
573
+ const code = await spawnCommand(step.command, step.args, targetDir);
574
+ fieldTestSteps.push({ name: step.name, ok: code === 0, command: [step.command, ...step.args].join(" ") });
575
+ if (code !== 0) {
576
+ break;
577
+ }
578
+ }
579
+ if (gitInitialized && fieldTestSteps.every((step) => step.ok)) {
580
+ const addCode = await spawnCommand("git", ["add", "-A"], targetDir);
581
+ const commitCode = addCode === 0
582
+ ? await spawnCommand("git", [
583
+ "-c",
584
+ "user.name=ForgeOS",
585
+ "-c",
586
+ "user.email=forgeos@example.invalid",
587
+ "commit",
588
+ "-m",
589
+ "Initial ForgeOS field-test scaffold",
590
+ ], targetDir)
591
+ : 1;
592
+ fieldTestSteps.push({
593
+ name: "baseline-commit",
594
+ ok: addCode === 0 && commitCode === 0,
595
+ command: 'git add -A && git commit -m "Initial ForgeOS field-test scaffold"',
596
+ });
597
+ }
598
+ }
599
+ const fieldTest = {
600
+ requested: Boolean(options.fieldTest),
601
+ ok: !options.fieldTest || (installed && fieldTestSteps.length > 0 && fieldTestSteps.every((step) => step.ok)),
602
+ steps: fieldTestSteps,
603
+ };
604
+
496
605
  const nextSteps = [
497
- `cd ${options.name}`,
606
+ ...(currentDirectoryTarget ? [] : [`cd ${options.name}`]),
498
607
  ...(installed ? [] : [`${options.packageManager} install`]),
499
608
  ...(generated ? [] : [`${options.packageManager} run generate`]),
609
+ ...(options.fieldTest ? [`${options.packageManager} run forge -- agent onboard --target codex --json`] : []),
500
610
  `${options.packageManager} run dev -- --open`,
501
611
  `${options.packageManager} run verify`,
502
612
  ];
503
613
  const gitHygiene = analyzeGitHygiene(targetDir);
504
614
 
505
615
  return {
506
- name: options.name,
616
+ name: appName,
507
617
  template: options.template,
508
618
  targetDir,
509
619
  packageManager: options.packageManager,
510
620
  installed,
511
621
  gitInitialized,
622
+ fieldTest,
512
623
  generated,
513
624
  gitHygiene,
514
- exitCode: 0,
515
- message: `Created ${options.name} from template ${options.template}.`,
625
+ exitCode: fieldTest.ok ? 0 : 1,
626
+ message: `Created ${appName} from template ${options.template}.`,
516
627
  nextSteps,
517
628
  };
518
629
  }
@@ -524,6 +635,13 @@ export function formatNewHuman(result: NewCommandResult): string {
524
635
 
525
636
  return [
526
637
  result.message,
638
+ ...(result.fieldTest.requested
639
+ ? [
640
+ result.fieldTest.ok
641
+ ? "Field-test setup completed: WorkOS, auth.md, check, and baseline commit are ready."
642
+ : `warning: field-test setup did not complete: ${result.fieldTest.steps.find((step) => !step.ok)?.name ?? "not-run"}`,
643
+ ]
644
+ : []),
527
645
  ...(result.gitHygiene.ok
528
646
  ? ["Generated and operational Forge files are ignored by git."]
529
647
  : [
@@ -5,6 +5,7 @@ import type {
5
5
  InspectResult,
6
6
  VerifyResult,
7
7
  } from "../compiler/types/cli.ts";
8
+ import { forgeCliCommandsForWorkspace } from "../workspace/forge-cli.ts";
8
9
  import { uniqueNextActions } from "./next-actions.ts";
9
10
 
10
11
  function failureKindFromDiagnostics(errors: Diagnostic[]): string | undefined {
@@ -55,7 +56,32 @@ function summarizeGeneratedArtifacts(paths: string[]): Record<string, number> {
55
56
  return groups;
56
57
  }
57
58
 
59
+ function compactList(items: string[], sampleSize = 12): { count: number; sample: string[]; hidden: number } {
60
+ return {
61
+ count: items.length,
62
+ sample: items.slice(0, sampleSize),
63
+ hidden: Math.max(0, items.length - sampleSize),
64
+ };
65
+ }
66
+
67
+ function summarizeDiagnostics(diagnostics: Diagnostic[]): Record<string, number> {
68
+ const summary: Record<string, number> = {};
69
+ for (const diagnostic of diagnostics) {
70
+ summary[diagnostic.code] = (summary[diagnostic.code] ?? 0) + 1;
71
+ }
72
+ return summary;
73
+ }
74
+
58
75
  function buildGenerateNextActions(result: GenerateResult): string[] {
76
+ if (result.exitCode !== 0 && result.errors.length === 0 && result.changed.length > 0) {
77
+ return [
78
+ "forge generate --json",
79
+ "forge generate --check --json",
80
+ "forge changed --review --json",
81
+ "forge check --json",
82
+ ];
83
+ }
84
+
59
85
  const suggested = new Set<string>();
60
86
  for (const diagnostic of [...result.errors, ...result.warnings]) {
61
87
  for (const command of diagnostic.suggestedCommands ?? []) {
@@ -80,8 +106,29 @@ function buildGenerateNextActions(result: GenerateResult): string[] {
80
106
  return [...suggested];
81
107
  }
82
108
 
83
- export function buildGenerateJson(result: GenerateResult): Record<string, unknown> {
109
+ export function buildGenerateJson(
110
+ result: GenerateResult,
111
+ options: { workspaceRoot?: string } = {},
112
+ ): Record<string, unknown> {
84
113
  const diagnostics = [...result.errors, ...result.warnings];
114
+ const changed = compactList(result.changed);
115
+ const unchanged = compactList(result.unchanged);
116
+ const drift =
117
+ result.exitCode !== 0 && result.errors.length === 0 && result.changed.length > 0
118
+ ? {
119
+ kind: "generated-drift",
120
+ message: `${result.changed.length} generated artifact(s) would change; run forge generate before handoff or verification.`,
121
+ changedGroups: summarizeGeneratedArtifacts(result.changed),
122
+ sampleChanged: changed.sample,
123
+ hiddenChanged: changed.hidden,
124
+ repairCommand: options.workspaceRoot
125
+ ? forgeCliCommandsForWorkspace(options.workspaceRoot, ["forge generate"])[0]
126
+ : "forge generate",
127
+ checkCommand: options.workspaceRoot
128
+ ? forgeCliCommandsForWorkspace(options.workspaceRoot, ["forge generate --check --json"])[0]
129
+ : "forge generate --check --json",
130
+ }
131
+ : null;
85
132
  return {
86
133
  schemaVersion: "0.1.0",
87
134
  ok: result.exitCode === 0,
@@ -92,20 +139,32 @@ export function buildGenerateJson(result: GenerateResult): Record<string, unknow
92
139
  errors: result.errors.length,
93
140
  changedGroups: summarizeGeneratedArtifacts(result.changed),
94
141
  unchangedGroups: summarizeGeneratedArtifacts(result.unchanged),
142
+ changedSample: changed.sample,
143
+ hiddenChanged: changed.hidden,
144
+ unchangedSample: unchanged.sample,
145
+ hiddenUnchanged: unchanged.hidden,
146
+ diagnosticGroups: summarizeDiagnostics(diagnostics),
147
+ ...(drift ? { message: drift.message } : {}),
95
148
  },
149
+ ...(drift ? { drift } : {}),
96
150
  changed: result.changed,
97
151
  unchanged: result.unchanged,
98
152
  warnings: result.warnings,
99
153
  errors: result.errors,
100
154
  diagnostics,
101
- nextActions: buildGenerateNextActions(result),
155
+ nextActions: options.workspaceRoot
156
+ ? forgeCliCommandsForWorkspace(options.workspaceRoot, buildGenerateNextActions(result))
157
+ : buildGenerateNextActions(result),
102
158
  durationMs: null,
103
159
  exitCode: result.exitCode,
104
160
  failureKind: result.failureKind ?? null,
105
161
  };
106
162
  }
107
163
 
108
- export function buildCheckJson(result: GenerateResult): Record<string, unknown> {
164
+ export function buildCheckJson(
165
+ result: GenerateResult,
166
+ options: { workspaceRoot?: string } = {},
167
+ ): Record<string, unknown> {
109
168
  const diagnostics = [...result.errors, ...result.warnings];
110
169
  const suggested = new Set<string>();
111
170
  for (const diagnostic of diagnostics) {
@@ -135,15 +194,20 @@ export function buildCheckJson(result: GenerateResult): Record<string, unknown>
135
194
  warnings: result.warnings,
136
195
  errors: result.errors,
137
196
  diagnostics,
138
- nextActions: uniqueNextActions([...suggested]),
197
+ nextActions: options.workspaceRoot
198
+ ? forgeCliCommandsForWorkspace(options.workspaceRoot, uniqueNextActions([...suggested]))
199
+ : uniqueNextActions([...suggested]),
139
200
  durationMs: null,
140
201
  exitCode: result.exitCode,
141
202
  failureKind: result.failureKind ?? null,
142
203
  };
143
204
  }
144
205
 
145
- export function buildAddJson(result: ForgeAddResult): Record<string, unknown> {
146
- const base = buildGenerateJson(result);
206
+ export function buildAddJson(
207
+ result: ForgeAddResult,
208
+ options: { workspaceRoot?: string } = {},
209
+ ): Record<string, unknown> {
210
+ const base = buildGenerateJson(result, options);
147
211
  const packageInspectName = result.packageName ?? result.alias;
148
212
  const packageNextActions =
149
213
  result.mode === "package" && packageInspectName
@@ -168,8 +232,8 @@ export function buildAddJson(result: ForgeAddResult): Record<string, unknown> {
168
232
  "forge workos install --yes --json",
169
233
  "forge workos doctor --json",
170
234
  "forge workos doctor --yes --json",
171
- "forge workos seed --file src/forge/_generated/integrations/workos/workos-seed.yml --json",
172
- "forge workos seed --file src/forge/_generated/integrations/workos/workos-seed.yml --yes --json",
235
+ "forge workos seed --file workos-seed.yml --json",
236
+ "forge workos seed --file workos-seed.yml --yes --json",
173
237
  "forge auth check --json",
174
238
  "forge auth prove --json",
175
239
  ]
@@ -199,7 +263,9 @@ export function buildAddJson(result: ForgeAddResult): Record<string, unknown> {
199
263
  ...(result.installCwd ? { installCwd: result.installCwd } : {}),
200
264
  ...(result.installWorkspace ? { installWorkspace: result.installWorkspace } : {}),
201
265
  ...base,
202
- nextActions: packageNextActions ?? integrationNextActions ?? base.nextActions,
266
+ nextActions: options.workspaceRoot
267
+ ? forgeCliCommandsForWorkspace(options.workspaceRoot, packageNextActions ?? integrationNextActions ?? (base.nextActions as string[]))
268
+ : packageNextActions ?? integrationNextActions ?? base.nextActions,
203
269
  };
204
270
  }
205
271
 
@@ -347,15 +413,31 @@ export function buildInspectJson(result: InspectResult): Record<string, unknown>
347
413
  }
348
414
 
349
415
  export function writeHumanGenerate(result: GenerateResult): void {
350
- for (const path of result.changed) {
416
+ const changed = compactList(result.changed, 20);
417
+ const unchanged = compactList(result.unchanged, 20);
418
+ if (result.exitCode !== 0 && result.errors.length === 0 && result.changed.length > 0) {
419
+ console.error(`generated drift: ${result.changed.length} artifact(s) would change; run forge generate`);
420
+ }
421
+ for (const path of changed.sample) {
351
422
  console.log(`changed: ${path}`);
352
423
  }
353
- for (const path of result.unchanged) {
424
+ if (changed.hidden > 0) {
425
+ console.log(`changed: ... ${changed.hidden} more`);
426
+ }
427
+ for (const path of unchanged.sample) {
354
428
  console.log(`unchanged: ${path}`);
355
429
  }
356
- for (const diagnostic of [...result.warnings, ...result.errors]) {
430
+ if (unchanged.hidden > 0) {
431
+ console.log(`unchanged: ... ${unchanged.hidden} more`);
432
+ }
433
+ const diagnostics = [...result.warnings, ...result.errors];
434
+ const diagnosticSample = diagnostics.slice(0, 20);
435
+ for (const diagnostic of diagnosticSample) {
357
436
  console.error(`${diagnostic.severity} ${diagnostic.code}: ${diagnostic.message}`);
358
437
  }
438
+ if (diagnostics.length > diagnosticSample.length) {
439
+ console.error(`diagnostics: ... ${diagnostics.length - diagnosticSample.length} more`);
440
+ }
359
441
  }
360
442
 
361
443
  export function writeHumanAdd(result: ForgeAddResult): void {