@jamie-tam/forge 6.0.0

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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +389 -0
  3. package/agents/architect.md +92 -0
  4. package/agents/builder.md +122 -0
  5. package/agents/code-reviewer.md +107 -0
  6. package/agents/concept-designer.md +207 -0
  7. package/agents/craft-reviewer.md +132 -0
  8. package/agents/critic.md +130 -0
  9. package/agents/doc-writer.md +85 -0
  10. package/agents/dreamer.md +129 -0
  11. package/agents/e2e-runner.md +89 -0
  12. package/agents/gotcha-hunter.md +127 -0
  13. package/agents/prototype-builder.md +193 -0
  14. package/agents/prototype-codifier.md +204 -0
  15. package/agents/prototype-reviewer.md +163 -0
  16. package/agents/security-reviewer.md +108 -0
  17. package/agents/spec-reviewer.md +94 -0
  18. package/agents/tracer.md +98 -0
  19. package/agents/wireframer.md +109 -0
  20. package/commands/abort.md +25 -0
  21. package/commands/bugfix.md +151 -0
  22. package/commands/evolve.md +118 -0
  23. package/commands/feature.md +236 -0
  24. package/commands/forge.md +100 -0
  25. package/commands/greenfield.md +185 -0
  26. package/commands/hotfix.md +98 -0
  27. package/commands/refactor.md +147 -0
  28. package/commands/resume.md +25 -0
  29. package/commands/setup.md +201 -0
  30. package/commands/status.md +27 -0
  31. package/commands/task-force.md +110 -0
  32. package/commands/validate.md +12 -0
  33. package/dist/__tests__/active-manifest.test.js +272 -0
  34. package/dist/__tests__/copy.test.js +96 -0
  35. package/dist/__tests__/gate-check.test.js +384 -0
  36. package/dist/__tests__/wiki.test.js +472 -0
  37. package/dist/__tests__/work-manifest.test.js +304 -0
  38. package/dist/active-manifest.js +229 -0
  39. package/dist/cli.js +158 -0
  40. package/dist/copy.js +124 -0
  41. package/dist/gate-check.js +326 -0
  42. package/dist/hooks.js +60 -0
  43. package/dist/init.js +140 -0
  44. package/dist/manifest.js +90 -0
  45. package/dist/merge.js +77 -0
  46. package/dist/paths.js +36 -0
  47. package/dist/uninstall.js +216 -0
  48. package/dist/update.js +158 -0
  49. package/dist/verify-manifest.js +65 -0
  50. package/dist/verify.js +98 -0
  51. package/dist/wiki-ui.js +310 -0
  52. package/dist/wiki.js +364 -0
  53. package/dist/work-manifest.js +798 -0
  54. package/hooks/config/gate-requirements.json +79 -0
  55. package/hooks/hooks.json +143 -0
  56. package/hooks/scripts/analyze-telemetry.sh +114 -0
  57. package/hooks/scripts/gate-enforcer.sh +164 -0
  58. package/hooks/scripts/pre-compact.sh +90 -0
  59. package/hooks/scripts/session-start.sh +81 -0
  60. package/hooks/scripts/telemetry.sh +41 -0
  61. package/hooks/scripts/wiki-lint.sh +87 -0
  62. package/hooks/templates/AGENTS.md.template +48 -0
  63. package/hooks/templates/CLAUDE.md.template +45 -0
  64. package/package.json +55 -0
  65. package/protocols/README.md +40 -0
  66. package/protocols/codex.md +151 -0
  67. package/protocols/graphify.md +156 -0
  68. package/references/common/agent-coordination.md +65 -0
  69. package/references/common/coding-standards.md +54 -0
  70. package/references/common/feature-tracking.md +21 -0
  71. package/references/common/io-protocol.md +36 -0
  72. package/references/common/phases.md +57 -0
  73. package/references/common/quality-gates.md +130 -0
  74. package/references/common/skill-authoring.md +154 -0
  75. package/references/common/skill-compliance.md +30 -0
  76. package/references/python/standards.md +44 -0
  77. package/references/react/standards.md +61 -0
  78. package/references/typescript/standards.md +42 -0
  79. package/rules/common/forge-system.md +59 -0
  80. package/rules/common/git-workflow.md +40 -0
  81. package/rules/common/guardrails.md +37 -0
  82. package/rules/common/quality-gates.md +18 -0
  83. package/rules/common/security.md +50 -0
  84. package/rules/common/skill-selection.md +78 -0
  85. package/rules/common/testing.md +58 -0
  86. package/rules/common/verification.md +39 -0
  87. package/skills/build-pr-workflow/SKILL.md +301 -0
  88. package/skills/build-pr-workflow/references/pr-template.md +62 -0
  89. package/skills/build-pr-workflow/references/subagent-merge.md +47 -0
  90. package/skills/build-pr-workflow/references/worktree-setup.md +125 -0
  91. package/skills/build-prototype/SKILL.md +264 -0
  92. package/skills/build-scaffold/SKILL.md +340 -0
  93. package/skills/build-tdd/SKILL.md +89 -0
  94. package/skills/build-wireframe/SKILL.md +110 -0
  95. package/skills/build-wireframe/assets/baseline-template.html +486 -0
  96. package/skills/build-wireframe/references/demo-walkthroughs.md +170 -0
  97. package/skills/build-wireframe/references/gotchas.md +188 -0
  98. package/skills/build-wireframe/references/legend-lines.md +141 -0
  99. package/skills/concept-slides/SKILL.md +192 -0
  100. package/skills/deliver-db-migration/SKILL.md +466 -0
  101. package/skills/deliver-deploy/SKILL.md +407 -0
  102. package/skills/deliver-onboarding/SKILL.md +198 -0
  103. package/skills/deliver-onboarding/references/document-templates.md +393 -0
  104. package/skills/deliver-onboarding/templates/getting-started.md +122 -0
  105. package/skills/discover-codebase-analysis/SKILL.md +448 -0
  106. package/skills/discover-requirements/SKILL.md +418 -0
  107. package/skills/discover-requirements/templates/prd.md +99 -0
  108. package/skills/discover-requirements/templates/technical-spec.md +123 -0
  109. package/skills/discover-requirements/templates/user-stories.md +76 -0
  110. package/skills/harden/SKILL.md +214 -0
  111. package/skills/iterate-prototype/SKILL.md +241 -0
  112. package/skills/plan-architecture/SKILL.md +457 -0
  113. package/skills/plan-architecture/templates/adr-template.md +52 -0
  114. package/skills/plan-architecture/templates/api-contract.md +99 -0
  115. package/skills/plan-architecture/templates/db-schema.md +81 -0
  116. package/skills/plan-architecture/templates/system-design.md +111 -0
  117. package/skills/plan-brainstorm/SKILL.md +433 -0
  118. package/skills/plan-design-system/SKILL.md +279 -0
  119. package/skills/plan-task-decompose/SKILL.md +454 -0
  120. package/skills/quality-code-review/SKILL.md +286 -0
  121. package/skills/quality-security-audit/SKILL.md +292 -0
  122. package/skills/quality-security-audit/references/audit-report-template.md +89 -0
  123. package/skills/quality-security-audit/references/owasp-checks.md +178 -0
  124. package/skills/quality-test-execution/SKILL.md +435 -0
  125. package/skills/quality-test-plan/SKILL.md +297 -0
  126. package/skills/quality-test-plan/references/test-type-guide.md +263 -0
  127. package/skills/quality-test-plan/templates/e2e-test-plan.md +72 -0
  128. package/skills/quality-test-plan/templates/integration-test-plan.md +74 -0
  129. package/skills/quality-test-plan/templates/load-test-plan.md +111 -0
  130. package/skills/quality-test-plan/templates/smoke-test-plan.md +68 -0
  131. package/skills/quality-test-plan/templates/unit-test-plan.md +56 -0
  132. package/skills/quality-uiux/SKILL.md +481 -0
  133. package/skills/support-debug/SKILL.md +464 -0
  134. package/skills/support-dream/SKILL.md +213 -0
  135. package/skills/support-gotcha/SKILL.md +249 -0
  136. package/skills/support-runtime-reachability/SKILL.md +190 -0
  137. package/skills/support-runtime-reachability/scripts/__fixtures__/case-01-passes-app-use/src/app.ts +7 -0
  138. package/skills/support-runtime-reachability/scripts/__fixtures__/case-01-passes-app-use/src/handlers/cases.ts +7 -0
  139. package/skills/support-runtime-reachability/scripts/__fixtures__/case-02-orphan-no-app-use/src/app.ts +8 -0
  140. package/skills/support-runtime-reachability/scripts/__fixtures__/case-02-orphan-no-app-use/src/handlers/cases.ts +7 -0
  141. package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/App.tsx +5 -0
  142. package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/components/RingingBanner.tsx +7 -0
  143. package/skills/support-runtime-reachability/scripts/__fixtures__/case-03-orphan-import-only/src/hooks/useTwilio.ts +6 -0
  144. package/skills/support-runtime-reachability/scripts/__fixtures__/case-04-jsx-component-rendered/src/App.tsx +5 -0
  145. package/skills/support-runtime-reachability/scripts/__fixtures__/case-04-jsx-component-rendered/src/components/MyComp.tsx +3 -0
  146. package/skills/support-runtime-reachability/scripts/__fixtures__/case-05-jsx-component-not-rendered/src/App.tsx +3 -0
  147. package/skills/support-runtime-reachability/scripts/__fixtures__/case-05-jsx-component-not-rendered/src/components/Orphan.tsx +3 -0
  148. package/skills/support-runtime-reachability/scripts/__fixtures__/case-06-class-instantiated/src/lib/Service.ts +6 -0
  149. package/skills/support-runtime-reachability/scripts/__fixtures__/case-06-class-instantiated/src/main.ts +4 -0
  150. package/skills/support-runtime-reachability/scripts/__fixtures__/case-07-class-not-instantiated/src/lib/Lonely.ts +5 -0
  151. package/skills/support-runtime-reachability/scripts/__fixtures__/case-07-class-not-instantiated/src/main.ts +2 -0
  152. package/skills/support-runtime-reachability/scripts/__fixtures__/case-08-default-export-imported-and-called/src/handler.ts +3 -0
  153. package/skills/support-runtime-reachability/scripts/__fixtures__/case-08-default-export-imported-and-called/src/main.ts +3 -0
  154. package/skills/support-runtime-reachability/scripts/__fixtures__/case-09-default-export-orphan/src/handler.ts +3 -0
  155. package/skills/support-runtime-reachability/scripts/__fixtures__/case-09-default-export-orphan/src/main.ts +2 -0
  156. package/skills/support-runtime-reachability/scripts/__fixtures__/case-10-aliased-named-export/src/lib.ts +5 -0
  157. package/skills/support-runtime-reachability/scripts/__fixtures__/case-10-aliased-named-export/src/main.ts +3 -0
  158. package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/lib/index.ts +1 -0
  159. package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/lib/internal.ts +3 -0
  160. package/skills/support-runtime-reachability/scripts/__fixtures__/case-11-re-export-chain/src/main.ts +3 -0
  161. package/skills/support-runtime-reachability/scripts/__fixtures__/case-12-test-only-caller/src/util.test.ts +5 -0
  162. package/skills/support-runtime-reachability/scripts/__fixtures__/case-12-test-only-caller/src/util.ts +3 -0
  163. package/skills/support-runtime-reachability/scripts/__fixtures__/case-13-gated-pending-annotation/src/future.ts +4 -0
  164. package/skills/support-runtime-reachability/scripts/__fixtures__/case-14-untraceable-annotation/src/decorated.ts +4 -0
  165. package/skills/support-runtime-reachability/scripts/__fixtures__/case-15-untraceable-empty/src/lazy.ts +4 -0
  166. package/skills/support-runtime-reachability/scripts/__fixtures__/case-16-python-module/src/lib.py +15 -0
  167. package/skills/support-runtime-reachability/scripts/__fixtures__/case-16-python-module/src/main.py +5 -0
  168. package/skills/support-runtime-reachability/scripts/__fixtures__/case-17-router-use/src/parent.ts +5 -0
  169. package/skills/support-runtime-reachability/scripts/__fixtures__/case-17-router-use/src/routes/cases.ts +5 -0
  170. package/skills/support-runtime-reachability/scripts/__fixtures__/case-18-shadowed-name-fp/src/lib/foo.ts +3 -0
  171. package/skills/support-runtime-reachability/scripts/__fixtures__/case-18-shadowed-name-fp/src/other.ts +8 -0
  172. package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/handlers/cases.ts +4 -0
  173. package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/handlers/users.ts +4 -0
  174. package/skills/support-runtime-reachability/scripts/__fixtures__/case-19-same-name-different-module/src/main.ts +5 -0
  175. package/skills/support-runtime-reachability/scripts/__fixtures__/case-20-aliased-import-usage/src/handlers/cases.ts +3 -0
  176. package/skills/support-runtime-reachability/scripts/__fixtures__/case-20-aliased-import-usage/src/main.ts +4 -0
  177. package/skills/support-runtime-reachability/scripts/__fixtures__/case-21-mixed-default-and-named/src/lib.ts +5 -0
  178. package/skills/support-runtime-reachability/scripts/__fixtures__/case-21-mixed-default-and-named/src/main.ts +5 -0
  179. package/skills/support-runtime-reachability/scripts/__fixtures__/case-22-dynamic-import-then-caller/src/lib.ts +3 -0
  180. package/skills/support-runtime-reachability/scripts/__fixtures__/case-22-dynamic-import-then-caller/src/main.ts +8 -0
  181. package/skills/support-runtime-reachability/scripts/__fixtures__/case-23-dynamic-import-with-space/src/lib.ts +3 -0
  182. package/skills/support-runtime-reachability/scripts/__fixtures__/case-23-dynamic-import-with-space/src/main.ts +7 -0
  183. package/skills/support-runtime-reachability/scripts/check.mjs +638 -0
  184. package/skills/support-runtime-reachability/scripts/check.test.mjs +244 -0
  185. package/skills/support-skill-validator/SKILL.md +194 -0
  186. package/skills/support-skill-validator/references/false-positives.md +59 -0
  187. package/skills/support-skill-validator/references/validation-checks.md +280 -0
  188. package/skills/support-system-guide/SKILL.md +311 -0
  189. package/skills/support-task-force/SKILL.md +265 -0
  190. package/skills/support-task-force/references/dispatch-pattern.md +178 -0
  191. package/skills/support-task-force/references/synthesis-template.md +126 -0
  192. package/skills/support-wiki-bootstrap/SKILL.md +37 -0
  193. package/skills/support-wiki-lint/SKILL.md +196 -0
  194. package/skills/support-wiki-lint/scripts/lint.mjs +488 -0
  195. package/skills/support-wiki-lint/scripts/lint.test.mjs +196 -0
  196. package/templates/README.md +23 -0
  197. package/templates/aiwiki/CLAUDE.md.template +78 -0
  198. package/templates/aiwiki/schemas/architecture.md +118 -0
  199. package/templates/aiwiki/schemas/convention.md +112 -0
  200. package/templates/aiwiki/schemas/decision.md +144 -0
  201. package/templates/aiwiki/schemas/gotcha.md +118 -0
  202. package/templates/aiwiki/schemas/oracle.md +105 -0
  203. package/templates/aiwiki/schemas/session.md +125 -0
  204. package/templates/manifests/bugfix.yaml +41 -0
  205. package/templates/manifests/feature.yaml +69 -0
  206. package/templates/manifests/greenfield.yaml +61 -0
  207. package/templates/manifests/hotfix.yaml +45 -0
  208. package/templates/manifests/refactor.yaml +44 -0
  209. package/templates/manifests/v5/SCHEMA.md +327 -0
  210. package/templates/manifests/v5/feature.yaml +77 -0
  211. package/templates/manifests/v6/SCHEMA.md +199 -0
  212. package/templates/wiki-html/dream-detail.html +378 -0
  213. package/templates/wiki-html/dreams-list.html +155 -0
@@ -0,0 +1,90 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+ /** SHA-256 hash of file contents */
5
+ export function hashFile(filePath) {
6
+ const content = readFileSync(filePath);
7
+ return createHash("sha256").update(content).digest("hex");
8
+ }
9
+ /** Recursively collect all files under a directory with their hashes, relative to base */
10
+ export function collectHashes(dir, base) {
11
+ const result = {};
12
+ if (!existsSync(dir))
13
+ return result;
14
+ for (const entry of readdirSync(dir)) {
15
+ const fullPath = join(dir, entry);
16
+ const relPath = relative(base, fullPath);
17
+ if (statSync(fullPath).isDirectory()) {
18
+ Object.assign(result, collectHashes(fullPath, base));
19
+ }
20
+ else {
21
+ result[relPath] = hashFile(fullPath);
22
+ }
23
+ }
24
+ return result;
25
+ }
26
+ /** Read existing manifest from forge.json */
27
+ export function readManifest(forgeJsonPath) {
28
+ if (!existsSync(forgeJsonPath))
29
+ return null;
30
+ try {
31
+ return JSON.parse(readFileSync(forgeJsonPath, "utf-8"));
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ /** Write manifest to forge.json */
38
+ export function writeManifest(forgeJsonPath, manifest) {
39
+ writeFileSync(forgeJsonPath, JSON.stringify(manifest, null, 2) + "\n");
40
+ }
41
+ /**
42
+ * Classify files into categories for update/uninstall operations.
43
+ * Compares installed manifest against current state and new source.
44
+ */
45
+ export function classifyFiles(installedFiles, currentFiles, sourceFiles) {
46
+ const result = {
47
+ unchanged: [],
48
+ forgeUpdated: [],
49
+ userModified: [],
50
+ userOnly: [],
51
+ stale: [],
52
+ newFiles: [],
53
+ };
54
+ // Check files that were installed
55
+ for (const [file, installedHash] of Object.entries(installedFiles)) {
56
+ const currentHash = currentFiles[file];
57
+ const sourceHash = sourceFiles[file];
58
+ if (!sourceHash) {
59
+ // Forge no longer ships this file
60
+ result.stale.push(file);
61
+ }
62
+ else if (!currentHash) {
63
+ // File was deleted by user — forge will re-create
64
+ result.newFiles.push(file);
65
+ }
66
+ else if (currentHash === installedHash && sourceHash === installedHash) {
67
+ // Nobody changed it
68
+ result.unchanged.push(file);
69
+ }
70
+ else if (currentHash === installedHash && sourceHash !== installedHash) {
71
+ // Forge updated, user didn't touch — safe overwrite
72
+ result.forgeUpdated.push(file);
73
+ }
74
+ else if (currentHash !== installedHash && sourceHash === installedHash) {
75
+ // User modified, forge didn't change — preserve user version
76
+ result.userOnly.push(file);
77
+ }
78
+ else if (currentHash !== installedHash && sourceHash !== installedHash) {
79
+ // Both changed — conflict
80
+ result.userModified.push(file);
81
+ }
82
+ }
83
+ // Check files in source that weren't previously installed
84
+ for (const file of Object.keys(sourceFiles)) {
85
+ if (!installedFiles[file]) {
86
+ result.newFiles.push(file);
87
+ }
88
+ }
89
+ return result;
90
+ }
package/dist/merge.js ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Marker-based merge for co-edited files (CLAUDE.md, AGENTS.md).
3
+ *
4
+ * Files are partitioned into user-owned zones (before/after markers)
5
+ * and a framework-managed zone (between markers). On update, the
6
+ * framework zone is replaced while user content is preserved.
7
+ */
8
+ export const FORGE_START = "<!-- FORGE:START — do not edit between markers; updated by forge -->";
9
+ export const FORGE_END = "<!-- FORGE:END -->";
10
+ /** Count non-overlapping occurrences of `needle` as a full line in `text`. */
11
+ function countMarkerLines(text, needle) {
12
+ let count = 0;
13
+ for (const line of text.split("\n")) {
14
+ if (line.trim() === needle)
15
+ count++;
16
+ }
17
+ return count;
18
+ }
19
+ /** Find the byte offset of a marker line within `text`. Returns -1 if not found. */
20
+ function markerOffset(text, needle) {
21
+ const lines = text.split("\n");
22
+ let offset = 0;
23
+ for (const line of lines) {
24
+ if (line.trim() === needle)
25
+ return offset;
26
+ offset += line.length + 1; // +1 for the \n
27
+ }
28
+ return -1;
29
+ }
30
+ /** True if content contains exactly one START and one END marker in correct order. */
31
+ export function hasForgeMarkers(content) {
32
+ return (countMarkerLines(content, FORGE_START) === 1 &&
33
+ countMarkerLines(content, FORGE_END) === 1 &&
34
+ markerOffset(content, FORGE_START) < markerOffset(content, FORGE_END));
35
+ }
36
+ /**
37
+ * Extract the forge block (START line through END line, inclusive) from content.
38
+ * Returns the block string and the before/after slices.
39
+ */
40
+ function splitAtMarkers(content) {
41
+ if (!hasForgeMarkers(content))
42
+ return null;
43
+ const startOffset = markerOffset(content, FORGE_START);
44
+ const endOffset = markerOffset(content, FORGE_END);
45
+ const endLineEnd = content.indexOf("\n", endOffset);
46
+ const blockEnd = endLineEnd === -1 ? content.length : endLineEnd + 1;
47
+ return {
48
+ before: content.slice(0, startOffset),
49
+ block: content.slice(startOffset, blockEnd),
50
+ after: content.slice(blockEnd),
51
+ };
52
+ }
53
+ /**
54
+ * Merge framework content from `template` into `existing` using markers.
55
+ *
56
+ * - Extracts the FORGE block from both strings.
57
+ * - Replaces the existing block with the template block.
58
+ * - Preserves user content outside the markers exactly as-is.
59
+ *
60
+ * Returns the merged string, or null if merge is not possible
61
+ * (legacy file without markers, corrupted markers, duplicate markers).
62
+ *
63
+ * Throws if the template is missing valid markers (developer error).
64
+ */
65
+ export function mergeForgeBlock(existing, template) {
66
+ // Validate template — missing markers is a developer bug
67
+ const tParts = splitAtMarkers(template);
68
+ if (!tParts) {
69
+ throw new Error("Template is missing valid FORGE:START/FORGE:END markers. This is a bug in the template.");
70
+ }
71
+ // Parse existing — missing/invalid markers means legacy file
72
+ const eParts = splitAtMarkers(existing);
73
+ if (!eParts)
74
+ return null;
75
+ // Replace the framework block, preserve user content
76
+ return eParts.before + tParts.block + eParts.after;
77
+ }
package/dist/paths.js ADDED
@@ -0,0 +1,36 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ const __dirname = dirname(fileURLToPath(import.meta.url));
4
+ /** Root of the forge package (one level up from dist/) */
5
+ export const PACKAGE_ROOT = resolve(__dirname, "..");
6
+ /** Source asset directories inside the package */
7
+ export const SOURCE = {
8
+ agents: resolve(PACKAGE_ROOT, "agents"),
9
+ commands: resolve(PACKAGE_ROOT, "commands"),
10
+ skills: resolve(PACKAGE_ROOT, "skills"),
11
+ rules: resolve(PACKAGE_ROOT, "rules"),
12
+ protocols: resolve(PACKAGE_ROOT, "protocols"),
13
+ references: resolve(PACKAGE_ROOT, "references"),
14
+ hooks: resolve(PACKAGE_ROOT, "hooks"),
15
+ templates: resolve(PACKAGE_ROOT, "templates"),
16
+ claudeMdTemplate: resolve(PACKAGE_ROOT, "hooks", "templates", "CLAUDE.md.template"),
17
+ agentsMdTemplate: resolve(PACKAGE_ROOT, "hooks", "templates", "AGENTS.md.template"),
18
+ };
19
+ /** Target directories inside the user's project */
20
+ export function targets(projectRoot) {
21
+ const claude = resolve(projectRoot, ".claude");
22
+ return {
23
+ claude,
24
+ agents: resolve(claude, "agents"),
25
+ commands: resolve(claude, "commands"),
26
+ skills: resolve(claude, "skills"),
27
+ rules: resolve(claude, "rules"),
28
+ protocols: resolve(claude, "protocols"),
29
+ references: resolve(claude, "references"),
30
+ claudeMd: resolve(claude, "CLAUDE.md"),
31
+ agentsMd: resolve(projectRoot, "AGENTS.md"),
32
+ templates: resolve(claude, "templates"),
33
+ forgeState: resolve(projectRoot, ".forge"),
34
+ forgeJson: resolve(projectRoot, ".forge", "forge.json"),
35
+ };
36
+ }
@@ -0,0 +1,216 @@
1
+ import { existsSync, readdirSync, rmSync, statSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { cwd } from "node:process";
4
+ import { SOURCE, targets } from "./paths.js";
5
+ import { readManifest } from "./manifest.js";
6
+ /**
7
+ * Remove only forge-managed files from a project.
8
+ * Uses the installed manifest (forge.json) when available for precision.
9
+ * Falls back to comparing against source package if manifest lacks file tracking.
10
+ */
11
+ export async function uninstall(opts) {
12
+ const projectRoot = cwd();
13
+ const t = targets(projectRoot);
14
+ if (!existsSync(t.forgeJson)) {
15
+ console.log("Forge is not installed in this project.");
16
+ return;
17
+ }
18
+ const manifest = readManifest(t.forgeJson);
19
+ const prefix = opts.dryRun ? "Would remove" : "Removed";
20
+ console.log(`forge uninstall${opts.dryRun ? " --dry-run" : ""}${opts.purge ? " --purge" : ""}\n`);
21
+ let removedCount = 0;
22
+ let preservedCount = 0;
23
+ // Strategy: use manifest file list if available, fall back to source comparison
24
+ if (manifest?.files && Object.keys(manifest.files).length > 0) {
25
+ // Manifest-based removal — precise
26
+ console.log("Using installed file manifest for precise removal.\n");
27
+ for (const relPath of Object.keys(manifest.files)) {
28
+ const target = join(t.claude, relPath);
29
+ if (existsSync(target)) {
30
+ if (!opts.dryRun)
31
+ rmSync(target);
32
+ console.log(` ${prefix}: .claude/${relPath}`);
33
+ removedCount++;
34
+ }
35
+ }
36
+ // Clean up empty directories left behind
37
+ if (!opts.dryRun) {
38
+ for (const relPath of Object.keys(manifest.files)) {
39
+ let dir = dirname(join(t.claude, relPath));
40
+ while (dir !== t.claude && existsSync(dir)) {
41
+ if (readdirSync(dir).length === 0) {
42
+ rmSync(dir, { recursive: true });
43
+ }
44
+ else {
45
+ break;
46
+ }
47
+ dir = dirname(dir);
48
+ }
49
+ }
50
+ }
51
+ }
52
+ else {
53
+ // Fallback: compare against source package
54
+ console.log("No file manifest — using source comparison.\n");
55
+ // Remove flat-dir assets
56
+ for (const [label, src, dest] of [
57
+ ["agents", SOURCE.agents, t.agents],
58
+ ["commands", SOURCE.commands, t.commands],
59
+ ["protocols", SOURCE.protocols, t.protocols],
60
+ ]) {
61
+ if (!existsSync(src) || !existsSync(dest))
62
+ continue;
63
+ const forgeFiles = readdirSync(src).filter((f) => f.endsWith(".md"));
64
+ for (const file of forgeFiles) {
65
+ const target = join(dest, file);
66
+ if (existsSync(target)) {
67
+ if (!opts.dryRun)
68
+ rmSync(target);
69
+ console.log(` ${prefix}: ${label}/${file}`);
70
+ removedCount++;
71
+ }
72
+ }
73
+ if (!opts.dryRun && existsSync(dest) && readdirSync(dest).length === 0) {
74
+ rmSync(dest, { recursive: true });
75
+ }
76
+ }
77
+ // Remove tree-dir assets
78
+ for (const [label, src, dest] of [
79
+ ["skills", SOURCE.skills, t.skills],
80
+ ["templates", SOURCE.templates, t.templates],
81
+ ]) {
82
+ if (!existsSync(src) || !existsSync(dest))
83
+ continue;
84
+ const forgeDirs = readdirSync(src).filter((f) => statSync(join(src, f)).isDirectory());
85
+ for (const dir of forgeDirs) {
86
+ const target = join(dest, dir);
87
+ if (existsSync(target)) {
88
+ if (!opts.dryRun)
89
+ rmSync(target, { recursive: true });
90
+ console.log(` ${prefix}: ${label}/${dir}/`);
91
+ removedCount++;
92
+ }
93
+ }
94
+ if (!opts.dryRun && existsSync(dest) && readdirSync(dest).length === 0) {
95
+ rmSync(dest, { recursive: true });
96
+ }
97
+ }
98
+ // Remove references (per-file in subdirs, same structure as rules)
99
+ if (existsSync(SOURCE.references) && existsSync(t.references)) {
100
+ for (const subdir of readdirSync(SOURCE.references)) {
101
+ const srcSub = join(SOURCE.references, subdir);
102
+ const destSub = join(t.references, subdir);
103
+ if (!statSync(srcSub).isDirectory() || !existsSync(destSub))
104
+ continue;
105
+ for (const file of readdirSync(srcSub).filter((f) => f.endsWith(".md"))) {
106
+ const target = join(destSub, file);
107
+ if (existsSync(target)) {
108
+ if (!opts.dryRun)
109
+ rmSync(target);
110
+ console.log(` ${prefix}: references/${subdir}/${file}`);
111
+ removedCount++;
112
+ }
113
+ }
114
+ if (!opts.dryRun && existsSync(destSub) && readdirSync(destSub).length === 0) {
115
+ rmSync(destSub, { recursive: true });
116
+ }
117
+ }
118
+ if (!opts.dryRun && existsSync(t.references) && readdirSync(t.references).length === 0) {
119
+ rmSync(t.references, { recursive: true });
120
+ }
121
+ }
122
+ // Remove rules (per-file in subdirs)
123
+ if (existsSync(SOURCE.rules) && existsSync(t.rules)) {
124
+ for (const subdir of readdirSync(SOURCE.rules)) {
125
+ const srcSub = join(SOURCE.rules, subdir);
126
+ const destSub = join(t.rules, subdir);
127
+ if (!statSync(srcSub).isDirectory() || !existsSync(destSub))
128
+ continue;
129
+ for (const file of readdirSync(srcSub).filter((f) => f.endsWith(".md"))) {
130
+ const target = join(destSub, file);
131
+ if (existsSync(target)) {
132
+ if (!opts.dryRun)
133
+ rmSync(target);
134
+ console.log(` ${prefix}: rules/${subdir}/${file}`);
135
+ removedCount++;
136
+ }
137
+ }
138
+ if (!opts.dryRun && existsSync(destSub) && readdirSync(destSub).length === 0) {
139
+ rmSync(destSub, { recursive: true });
140
+ }
141
+ }
142
+ if (!opts.dryRun && existsSync(t.rules) && readdirSync(t.rules).length === 0) {
143
+ rmSync(t.rules, { recursive: true });
144
+ }
145
+ }
146
+ }
147
+ // Check for user files that survived
148
+ for (const dir of [t.agents, t.commands, t.skills, t.rules, t.references, t.protocols, t.templates]) {
149
+ if (existsSync(dir)) {
150
+ const remaining = readdirSync(dir).length;
151
+ if (remaining > 0) {
152
+ preservedCount += remaining;
153
+ }
154
+ }
155
+ }
156
+ // Remove hooks (always forge-managed)
157
+ const hooksDir = join(t.claude, "hooks");
158
+ if (existsSync(hooksDir)) {
159
+ if (!opts.dryRun)
160
+ rmSync(hooksDir, { recursive: true });
161
+ console.log(` ${prefix}: hooks/`);
162
+ }
163
+ const settingsLocal = join(t.claude, "settings.local.json");
164
+ if (existsSync(settingsLocal)) {
165
+ if (!opts.dryRun)
166
+ rmSync(settingsLocal);
167
+ console.log(` ${prefix}: settings.local.json`);
168
+ }
169
+ // CLAUDE.md and AGENTS.md — only with --force
170
+ if (opts.force) {
171
+ if (existsSync(t.claudeMd)) {
172
+ if (!opts.dryRun)
173
+ rmSync(t.claudeMd);
174
+ console.log(` ${prefix}: CLAUDE.md`);
175
+ }
176
+ if (existsSync(t.agentsMd)) {
177
+ if (!opts.dryRun)
178
+ rmSync(t.agentsMd);
179
+ console.log(` ${prefix}: AGENTS.md`);
180
+ }
181
+ }
182
+ else {
183
+ if (existsSync(t.claudeMd))
184
+ console.log(" Preserved: CLAUDE.md (use --force to remove)");
185
+ if (existsSync(t.agentsMd))
186
+ console.log(" Preserved: AGENTS.md (use --force to remove)");
187
+ }
188
+ // .forge/ state — only with --purge
189
+ if (opts.purge) {
190
+ if (existsSync(t.forgeState)) {
191
+ if (!opts.dryRun)
192
+ rmSync(t.forgeState, { recursive: true });
193
+ console.log(` ${prefix}: .forge/ (state, manifests, decisions, gotchas)`);
194
+ }
195
+ }
196
+ else if (existsSync(t.forgeState)) {
197
+ if (!opts.dryRun) {
198
+ // Remove forge.json (install marker) but keep user state
199
+ if (existsSync(t.forgeJson))
200
+ rmSync(t.forgeJson);
201
+ }
202
+ console.log(" Preserved: .forge/ state (use --purge to remove)");
203
+ }
204
+ // Clean up .claude/ if empty
205
+ if (!opts.dryRun && existsSync(t.claude)) {
206
+ const remaining = readdirSync(t.claude);
207
+ if (remaining.length === 0) {
208
+ rmSync(t.claude, { recursive: true });
209
+ console.log(" Removed: empty .claude/");
210
+ }
211
+ }
212
+ const summary = [`${removedCount} forge files removed`];
213
+ if (preservedCount > 0)
214
+ summary.push(`${preservedCount} user files preserved`);
215
+ console.log(`\n${opts.dryRun ? "Dry run complete" : "Forge uninstalled"}. ${summary.join(", ")}.`);
216
+ }
package/dist/update.js ADDED
@@ -0,0 +1,158 @@
1
+ import { existsSync, readdirSync, readFileSync, rmdirSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { cwd } from "node:process";
4
+ import { SOURCE, targets } from "./paths.js";
5
+ import { init } from "./init.js";
6
+ import { classifyFiles, collectHashes, readManifest } from "./manifest.js";
7
+ /**
8
+ * Recursively walk `dir` bottom-up; remove any empty subdirectories.
9
+ * Leaves `dir` itself untouched even if it becomes empty (caller decides).
10
+ * Silently skips on read/rm errors — empty-dir cleanup is best-effort.
11
+ */
12
+ function pruneEmptyDirs(dir) {
13
+ if (!existsSync(dir))
14
+ return;
15
+ let entries;
16
+ try {
17
+ entries = readdirSync(dir);
18
+ }
19
+ catch {
20
+ return;
21
+ }
22
+ for (const entry of entries) {
23
+ const child = join(dir, entry);
24
+ let st;
25
+ try {
26
+ st = statSync(child);
27
+ }
28
+ catch {
29
+ continue;
30
+ }
31
+ if (st.isDirectory()) {
32
+ pruneEmptyDirs(child);
33
+ try {
34
+ if (readdirSync(child).length === 0) {
35
+ rmdirSync(child);
36
+ }
37
+ }
38
+ catch {
39
+ // not empty or unreadable — leave it
40
+ }
41
+ }
42
+ }
43
+ }
44
+ export async function update(opts) {
45
+ const projectRoot = cwd();
46
+ const t = targets(projectRoot);
47
+ if (!existsSync(t.forgeJson)) {
48
+ console.log("Forge not initialized. Run 'forge init' first.");
49
+ return;
50
+ }
51
+ const manifest = readManifest(t.forgeJson);
52
+ if (!manifest) {
53
+ console.log("Corrupt forge.json. Run 'forge init --force' to re-install.");
54
+ return;
55
+ }
56
+ const pkg = JSON.parse(readFileSync(resolve(SOURCE.agents, "..", "package.json"), "utf-8"));
57
+ if (manifest.version === pkg.version && !opts.force) {
58
+ console.log(`Already at v${manifest.version}. Use --force to re-sync.`);
59
+ return;
60
+ }
61
+ console.log(`forge update: v${manifest.version} → v${pkg.version}\n`);
62
+ // Collect current state of installed files
63
+ const currentFiles = {};
64
+ for (const dir of [t.agents, t.commands, t.skills, t.rules, t.references, t.protocols, t.templates]) {
65
+ if (existsSync(dir)) {
66
+ Object.assign(currentFiles, collectHashes(dir, t.claude));
67
+ }
68
+ }
69
+ // Collect source files (what the new version ships)
70
+ const sourceFiles = {};
71
+ for (const [src, prefix] of [
72
+ [SOURCE.agents, "agents"],
73
+ [SOURCE.commands, "commands"],
74
+ [SOURCE.skills, "skills"],
75
+ [SOURCE.rules, "rules"],
76
+ [SOURCE.references, "references"],
77
+ [SOURCE.protocols, "protocols"],
78
+ [SOURCE.templates, "templates"],
79
+ ]) {
80
+ if (existsSync(src)) {
81
+ const hashes = collectHashes(src, resolve(src, ".."));
82
+ Object.assign(sourceFiles, hashes);
83
+ }
84
+ }
85
+ // Classify files
86
+ const installedFiles = manifest.files || {};
87
+ const classified = classifyFiles(installedFiles, currentFiles, sourceFiles);
88
+ // Report what will happen
89
+ console.log("Update plan:");
90
+ if (classified.newFiles.length)
91
+ console.log(` + ${classified.newFiles.length} new files`);
92
+ if (classified.forgeUpdated.length)
93
+ console.log(` ~ ${classified.forgeUpdated.length} forge-updated (safe to overwrite)`);
94
+ if (classified.stale.length)
95
+ console.log(` - ${classified.stale.length} stale (forge no longer ships)`);
96
+ if (classified.userOnly.length)
97
+ console.log(` ✓ ${classified.userOnly.length} user-modified (forge unchanged — preserving)`);
98
+ if (classified.unchanged.length)
99
+ console.log(` = ${classified.unchanged.length} unchanged`);
100
+ // Handle conflicts (both forge and user changed)
101
+ if (classified.userModified.length > 0) {
102
+ console.log(`\n ⚠ ${classified.userModified.length} conflicts (both forge and user changed):`);
103
+ for (const f of classified.userModified) {
104
+ console.log(` ${f}`);
105
+ }
106
+ if (!opts.force) {
107
+ console.log("\n Use --force to overwrite conflicts, or manually merge these files.");
108
+ console.log(" Without --force, conflicting files are skipped.");
109
+ }
110
+ else {
111
+ console.log("\n --force: forge version will overwrite user changes.");
112
+ }
113
+ }
114
+ if (opts.dryRun) {
115
+ console.log("\n--dry-run: No files were changed.");
116
+ return;
117
+ }
118
+ // Step 1: Remove stale files
119
+ for (const file of classified.stale) {
120
+ const target = join(t.claude, file);
121
+ if (existsSync(target)) {
122
+ rmSync(target);
123
+ console.log(` Removed: ${file}`);
124
+ }
125
+ }
126
+ // Step 1b: Prune directories that became empty after stale-file removal.
127
+ // Only walks forge-owned subtrees under .claude/ — never touches the .claude/
128
+ // root itself (which the user owns and may contain other things).
129
+ for (const dir of [t.agents, t.commands, t.skills, t.rules, t.references, t.protocols, t.templates]) {
130
+ pruneEmptyDirs(dir);
131
+ }
132
+ // Step 2: Back up user-customized files before re-sync
133
+ // userOnly = user changed, forge didn't — always preserve
134
+ // userModified = both changed — preserve unless --force
135
+ const backups = new Map();
136
+ for (const file of classified.userOnly) {
137
+ const target = join(t.claude, file);
138
+ if (existsSync(target)) {
139
+ backups.set(file, readFileSync(target));
140
+ }
141
+ }
142
+ if (!opts.force) {
143
+ for (const file of classified.userModified) {
144
+ const target = join(t.claude, file);
145
+ if (existsSync(target)) {
146
+ backups.set(file, readFileSync(target));
147
+ }
148
+ }
149
+ }
150
+ // Step 3: Re-sync (init with force:true to overwrite forge-updated files)
151
+ await init({ force: true, mergeMarkers: true });
152
+ // Step 4: Restore user-customized files from backup
153
+ for (const [file, content] of backups) {
154
+ const target = join(t.claude, file);
155
+ writeFileSync(target, content);
156
+ console.log(` Preserved: ${file}`);
157
+ }
158
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * `npx @jamie-tam/forge verify-manifest [path]` — v5 work-manifest verifier CLI.
3
+ *
4
+ * Spec: templates/manifests/v5/SCHEMA.md §5.2.
5
+ *
6
+ * Exit codes:
7
+ * 0 No errors
8
+ * 1 Verification errors (errors written to stdout)
9
+ * 2 Internal error (file not found, bad YAML syntax, etc.)
10
+ */
11
+ import { existsSync } from "node:fs";
12
+ import { resolve } from "node:path";
13
+ import { readManifest, } from "./work-manifest.js";
14
+ /** Per SCHEMA.md §5.2: exit 2 = "internal error (file not found, bad YAML, etc.)";
15
+ * exit 1 = verification errors. These error codes denote internal class. */
16
+ const INTERNAL_ERROR_CODES = new Set([
17
+ "E_FILE_NOT_FOUND",
18
+ "E_YAML_PARSE",
19
+ ]);
20
+ function chooseExitCode(errors) {
21
+ if (errors.length === 0)
22
+ return 0;
23
+ // If ALL errors are internal-class → exit 2. Otherwise verification → exit 1.
24
+ // (A mixed list signals real verification work; user fixes those.)
25
+ return errors.every((e) => INTERNAL_ERROR_CODES.has(e.code)) ? 2 : 1;
26
+ }
27
+ export async function verifyManifestCli(opts) {
28
+ const path = resolve(opts.path);
29
+ if (!existsSync(path)) {
30
+ return emitErrors(opts.json, [
31
+ { code: "E_FILE_NOT_FOUND", message: `manifest not found: ${path}`, path },
32
+ ]);
33
+ }
34
+ const result = readManifest(path);
35
+ if (!result.ok)
36
+ return emitErrors(opts.json, result.errors);
37
+ return emitOk(opts.json, result, `${opts.path} OK (schema=${result.schema})`);
38
+ }
39
+ function emitOk(json, result, msg) {
40
+ if (json) {
41
+ process.stdout.write(JSON.stringify({ ok: true, schema: result.schema, warnings: result.warnings ?? [] }) + "\n");
42
+ }
43
+ else {
44
+ console.log(msg);
45
+ for (const w of result.warnings ?? []) {
46
+ console.warn(`${w.code} ${w.path}`);
47
+ console.warn(` ${w.message}`);
48
+ }
49
+ }
50
+ return 0;
51
+ }
52
+ function emitErrors(json, errors) {
53
+ const exitCode = chooseExitCode(errors);
54
+ if (json) {
55
+ process.stdout.write(JSON.stringify({ ok: false, errors }) + "\n");
56
+ }
57
+ else {
58
+ for (const err of errors) {
59
+ console.error(`${err.code} ${err.path}`);
60
+ console.error(` ${err.message}`);
61
+ }
62
+ console.error(`\n${errors.length} error(s).`);
63
+ }
64
+ return exitCode;
65
+ }