@izkac/forgekit 0.1.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 (143) hide show
  1. package/bin/forge.mjs +100 -0
  2. package/bin/forgekit.mjs +84 -0
  3. package/bin/review.mjs +82 -0
  4. package/package.json +46 -0
  5. package/scripts/prepack.mjs +78 -0
  6. package/scripts/run-tests.mjs +43 -0
  7. package/src/adr.mjs +236 -0
  8. package/src/adr.test.mjs +170 -0
  9. package/src/change.mjs +234 -0
  10. package/src/change.test.mjs +83 -0
  11. package/src/cleanup-sessions.mjs +70 -0
  12. package/src/config.mjs +103 -0
  13. package/src/defer.mjs +75 -0
  14. package/src/doctor.mjs +341 -0
  15. package/src/doctor.test.mjs +114 -0
  16. package/src/init.mjs +575 -0
  17. package/src/install.mjs +777 -0
  18. package/src/install.test.mjs +104 -0
  19. package/src/integrity-check.mjs +58 -0
  20. package/src/integrity.mjs +317 -0
  21. package/src/integrity.test.mjs +296 -0
  22. package/src/lib/workspaces.mjs +55 -0
  23. package/src/lib.mjs +138 -0
  24. package/src/models.defaults.json +41 -0
  25. package/src/new-session.mjs +82 -0
  26. package/src/openspec-overlays/README.md +19 -0
  27. package/src/openspec-overlays/openspec-apply-change-footer.md +14 -0
  28. package/src/openspec-overlays/opsx-apply-completion-step.md +1 -0
  29. package/src/openspec-overlays/opsx-apply-implement-step.md +11 -0
  30. package/src/paths.mjs +92 -0
  31. package/src/plan-engine.mjs +260 -0
  32. package/src/plan-engine.test.mjs +245 -0
  33. package/src/preferences.defaults.json +78 -0
  34. package/src/preferences.mjs +438 -0
  35. package/src/preferences.test.mjs +174 -0
  36. package/src/record-evidence.mjs +204 -0
  37. package/src/record-evidence.test.mjs +260 -0
  38. package/src/resolve-model.mjs +312 -0
  39. package/src/resolve-model.test.mjs +194 -0
  40. package/src/review/carryforward.mjs +413 -0
  41. package/src/review/carryforward.test.mjs +587 -0
  42. package/src/review/cli.test.mjs +117 -0
  43. package/src/review/export.mjs +172 -0
  44. package/src/review/export.test.mjs +197 -0
  45. package/src/review/fixtures/valid-review.json +42 -0
  46. package/src/review/lib.mjs +894 -0
  47. package/src/review/lib.test.mjs +266 -0
  48. package/src/review/merge-tentative.mjs +292 -0
  49. package/src/review/merge-tentative.test.mjs +363 -0
  50. package/src/review/new-review.mjs +200 -0
  51. package/src/review/render.mjs +108 -0
  52. package/src/review/schema-consistency.test.mjs +83 -0
  53. package/src/review/schema.json +196 -0
  54. package/src/review/signals.mjs +144 -0
  55. package/src/review/signals.test.mjs +62 -0
  56. package/src/score-cli.mjs +68 -0
  57. package/src/score.mjs +489 -0
  58. package/src/score.test.mjs +253 -0
  59. package/src/session-reminder.mjs +168 -0
  60. package/src/session-status.mjs +70 -0
  61. package/src/set-models.mjs +186 -0
  62. package/src/set-phase.mjs +177 -0
  63. package/src/set-phase.test.mjs +317 -0
  64. package/src/set-prefs.mjs +294 -0
  65. package/src/spine.mjs +91 -0
  66. package/src/triage-prompt.mjs +175 -0
  67. package/src/triage-prompt.test.mjs +50 -0
  68. package/src/vendor-openspec-overlays.mjs +176 -0
  69. package/src/vendor-openspec-overlays.test.mjs +62 -0
  70. package/vendor/skills/archive-to-adr/SKILL.md +149 -0
  71. package/vendor/skills/forge/SKILL.md +136 -0
  72. package/vendor/skills/forge/phases/brainstorm.md +23 -0
  73. package/vendor/skills/forge/phases/finish.md +87 -0
  74. package/vendor/skills/forge/phases/implement.md +76 -0
  75. package/vendor/skills/forge/phases/plan-openspec.md +40 -0
  76. package/vendor/skills/forge/phases/plan-specs.md +97 -0
  77. package/vendor/skills/forge/phases/review.md +25 -0
  78. package/vendor/skills/forge/phases/verify.md +120 -0
  79. package/vendor/skills/forge/references/forge-layout.md +85 -0
  80. package/vendor/skills/forge/references/pace.md +115 -0
  81. package/vendor/skills/forge/references/plan-routing.md +51 -0
  82. package/vendor/skills/forge/references/runtime-integrity.md +157 -0
  83. package/vendor/skills/forge/references/substantial-work.md +37 -0
  84. package/vendor/skills/forge/references/tdd-core.md +29 -0
  85. package/vendor/skills/forge/references/test-evidence.md +30 -0
  86. package/vendor/skills/forge/references/test-strategy.md +68 -0
  87. package/vendor/skills/forge/skills/NOTICE.md +17 -0
  88. package/vendor/skills/forge/skills/brainstorming/SKILL.md +120 -0
  89. package/vendor/skills/forge/skills/requesting-code-review/SKILL.md +67 -0
  90. package/vendor/skills/forge/skills/requesting-code-review/code-reviewer.md +146 -0
  91. package/vendor/skills/forge/skills/subagent-driven-development/SKILL.md +87 -0
  92. package/vendor/skills/forge/skills/systematic-debugging/SKILL.md +234 -0
  93. package/vendor/skills/forge/skills/systematic-debugging/condition-based-waiting.md +115 -0
  94. package/vendor/skills/forge/skills/systematic-debugging/defense-in-depth.md +122 -0
  95. package/vendor/skills/forge/skills/systematic-debugging/find-polluter.sh +63 -0
  96. package/vendor/skills/forge/skills/systematic-debugging/root-cause-tracing.md +169 -0
  97. package/vendor/skills/forge/skills/test-driven-development/SKILL.md +290 -0
  98. package/vendor/skills/forge/skills/test-driven-development/testing-anti-patterns.md +299 -0
  99. package/vendor/skills/forge/skills/verification-before-completion/SKILL.md +59 -0
  100. package/vendor/skills/forge/subagents/final-reviewer-prompt.md +53 -0
  101. package/vendor/skills/forge/subagents/implementer-prompt.md +38 -0
  102. package/vendor/skills/forge/subagents/task-reviewer-prompt.md +61 -0
  103. package/vendor/skills/git-resolve-adr-conflict/SKILL.md +132 -0
  104. package/vendor/skills/thorough-code-review/SKILL.md +290 -0
  105. package/vendor/skills/thorough-code-review/examples/accepted-risks-janus.md +32 -0
  106. package/vendor/skills/thorough-code-review/examples.md +133 -0
  107. package/vendor/skills/thorough-code-review/reference/accepted-risks.md +26 -0
  108. package/vendor/skills/thorough-code-review/reference/lenses.md +96 -0
  109. package/vendor/skills/thorough-code-review/reference/phase1-scout.md +62 -0
  110. package/vendor/skills/thorough-code-review/reference/phase1c-coverage.md +44 -0
  111. package/vendor/skills/thorough-code-review/reference/phase2-skeptic.md +105 -0
  112. package/vendor/skills/thorough-code-review/reference/report-schema.json +222 -0
  113. package/vendor/skills/thorough-code-review/reference/report-template.md +115 -0
  114. package/vendor/skills/thorough-code-review/reference/severity-rubric.md +49 -0
  115. package/vendor/skills/thorough-code-review/reference/signals-preflight.md +55 -0
  116. package/vendor/templates/adr/README.md +7 -0
  117. package/vendor/templates/adr/decisions.md +141 -0
  118. package/vendor/templates/adr/hooks/check-pending-adrs.mjs +74 -0
  119. package/vendor/templates/adr/hooks/check-pending-adrs.sh +3 -0
  120. package/vendor/templates/adr/hooks/openspec-archive-agent-message.mjs +52 -0
  121. package/vendor/templates/adr/hooks/openspec-archive-agent-message.sh +3 -0
  122. package/vendor/templates/project/claude/commands/forge-apply.md +75 -0
  123. package/vendor/templates/project/claude/commands/forge-brainstorm.md +7 -0
  124. package/vendor/templates/project/claude/commands/forge-build.md +17 -0
  125. package/vendor/templates/project/claude/commands/forge-plan.md +12 -0
  126. package/vendor/templates/project/claude/commands/forge-skip.md +14 -0
  127. package/vendor/templates/project/claude/commands/forge-status.md +16 -0
  128. package/vendor/templates/project/claude/commands/forge.md +16 -0
  129. package/vendor/templates/project/claude/hooks/forge-prompt-hook.mjs +73 -0
  130. package/vendor/templates/project/claude/hooks/forge-session-start.mjs +19 -0
  131. package/vendor/templates/project/claude/hooks/forge-triage-hook.mjs +77 -0
  132. package/vendor/templates/project/claude/rules/forge.md +16 -0
  133. package/vendor/templates/project/codex/rules/forge.md +10 -0
  134. package/vendor/templates/project/cursor/commands/forge-apply.md +75 -0
  135. package/vendor/templates/project/cursor/commands/forge-brainstorm.md +10 -0
  136. package/vendor/templates/project/cursor/commands/forge-build.md +17 -0
  137. package/vendor/templates/project/cursor/commands/forge-plan.md +15 -0
  138. package/vendor/templates/project/cursor/commands/forge-skip.md +14 -0
  139. package/vendor/templates/project/cursor/commands/forge-status.md +16 -0
  140. package/vendor/templates/project/cursor/commands/forge.md +16 -0
  141. package/vendor/templates/project/cursor/hooks/forge-session-start.mjs +30 -0
  142. package/vendor/templates/project/cursor/hooks/forge-session-start.sh +3 -0
  143. package/vendor/templates/project/cursor/rules/forge.mdc +21 -0
package/bin/forge.mjs ADDED
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Forge CLI — session orchestration, prefs, models, project init.
4
+ *
5
+ * Usage: forge <command> [args...]
6
+ * Install: forge install → forgekit install --skills forge …
7
+ */
8
+
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { spawnSync } from 'node:child_process';
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+ const SRC = path.join(__dirname, '..', 'src');
15
+
16
+ /** @type {Record<string, { script: string, aliases?: string[], prependArgs?: string[] }>} */
17
+ const COMMANDS = {
18
+ new: { script: 'new-session.mjs', aliases: ['session-new'] },
19
+ status: { script: 'session-status.mjs' },
20
+ cleanup: { script: 'cleanup-sessions.mjs' },
21
+ phase: { script: 'set-phase.mjs', aliases: ['set-phase'] },
22
+ prefs: { script: 'set-prefs.mjs' },
23
+ models: { script: 'set-models.mjs' },
24
+ 'resolve-model': { script: 'resolve-model.mjs' },
25
+ doctor: { script: 'doctor.mjs' },
26
+ evidence: { script: 'record-evidence.mjs' },
27
+ reminder: { script: 'session-reminder.mjs' },
28
+ overlay: { script: 'vendor-openspec-overlays.mjs', aliases: ['overlays'] },
29
+ install: { script: 'install.mjs', prependArgs: ['--skills', 'forge'] },
30
+ init: { script: 'init.mjs' },
31
+ triage: { script: 'triage-prompt.mjs' },
32
+ change: { script: 'change.mjs' },
33
+ spine: { script: 'spine.mjs' },
34
+ defer: { script: 'defer.mjs' },
35
+ 'integrity-check': { script: 'integrity-check.mjs', aliases: ['integrity'] },
36
+ score: { script: 'score-cli.mjs', aliases: ['scorecard'] },
37
+ };
38
+
39
+ function printHelp() {
40
+ process.stdout.write(`Forge — disciplined development workflow
41
+
42
+ Usage:
43
+ forge <command> [args...]
44
+
45
+ Commands:
46
+ new <slug> Create a Forge session under .forge/
47
+ status Show active session
48
+ phase <phase> Update session phase
49
+ cleanup Prune old/finished sessions
50
+ prefs [pace] Get/set pace preferences
51
+ models [lane] Get/set subagent billing (included|metered)
52
+ resolve-model --tier … Resolve subagent model JSON
53
+ doctor Plan-engine readiness (OpenSpec or specs)
54
+ evidence Stamp tier-2 test-evidence.md
55
+ reminder Session reminder (for hooks)
56
+ overlay Re-apply OpenSpec vendor overlays in this project
57
+ install Alias → forgekit install --skills forge
58
+ init Wire Forge commands/hooks/rules into this project
59
+ triage Classify whether a prompt needs Forge triage
60
+ change new|archive Specs-engine change scaffold / archive
61
+ spine init|check Capability→runtime spine matrix (spine.json)
62
+ defer add|resolve|list Deferral registry (deferred wiring = tracked debt)
63
+ integrity-check Mechanical integrity gate (runs at phase done)
64
+ score [--write] L2 session scorecard (auto-written at phase done)
65
+
66
+ Prefer \`forgekit install\` to pick multiple skills + agents at once.
67
+
68
+ Global:
69
+ forge --help
70
+ forge <command> --help
71
+ `);
72
+ }
73
+
74
+ const argv = process.argv.slice(2);
75
+ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
76
+ printHelp();
77
+ process.exit(0);
78
+ }
79
+
80
+ const [cmd, ...rest] = argv;
81
+ const entry = COMMANDS[cmd] ?? Object.values(COMMANDS).find((c) => c.aliases?.includes(cmd));
82
+
83
+ if (!entry) {
84
+ process.stderr.write(`Unknown command: ${cmd}\n\n`);
85
+ printHelp();
86
+ process.exit(1);
87
+ }
88
+
89
+ const args = [...(entry.prependArgs ?? []), ...rest];
90
+ const r = spawnSync(process.execPath, [path.join(SRC, entry.script), ...args], {
91
+ stdio: 'inherit',
92
+ cwd: process.cwd(),
93
+ env: {
94
+ ...process.env,
95
+ FORGEKIT_ROOT: path.resolve(__dirname, '..', '..', '..'),
96
+ FORGEKIT_CLI_ROOT: path.resolve(__dirname, '..'),
97
+ },
98
+ });
99
+
100
+ process.exit(r.status ?? 1);
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Forgekit meta CLI — install / list / update / uninstall skills across agents.
4
+ *
5
+ * Skill day-to-day commands stay on `forge` and `review`.
6
+ *
7
+ * Usage: forgekit <command> [args...]
8
+ */
9
+
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { spawnSync } from 'node:child_process';
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const SRC = path.join(__dirname, '..', 'src');
16
+
17
+ /** @type {Record<string, { script: string, prependArgs?: string[] }>} */
18
+ const COMMANDS = {
19
+ install: { script: 'install.mjs' },
20
+ list: { script: 'install.mjs', prependArgs: ['--list'] },
21
+ update: { script: 'install.mjs', prependArgs: ['--update'] },
22
+ uninstall: { script: 'install.mjs', prependArgs: ['--uninstall'] },
23
+ };
24
+
25
+ function printHelp() {
26
+ process.stdout.write(`Forgekit — portable agent skills
27
+
28
+ Usage:
29
+ forgekit <command> [args...]
30
+
31
+ Commands:
32
+ install Install skills into ~/.cursor|claude|codex/skills/
33
+ list Show installed / missing / outdated (skill × agent)
34
+ update Reinstall outdated skills
35
+ uninstall Remove installed skill directories
36
+
37
+ Install picks skills and agents (interactive on TTY, or via flags).
38
+ Also asks about planning engine (OpenSpec vs specs) and ADRs:
39
+
40
+ forgekit install
41
+ forgekit install --skills forge,thorough-code-review --agents cursor,claude
42
+ forgekit install --adr --adr-dir docs/adr --adr-project
43
+ forgekit install --all-skills --all-agents --force
44
+ forgekit list
45
+ forgekit update
46
+ forgekit uninstall --skills forge --agents cursor
47
+
48
+ Day-to-day skill CLIs (same package):
49
+
50
+ forge … Forge workflow sessions
51
+ review … Thorough code review pipeline
52
+
53
+ forge install … Alias → forgekit install --skills forge …
54
+ review install … Alias → forgekit install --skills thorough-code-review …
55
+ `);
56
+ }
57
+
58
+ const argv = process.argv.slice(2);
59
+ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
60
+ printHelp();
61
+ process.exit(0);
62
+ }
63
+
64
+ const [cmd, ...rest] = argv;
65
+ const entry = COMMANDS[cmd];
66
+
67
+ if (!entry) {
68
+ process.stderr.write(`Unknown command: ${cmd}\n\n`);
69
+ printHelp();
70
+ process.exit(1);
71
+ }
72
+
73
+ const args = [...(entry.prependArgs ?? []), ...rest];
74
+ const r = spawnSync(process.execPath, [path.join(SRC, entry.script), ...args], {
75
+ stdio: 'inherit',
76
+ cwd: process.cwd(),
77
+ env: {
78
+ ...process.env,
79
+ FORGEKIT_ROOT: path.resolve(__dirname, '..', '..', '..'),
80
+ FORGEKIT_CLI_ROOT: path.resolve(__dirname, '..'),
81
+ },
82
+ });
83
+
84
+ process.exit(r.status ?? 1);
package/bin/review.mjs ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Review CLI — thorough-code-review scaffolding, render, export, signals.
4
+ *
5
+ * Usage: review <command> [args...]
6
+ * Install: review install → forgekit install --skills thorough-code-review …
7
+ */
8
+
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { spawnSync } from 'node:child_process';
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
+ const SRC = path.join(__dirname, '..', 'src');
15
+ const REVIEW_SRC = path.join(SRC, 'review');
16
+
17
+ /** @type {Record<string, { script: string, prependArgs?: string[] }>} */
18
+ const COMMANDS = {
19
+ new: { script: path.join(REVIEW_SRC, 'new-review.mjs') },
20
+ render: { script: path.join(REVIEW_SRC, 'render.mjs') },
21
+ export: { script: path.join(REVIEW_SRC, 'export.mjs') },
22
+ carryforward: { script: path.join(REVIEW_SRC, 'carryforward.mjs') },
23
+ merge: { script: path.join(REVIEW_SRC, 'merge-tentative.mjs') },
24
+ signals: { script: path.join(REVIEW_SRC, 'signals.mjs') },
25
+ install: {
26
+ script: path.join(SRC, 'install.mjs'),
27
+ prependArgs: ['--skills', 'thorough-code-review'],
28
+ },
29
+ };
30
+
31
+ function printHelp() {
32
+ process.stdout.write(`Review — thorough code review tooling
33
+
34
+ Usage:
35
+ review <command> [args...]
36
+
37
+ Commands:
38
+ new <slug> Scaffold .reviews/<id>-review.json
39
+ render Generate markdown from JSON
40
+ export Validate + summarize (+ optional CI gate)
41
+ carryforward Inherit prior verdicts for unchanged files
42
+ merge Merge scout tentative JSON shards
43
+ signals Plan typecheck/lint/test grounding commands
44
+ install Alias → forgekit install --skills thorough-code-review
45
+
46
+ Prefer \`forgekit install\` to pick multiple skills + agents at once.
47
+
48
+ Global:
49
+ review --help
50
+ review <command> --help
51
+
52
+ After install, invoke the skill explicitly in your agent (it does not auto-load).
53
+ `);
54
+ }
55
+
56
+ const argv = process.argv.slice(2);
57
+ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
58
+ printHelp();
59
+ process.exit(0);
60
+ }
61
+
62
+ const [cmd, ...rest] = argv;
63
+ const entry = COMMANDS[cmd];
64
+
65
+ if (!entry) {
66
+ process.stderr.write(`Unknown command: ${cmd}\n\n`);
67
+ printHelp();
68
+ process.exit(1);
69
+ }
70
+
71
+ const args = [...(entry.prependArgs ?? []), ...rest];
72
+ const r = spawnSync(process.execPath, [entry.script, ...args], {
73
+ stdio: 'inherit',
74
+ cwd: process.cwd(),
75
+ env: {
76
+ ...process.env,
77
+ FORGEKIT_ROOT: path.resolve(__dirname, '..', '..', '..'),
78
+ FORGEKIT_CLI_ROOT: path.resolve(__dirname, '..'),
79
+ },
80
+ });
81
+
82
+ process.exit(r.status ?? 1);
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@izkac/forgekit",
3
+ "version": "0.1.0",
4
+ "description": "Forgekit CLIs — forgekit (install), forge (workflow), review (code review)",
5
+ "type": "module",
6
+ "bin": {
7
+ "forgekit": "./bin/forgekit.mjs",
8
+ "forge": "./bin/forge.mjs",
9
+ "review": "./bin/review.mjs"
10
+ },
11
+ "files": [
12
+ "bin",
13
+ "src",
14
+ "vendor",
15
+ "scripts"
16
+ ],
17
+ "scripts": {
18
+ "prepack": "node scripts/prepack.mjs",
19
+ "test": "node scripts/run-tests.mjs",
20
+ "lint": "eslint src bin scripts"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/izkac/forgekit.git",
31
+ "directory": "packages/cli"
32
+ },
33
+ "homepage": "https://github.com/izkac/forgekit#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/izkac/forgekit/issues"
36
+ },
37
+ "keywords": [
38
+ "forge",
39
+ "openspec",
40
+ "agent-skills",
41
+ "cursor",
42
+ "claude",
43
+ "codex"
44
+ ],
45
+ "license": "MIT"
46
+ }
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Copy skills/ and templates/ from the monorepo root into packages/cli/vendor/
4
+ * so the published npm package is self-contained.
5
+ *
6
+ * Usage: node packages/cli/scripts/prepack.mjs
7
+ * Triggered by npm prepack in @izkac/forgekit.
8
+ */
9
+
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const cliRoot = path.resolve(__dirname, '..');
16
+ const monoRoot = path.resolve(cliRoot, '..', '..');
17
+ const vendorRoot = path.join(cliRoot, 'vendor');
18
+
19
+ /**
20
+ * @param {string} src
21
+ * @param {string} dest
22
+ */
23
+ function copyDirRecursive(src, dest) {
24
+ fs.mkdirSync(dest, { recursive: true });
25
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
26
+ const from = path.join(src, entry.name);
27
+ const to = path.join(dest, entry.name);
28
+ if (entry.isDirectory()) copyDirRecursive(from, to);
29
+ else fs.copyFileSync(from, to);
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Best-effort clear. On Windows EPERM, leave the tree and overwrite in place.
35
+ * @param {string} dir
36
+ * @returns {boolean} true if cleared
37
+ */
38
+ function tryEmptyDir(dir) {
39
+ if (!fs.existsSync(dir)) return true;
40
+ try {
41
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
42
+ fs.rmSync(path.join(dir, entry.name), {
43
+ recursive: true,
44
+ force: true,
45
+ maxRetries: 5,
46
+ retryDelay: 100,
47
+ });
48
+ }
49
+ return true;
50
+ } catch (err) {
51
+ process.stderr.write(
52
+ `prepack: could not empty ${dir} (${err.code || err.message}); overwriting in place\n`,
53
+ );
54
+ return false;
55
+ }
56
+ }
57
+
58
+ function main() {
59
+ const skillsSrc = path.join(monoRoot, 'skills');
60
+ const templatesSrc = path.join(monoRoot, 'templates');
61
+ if (!fs.existsSync(skillsSrc) || !fs.existsSync(templatesSrc)) {
62
+ if (fs.existsSync(path.join(vendorRoot, 'skills'))) {
63
+ process.stdout.write(`prepack: vendor already present at ${vendorRoot}\n`);
64
+ return;
65
+ }
66
+ throw new Error(
67
+ `prepack: monorepo skills/templates not found at ${monoRoot} and vendor/ missing`,
68
+ );
69
+ }
70
+
71
+ fs.mkdirSync(vendorRoot, { recursive: true });
72
+ tryEmptyDir(vendorRoot);
73
+ copyDirRecursive(skillsSrc, path.join(vendorRoot, 'skills'));
74
+ copyDirRecursive(templatesSrc, path.join(vendorRoot, 'templates'));
75
+ process.stdout.write(`prepack: vendored skills + templates → ${vendorRoot}\n`);
76
+ }
77
+
78
+ main();
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Run all src test files (*.test.mjs) via node --test.
4
+ *
5
+ * Node 20 does not expand globs passed to `node --test`; Node 22+ does.
6
+ * Explicit file discovery keeps CI green on the engines.node >=20 matrix.
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { spawnSync } from 'node:child_process';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const cliRoot = path.resolve(__dirname, '..');
16
+ const srcRoot = path.join(cliRoot, 'src');
17
+
18
+ /**
19
+ * @param {string} dir
20
+ * @returns {string[]}
21
+ */
22
+ function findTestFiles(dir) {
23
+ /** @type {string[]} */
24
+ const out = [];
25
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
26
+ const full = path.join(dir, entry.name);
27
+ if (entry.isDirectory()) out.push(...findTestFiles(full));
28
+ else if (entry.name.endsWith('.test.mjs')) out.push(full);
29
+ }
30
+ return out;
31
+ }
32
+
33
+ const files = findTestFiles(srcRoot).sort();
34
+ if (files.length === 0) {
35
+ console.error(`No *.test.mjs files under ${srcRoot}`);
36
+ process.exit(1);
37
+ }
38
+
39
+ const result = spawnSync(process.execPath, ['--test', ...files], {
40
+ cwd: cliRoot,
41
+ stdio: 'inherit',
42
+ });
43
+ process.exit(result.status ?? 1);
package/src/adr.mjs ADDED
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ADR project/user config + scaffolding for forgekit.
4
+ *
5
+ * User defaults: ~/.forgekit/config.json → { adr: { enabled, dir } }
6
+ * Project: <repo>/.forge/config.json → { adr: { enabled, dir, decisionsDoc } }
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import {
12
+ loadProjectConfig,
13
+ loadUserConfig,
14
+ saveProjectConfig,
15
+ saveUserConfig as saveUserConfigBase,
16
+ userConfigPath,
17
+ projectConfigPath,
18
+ } from './config.mjs';
19
+ import { resolveAsset } from './paths.mjs';
20
+
21
+ export {
22
+ loadProjectConfig,
23
+ loadUserConfig,
24
+ userConfigPath,
25
+ projectConfigPath,
26
+ };
27
+
28
+ export const DEFAULT_ADR_DIR = 'docs/adr';
29
+ export const ADR_SKILLS = Object.freeze(['archive-to-adr', 'git-resolve-adr-conflict']);
30
+
31
+ /**
32
+ * @param {string} adrDir posix-ish relative path
33
+ * @returns {string}
34
+ */
35
+ export function decisionsDocFor(adrDir) {
36
+ const normalized = normalizeAdrDir(adrDir);
37
+ const parent = path.posix.dirname(normalized);
38
+ if (!parent || parent === '.') return 'decisions.md';
39
+ return path.posix.join(parent, 'decisions.md');
40
+ }
41
+
42
+ /**
43
+ * @param {string} raw
44
+ * @returns {string}
45
+ */
46
+ export function normalizeAdrDir(raw) {
47
+ let s = String(raw ?? '')
48
+ .trim()
49
+ .replace(/\\/g, '/');
50
+ if (!s) s = DEFAULT_ADR_DIR;
51
+ s = s.replace(/^\.\/+/, '').replace(/\/+$/, '');
52
+ if (!s) s = DEFAULT_ADR_DIR;
53
+ if (path.isAbsolute(s) || s.startsWith('..') || s.split('/').includes('..')) {
54
+ throw new Error(
55
+ `ADR directory must be a relative path inside the repo (got: ${raw})`,
56
+ );
57
+ }
58
+ return s;
59
+ }
60
+
61
+ /**
62
+ * @param {{ adr?: { enabled?: boolean, dir?: string } }} patch
63
+ * @param {string} [home]
64
+ */
65
+ export function saveUserConfig(patch, home) {
66
+ return saveUserConfigBase(patch, home);
67
+ }
68
+
69
+ /**
70
+ * @param {string} cwd
71
+ * @param {{ adr: { enabled: boolean, dir?: string, decisionsDoc?: string } }} patch
72
+ */
73
+ export function writeProjectAdrConfig(cwd, patch) {
74
+ const current = loadProjectConfig(cwd);
75
+ const enabled = Boolean(patch.adr.enabled);
76
+ /** @type {{ enabled: boolean, dir?: string, decisionsDoc?: string }} */
77
+ const adr = { enabled };
78
+ if (enabled) {
79
+ const curAdr =
80
+ current.adr && typeof current.adr === 'object'
81
+ ? /** @type {{ dir?: string, decisionsDoc?: string }} */ (current.adr)
82
+ : {};
83
+ const dir = normalizeAdrDir(patch.adr.dir ?? curAdr.dir ?? DEFAULT_ADR_DIR);
84
+ adr.dir = dir;
85
+ adr.decisionsDoc =
86
+ patch.adr.decisionsDoc ?? curAdr.decisionsDoc ?? decisionsDocFor(dir);
87
+ }
88
+ return saveProjectConfig(cwd, { adr });
89
+ }
90
+
91
+ /**
92
+ * @returns {string}
93
+ */
94
+ export function resolveAdrTemplatesRoot() {
95
+ return resolveAsset('templates/adr');
96
+ }
97
+
98
+ /**
99
+ * @param {string} template
100
+ * @param {Record<string, string>} vars
101
+ */
102
+ function renderTemplate(template, vars) {
103
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? '');
104
+ }
105
+
106
+ /**
107
+ * Relative link from adr README to decisions doc.
108
+ * @param {string} adrDir
109
+ * @param {string} decisionsDoc
110
+ */
111
+ export function decisionsRelFromAdrReadme(adrDir, decisionsDoc) {
112
+ const fromDir = adrDir.replace(/\/+$/, '');
113
+ const target = decisionsDoc.replace(/\\/g, '/');
114
+ const rel = path.posix.relative(fromDir, target);
115
+ return rel.startsWith('.') ? rel : `./${rel}`;
116
+ }
117
+
118
+ /**
119
+ * Scaffold decisions.md, adr README, optional hooks.
120
+ * @param {string} cwd
121
+ * @param {{ dir?: string, decisionsDoc?: string, force?: boolean, hooks?: boolean }} opts
122
+ */
123
+ export function scaffoldAdr(cwd, opts = {}) {
124
+ const dir = normalizeAdrDir(opts.dir ?? DEFAULT_ADR_DIR);
125
+ const decisionsDoc = opts.decisionsDoc ?? decisionsDocFor(dir);
126
+ const templates = resolveAdrTemplatesRoot();
127
+ const force = Boolean(opts.force);
128
+ /** @type {{ file: string, status: string }[]} */
129
+ const files = [];
130
+
131
+ const write = (relPath, body) => {
132
+ const dest = path.join(cwd, ...relPath.split('/'));
133
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
134
+ if (fs.existsSync(dest) && !force) {
135
+ files.push({ file: relPath, status: 'skipped' });
136
+ return;
137
+ }
138
+ fs.writeFileSync(dest, body, 'utf8');
139
+ files.push({ file: relPath, status: 'written' });
140
+ };
141
+
142
+ const decisionsTpl = fs.readFileSync(path.join(templates, 'decisions.md'), 'utf8');
143
+ write(decisionsDoc, renderTemplate(decisionsTpl, { ADR_DIR: dir }));
144
+
145
+ const readmeTpl = fs.readFileSync(path.join(templates, 'README.md'), 'utf8');
146
+ write(
147
+ path.posix.join(dir, 'README.md'),
148
+ renderTemplate(readmeTpl, {
149
+ ADR_DIR: dir,
150
+ DECISIONS_REL: decisionsRelFromAdrReadme(dir, decisionsDoc),
151
+ }),
152
+ );
153
+
154
+ if (opts.hooks !== false) {
155
+ const hooksSrc = path.join(templates, 'hooks');
156
+ if (fs.existsSync(hooksSrc)) {
157
+ const hooksDestRel = 'scripts/hooks';
158
+ for (const name of fs.readdirSync(hooksSrc)) {
159
+ const from = path.join(hooksSrc, name);
160
+ if (!fs.statSync(from).isFile()) continue;
161
+ write(path.posix.join(hooksDestRel, name), fs.readFileSync(from, 'utf8'));
162
+ }
163
+ }
164
+ }
165
+
166
+ const config = writeProjectAdrConfig(cwd, {
167
+ adr: { enabled: true, dir, decisionsDoc },
168
+ });
169
+
170
+ return { dir, decisionsDoc, files, config };
171
+ }
172
+
173
+ /**
174
+ * @param {string} cwd
175
+ */
176
+ export function disableProjectAdr(cwd) {
177
+ return writeProjectAdrConfig(cwd, { adr: { enabled: false } });
178
+ }
179
+
180
+ /**
181
+ * @param {string} cwd
182
+ * @returns {boolean}
183
+ */
184
+ export function isGitRepo(cwd) {
185
+ return fs.existsSync(path.join(cwd, '.git'));
186
+ }
187
+
188
+ /**
189
+ * @param {string} cwd
190
+ * @param {{ home?: string }} [opts]
191
+ * @returns {{ enabled: boolean, dir: string, decisionsDoc: string, source: string }}
192
+ */
193
+ export function resolveProjectAdr(cwd, opts = {}) {
194
+ const project = loadProjectConfig(cwd);
195
+ const projectAdr =
196
+ project.adr && typeof project.adr === 'object'
197
+ ? /** @type {{ enabled?: boolean, dir?: string, decisionsDoc?: string }} */ (project.adr)
198
+ : null;
199
+ if (projectAdr && typeof projectAdr.enabled === 'boolean') {
200
+ const dir = normalizeAdrDir(projectAdr.dir ?? DEFAULT_ADR_DIR);
201
+ return {
202
+ enabled: projectAdr.enabled,
203
+ dir,
204
+ decisionsDoc: projectAdr.decisionsDoc ?? decisionsDocFor(dir),
205
+ source: 'project',
206
+ };
207
+ }
208
+ const user = loadUserConfig(opts.home);
209
+ const userAdr =
210
+ user.adr && typeof user.adr === 'object'
211
+ ? /** @type {{ enabled?: boolean, dir?: string }} */ (user.adr)
212
+ : null;
213
+ if (userAdr && typeof userAdr.enabled === 'boolean') {
214
+ const dir = normalizeAdrDir(userAdr.dir ?? DEFAULT_ADR_DIR);
215
+ return {
216
+ enabled: userAdr.enabled,
217
+ dir,
218
+ decisionsDoc: decisionsDocFor(dir),
219
+ source: 'user',
220
+ };
221
+ }
222
+ if (fs.existsSync(path.join(cwd, DEFAULT_ADR_DIR))) {
223
+ return {
224
+ enabled: true,
225
+ dir: DEFAULT_ADR_DIR,
226
+ decisionsDoc: decisionsDocFor(DEFAULT_ADR_DIR),
227
+ source: 'heuristic',
228
+ };
229
+ }
230
+ return {
231
+ enabled: false,
232
+ dir: DEFAULT_ADR_DIR,
233
+ decisionsDoc: decisionsDocFor(DEFAULT_ADR_DIR),
234
+ source: 'default',
235
+ };
236
+ }