forge-workflow 0.0.3 → 0.0.5

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 (209) hide show
  1. package/.claude/commands/dev.md +340 -314
  2. package/.claude/commands/plan.md +521 -478
  3. package/.claude/commands/premerge.md +176 -179
  4. package/.claude/commands/research.md +42 -42
  5. package/.claude/commands/review.md +442 -442
  6. package/.claude/commands/rollback.md +721 -721
  7. package/.claude/commands/ship.md +164 -134
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +48 -77
  10. package/.claude/commands/validate.md +282 -237
  11. package/.claude/commands/verify.md +221 -221
  12. package/.claude/rules/greptile-review-process.md +285 -285
  13. package/.claude/rules/workflow.md +105 -105
  14. package/.claude/scripts/greptile-resolve.sh +526 -526
  15. package/.claude/scripts/load-env.sh +32 -32
  16. package/.cline/workflows/dev.md +337 -311
  17. package/.cline/workflows/plan.md +518 -475
  18. package/.cline/workflows/premerge.md +173 -176
  19. package/.cline/workflows/research.md +39 -39
  20. package/.cline/workflows/review.md +439 -439
  21. package/.cline/workflows/rollback.md +718 -718
  22. package/.cline/workflows/ship.md +161 -131
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +45 -74
  25. package/.cline/workflows/validate.md +279 -234
  26. package/.cline/workflows/verify.md +218 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +340 -314
  29. package/.codex/skills/plan/SKILL.md +521 -478
  30. package/.codex/skills/premerge/SKILL.md +176 -179
  31. package/.codex/skills/research/SKILL.md +42 -42
  32. package/.codex/skills/review/SKILL.md +442 -442
  33. package/.codex/skills/rollback/SKILL.md +721 -721
  34. package/.codex/skills/ship/SKILL.md +164 -134
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +48 -77
  37. package/.codex/skills/validate/SKILL.md +282 -237
  38. package/.codex/skills/verify/SKILL.md +221 -221
  39. package/.cursor/commands/dev.md +337 -311
  40. package/.cursor/commands/plan.md +518 -475
  41. package/.cursor/commands/premerge.md +173 -176
  42. package/.cursor/commands/research.md +39 -39
  43. package/.cursor/commands/review.md +439 -439
  44. package/.cursor/commands/rollback.md +718 -718
  45. package/.cursor/commands/ship.md +161 -131
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +45 -74
  48. package/.cursor/commands/validate.md +279 -234
  49. package/.cursor/commands/verify.md +218 -218
  50. package/.cursor/rules/permissions-guidance.mdc +37 -37
  51. package/.forge/hooks/check-tdd.js +240 -240
  52. package/.github/PLUGIN_TEMPLATE.json +32 -32
  53. package/.github/prompts/dev.prompt.md +342 -316
  54. package/.github/prompts/plan.prompt.md +523 -480
  55. package/.github/prompts/premerge.prompt.md +178 -181
  56. package/.github/prompts/research.prompt.md +44 -44
  57. package/.github/prompts/review.prompt.md +444 -444
  58. package/.github/prompts/rollback.prompt.md +723 -723
  59. package/.github/prompts/ship.prompt.md +166 -136
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +50 -79
  62. package/.github/prompts/validate.prompt.md +284 -239
  63. package/.github/prompts/verify.prompt.md +223 -223
  64. package/.github/workflows/beads-to-github.yml +56 -0
  65. package/.github/workflows/github-to-beads.yml +97 -0
  66. package/.kilocode/workflows/dev.md +341 -315
  67. package/.kilocode/workflows/plan.md +522 -479
  68. package/.kilocode/workflows/premerge.md +177 -180
  69. package/.kilocode/workflows/research.md +43 -43
  70. package/.kilocode/workflows/review.md +443 -443
  71. package/.kilocode/workflows/rollback.md +722 -722
  72. package/.kilocode/workflows/ship.md +165 -135
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +49 -78
  75. package/.kilocode/workflows/validate.md +283 -238
  76. package/.kilocode/workflows/verify.md +222 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +340 -314
  79. package/.opencode/commands/plan.md +521 -478
  80. package/.opencode/commands/premerge.md +176 -179
  81. package/.opencode/commands/research.md +42 -42
  82. package/.opencode/commands/review.md +442 -442
  83. package/.opencode/commands/rollback.md +721 -721
  84. package/.opencode/commands/ship.md +164 -134
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +48 -77
  87. package/.opencode/commands/validate.md +282 -237
  88. package/.opencode/commands/verify.md +221 -221
  89. package/.roo/commands/dev.md +341 -315
  90. package/.roo/commands/plan.md +522 -479
  91. package/.roo/commands/premerge.md +177 -180
  92. package/.roo/commands/research.md +43 -43
  93. package/.roo/commands/review.md +443 -443
  94. package/.roo/commands/rollback.md +722 -722
  95. package/.roo/commands/ship.md +165 -135
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +49 -78
  98. package/.roo/commands/validate.md +283 -238
  99. package/.roo/commands/verify.md +222 -222
  100. package/AGENTS.md +175 -169
  101. package/CLAUDE.md +100 -99
  102. package/LICENSE +21 -21
  103. package/README.md +429 -414
  104. package/bin/forge-cmd.js +313 -313
  105. package/bin/{forge-validate.js → forge-preflight.js} +309 -303
  106. package/bin/forge.js +4596 -4232
  107. package/docs/AGENT_INSTALL_PROMPT.md +342 -342
  108. package/docs/BEADS_GITHUB_SYNC.md +251 -0
  109. package/docs/ENHANCED_ONBOARDING.md +602 -602
  110. package/docs/EXAMPLES.md +482 -482
  111. package/docs/GREPTILE_SETUP.md +400 -400
  112. package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
  113. package/docs/ROADMAP.md +359 -359
  114. package/docs/SETUP.md +663 -632
  115. package/docs/TOOLCHAIN.md +630 -630
  116. package/docs/VALIDATION.md +363 -363
  117. package/install.sh +40 -1058
  118. package/lefthook.yml +39 -39
  119. package/lib/agents/README.md +198 -198
  120. package/lib/agents/claude.plugin.json +28 -28
  121. package/lib/agents/cline.plugin.json +22 -22
  122. package/lib/agents/codex.plugin.json +19 -19
  123. package/lib/agents/copilot.plugin.json +24 -24
  124. package/lib/agents/cursor.plugin.json +25 -25
  125. package/lib/agents/kilocode.plugin.json +22 -22
  126. package/lib/agents/opencode.plugin.json +20 -20
  127. package/lib/agents/roo.plugin.json +23 -23
  128. package/lib/agents-config.js +2112 -2112
  129. package/lib/beads-health-check.js +143 -0
  130. package/lib/beads-setup.js +341 -0
  131. package/lib/beads-sync-scaffold.js +260 -0
  132. package/lib/commands/dev.js +513 -513
  133. package/lib/commands/plan.js +692 -692
  134. package/lib/commands/recommend.js +119 -119
  135. package/lib/commands/ship.js +377 -377
  136. package/lib/commands/status.js +378 -378
  137. package/lib/commands/validate.js +602 -602
  138. package/lib/context-merge.js +359 -359
  139. package/lib/dep-guard/analyzer.js +294 -294
  140. package/lib/dep-guard/behavior-detector.js +98 -98
  141. package/lib/dep-guard/contract-detector.js +162 -162
  142. package/lib/dep-guard/import-detector.js +498 -498
  143. package/lib/dep-guard/path-utils.js +13 -13
  144. package/lib/dep-guard/rubric.js +120 -120
  145. package/lib/dep-guard/task-parser.js +318 -318
  146. package/lib/detect-agent.js +191 -0
  147. package/lib/detect-worktree.js +47 -0
  148. package/lib/file-hash.js +26 -0
  149. package/lib/husky-migration.js +450 -0
  150. package/lib/lefthook-check.js +65 -0
  151. package/lib/pat-setup.js +207 -0
  152. package/lib/plugin-catalog.js +350 -350
  153. package/lib/plugin-manager.js +166 -166
  154. package/lib/plugin-recommender.js +141 -141
  155. package/lib/project-discovery.js +491 -491
  156. package/lib/setup-action-log.js +139 -0
  157. package/lib/setup-summary-renderer.js +106 -0
  158. package/lib/setup-utils.js +96 -0
  159. package/lib/setup.js +192 -118
  160. package/lib/smart-merge.js +64 -0
  161. package/lib/symlink-utils.js +81 -0
  162. package/lib/workflow-profiles.js +197 -197
  163. package/package.json +131 -129
  164. package/scripts/beads-context.sh +291 -0
  165. package/scripts/beads-context.test.js +563 -0
  166. package/scripts/behavioral-judge.sh +378 -0
  167. package/scripts/benchmark.js +85 -0
  168. package/scripts/branch-protection.js +183 -0
  169. package/scripts/check-agents.js +172 -0
  170. package/scripts/commitlint.js +42 -0
  171. package/scripts/conflict-detect.sh +323 -0
  172. package/scripts/dep-guard-analyze.js +71 -0
  173. package/scripts/dep-guard.sh +811 -0
  174. package/scripts/eval_win.py +249 -0
  175. package/scripts/file-index.sh +399 -0
  176. package/scripts/github-beads-sync/comment.mjs +64 -0
  177. package/scripts/github-beads-sync/config.mjs +148 -0
  178. package/scripts/github-beads-sync/github-api.mjs +131 -0
  179. package/scripts/github-beads-sync/index.mjs +332 -0
  180. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  181. package/scripts/github-beads-sync/mapping.mjs +78 -0
  182. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  183. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  184. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  185. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  186. package/scripts/github-beads-sync.config.json +26 -0
  187. package/scripts/improve-command.js +375 -0
  188. package/scripts/lib/eval-runner.js +229 -0
  189. package/scripts/lib/eval-schema.js +135 -0
  190. package/scripts/lib/eval-storage.js +78 -0
  191. package/scripts/lib/grading.js +203 -0
  192. package/scripts/lib/transcript-parser.js +63 -0
  193. package/scripts/lint.js +47 -0
  194. package/scripts/migrate-to-bun-test.js +412 -0
  195. package/scripts/run-command-eval.js +236 -0
  196. package/scripts/smart-status.sh +782 -0
  197. package/scripts/sync-commands.js +571 -0
  198. package/scripts/sync-utils.sh +460 -0
  199. package/scripts/test-dashboard.js +123 -0
  200. package/scripts/test.js +44 -0
  201. package/scripts/validate.sh +94 -0
  202. package/skills/parallel-deep-research/SKILL.md +108 -108
  203. package/skills/parallel-deep-research/evals/README.md +27 -27
  204. package/skills/parallel-deep-research/evals/evals.json +62 -62
  205. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  206. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  207. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  208. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  209. package/docs/WORKFLOW.md +0 -400
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Centralized action log for setup operations.
3
+ *
4
+ * Collects file-level actions (created, skipped, merged, etc.) during
5
+ * `forge setup` so the CLI can display a structured summary at the end.
6
+ *
7
+ * @module setup-action-log
8
+ */
9
+
10
+ /** Map of directory prefixes to human-readable agent names. */
11
+ const AGENT_PREFIXES = {
12
+ '.claude/': 'Claude Code',
13
+ '.cursor/': 'Cursor',
14
+ '.windsurf/': 'Windsurf',
15
+ '.cline/': 'Cline',
16
+ '.codex/': 'Codex',
17
+ '.opencode/': 'OpenCode',
18
+ '.kilocode/': 'Kilocode',
19
+ '.roo/': 'Roo Code',
20
+ '.github/prompts/': 'GitHub Copilot'
21
+ };
22
+
23
+ /**
24
+ * Detect the agent name from a file path.
25
+ *
26
+ * @param {string} filePath - Relative file path (e.g. `.claude/settings.json`)
27
+ * @returns {string} Agent name or `'General'` if no agent prefix matches
28
+ */
29
+ function detectAgent(filePath) {
30
+ const normalized = filePath.replace(/\\/g, '/');
31
+ for (const [prefix, name] of Object.entries(AGENT_PREFIXES)) {
32
+ if (normalized.startsWith(prefix)) {
33
+ return name;
34
+ }
35
+ }
36
+ return 'General';
37
+ }
38
+
39
+ /**
40
+ * Strip the agent directory prefix from a file path.
41
+ *
42
+ * @param {string} filePath - Relative file path
43
+ * @returns {string} Path with the leading agent directory removed
44
+ */
45
+ function stripAgentPrefix(filePath) {
46
+ const normalized = filePath.replace(/\\/g, '/');
47
+ for (const prefix of Object.keys(AGENT_PREFIXES)) {
48
+ if (normalized.startsWith(prefix)) {
49
+ return normalized.slice(prefix.length);
50
+ }
51
+ }
52
+ return normalized;
53
+ }
54
+
55
+ class SetupActionLog {
56
+ constructor() {
57
+ /** @type {Array<{file: string, action: string, detail: string|null}>} */
58
+ this.actions = [];
59
+ }
60
+
61
+ /**
62
+ * Record a setup action.
63
+ *
64
+ * @param {string} file - Relative file path that was acted on
65
+ * @param {string} action - One of: created, skipped, merged, conflict, removed, force-created
66
+ * @param {string|null} [detail=null] - Optional human-readable detail
67
+ */
68
+ add(file, action, detail = null) {
69
+ this.actions.push({ file, action, detail });
70
+ }
71
+
72
+ /**
73
+ * Get counts grouped by action type.
74
+ *
75
+ * @returns {Record<string, number>} e.g. `{ created: 4, skipped: 2 }`
76
+ */
77
+ getSummary() {
78
+ const counts = {};
79
+ for (const { action } of this.actions) {
80
+ counts[action] = (counts[action] || 0) + 1;
81
+ }
82
+ return counts;
83
+ }
84
+
85
+ /**
86
+ * Get the full ordered list of actions.
87
+ *
88
+ * @returns {Array<{file: string, action: string, detail: string|null}>}
89
+ */
90
+ getVerbose() {
91
+ return this.actions;
92
+ }
93
+
94
+ /**
95
+ * Group files by detected agent name.
96
+ *
97
+ * Each agent key maps to an object whose keys are action types and whose
98
+ * values are arrays of file paths (with the agent prefix stripped).
99
+ *
100
+ * @returns {Record<string, Record<string, string[]>>}
101
+ */
102
+ getAgentSummary() {
103
+ const agents = {};
104
+ for (const { file, action } of this.actions) {
105
+ const agent = detectAgent(file);
106
+ const stripped = stripAgentPrefix(file);
107
+
108
+ if (!agents[agent]) {
109
+ agents[agent] = {};
110
+ }
111
+ if (!agents[agent][action]) {
112
+ agents[agent][action] = [];
113
+ }
114
+ agents[agent][action].push(stripped);
115
+ }
116
+ return agents;
117
+ }
118
+
119
+ /**
120
+ * Filter actions by type.
121
+ *
122
+ * @param {string} action - The action type to filter on
123
+ * @returns {Array<{file: string, action: string, detail: string|null}>}
124
+ */
125
+ getByAction(action) {
126
+ return this.actions.filter(a => a.action === action);
127
+ }
128
+
129
+ /**
130
+ * Total number of recorded actions.
131
+ *
132
+ * @returns {number}
133
+ */
134
+ get length() {
135
+ return this.actions.length;
136
+ }
137
+ }
138
+
139
+ module.exports = { SetupActionLog };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Renders a clean summary from SetupActionLog data.
3
+ *
4
+ * Default mode: 3-line concise output.
5
+ * Verbose mode: file-by-file detail grouped by agent.
6
+ *
7
+ * @module setup-summary-renderer
8
+ */
9
+
10
+ /**
11
+ * Capitalize the first letter of a string.
12
+ *
13
+ * @param {string} str
14
+ * @returns {string}
15
+ */
16
+ function capitalize(str) {
17
+ if (!str) return str;
18
+ return str.charAt(0).toUpperCase() + str.slice(1);
19
+ }
20
+
21
+ /**
22
+ * Render setup summary as a string.
23
+ *
24
+ * @param {import('./setup-action-log').SetupActionLog} actionLog - The populated action log
25
+ * @param {string[]} agentNames - List of configured agent slugs (e.g. ['claude', 'cursor'])
26
+ * @param {boolean} verbose - Whether to show file-by-file detail
27
+ * @returns {string} The formatted summary output
28
+ */
29
+ function renderSetupSummary(actionLog, agentNames, verbose) {
30
+ if (verbose) {
31
+ return renderVerbose(actionLog, agentNames);
32
+ }
33
+ return renderDefault(actionLog, agentNames);
34
+ }
35
+
36
+ /**
37
+ * Render the default 3-line concise summary.
38
+ *
39
+ * @param {import('./setup-action-log').SetupActionLog} actionLog
40
+ * @param {string[]} agentNames
41
+ * @returns {string}
42
+ */
43
+ function renderDefault(actionLog, agentNames) {
44
+ const agentCount = agentNames.length;
45
+ const agentLabel = agentCount === 1 ? '1 agent' : `${agentCount} agents`;
46
+ const agentList = agentNames.length > 0 ? ` (${agentNames.join(', ')})` : '';
47
+
48
+ const summary = actionLog.getSummary();
49
+
50
+ // Build counts line — only include non-zero actions
51
+ const displayOrder = ['created', 'skipped', 'merged', 'force-created', 'updated', 'conflict', 'removed'];
52
+ const parts = [];
53
+ for (const action of displayOrder) {
54
+ if (summary[action] && summary[action] > 0) {
55
+ parts.push(`${capitalize(action)}: ${summary[action]} ${summary[action] === 1 ? 'file' : 'files'}`);
56
+ }
57
+ }
58
+ // Include any actions not in displayOrder
59
+ for (const [action, count] of Object.entries(summary)) {
60
+ if (!displayOrder.includes(action) && count > 0) {
61
+ parts.push(`${capitalize(action)}: ${count} ${count === 1 ? 'file' : 'files'}`);
62
+ }
63
+ }
64
+
65
+ const lines = [];
66
+ lines.push(`Forge setup complete — ${agentLabel} configured${agentList}`);
67
+
68
+ if (parts.length > 0) {
69
+ lines.push(` ${parts.join(' | ')}`);
70
+ } else {
71
+ lines.push(' 0 files changed');
72
+ }
73
+
74
+ lines.push(' Run forge setup --verbose to see all files');
75
+
76
+ return lines.join('\n');
77
+ }
78
+
79
+ /**
80
+ * Render verbose file-by-file output grouped by agent.
81
+ *
82
+ * @param {import('./setup-action-log').SetupActionLog} actionLog
83
+ * @param {string[]} _agentNames - Not used in verbose (agents come from log data)
84
+ * @returns {string}
85
+ */
86
+ function renderVerbose(actionLog, _agentNames) {
87
+ const agentSummary = actionLog.getAgentSummary();
88
+ const lines = [];
89
+
90
+ for (const [agent, actions] of Object.entries(agentSummary)) {
91
+ for (const [action, files] of Object.entries(actions)) {
92
+ const fileCount = files.length;
93
+ const fileLabel = fileCount === 1 ? '1 file' : `${fileCount} files`;
94
+ const fileList = files.join(', ');
95
+ lines.push(`${agent}: ${fileList} (${fileLabel}) [${action}]`);
96
+ }
97
+ }
98
+
99
+ if (lines.length === 0) {
100
+ return 'No file operations recorded.';
101
+ }
102
+
103
+ return lines.join('\n');
104
+ }
105
+
106
+ module.exports = { renderSetupSummary };
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Foundational utilities for `forge setup` command.
3
+ *
4
+ * - `ActionCollector` — collects planned/completed file actions for dry-run
5
+ * summaries and normal-mode logging.
6
+ * - `isNonInteractive()` — detects CI environments and piped stdin so
7
+ * interactive prompts can fall back to defaults.
8
+ *
9
+ * @module setup-utils
10
+ */
11
+
12
+ /**
13
+ * Collects file-level actions during setup.
14
+ *
15
+ * Used by `--dry-run` to collect planned actions and by normal mode
16
+ * to log what was actually done.
17
+ */
18
+ class ActionCollector {
19
+ constructor() {
20
+ /** @type {Array<{type: string, path: string, description: string}>} */
21
+ this._actions = [];
22
+ }
23
+
24
+ /**
25
+ * Record a setup action.
26
+ *
27
+ * @param {'create'|'modify'|'skip'} type - Action type
28
+ * @param {string} path - File path that was (or would be) acted on
29
+ * @param {string} description - Human-readable explanation
30
+ */
31
+ add(type, path, description) {
32
+ this._actions.push({ type, path, description });
33
+ }
34
+
35
+ /**
36
+ * Return a copy of all recorded actions.
37
+ *
38
+ * @returns {Array<{type: string, path: string, description: string}>}
39
+ */
40
+ list() {
41
+ return [...this._actions];
42
+ }
43
+
44
+ /**
45
+ * Print a formatted summary of all recorded actions to stdout.
46
+ */
47
+ print() {
48
+ if (this._actions.length === 0) {
49
+ return;
50
+ }
51
+
52
+ const icons = {
53
+ create: '+',
54
+ modify: '~',
55
+ skip: '-'
56
+ };
57
+
58
+ for (const { type, path, description } of this._actions) {
59
+ const icon = icons[type] || '?';
60
+ process.stdout.write(` [${icon}] ${type.padEnd(6)} ${path} — ${description}\n`);
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Detect whether the current environment is non-interactive.
67
+ *
68
+ * Returns `true` if any of:
69
+ * - `process.env.CI` is truthy (non-empty)
70
+ * - `process.env.GITHUB_ACTIONS` exists
71
+ * - `process.env.GITLAB_CI` exists
72
+ * - `process.stdin.isTTY` is falsy
73
+ * - `--non-interactive` flag was passed in `process.argv`
74
+ *
75
+ * @returns {boolean}
76
+ */
77
+ function isNonInteractive() {
78
+ if (process.env.CI && process.env.CI !== '') {
79
+ return true;
80
+ }
81
+ if (process.env.GITHUB_ACTIONS !== undefined) {
82
+ return true;
83
+ }
84
+ if (process.env.GITLAB_CI !== undefined) {
85
+ return true;
86
+ }
87
+ if (!process.stdin.isTTY) {
88
+ return true;
89
+ }
90
+ if (process.argv.includes('--non-interactive')) {
91
+ return true;
92
+ }
93
+ return false;
94
+ }
95
+
96
+ module.exports = { ActionCollector, isNonInteractive };
package/lib/setup.js CHANGED
@@ -1,118 +1,192 @@
1
- const fs = require('node:fs');
2
- const path = require('node:path');
3
-
4
- /**
5
- * Save setup state to .forge/setup-state.json
6
- * @param {string} projectPath - Path to the project root
7
- * @param {Object} state - Setup state object
8
- * @param {string} state.version - Forge version
9
- * @param {string[]} state.completed_steps - List of completed steps
10
- * @param {string[]} state.pending_steps - List of pending steps
11
- * @param {string} [state.last_run] - ISO timestamp of last run
12
- * @returns {Promise<void>}
13
- */
14
- async function saveSetupState(projectPath, state) {
15
- const forgeDir = path.join(projectPath, '.forge');
16
- const setupStatePath = path.join(forgeDir, 'setup-state.json');
17
-
18
- // Ensure .forge directory exists
19
- await fs.promises.mkdir(forgeDir, { recursive: true });
20
-
21
- // Add last_run timestamp if not provided
22
- if (!state.last_run) {
23
- state.last_run = new Date().toISOString();
24
- }
25
-
26
- // Write state as JSON
27
- await fs.promises.writeFile(setupStatePath, JSON.stringify(state, null, 2), 'utf-8');
28
- }
29
-
30
- /**
31
- * Load setup state from .forge/setup-state.json
32
- * @param {string} projectPath - Path to the project root
33
- * @returns {Promise<Object|null>} Setup state object, or null if not found or invalid
34
- */
35
- async function loadSetupState(projectPath) {
36
- const setupStatePath = path.join(projectPath, '.forge', 'setup-state.json');
37
-
38
- try {
39
- const content = await fs.promises.readFile(setupStatePath, 'utf-8');
40
- return JSON.parse(content);
41
- } catch (_error) {
42
- // File doesn't exist or invalid JSON
43
- return null;
44
- }
45
- }
46
-
47
- /**
48
- * Check if setup is complete (no pending steps)
49
- * @param {string} projectPath - Path to the project root
50
- * @returns {Promise<boolean>} True if setup complete, false otherwise
51
- */
52
- async function isSetupComplete(projectPath) {
53
- const state = await loadSetupState(projectPath);
54
-
55
- if (!state) {
56
- return false;
57
- }
58
-
59
- // Setup is complete if pending_steps is empty
60
- return state.pending_steps.length === 0;
61
- }
62
-
63
- /**
64
- * Get the next pending step
65
- * @param {string} projectPath - Path to the project root
66
- * @returns {Promise<string|null>} Next step name, or null if none
67
- */
68
- async function getNextStep(projectPath) {
69
- const state = await loadSetupState(projectPath);
70
-
71
- if (!state || !state.pending_steps || state.pending_steps.length === 0) {
72
- return null;
73
- }
74
-
75
- // Return first pending step
76
- return state.pending_steps[0];
77
- }
78
-
79
- /**
80
- * Mark a step as complete (move from pending to completed)
81
- * @param {string} projectPath - Path to the project root
82
- * @param {string} stepName - Name of the step to mark complete
83
- * @returns {Promise<void>}
84
- */
85
- async function markStepComplete(projectPath, stepName) {
86
- let state = await loadSetupState(projectPath);
87
-
88
- // If no state exists, create initial state
89
- if (!state) {
90
- state = {
91
- version: '1.6.0',
92
- completed_steps: [],
93
- pending_steps: []
94
- };
95
- }
96
-
97
- // Remove from pending_steps if present
98
- state.pending_steps = state.pending_steps.filter(step => step !== stepName);
99
-
100
- // Add to completed_steps if not already there
101
- if (!state.completed_steps.includes(stepName)) {
102
- state.completed_steps.push(stepName);
103
- }
104
-
105
- // Update last_run timestamp
106
- state.last_run = new Date().toISOString();
107
-
108
- // Save updated state
109
- await saveSetupState(projectPath, state);
110
- }
111
-
112
- module.exports = {
113
- saveSetupState,
114
- loadSetupState,
115
- isSetupComplete,
116
- getNextStep,
117
- markStepComplete
118
- };
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ /**
5
+ * Save setup state to .forge/setup-state.json
6
+ * @param {string} projectPath - Path to the project root
7
+ * @param {Object} state - Setup state object
8
+ * @param {string} state.version - Forge version
9
+ * @param {string[]} state.completed_steps - List of completed steps
10
+ * @param {string[]} state.pending_steps - List of pending steps
11
+ * @param {string} [state.last_run] - ISO timestamp of last run
12
+ * @returns {Promise<void>}
13
+ */
14
+ async function saveSetupState(projectPath, state) {
15
+ const forgeDir = path.join(projectPath, '.forge');
16
+ const setupStatePath = path.join(forgeDir, 'setup-state.json');
17
+
18
+ // Ensure .forge directory exists
19
+ await fs.promises.mkdir(forgeDir, { recursive: true });
20
+
21
+ // Add last_run timestamp if not provided
22
+ if (!state.last_run) {
23
+ state.last_run = new Date().toISOString();
24
+ }
25
+
26
+ // Write state as JSON
27
+ await fs.promises.writeFile(setupStatePath, JSON.stringify(state, null, 2), 'utf-8');
28
+ }
29
+
30
+ /**
31
+ * Load setup state from .forge/setup-state.json
32
+ * @param {string} projectPath - Path to the project root
33
+ * @returns {Promise<Object|null>} Setup state object, or null if not found or invalid
34
+ */
35
+ async function loadSetupState(projectPath) {
36
+ const setupStatePath = path.join(projectPath, '.forge', 'setup-state.json');
37
+
38
+ try {
39
+ const content = await fs.promises.readFile(setupStatePath, 'utf-8');
40
+ return JSON.parse(content);
41
+ } catch (_error) {
42
+ // File doesn't exist or invalid JSON
43
+ return null;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Check if setup is complete (no pending steps)
49
+ * @param {string} projectPath - Path to the project root
50
+ * @returns {Promise<boolean>} True if setup complete, false otherwise
51
+ */
52
+ async function isSetupComplete(projectPath) {
53
+ const state = await loadSetupState(projectPath);
54
+
55
+ if (!state) {
56
+ return false;
57
+ }
58
+
59
+ // Setup is complete if pending_steps is empty
60
+ return state.pending_steps.length === 0;
61
+ }
62
+
63
+ /**
64
+ * Get the next pending step
65
+ * @param {string} projectPath - Path to the project root
66
+ * @returns {Promise<string|null>} Next step name, or null if none
67
+ */
68
+ async function getNextStep(projectPath) {
69
+ const state = await loadSetupState(projectPath);
70
+
71
+ if (!state || !state.pending_steps || state.pending_steps.length === 0) {
72
+ return null;
73
+ }
74
+
75
+ // Return first pending step
76
+ return state.pending_steps[0];
77
+ }
78
+
79
+ /**
80
+ * Mark a step as complete (move from pending to completed)
81
+ * @param {string} projectPath - Path to the project root
82
+ * @param {string} stepName - Name of the step to mark complete
83
+ * @returns {Promise<void>}
84
+ */
85
+ async function markStepComplete(projectPath, stepName) {
86
+ let state = await loadSetupState(projectPath);
87
+
88
+ // If no state exists, create initial state
89
+ if (!state) {
90
+ state = {
91
+ version: '1.6.0',
92
+ completed_steps: [],
93
+ pending_steps: []
94
+ };
95
+ }
96
+
97
+ // Remove from pending_steps if present
98
+ state.pending_steps = state.pending_steps.filter(step => step !== stepName);
99
+
100
+ // Add to completed_steps if not already there
101
+ if (!state.completed_steps.includes(stepName)) {
102
+ state.completed_steps.push(stepName);
103
+ }
104
+
105
+ // Update last_run timestamp
106
+ state.last_run = new Date().toISOString();
107
+
108
+ // Save updated state
109
+ await saveSetupState(projectPath, state);
110
+ }
111
+
112
+ /**
113
+ * Scaffold GitHub-Beads sync files into a target project.
114
+ * Copies workflow, config, and mapping template — never overwrites existing files.
115
+ *
116
+ * @param {string} projectPath - Target project root
117
+ * @param {string} pkgDir - Forge package directory (source of template files)
118
+ * @returns {Promise<{created: string[], skipped: string[]}>}
119
+ */
120
+ async function scaffoldGithubBeadsSync(projectPath, pkgDir) {
121
+ const created = [];
122
+ const skipped = [];
123
+
124
+ // Sync script modules that workflows depend on at runtime
125
+ const syncScripts = [
126
+ 'config.mjs', 'mapping.mjs', 'comment.mjs', 'github-api.mjs',
127
+ 'sanitize.mjs', 'run-bd.mjs', 'label-mapper.mjs', 'index.mjs',
128
+ 'reverse-sync.mjs', 'reverse-sync-cli.mjs',
129
+ ];
130
+
131
+ const files = [
132
+ // Phase 1: GitHub → Beads
133
+ {
134
+ src: path.join(pkgDir, '.github', 'workflows', 'github-to-beads.yml'),
135
+ dest: path.join('.github', 'workflows', 'github-to-beads.yml'),
136
+ },
137
+ // Phase 2: Beads → GitHub
138
+ {
139
+ src: path.join(pkgDir, '.github', 'workflows', 'beads-to-github.yml'),
140
+ dest: path.join('.github', 'workflows', 'beads-to-github.yml'),
141
+ },
142
+ {
143
+ src: path.join(pkgDir, 'scripts', 'github-beads-sync.config.json'),
144
+ dest: path.join('scripts', 'github-beads-sync.config.json'),
145
+ },
146
+ {
147
+ src: null, // generated in-place (empty mapping template)
148
+ dest: path.join('.github', 'beads-mapping.json'),
149
+ content: '{}',
150
+ },
151
+ // Sync script modules
152
+ ...syncScripts.map((name) => ({
153
+ src: path.join(pkgDir, 'scripts', 'github-beads-sync', name),
154
+ dest: path.join('scripts', 'github-beads-sync', name),
155
+ })),
156
+ ];
157
+
158
+ for (const file of files) {
159
+ const destPath = path.join(projectPath, file.dest);
160
+
161
+ // Never overwrite existing files — preserve user customizations
162
+ if (fs.existsSync(destPath)) {
163
+ skipped.push(file.dest);
164
+ continue;
165
+ }
166
+
167
+ // Ensure parent directory exists
168
+ const destDir = path.dirname(destPath);
169
+ await fs.promises.mkdir(destDir, { recursive: true });
170
+
171
+ if (file.src) {
172
+ // Copy from template
173
+ await fs.promises.copyFile(file.src, destPath);
174
+ } else {
175
+ // Write generated content
176
+ await fs.promises.writeFile(destPath, file.content, 'utf-8');
177
+ }
178
+
179
+ created.push(file.dest);
180
+ }
181
+
182
+ return { created, skipped };
183
+ }
184
+
185
+ module.exports = {
186
+ saveSetupState,
187
+ loadSetupState,
188
+ isSetupComplete,
189
+ getNextStep,
190
+ markStepComplete,
191
+ scaffoldGithubBeadsSync
192
+ };