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.
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +30 -0
- package/docs/changelog.md +55 -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/agent-adapters/index.ts +28 -2
- package/src/forge/cli/auth.ts +134 -9
- package/src/forge/cli/baseline.ts +112 -0
- package/src/forge/cli/changed.ts +52 -15
- package/src/forge/cli/commands.ts +117 -31
- package/src/forge/cli/db.ts +92 -4
- package/src/forge/cli/dev.ts +229 -44
- package/src/forge/cli/doctor.ts +81 -0
- package/src/forge/cli/handoff.ts +63 -5
- package/src/forge/cli/last-run.ts +84 -0
- package/src/forge/cli/main.ts +3 -1
- package/src/forge/cli/new.ts +130 -12
- package/src/forge/cli/output.ts +94 -12
- package/src/forge/cli/parse.ts +72 -10
- package/src/forge/cli/progress.ts +51 -0
- package/src/forge/cli/studio.ts +15 -14
- package/src/forge/cli/verify.ts +31 -0
- package/src/forge/cli/windows.ts +23 -0
- package/src/forge/cli/workos.ts +5 -4
- package/src/forge/compiler/agent-contract/build.ts +3 -3
- package/src/forge/compiler/data-graph/sql/ddl.ts +11 -1
- package/src/forge/compiler/diagnostics/codes.ts +4 -0
- package/src/forge/compiler/integration/templates/render.ts +1 -0
- package/src/forge/compiler/integration/templates/workos.ts +5 -5
- package/src/forge/compiler/make-registry/build.ts +2 -2
- package/src/forge/compiler/orchestrator/generate-lock.ts +14 -3
- package/src/forge/compiler/recipes/definitions.ts +1 -1
- package/src/forge/delta/status.ts +79 -12
- package/src/forge/dev/server.ts +4 -4
- package/src/forge/dev-console/cycle.ts +55 -20
- package/src/forge/dev-console/types.ts +1 -0
- package/src/forge/make/fields.ts +26 -0
- package/src/forge/make/index.ts +6 -1
- package/src/forge/runtime/db/factory.ts +1 -3
- package/src/forge/runtime/db/memory-adapter.ts +139 -32
- package/src/forge/runtime/db/pglite-adapter.ts +188 -2
- package/src/forge/ui/index.ts +174 -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 +8 -1
- package/src/forge/workspace/forge-cli.ts +29 -1
- 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
|
+
}
|
package/src/forge/cli/main.ts
CHANGED
|
@@ -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
|
|
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",
|
package/src/forge/cli/new.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
409
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 ${
|
|
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
|
: [
|
package/src/forge/cli/output.ts
CHANGED
|
@@ -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(
|
|
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:
|
|
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(
|
|
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:
|
|
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(
|
|
146
|
-
|
|
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
|
|
172
|
-
"forge workos seed --file
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|