@warpgogol/forge 5.1.0 → 5.1.2

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 (84) hide show
  1. package/AGENTS.md +8 -1
  2. package/CHANGELOG.md +72 -0
  3. package/README.md +8 -8
  4. package/README.uk.md +5 -5
  5. package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
  6. package/dist/os/compass/handlers/compass-audit-handler.js +3 -3
  7. package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
  8. package/dist/os/compass/handlers/compass-change-summary-handler.d.ts.map +1 -1
  9. package/dist/os/compass/handlers/compass-change-summary-handler.js +1 -1
  10. package/dist/os/compass/handlers/compass-change-summary-handler.js.map +1 -1
  11. package/dist/os/compass/handlers/compass-inventory-handler.d.ts.map +1 -1
  12. package/dist/os/compass/handlers/compass-inventory-handler.js +1 -1
  13. package/dist/os/compass/handlers/compass-inventory-handler.js.map +1 -1
  14. package/dist/os/compass/handlers/compass-inventory.d.ts +1 -1
  15. package/dist/os/compass/handlers/compass-inventory.d.ts.map +1 -1
  16. package/dist/os/compass/handlers/compass-inventory.js +20 -5
  17. package/dist/os/compass/handlers/compass-inventory.js.map +1 -1
  18. package/dist/os/compass/handlers/compass-migrate-handler.d.ts.map +1 -1
  19. package/dist/os/compass/handlers/compass-migrate-handler.js +1 -0
  20. package/dist/os/compass/handlers/compass-migrate-handler.js.map +1 -1
  21. package/dist/os/compass/handlers/compass-migrate.d.ts +2 -0
  22. package/dist/os/compass/handlers/compass-migrate.d.ts.map +1 -1
  23. package/dist/os/compass/handlers/compass-migrate.js +1 -1
  24. package/dist/os/compass/handlers/compass-migrate.js.map +1 -1
  25. package/dist/os/compass/handlers/resolve-scan-root.js +1 -1
  26. package/dist/os/core/core.module.js +2 -2
  27. package/dist/os/core/core.module.js.map +1 -1
  28. package/dist/os/core/handlers/pinned-validate.d.ts.map +1 -1
  29. package/dist/os/core/handlers/pinned-validate.js +26 -6
  30. package/dist/os/core/handlers/pinned-validate.js.map +1 -1
  31. package/dist/src/config/forge-config.d.ts +20 -1
  32. package/dist/src/config/forge-config.d.ts.map +1 -1
  33. package/dist/src/config/forge-config.js +76 -5
  34. package/dist/src/config/forge-config.js.map +1 -1
  35. package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
  36. package/dist/src/onboarding/agents-generate.js +11 -7
  37. package/dist/src/onboarding/agents-generate.js.map +1 -1
  38. package/dist/src/onboarding/create.d.ts.map +1 -1
  39. package/dist/src/onboarding/create.js +4 -8
  40. package/dist/src/onboarding/create.js.map +1 -1
  41. package/dist/src/onboarding/doctor.d.ts.map +1 -1
  42. package/dist/src/onboarding/doctor.js +65 -0
  43. package/dist/src/onboarding/doctor.js.map +1 -1
  44. package/dist/src/onboarding/init.d.ts.map +1 -1
  45. package/dist/src/onboarding/init.js +11 -0
  46. package/dist/src/onboarding/init.js.map +1 -1
  47. package/dist/src/onboarding/nested-agents-templates.d.ts.map +1 -1
  48. package/dist/src/onboarding/nested-agents-templates.js +5 -2
  49. package/dist/src/onboarding/nested-agents-templates.js.map +1 -1
  50. package/dist/src/onboarding/upgrade.d.ts.map +1 -1
  51. package/dist/src/onboarding/upgrade.js +4 -2
  52. package/dist/src/onboarding/upgrade.js.map +1 -1
  53. package/docs/getting-started.md +1 -1
  54. package/docs/reference/profiles.md +8 -8
  55. package/os/compass/handlers/compass-audit-handler.ts +3 -0
  56. package/os/compass/handlers/compass-change-summary-handler.ts +1 -0
  57. package/os/compass/handlers/compass-inventory-handler.ts +1 -0
  58. package/os/compass/handlers/compass-inventory.ts +20 -6
  59. package/os/compass/handlers/compass-migrate-handler.ts +1 -0
  60. package/os/compass/handlers/compass-migrate.ts +9 -2
  61. package/os/compass/handlers/resolve-scan-root.ts +1 -1
  62. package/os/compass/handlers/tests/compass-policy.test.ts +2 -2
  63. package/os/core/core.module.ts +2 -2
  64. package/os/core/handlers/pinned-validate.ts +33 -7
  65. package/package.json +1 -1
  66. package/profiles/{godot-csharp.yaml → godot-game.yaml} +2 -2
  67. package/profiles/{knowledge-typescript-turborepo.yaml → knowledge.yaml} +1 -1
  68. package/profiles/{phaser-turborepo.yaml → phaser-game.yaml} +2 -2
  69. package/profiles/{typescript-turborepo.yaml → typescript.yaml} +2 -2
  70. package/src/config/forge-config.ts +83 -5
  71. package/src/onboarding/agents-generate.ts +16 -7
  72. package/src/onboarding/create.ts +4 -7
  73. package/src/onboarding/doctor.ts +70 -0
  74. package/src/onboarding/init.ts +19 -1
  75. package/src/onboarding/nested-agents-templates.ts +5 -2
  76. package/src/onboarding/upgrade.ts +4 -1
  77. package/src/tests/agents-generate-domain.test.ts +3 -3
  78. package/src/tests/create.test.ts +4 -4
  79. package/src/tests/doctor-profile-id-known.test.ts +237 -0
  80. package/src/tests/package-files.test.ts +53 -1
  81. package/src/tests/pinned-validate.test.ts +134 -0
  82. package/src/tests/profile-schema.test.ts +4 -4
  83. package/src/tests/scaffold-project.test.ts +3 -3
  84. package/src/tests/stack-profile.test.ts +12 -12
@@ -10,8 +10,18 @@ but are missing from the published package.</purpose>
10
10
  */
11
11
 
12
12
  import { test, expect } from "vitest";
13
- import { readFileSync, readdirSync } from "node:fs";
13
+ import {
14
+ existsSync,
15
+ mkdirSync,
16
+ mkdtempSync,
17
+ readFileSync,
18
+ readdirSync,
19
+ rmSync,
20
+ writeFileSync,
21
+ } from "node:fs";
22
+ import { tmpdir } from "node:os";
14
23
  import { join } from "node:path";
24
+ import { resolveForgePackageRoot } from "../config/forge-config.ts";
15
25
 
16
26
  const PACKAGE_ROOT = join(import.meta.dirname, "..", "..");
17
27
 
@@ -52,3 +62,45 @@ test("root AGENTS.md templates exist and are readable", () => {
52
62
  expect(content.length).toBeGreaterThan(0);
53
63
  }
54
64
  });
65
+
66
+ // Regression: published 5.1.0 resolved templates/profiles via import.meta.dirname,
67
+ // which points at dist/src/onboarding/ in the compiled package — tsc never copies
68
+ // .md assets into dist/, so lookups silently failed. resolveForgePackageRoot must
69
+ // find the package root from both source and compiled layouts.
70
+ test("resolveForgePackageRoot finds package root from compiled dist layout", () => {
71
+ const tmp = mkdtempSync(join(tmpdir(), "forge-pkgroot-"));
72
+ try {
73
+ writeFileSync(join(tmp, "package.json"), JSON.stringify({ name: "@warpgogol/forge" }));
74
+ const distOnboarding = join(tmp, "dist", "src", "onboarding");
75
+ mkdirSync(distOnboarding, { recursive: true });
76
+ const srcTemplates = join(tmp, "src", "onboarding", "templates");
77
+ mkdirSync(srcTemplates, { recursive: true });
78
+
79
+ const root = resolveForgePackageRoot(distOnboarding);
80
+ expect(root).toBe(tmp);
81
+ expect(existsSync(join(root, "src", "onboarding", "templates"))).toBe(true);
82
+ expect(existsSync(join(root, "profiles"))).toBe(false);
83
+ } finally {
84
+ rmSync(tmp, { recursive: true, force: true });
85
+ }
86
+ });
87
+
88
+ test("resolveForgePackageRoot finds package root from source layout", () => {
89
+ const tmp = mkdtempSync(join(tmpdir(), "forge-pkgroot-"));
90
+ try {
91
+ writeFileSync(join(tmp, "package.json"), JSON.stringify({ name: "@warpgogol/forge" }));
92
+ const srcOnboarding = join(tmp, "src", "onboarding");
93
+ mkdirSync(srcOnboarding, { recursive: true });
94
+
95
+ expect(resolveForgePackageRoot(srcOnboarding)).toBe(tmp);
96
+ } finally {
97
+ rmSync(tmp, { recursive: true, force: true });
98
+ }
99
+ });
100
+
101
+ test("resolveForgePackageRoot resolves the real package root from src/onboarding", () => {
102
+ const root = resolveForgePackageRoot(join(PACKAGE_ROOT, "src", "onboarding"));
103
+ expect(root).toBe(PACKAGE_ROOT);
104
+ expect(existsSync(join(root, "src", "onboarding", "templates"))).toBe(true);
105
+ expect(existsSync(join(root, "profiles"))).toBe(true);
106
+ });
@@ -0,0 +1,134 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ import { join } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+ import { runPinnedValidate } from "../../os/core/handlers/pinned-validate.ts";
8
+ import type { ForgeCommandInput, ForgeRuntimeContext } from "../../src/types.ts";
9
+
10
+ const execFileAsync = promisify(execFile);
11
+
12
+ async function makeTempDir(): Promise<string> {
13
+ return mkdtemp(join(tmpdir(), "forge-pinned-validate-test-"));
14
+ }
15
+
16
+ function makeContext(workspaceRoot: string): ForgeRuntimeContext {
17
+ return {
18
+ workspaceRoot,
19
+ logger: {
20
+ section: () => {},
21
+ info: () => {},
22
+ warn: () => {},
23
+ error: () => {},
24
+ success: () => {},
25
+ },
26
+ dryRun: false,
27
+ outputFormat: "json",
28
+ };
29
+ }
30
+
31
+ function makeInput(): ForgeCommandInput {
32
+ return { argv: [], flags: {} };
33
+ }
34
+
35
+ async function git(dir: string, args: string[]): Promise<void> {
36
+ await execFileAsync("git", args, { cwd: dir });
37
+ }
38
+
39
+ /**
40
+ * Scaffold a git repo with a committed pinned.yaml protecting docs/rfcs/
41
+ * and one committed RFC file. Returns the repo dir.
42
+ */
43
+ async function makeRepo(): Promise<string> {
44
+ const dir = await makeTempDir();
45
+ await git(dir, ["init"]);
46
+ await git(dir, ["config", "user.email", "test@example.com"]);
47
+ await git(dir, ["config", "user.name", "Test"]);
48
+ await mkdir(join(dir, ".forge"), { recursive: true });
49
+ await writeFile(
50
+ join(dir, ".forge", "pinned.yaml"),
51
+ "pinned:\n - path: docs/rfcs/\n mode: protect\n reason: RFC directory\n",
52
+ );
53
+ await mkdir(join(dir, "docs", "rfcs"), { recursive: true });
54
+ await writeFile(join(dir, "docs", "rfcs", "rfc-0001.md"), "# RFC 1\n");
55
+ await git(dir, ["add", "-A"]);
56
+ await git(dir, ["commit", "-m", "init"]);
57
+ return dir;
58
+ }
59
+
60
+ describe("runPinnedValidate", () => {
61
+ test("passes for intra-directory move inside a pinned dir (docs.archive)", async () => {
62
+ const dir = await makeRepo();
63
+ try {
64
+ // Simulate docs.archive: docs/rfcs/rfc-0001.md → docs/rfcs/archive/implemented/rfc-0001.md
65
+ await mkdir(join(dir, "docs", "rfcs", "archive", "implemented"), { recursive: true });
66
+ await git(dir, ["mv", "docs/rfcs/rfc-0001.md", "docs/rfcs/archive/implemented/rfc-0001.md"]);
67
+
68
+ const result = await runPinnedValidate(makeInput(), makeContext(dir));
69
+ expect(result.data?.status).toBe("pass");
70
+ expect(result.data?.violations).toHaveLength(0);
71
+ expect(result.exitCode).toBe(0);
72
+ } finally {
73
+ await rm(dir, { recursive: true, force: true });
74
+ }
75
+ });
76
+
77
+ test("flags move out of a pinned dir", async () => {
78
+ const dir = await makeRepo();
79
+ try {
80
+ await mkdir(join(dir, "docs", "other"), { recursive: true });
81
+ await git(dir, ["mv", "docs/rfcs/rfc-0001.md", "docs/other/rfc-0001.md"]);
82
+
83
+ const result = await runPinnedValidate(makeInput(), makeContext(dir));
84
+ expect(result.data?.status).toBe("fail");
85
+ expect(result.data?.violations.length).toBeGreaterThan(0);
86
+ expect(
87
+ result.data?.violations.some(
88
+ (v) => v.path === "docs/rfcs/rfc-0001.md" && v.operation === "move",
89
+ ),
90
+ ).toBe(true);
91
+ } finally {
92
+ await rm(dir, { recursive: true, force: true });
93
+ }
94
+ });
95
+
96
+ test("flags move into a pinned dir", async () => {
97
+ const dir = await makeRepo();
98
+ try {
99
+ // Source must be committed first — otherwise git records the staged
100
+ // result as A (add), not R (rename).
101
+ await writeFile(join(dir, "incoming.md"), "# incoming\n");
102
+ await git(dir, ["add", "incoming.md"]);
103
+ await git(dir, ["commit", "-m", "add incoming"]);
104
+ await git(dir, ["mv", "incoming.md", "docs/rfcs/incoming.md"]);
105
+
106
+ const result = await runPinnedValidate(makeInput(), makeContext(dir));
107
+ expect(result.data?.status).toBe("fail");
108
+ expect(
109
+ result.data?.violations.some(
110
+ (v) => v.path === "docs/rfcs/incoming.md" && v.operation === "move",
111
+ ),
112
+ ).toBe(true);
113
+ } finally {
114
+ await rm(dir, { recursive: true, force: true });
115
+ }
116
+ });
117
+
118
+ test("flags delete of a pinned file", async () => {
119
+ const dir = await makeRepo();
120
+ try {
121
+ await git(dir, ["rm", "docs/rfcs/rfc-0001.md"]);
122
+
123
+ const result = await runPinnedValidate(makeInput(), makeContext(dir));
124
+ expect(result.data?.status).toBe("fail");
125
+ expect(
126
+ result.data?.violations.some(
127
+ (v) => v.path === "docs/rfcs/rfc-0001.md" && v.operation === "delete",
128
+ ),
129
+ ).toBe(true);
130
+ } finally {
131
+ await rm(dir, { recursive: true, force: true });
132
+ }
133
+ });
134
+ });
@@ -162,16 +162,16 @@ test("all shipped profiles parse without changes", () => {
162
162
  expect(profile.id).toBeDefined();
163
163
  expect(profile.workspace.dirs.length).toBeGreaterThan(0);
164
164
  // Domain fields should be undefined for existing software-domain profiles
165
- if (profile.id !== "godot-csharp") {
165
+ if (profile.id !== "godot-game") {
166
166
  expect(profile.domain).toBeUndefined();
167
167
  expect(profile.register).toBeUndefined();
168
168
  }
169
169
  }
170
170
  });
171
171
 
172
- test("shipped phaser-turborepo profile loads via loadStackProfile", () => {
173
- const profile = loadStackProfile(join(FORGE_ROOT, "profiles", "phaser-turborepo.yaml"));
174
- expect(profile.id).toBe("phaser-turborepo");
172
+ test("shipped phaser-game profile loads via loadStackProfile", () => {
173
+ const profile = loadStackProfile(join(FORGE_ROOT, "profiles", "phaser-game.yaml"));
174
+ expect(profile.id).toBe("phaser-game");
175
175
  });
176
176
 
177
177
  test("UNIVERSAL_TERMINOLOGY_KEYS contains the 7 documented keys", () => {
@@ -73,7 +73,7 @@ test("forge.scaffold fails on missing --profile", async () => {
73
73
 
74
74
  test("forge.scaffold derives name from folder when --name omitted", async () => {
75
75
  const result = await runScaffoldProject(
76
- { argv: [], flags: { profile: "phaser-turborepo" } },
76
+ { argv: [], flags: { profile: "phaser-game" } },
77
77
  { ...makeContext(tempDir), forgeRoot: FORGE_ROOT },
78
78
  );
79
79
  // Name is now derived from the folder name (consistent with forge.create)
@@ -89,7 +89,7 @@ test("forge.scaffold derives name from folder when --name omitted", async () =>
89
89
 
90
90
  test("forge.scaffold fails on non-kebab-case name", async () => {
91
91
  const result = await runScaffoldProject(
92
- { argv: [], flags: { profile: "phaser-turborepo", name: "MySite" } },
92
+ { argv: [], flags: { profile: "phaser-game", name: "MySite" } },
93
93
  { ...makeContext(tempDir), forgeRoot: FORGE_ROOT },
94
94
  );
95
95
  expect(result.exitCode).toBe(1);
@@ -129,7 +129,7 @@ test("forge.init --from detects stack from existing project", async () => {
129
129
  const result = runInit({ flags: { from: fakeProject } }, { workspaceRoot: WORKSPACE_ROOT });
130
130
  expect(result.status).toBe("pass");
131
131
  expect(result.detection).toBeDefined();
132
- expect(result.detection?.profile).toBe("phaser-turborepo");
132
+ expect(result.detection?.profile).toBe("phaser-game");
133
133
  });
134
134
 
135
135
  test("forge.init --from reports null when stack undetectable", async () => {
@@ -139,31 +139,31 @@ test("detectStack matches on exact filename", async () => {
139
139
  expect(match?.id).toBe("ts");
140
140
  });
141
141
 
142
- test("shipped phaser-turborepo profile validates", () => {
143
- const profile = loadStackProfile(join(FORGE_ROOT, "profiles", "phaser-turborepo.yaml"));
144
- expect(profile.id).toBe("phaser-turborepo");
142
+ test("shipped phaser-game profile validates", () => {
143
+ const profile = loadStackProfile(join(FORGE_ROOT, "profiles", "phaser-game.yaml"));
144
+ expect(profile.id).toBe("phaser-game");
145
145
  expect(profile.workspace.dirs.length).toBeGreaterThan(0);
146
146
  });
147
147
 
148
- test("shipped godot-csharp profile validates", () => {
149
- const profile = loadStackProfile(join(FORGE_ROOT, "profiles", "godot-csharp.yaml"));
150
- expect(profile.id).toBe("godot-csharp");
148
+ test("shipped godot-game profile validates", () => {
149
+ const profile = loadStackProfile(join(FORGE_ROOT, "profiles", "godot-game.yaml"));
150
+ expect(profile.id).toBe("godot-game");
151
151
  expect(profile.workspace.dirs.length).toBeGreaterThan(0);
152
152
  });
153
153
 
154
154
  test("listStackProfiles finds all shipped profiles", () => {
155
155
  const profiles = listStackProfiles(FORGE_ROOT);
156
156
  const ids = profiles.map((p) => p.id).sort();
157
- expect(ids).toContain("phaser-turborepo");
158
- expect(ids).toContain("godot-csharp");
157
+ expect(ids).toContain("phaser-game");
158
+ expect(ids).toContain("godot-game");
159
159
  expect(ids).toContain("forge-shell");
160
160
  });
161
161
 
162
162
  test("all shipped profiles include @warpgogol/forge in install steps or package.json template", () => {
163
163
  const profiles = listStackProfiles(FORGE_ROOT);
164
164
  for (const profile of profiles) {
165
- // knowledge-typescript-turborepo uses @warpgogol/werkstatt-knowledge plugin, not forge
166
- if (profile.id === "knowledge-typescript-turborepo") continue;
165
+ // knowledge uses @warpgogol/werkstatt-knowledge plugin, not forge
166
+ if (profile.id === "knowledge") continue;
167
167
  const hasForgeInInstall = profile.install.some((cmd) => cmd.includes("@warpgogol/forge"));
168
168
  const pkgFile = profile.workspace.files.find((f) => f.path === "package.json");
169
169
  const hasForgeInPkg = pkgFile?.content.includes("@warpgogol/forge") ?? false;
@@ -174,8 +174,8 @@ test("all shipped profiles include @warpgogol/forge in install steps or package.
174
174
  test("all shipped profiles include operator-profile.md in .gitignore content", () => {
175
175
  const profiles = listStackProfiles(FORGE_ROOT);
176
176
  for (const profile of profiles) {
177
- // knowledge-typescript-turborepo uses a different plugin and gitignore template
178
- if (profile.id === "knowledge-typescript-turborepo") continue;
177
+ // knowledge uses a different plugin and gitignore template
178
+ if (profile.id === "knowledge") continue;
179
179
  const gitignoreFile = profile.workspace.files.find((f) => f.path === ".gitignore");
180
180
  expect(gitignoreFile).toBeDefined();
181
181
  expect(gitignoreFile?.content).toContain("operator-profile.md");