forge-workflow 0.0.4 → 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.
- package/.claude/commands/dev.md +340 -340
- package/.claude/commands/plan.md +521 -521
- package/.claude/commands/premerge.md +176 -176
- package/.claude/commands/research.md +42 -42
- package/.claude/commands/review.md +442 -442
- package/.claude/commands/rollback.md +721 -721
- package/.claude/commands/ship.md +164 -164
- package/.claude/commands/sonarcloud.md +152 -152
- package/.claude/commands/status.md +48 -48
- package/.claude/commands/validate.md +282 -282
- package/.claude/commands/verify.md +221 -221
- package/.claude/rules/greptile-review-process.md +285 -285
- package/.claude/rules/workflow.md +105 -105
- package/.claude/scripts/greptile-resolve.sh +526 -526
- package/.claude/scripts/load-env.sh +32 -32
- package/.cline/workflows/dev.md +337 -337
- package/.cline/workflows/plan.md +518 -518
- package/.cline/workflows/premerge.md +173 -173
- package/.cline/workflows/research.md +39 -39
- package/.cline/workflows/review.md +439 -439
- package/.cline/workflows/rollback.md +718 -718
- package/.cline/workflows/ship.md +161 -161
- package/.cline/workflows/sonarcloud.md +146 -146
- package/.cline/workflows/status.md +45 -45
- package/.cline/workflows/validate.md +279 -279
- package/.cline/workflows/verify.md +218 -218
- package/.codex/config.toml +11 -11
- package/.codex/skills/dev/SKILL.md +340 -340
- package/.codex/skills/plan/SKILL.md +521 -521
- package/.codex/skills/premerge/SKILL.md +176 -176
- package/.codex/skills/research/SKILL.md +42 -42
- package/.codex/skills/review/SKILL.md +442 -442
- package/.codex/skills/rollback/SKILL.md +721 -721
- package/.codex/skills/ship/SKILL.md +164 -164
- package/.codex/skills/sonarcloud/SKILL.md +149 -149
- package/.codex/skills/status/SKILL.md +48 -48
- package/.codex/skills/validate/SKILL.md +282 -282
- package/.codex/skills/verify/SKILL.md +221 -221
- package/.cursor/commands/dev.md +337 -337
- package/.cursor/commands/plan.md +518 -518
- package/.cursor/commands/premerge.md +173 -173
- package/.cursor/commands/research.md +39 -39
- package/.cursor/commands/review.md +439 -439
- package/.cursor/commands/rollback.md +718 -718
- package/.cursor/commands/ship.md +161 -161
- package/.cursor/commands/sonarcloud.md +146 -146
- package/.cursor/commands/status.md +45 -45
- package/.cursor/commands/validate.md +279 -279
- package/.cursor/commands/verify.md +218 -218
- package/.cursor/rules/permissions-guidance.mdc +37 -37
- package/.forge/hooks/check-tdd.js +240 -240
- package/.github/PLUGIN_TEMPLATE.json +32 -32
- package/.github/prompts/dev.prompt.md +342 -342
- package/.github/prompts/plan.prompt.md +523 -523
- package/.github/prompts/premerge.prompt.md +178 -178
- package/.github/prompts/research.prompt.md +44 -44
- package/.github/prompts/review.prompt.md +444 -444
- package/.github/prompts/rollback.prompt.md +723 -723
- package/.github/prompts/ship.prompt.md +166 -166
- package/.github/prompts/sonarcloud.prompt.md +151 -151
- package/.github/prompts/status.prompt.md +50 -50
- package/.github/prompts/validate.prompt.md +284 -284
- package/.github/prompts/verify.prompt.md +223 -223
- package/.github/workflows/beads-to-github.yml +56 -0
- package/.github/workflows/github-to-beads.yml +97 -0
- package/.kilocode/workflows/dev.md +341 -341
- package/.kilocode/workflows/plan.md +522 -522
- package/.kilocode/workflows/premerge.md +177 -177
- package/.kilocode/workflows/research.md +43 -43
- package/.kilocode/workflows/review.md +443 -443
- package/.kilocode/workflows/rollback.md +722 -722
- package/.kilocode/workflows/ship.md +165 -165
- package/.kilocode/workflows/sonarcloud.md +150 -150
- package/.kilocode/workflows/status.md +49 -49
- package/.kilocode/workflows/validate.md +283 -283
- package/.kilocode/workflows/verify.md +222 -222
- package/.mcp.json.example +12 -12
- package/.opencode/commands/dev.md +340 -340
- package/.opencode/commands/plan.md +521 -521
- package/.opencode/commands/premerge.md +176 -176
- package/.opencode/commands/research.md +42 -42
- package/.opencode/commands/review.md +442 -442
- package/.opencode/commands/rollback.md +721 -721
- package/.opencode/commands/ship.md +164 -164
- package/.opencode/commands/sonarcloud.md +149 -149
- package/.opencode/commands/status.md +48 -48
- package/.opencode/commands/validate.md +282 -282
- package/.opencode/commands/verify.md +221 -221
- package/.roo/commands/dev.md +341 -341
- package/.roo/commands/plan.md +522 -522
- package/.roo/commands/premerge.md +177 -177
- package/.roo/commands/research.md +43 -43
- package/.roo/commands/review.md +443 -443
- package/.roo/commands/rollback.md +722 -722
- package/.roo/commands/ship.md +165 -165
- package/.roo/commands/sonarcloud.md +150 -150
- package/.roo/commands/status.md +49 -49
- package/.roo/commands/validate.md +283 -283
- package/.roo/commands/verify.md +222 -222
- package/AGENTS.md +175 -175
- package/CLAUDE.md +100 -100
- package/README.md +429 -416
- package/bin/forge-cmd.js +313 -313
- package/bin/forge-preflight.js +309 -309
- package/bin/forge.js +4596 -4303
- package/docs/AGENT_INSTALL_PROMPT.md +342 -342
- package/docs/BEADS_GITHUB_SYNC.md +251 -251
- package/docs/ENHANCED_ONBOARDING.md +602 -602
- package/docs/EXAMPLES.md +482 -482
- package/docs/GREPTILE_SETUP.md +400 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
- package/docs/ROADMAP.md +359 -359
- package/docs/SETUP.md +663 -631
- package/docs/TOOLCHAIN.md +630 -630
- package/docs/VALIDATION.md +363 -363
- package/install.sh +40 -1056
- package/lefthook.yml +39 -39
- package/lib/agents/README.md +198 -198
- package/lib/agents/claude.plugin.json +28 -28
- package/lib/agents/cline.plugin.json +22 -22
- package/lib/agents/codex.plugin.json +19 -19
- package/lib/agents/copilot.plugin.json +24 -24
- package/lib/agents/cursor.plugin.json +25 -25
- package/lib/agents/kilocode.plugin.json +22 -22
- package/lib/agents/opencode.plugin.json +20 -20
- package/lib/agents/roo.plugin.json +23 -23
- package/lib/agents-config.js +2112 -2112
- package/lib/beads-health-check.js +143 -0
- package/lib/beads-setup.js +341 -0
- package/lib/beads-sync-scaffold.js +260 -0
- package/lib/commands/dev.js +513 -513
- package/lib/commands/plan.js +692 -692
- package/lib/commands/recommend.js +119 -119
- package/lib/commands/ship.js +377 -377
- package/lib/commands/status.js +378 -378
- package/lib/commands/validate.js +602 -602
- package/lib/context-merge.js +359 -359
- package/lib/dep-guard/analyzer.js +294 -294
- package/lib/dep-guard/behavior-detector.js +98 -98
- package/lib/dep-guard/contract-detector.js +162 -162
- package/lib/dep-guard/import-detector.js +498 -498
- package/lib/dep-guard/path-utils.js +13 -13
- package/lib/dep-guard/rubric.js +120 -120
- package/lib/dep-guard/task-parser.js +318 -318
- package/lib/detect-agent.js +191 -191
- package/lib/detect-worktree.js +47 -47
- package/lib/file-hash.js +26 -26
- package/lib/husky-migration.js +450 -0
- package/lib/lefthook-check.js +65 -0
- package/lib/pat-setup.js +207 -0
- package/lib/plugin-catalog.js +350 -350
- package/lib/plugin-manager.js +166 -166
- package/lib/plugin-recommender.js +141 -141
- package/lib/project-discovery.js +491 -491
- package/lib/setup-action-log.js +139 -139
- package/lib/setup-summary-renderer.js +106 -106
- package/lib/setup-utils.js +96 -0
- package/lib/setup.js +192 -192
- package/lib/smart-merge.js +64 -0
- package/lib/symlink-utils.js +81 -0
- package/lib/workflow-profiles.js +197 -197
- package/package.json +131 -128
- package/scripts/beads-context.sh +291 -0
- package/scripts/beads-context.test.js +563 -0
- package/scripts/behavioral-judge.sh +378 -0
- package/scripts/benchmark.js +85 -0
- package/scripts/branch-protection.js +183 -0
- package/scripts/check-agents.js +172 -0
- package/scripts/commitlint.js +42 -0
- package/scripts/conflict-detect.sh +323 -0
- package/scripts/dep-guard-analyze.js +71 -0
- package/scripts/dep-guard.sh +811 -0
- package/scripts/eval_win.py +249 -0
- package/scripts/file-index.sh +399 -0
- package/scripts/github-beads-sync/comment.mjs +64 -0
- package/scripts/github-beads-sync/config.mjs +148 -0
- package/scripts/github-beads-sync/github-api.mjs +131 -0
- package/scripts/github-beads-sync/index.mjs +332 -0
- package/scripts/github-beads-sync/label-mapper.mjs +54 -0
- package/scripts/github-beads-sync/mapping.mjs +78 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
- package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
- package/scripts/github-beads-sync/run-bd.mjs +159 -0
- package/scripts/github-beads-sync/sanitize.mjs +121 -0
- package/scripts/github-beads-sync.config.json +26 -0
- package/scripts/improve-command.js +375 -0
- package/scripts/lib/eval-runner.js +229 -0
- package/scripts/lib/eval-schema.js +135 -0
- package/scripts/lib/eval-storage.js +78 -0
- package/scripts/lib/grading.js +203 -0
- package/scripts/lib/transcript-parser.js +63 -0
- package/scripts/lint.js +47 -0
- package/scripts/migrate-to-bun-test.js +412 -0
- package/scripts/run-command-eval.js +236 -0
- package/scripts/smart-status.sh +782 -0
- package/scripts/sync-commands.js +571 -0
- package/scripts/sync-utils.sh +460 -0
- package/scripts/test-dashboard.js +123 -0
- package/scripts/test.js +44 -0
- package/scripts/validate.sh +94 -0
- package/skills/parallel-deep-research/SKILL.md +108 -108
- package/skills/parallel-deep-research/evals/README.md +27 -27
- package/skills/parallel-deep-research/evals/evals.json +62 -62
- package/skills/sonarcloud-analysis/SKILL.md +171 -171
- package/skills/sonarcloud-analysis/evals/README.md +27 -27
- package/skills/sonarcloud-analysis/evals/evals.json +50 -50
- package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
- package/.cursor/hooks/state/continual-learning-index.json +0 -19
- package/.cursor/hooks/state/continual-learning.json +0 -8
package/lib/setup.js
CHANGED
|
@@ -1,192 +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
|
-
/**
|
|
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
|
-
};
|
|
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
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Smart merge for AGENTS.md - preserves USER sections, updates FORGE sections.
|
|
5
|
+
*
|
|
6
|
+
* Handles four cases:
|
|
7
|
+
* 1. No markers at all: Wrap existing content in USER markers, append FORGE section
|
|
8
|
+
* 2. USER markers but no FORGE markers: Keep USER section, insert FORGE section
|
|
9
|
+
* 3. Both markers present: Preserve USER section, update FORGE section (existing behavior)
|
|
10
|
+
* 4. Empty existing content: Return only FORGE section (no empty USER block)
|
|
11
|
+
*
|
|
12
|
+
* @param {string} existingContent - The current AGENTS.md content
|
|
13
|
+
* @param {string} newContent - The new template content containing FORGE section
|
|
14
|
+
* @returns {string} Merged content, or empty string if merge not possible
|
|
15
|
+
*/
|
|
16
|
+
function smartMergeAgentsMd(existingContent, newContent) {
|
|
17
|
+
// Extract FORGE section from new content (needed in all cases)
|
|
18
|
+
const forgeStartMatch = (/(<!-- FORGE:START.*?-->[\s\S]*?<!-- FORGE:END -->)/).exec(newContent);
|
|
19
|
+
const forgeSection = forgeStartMatch ? forgeStartMatch[0] : '';
|
|
20
|
+
|
|
21
|
+
// Check if existing content has markers
|
|
22
|
+
const hasUserMarkers = existingContent.includes('<!-- USER:START') && existingContent.includes('<!-- USER:END');
|
|
23
|
+
|
|
24
|
+
let userSection;
|
|
25
|
+
|
|
26
|
+
if (hasUserMarkers) {
|
|
27
|
+
// Extract existing USER section (with markers)
|
|
28
|
+
const userMatch = (/(<!-- USER:START.*?-->[\s\S]*?<!-- USER:END -->)/).exec(existingContent);
|
|
29
|
+
userSection = userMatch ? userMatch[0] : '';
|
|
30
|
+
} else if (existingContent.trim() === '') {
|
|
31
|
+
// Empty existing content: no USER block at all
|
|
32
|
+
userSection = null;
|
|
33
|
+
} else {
|
|
34
|
+
// No markers: wrap entire existing content in USER markers
|
|
35
|
+
userSection = `<!-- USER:START -->\n${existingContent.trim()}\n<!-- USER:END -->`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Build merged content
|
|
39
|
+
const setupInstructions = newContent.includes('<!-- FORGE:SETUP-INSTRUCTIONS')
|
|
40
|
+
? (/(<!-- FORGE:SETUP-INSTRUCTIONS[\s\S]*?-->)/).exec(newContent)?.[0] || ''
|
|
41
|
+
: '';
|
|
42
|
+
|
|
43
|
+
let merged = '# AGENTS.md\n\n';
|
|
44
|
+
|
|
45
|
+
// Add setup instructions if this is first-time setup
|
|
46
|
+
if (setupInstructions && !existingContent.includes('FORGE:SETUP-INSTRUCTIONS')) {
|
|
47
|
+
merged += setupInstructions + '\n\n';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Add USER section (skip if empty existing content — no empty USER block)
|
|
51
|
+
if (userSection !== null) {
|
|
52
|
+
merged += userSection + '\n\n';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Add updated FORGE section
|
|
56
|
+
merged += forgeSection + '\n\n';
|
|
57
|
+
|
|
58
|
+
// Add footer
|
|
59
|
+
merged += `---\n\n## Improving This Workflow\n\nEvery time you give the same instruction twice, add it to this file:\n1. User-specific rules: Add to USER:START section above\n2. Forge workflow improvements: Suggest to forge maintainers\n\n**Keep this file updated as you learn about the project.**\n\n---\n\nSee \`AGENTS.md\` for complete workflow guide.\nSee \`docs/TOOLCHAIN.md\` for comprehensive tool reference.\n`;
|
|
60
|
+
|
|
61
|
+
return merged;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { smartMergeAgentsMd };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symlink utilities for Forge setup.
|
|
3
|
+
*
|
|
4
|
+
* Creates CLAUDE.md (or other agent link files) as a symlink to AGENTS.md,
|
|
5
|
+
* with an automatic fallback to copy when symlinks are not available
|
|
6
|
+
* (e.g., Windows without admin privileges).
|
|
7
|
+
*
|
|
8
|
+
* @module symlink-utils
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Header comment prepended to copies when symlink creation fails.
|
|
16
|
+
* Alerts users that the file is a copy and how to create a proper symlink.
|
|
17
|
+
*/
|
|
18
|
+
const HEADER_COMMENT =
|
|
19
|
+
'<!-- This file is a copy of AGENTS.md. Keep in sync manually or use: bunx forge setup --symlink -->';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Create a symlink from `linkPath` pointing to `target`.
|
|
23
|
+
* If symlink creation fails (e.g., EPERM on Windows without admin),
|
|
24
|
+
* falls back to a file copy with a header comment — unless `symlinkOnly`
|
|
25
|
+
* is true, in which case it reports the error and returns ''.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} target - Absolute path to the source file (e.g., AGENTS.md)
|
|
28
|
+
* @param {string} linkPath - Absolute path for the symlink/copy (e.g., CLAUDE.md)
|
|
29
|
+
* @param {Object} [options={}] - Options
|
|
30
|
+
* @param {boolean} [options.symlinkOnly=false] - When true, skip copy fallback (--symlink flag)
|
|
31
|
+
* @returns {'linked'|'copied'|''} Result indicator
|
|
32
|
+
*/
|
|
33
|
+
function createSymlinkOrCopy(target, linkPath, options = {}) {
|
|
34
|
+
try {
|
|
35
|
+
// Ensure target exists
|
|
36
|
+
if (!fs.existsSync(target)) {
|
|
37
|
+
console.error(` ✗ Source file does not exist: ${target}`);
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Remove existing file/symlink at linkPath
|
|
42
|
+
if (fs.existsSync(linkPath)) {
|
|
43
|
+
const stat = fs.lstatSync(linkPath);
|
|
44
|
+
if (stat.isDirectory()) {
|
|
45
|
+
console.warn(
|
|
46
|
+
` ⚠ Skipped ${linkPath} (a directory exists at this path). Remove it manually and re-run setup.`
|
|
47
|
+
);
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
fs.unlinkSync(linkPath);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Ensure parent directory exists
|
|
54
|
+
const linkDir = path.dirname(linkPath);
|
|
55
|
+
if (!fs.existsSync(linkDir)) {
|
|
56
|
+
fs.mkdirSync(linkDir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Attempt symlink (relative path for portability)
|
|
60
|
+
try {
|
|
61
|
+
const relPath = path.relative(linkDir, target);
|
|
62
|
+
fs.symlinkSync(relPath, linkPath);
|
|
63
|
+
return 'linked';
|
|
64
|
+
} catch (_symlinkErr) {
|
|
65
|
+
// Expected: symlink creation fails with EPERM on Windows without admin privileges — fall back to copy
|
|
66
|
+
if (options.symlinkOnly) {
|
|
67
|
+
console.warn(` ⚠ Symlink failed for ${linkPath} (--symlink requires symlink support)`);
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
// Fall back to copy with header
|
|
71
|
+
const content = fs.readFileSync(target, 'utf-8');
|
|
72
|
+
fs.writeFileSync(linkPath, HEADER_COMMENT + '\n' + content, 'utf-8');
|
|
73
|
+
return 'copied';
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error(` ✗ Failed to link/copy ${target} -> ${linkPath}: ${err.message}`);
|
|
77
|
+
return '';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { createSymlinkOrCopy, HEADER_COMMENT };
|