forge-workflow 0.0.4 → 0.0.6
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 +345 -340
- package/.claude/commands/plan.md +566 -521
- package/.claude/commands/premerge.md +186 -176
- package/.claude/commands/research.md +42 -42
- package/.claude/commands/review.md +448 -442
- package/.claude/commands/rollback.md +721 -721
- package/.claude/commands/ship.md +212 -164
- package/.claude/commands/sonarcloud.md +152 -152
- package/.claude/commands/status.md +90 -48
- package/.claude/commands/validate.md +288 -282
- package/.claude/commands/verify.md +269 -221
- package/.claude/rules/greptile-review-process.md +285 -285
- package/.claude/rules/workflow.md +121 -105
- package/.claude/scripts/greptile-resolve.sh +558 -526
- package/.claude/scripts/load-env.sh +32 -32
- package/.cline/workflows/dev.md +342 -337
- package/.cline/workflows/plan.md +563 -518
- package/.cline/workflows/premerge.md +183 -173
- package/.cline/workflows/research.md +39 -39
- package/.cline/workflows/review.md +445 -439
- package/.cline/workflows/rollback.md +718 -718
- package/.cline/workflows/ship.md +209 -161
- package/.cline/workflows/sonarcloud.md +146 -146
- package/.cline/workflows/status.md +87 -45
- package/.cline/workflows/validate.md +285 -279
- package/.cline/workflows/verify.md +266 -218
- package/.codex/config.toml +11 -11
- package/.codex/skills/dev/SKILL.md +345 -340
- package/.codex/skills/plan/SKILL.md +566 -521
- package/.codex/skills/premerge/SKILL.md +186 -176
- package/.codex/skills/research/SKILL.md +42 -42
- package/.codex/skills/review/SKILL.md +448 -442
- package/.codex/skills/rollback/SKILL.md +721 -721
- package/.codex/skills/ship/SKILL.md +212 -164
- package/.codex/skills/sonarcloud/SKILL.md +149 -149
- package/.codex/skills/status/SKILL.md +90 -48
- package/.codex/skills/validate/SKILL.md +288 -282
- package/.codex/skills/verify/SKILL.md +269 -221
- package/.cursor/commands/dev.md +342 -337
- package/.cursor/commands/plan.md +563 -518
- package/.cursor/commands/premerge.md +183 -173
- package/.cursor/commands/research.md +39 -39
- package/.cursor/commands/review.md +445 -439
- package/.cursor/commands/rollback.md +718 -718
- package/.cursor/commands/ship.md +209 -161
- package/.cursor/commands/sonarcloud.md +146 -146
- package/.cursor/commands/status.md +87 -45
- package/.cursor/commands/validate.md +285 -279
- package/.cursor/commands/verify.md +266 -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 +347 -342
- package/.github/prompts/plan.prompt.md +568 -523
- package/.github/prompts/premerge.prompt.md +188 -178
- package/.github/prompts/research.prompt.md +44 -44
- package/.github/prompts/review.prompt.md +450 -444
- package/.github/prompts/rollback.prompt.md +723 -723
- package/.github/prompts/ship.prompt.md +214 -166
- package/.github/prompts/sonarcloud.prompt.md +151 -151
- package/.github/prompts/status.prompt.md +92 -50
- package/.github/prompts/validate.prompt.md +290 -284
- package/.github/prompts/verify.prompt.md +271 -223
- package/.github/workflows/beads-to-github.yml +56 -0
- package/.github/workflows/github-to-beads.yml +97 -0
- package/.kilocode/workflows/dev.md +346 -341
- package/.kilocode/workflows/plan.md +567 -522
- package/.kilocode/workflows/premerge.md +187 -177
- package/.kilocode/workflows/research.md +43 -43
- package/.kilocode/workflows/review.md +449 -443
- package/.kilocode/workflows/rollback.md +722 -722
- package/.kilocode/workflows/ship.md +213 -165
- package/.kilocode/workflows/sonarcloud.md +150 -150
- package/.kilocode/workflows/status.md +91 -49
- package/.kilocode/workflows/validate.md +289 -283
- package/.kilocode/workflows/verify.md +270 -222
- package/.mcp.json.example +12 -12
- package/.opencode/commands/dev.md +345 -340
- package/.opencode/commands/plan.md +566 -521
- package/.opencode/commands/premerge.md +186 -176
- package/.opencode/commands/research.md +42 -42
- package/.opencode/commands/review.md +448 -442
- package/.opencode/commands/rollback.md +721 -721
- package/.opencode/commands/ship.md +212 -164
- package/.opencode/commands/sonarcloud.md +149 -149
- package/.opencode/commands/status.md +90 -48
- package/.opencode/commands/validate.md +288 -282
- package/.opencode/commands/verify.md +269 -221
- package/.roo/commands/dev.md +346 -341
- package/.roo/commands/plan.md +567 -522
- package/.roo/commands/premerge.md +187 -177
- package/.roo/commands/research.md +43 -43
- package/.roo/commands/review.md +449 -443
- package/.roo/commands/rollback.md +722 -722
- package/.roo/commands/ship.md +213 -165
- package/.roo/commands/sonarcloud.md +150 -150
- package/.roo/commands/status.md +91 -49
- package/.roo/commands/validate.md +289 -283
- package/.roo/commands/verify.md +270 -222
- package/AGENTS.md +272 -175
- package/CLAUDE.md +110 -100
- package/README.md +429 -416
- package/bin/forge-cmd.js +317 -313
- package/bin/forge-preflight.js +322 -309
- package/bin/forge.js +4765 -4303
- package/docs/AGENT_INSTALL_PROMPT.md +342 -342
- package/docs/BEADS_GITHUB_SYNC.md +251 -251
- package/docs/ENHANCED_ONBOARDING.md +612 -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 +653 -630
- package/docs/VALIDATION.md +363 -363
- package/install.sh +40 -1056
- package/lefthook.yml +50 -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/_registry.js +134 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/dev.js +571 -513
- package/lib/commands/plan.js +692 -692
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +119 -119
- package/lib/commands/ship.js +377 -377
- package/lib/commands/status.js +378 -378
- package/lib/commands/sync.js +55 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/validate.js +602 -602
- package/lib/commands/worktree.js +310 -0
- 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/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/file-hash.js +26 -26
- package/lib/freshness-token.js +148 -0
- package/lib/greptile-match.js +80 -0
- 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/reset.js +309 -0
- 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/task-ownership.js +117 -0
- package/lib/workflow-profiles.js +197 -197
- package/package.json +131 -128
- package/scripts/beads-context.sh +426 -0
- package/scripts/beads-context.test.js +567 -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/check-forge-token.js +98 -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 +789 -0
- package/scripts/eval_win.py +249 -0
- package/scripts/file-index.sh +493 -0
- package/scripts/forge-team/index.sh +86 -0
- package/scripts/forge-team/lib/agent-prompt.sh +52 -0
- package/scripts/forge-team/lib/claim.sh +256 -0
- package/scripts/forge-team/lib/dashboard.sh +341 -0
- package/scripts/forge-team/lib/epic.sh +332 -0
- package/scripts/forge-team/lib/hooks.sh +253 -0
- package/scripts/forge-team/lib/identity.sh +235 -0
- package/scripts/forge-team/lib/sync-github.sh +317 -0
- package/scripts/forge-team/lib/verify.sh +284 -0
- package/scripts/forge-team/lib/workload.sh +296 -0
- package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
- package/scripts/forge-team/tests/claim.test.sh +179 -0
- package/scripts/forge-team/tests/dashboard.test.sh +170 -0
- package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
- package/scripts/forge-team/tests/epic.test.sh +176 -0
- package/scripts/forge-team/tests/hooks.test.sh +239 -0
- package/scripts/forge-team/tests/identity.test.sh +176 -0
- package/scripts/forge-team/tests/integration.test.sh +371 -0
- package/scripts/forge-team/tests/sync-github.test.sh +209 -0
- package/scripts/forge-team/tests/verify.test.sh +314 -0
- package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
- package/scripts/forge-team/tests/workload.test.sh +209 -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 +268 -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/jsonl-lock.sh +48 -0
- package/scripts/lib/sanitize.sh +116 -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/pr-coordinator.sh +706 -0
- package/scripts/run-command-eval.js +236 -0
- package/scripts/smart-status.sh +809 -0
- package/scripts/sync-commands.js +571 -0
- package/scripts/sync-utils.sh +455 -0
- package/scripts/test-dashboard.js +123 -0
- package/scripts/test.js +46 -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
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { execFileSync } = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Detect the default branch of the repository.
|
|
9
|
+
*
|
|
10
|
+
* Strategy (in order):
|
|
11
|
+
* 1. `git symbolic-ref refs/remotes/origin/HEAD` -> parse branch name
|
|
12
|
+
* 2. `git remote show origin` -> parse "HEAD branch:" line
|
|
13
|
+
* 3. Fall back to `'main'`
|
|
14
|
+
*
|
|
15
|
+
* @param {string} projectRoot - Absolute path to the project root.
|
|
16
|
+
* @param {object} [options] - Options object.
|
|
17
|
+
* @param {Function} [options._exec] - Injected execFileSync for testing.
|
|
18
|
+
* @returns {string} The default branch name.
|
|
19
|
+
*/
|
|
20
|
+
function detectDefaultBranch(projectRoot, options = {}) {
|
|
21
|
+
const exec = options._exec || execFileSync;
|
|
22
|
+
|
|
23
|
+
// Strategy 1: symbolic-ref
|
|
24
|
+
try {
|
|
25
|
+
const out = exec('git', ['symbolic-ref', 'refs/remotes/origin/HEAD'], {
|
|
26
|
+
cwd: projectRoot,
|
|
27
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
28
|
+
});
|
|
29
|
+
const ref = out.toString().trim();
|
|
30
|
+
// refs/remotes/origin/main -> main
|
|
31
|
+
const parts = ref.split('/');
|
|
32
|
+
if (parts.length > 0) {
|
|
33
|
+
return parts[parts.length - 1];
|
|
34
|
+
}
|
|
35
|
+
} catch (_e) {
|
|
36
|
+
// Expected: symbolic-ref fails when origin/HEAD is not set — fall through to strategy 2
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Strategy 2: remote show origin
|
|
40
|
+
try {
|
|
41
|
+
const out = exec('git', ['remote', 'show', 'origin'], {
|
|
42
|
+
cwd: projectRoot,
|
|
43
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
44
|
+
});
|
|
45
|
+
const text = out.toString();
|
|
46
|
+
const match = text.match(/HEAD branch:\s*(.+)/);
|
|
47
|
+
if (match) {
|
|
48
|
+
return match[1].trim();
|
|
49
|
+
}
|
|
50
|
+
} catch (_e) {
|
|
51
|
+
// Expected: 'git remote show origin' fails when no remote is configured — fall through to fallback
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Strategy 3: fallback
|
|
55
|
+
return 'main';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Detect the installed Beads version.
|
|
60
|
+
*
|
|
61
|
+
* Strategy (in order):
|
|
62
|
+
* 1. `bd --version` -> parse version string (e.g. "beads version 0.52.0" -> "0.52.0")
|
|
63
|
+
* 2. Fall back to known-good default `'0.49.1'`
|
|
64
|
+
*
|
|
65
|
+
* @param {object} [options] - Options object.
|
|
66
|
+
* @param {Function} [options._exec] - Injected execFileSync for testing.
|
|
67
|
+
* @returns {string} The Beads version string.
|
|
68
|
+
*/
|
|
69
|
+
function detectBeadsVersion(options = {}) {
|
|
70
|
+
const exec = options._exec || execFileSync;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const out = exec('bd', ['--version'], {
|
|
74
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
75
|
+
});
|
|
76
|
+
const text = out.toString().trim();
|
|
77
|
+
// "beads version 0.52.0" -> "0.52.0"
|
|
78
|
+
const match = text.match(/(\d{1,4}\.\d{1,4}\.\d{1,4})/);
|
|
79
|
+
if (match) {
|
|
80
|
+
return match[1];
|
|
81
|
+
}
|
|
82
|
+
} catch (_e) {
|
|
83
|
+
// Expected: 'bd --version' fails when bd CLI is not installed — fall through to default version
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return '0.49.1';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Template workflow YAML files by replacing the default branch and Beads version.
|
|
91
|
+
*
|
|
92
|
+
* Templates only forge-created workflow files by replacing the default branch
|
|
93
|
+
* and Beads version. Skips user-owned workflows to avoid overwriting legitimate
|
|
94
|
+
* branch targets.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} workflowDir - Absolute path to the directory containing YAML files.
|
|
97
|
+
* @param {string} branch - The default branch name to substitute.
|
|
98
|
+
* @param {string} beadsVersion - The Beads version to substitute.
|
|
99
|
+
* @param {string[]} [createdFiles=[]] - List of file paths created by scaffoldBeadsSync. Only these are templated.
|
|
100
|
+
*/
|
|
101
|
+
function templateWorkflows(workflowDir, branch, beadsVersion, createdFiles = []) {
|
|
102
|
+
if (!fs.existsSync(workflowDir)) return;
|
|
103
|
+
|
|
104
|
+
const targetNames = new Set(createdFiles.map(f => path.basename(f)));
|
|
105
|
+
|
|
106
|
+
const entries = fs.readdirSync(workflowDir);
|
|
107
|
+
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
// Only template forge-created files; skip user-owned workflows
|
|
110
|
+
if (targetNames.size > 0 && !targetNames.has(entry)) continue;
|
|
111
|
+
|
|
112
|
+
const ext = path.extname(entry).toLowerCase();
|
|
113
|
+
if (ext !== '.yml' && ext !== '.yaml') {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const filePath = path.join(workflowDir, entry);
|
|
118
|
+
const stat = fs.statSync(filePath);
|
|
119
|
+
if (!stat.isFile()) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const original = fs.readFileSync(filePath, 'utf8');
|
|
124
|
+
let content = original;
|
|
125
|
+
content = content.replaceAll(
|
|
126
|
+
/branches:\s*\[master\]/g,
|
|
127
|
+
`branches: [${branch}]`
|
|
128
|
+
);
|
|
129
|
+
content = content.replaceAll(
|
|
130
|
+
/BD_VERSION="0\.49\.1"/g,
|
|
131
|
+
`BD_VERSION="${beadsVersion}"`
|
|
132
|
+
);
|
|
133
|
+
// Only write if content actually changed
|
|
134
|
+
if (content !== original) {
|
|
135
|
+
fs.writeFileSync(filePath, content);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @typedef {Object} ScaffoldResult
|
|
142
|
+
* @property {string[]} filesCreated - Relative paths of files written (new or overwritten)
|
|
143
|
+
* @property {string[]} filesSkipped - Relative paths of files that already existed and were preserved
|
|
144
|
+
*/
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Copy a single file from src to dest, creating parent directories as needed.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} src - Absolute source path
|
|
150
|
+
* @param {string} dest - Absolute destination path
|
|
151
|
+
* @returns {void}
|
|
152
|
+
*/
|
|
153
|
+
function copyFileWithDirs(src, dest) {
|
|
154
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
155
|
+
fs.copyFileSync(src, dest);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Scaffold Beads sync workflows and scripts to the user's project.
|
|
160
|
+
*
|
|
161
|
+
* Copies workflow templates, sync modules, config, and mapping stub into
|
|
162
|
+
* the project's `.github/` tree from the forge package source.
|
|
163
|
+
*
|
|
164
|
+
* @param {string} projectRoot - Absolute path to the user's project root
|
|
165
|
+
* @param {string} packageDir - Absolute path to the forge package install location
|
|
166
|
+
* @param {Object} [_options={}] - Reserved for future options
|
|
167
|
+
* @returns {ScaffoldResult}
|
|
168
|
+
*/
|
|
169
|
+
function scaffoldBeadsSync(projectRoot, packageDir, _options = {}) {
|
|
170
|
+
/** @type {string[]} */
|
|
171
|
+
const filesCreated = [];
|
|
172
|
+
/** @type {string[]} */
|
|
173
|
+
const filesSkipped = [];
|
|
174
|
+
|
|
175
|
+
const githubDir = path.join(projectRoot, '.github');
|
|
176
|
+
|
|
177
|
+
// --- Step a: Copy workflow templates ---
|
|
178
|
+
const workflowNames = ['github-to-beads.yml', 'beads-to-github.yml'];
|
|
179
|
+
const srcWorkflowsDir = path.join(packageDir, '.github', 'workflows');
|
|
180
|
+
|
|
181
|
+
for (const name of workflowNames) {
|
|
182
|
+
const src = path.join(srcWorkflowsDir, name);
|
|
183
|
+
const relPath = `.github/workflows/${name}`;
|
|
184
|
+
const dest = path.join(projectRoot, '.github', 'workflows', name);
|
|
185
|
+
|
|
186
|
+
if (!fs.existsSync(src)) continue;
|
|
187
|
+
if (fs.existsSync(dest)) {
|
|
188
|
+
filesSkipped.push(relPath);
|
|
189
|
+
} else {
|
|
190
|
+
copyFileWithDirs(src, dest);
|
|
191
|
+
filesCreated.push(relPath);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// --- Step b: Copy sync modules ---
|
|
196
|
+
const srcSyncDir = path.join(packageDir, 'scripts', 'github-beads-sync');
|
|
197
|
+
const destSyncDir = path.join(githubDir, 'scripts', 'beads-sync');
|
|
198
|
+
|
|
199
|
+
if (fs.existsSync(srcSyncDir)) {
|
|
200
|
+
const entries = fs.readdirSync(srcSyncDir);
|
|
201
|
+
for (const entry of entries) {
|
|
202
|
+
const srcFile = path.join(srcSyncDir, entry);
|
|
203
|
+
const stat = fs.statSync(srcFile);
|
|
204
|
+
if (stat.isFile()) {
|
|
205
|
+
const relPath = `.github/scripts/beads-sync/${entry}`;
|
|
206
|
+
const dest = path.join(destSyncDir, entry);
|
|
207
|
+
if (fs.existsSync(dest)) {
|
|
208
|
+
filesSkipped.push(relPath);
|
|
209
|
+
} else {
|
|
210
|
+
copyFileWithDirs(srcFile, dest);
|
|
211
|
+
filesCreated.push(relPath);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- Step c: Create beads-sync-config.json ---
|
|
218
|
+
const configRelPath = '.github/beads-sync-config.json';
|
|
219
|
+
const configDest = path.join(githubDir, 'beads-sync-config.json');
|
|
220
|
+
const srcConfig = path.join(packageDir, 'scripts', 'github-beads-sync.config.json');
|
|
221
|
+
|
|
222
|
+
if (fs.existsSync(configDest)) {
|
|
223
|
+
filesSkipped.push(configRelPath);
|
|
224
|
+
} else if (fs.existsSync(srcConfig)) {
|
|
225
|
+
copyFileWithDirs(srcConfig, configDest);
|
|
226
|
+
filesCreated.push(configRelPath);
|
|
227
|
+
} else {
|
|
228
|
+
// Create a sensible default config
|
|
229
|
+
const defaultConfig = {
|
|
230
|
+
defaultType: 'task',
|
|
231
|
+
defaultPriority: 2,
|
|
232
|
+
mapAssignee: true,
|
|
233
|
+
publicRepoGate: 'none'
|
|
234
|
+
};
|
|
235
|
+
fs.mkdirSync(path.dirname(configDest), { recursive: true });
|
|
236
|
+
fs.writeFileSync(configDest, JSON.stringify(defaultConfig, null, 2) + '\n');
|
|
237
|
+
filesCreated.push(configRelPath);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// --- Step d: Create beads-mapping.json if not exists ---
|
|
241
|
+
const mappingRelPath = '.github/beads-mapping.json';
|
|
242
|
+
const mappingDest = path.join(githubDir, 'beads-mapping.json');
|
|
243
|
+
|
|
244
|
+
if (fs.existsSync(mappingDest)) {
|
|
245
|
+
filesSkipped.push(mappingRelPath);
|
|
246
|
+
} else {
|
|
247
|
+
fs.mkdirSync(path.dirname(mappingDest), { recursive: true });
|
|
248
|
+
fs.writeFileSync(mappingDest, '{}');
|
|
249
|
+
filesCreated.push(mappingRelPath);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return { filesCreated, filesSkipped };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = {
|
|
256
|
+
detectDefaultBranch,
|
|
257
|
+
detectBeadsVersion,
|
|
258
|
+
templateWorkflows,
|
|
259
|
+
scaffoldBeadsSync,
|
|
260
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Registry — Auto-Discovery
|
|
3
|
+
*
|
|
4
|
+
* Scans a commands directory for .js files, validates each exports
|
|
5
|
+
* { name, description, handler }, and builds a routing Map.
|
|
6
|
+
*
|
|
7
|
+
* Follows the same fs.readdirSync() pattern as lib/plugin-manager.js.
|
|
8
|
+
*
|
|
9
|
+
* @module _registry
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { existsSync, readdirSync } = require('node:fs');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} CommandModule
|
|
17
|
+
* @property {string} name - Command name used for routing
|
|
18
|
+
* @property {string} description - Human-readable description
|
|
19
|
+
* @property {function(Array, Object, string): Promise<*>} handler - Async command handler
|
|
20
|
+
* @property {string} [usage] - Usage string (optional)
|
|
21
|
+
* @property {Object<string, string>} [flags] - Flag descriptions (optional)
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate that a module exports the required command interface.
|
|
26
|
+
*
|
|
27
|
+
* @param {*} mod - The required module
|
|
28
|
+
* @returns {{ valid: boolean, reason?: string }}
|
|
29
|
+
*/
|
|
30
|
+
function validateCommand(mod) {
|
|
31
|
+
if (!mod || typeof mod !== 'object') {
|
|
32
|
+
return { valid: false, reason: 'module does not export an object' };
|
|
33
|
+
}
|
|
34
|
+
if (typeof mod.name !== 'string' || !mod.name) {
|
|
35
|
+
return { valid: false, reason: 'missing or invalid "name" export' };
|
|
36
|
+
}
|
|
37
|
+
if (typeof mod.description !== 'string' || !mod.description) {
|
|
38
|
+
return { valid: false, reason: 'missing or invalid "description" export' };
|
|
39
|
+
}
|
|
40
|
+
if (typeof mod.handler !== 'function') {
|
|
41
|
+
return { valid: false, reason: 'missing or invalid "handler" export' };
|
|
42
|
+
}
|
|
43
|
+
return { valid: true };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Load and discover command modules from a directory.
|
|
48
|
+
*
|
|
49
|
+
* Scans `commandsDir` for `.js` files (excluding files starting with `_`),
|
|
50
|
+
* validates each module exports `{ name, description, handler }`, and builds
|
|
51
|
+
* a routing Map keyed by command name.
|
|
52
|
+
*
|
|
53
|
+
* Malformed modules are skipped with a `console.warn` — other commands
|
|
54
|
+
* continue loading. Duplicate command names warn and skip the later file.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} commandsDir - Absolute path to the commands directory
|
|
57
|
+
* @returns {{ commands: Map<string, CommandModule>, getHelp: () => string }}
|
|
58
|
+
*/
|
|
59
|
+
function loadCommands(commandsDir) {
|
|
60
|
+
/** @type {Map<string, CommandModule>} */
|
|
61
|
+
const commands = new Map();
|
|
62
|
+
|
|
63
|
+
if (!commandsDir || !existsSync(commandsDir)) {
|
|
64
|
+
return {
|
|
65
|
+
commands,
|
|
66
|
+
getHelp: () => buildHelp(commands),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const files = readdirSync(commandsDir)
|
|
71
|
+
.filter(f => f.endsWith('.js') && !f.startsWith('_'))
|
|
72
|
+
.sort(); // deterministic order — first file wins on duplicates
|
|
73
|
+
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
const filePath = path.join(commandsDir, file);
|
|
76
|
+
|
|
77
|
+
let mod;
|
|
78
|
+
try {
|
|
79
|
+
mod = require(filePath);
|
|
80
|
+
} catch (_err) {
|
|
81
|
+
console.warn(`[registry] Skipping ${file}: failed to load — ${_err.message}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const validation = validateCommand(mod);
|
|
86
|
+
if (!validation.valid) {
|
|
87
|
+
console.warn(`[registry] Skipping ${file}: ${validation.reason}`);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (commands.has(mod.name)) {
|
|
92
|
+
console.warn(
|
|
93
|
+
`[registry] Skipping ${file}: duplicate command name "${mod.name}"`
|
|
94
|
+
);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
commands.set(mod.name, mod);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
commands,
|
|
103
|
+
getHelp: () => buildHelp(commands),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build a formatted help string from discovered commands.
|
|
109
|
+
*
|
|
110
|
+
* @param {Map<string, CommandModule>} commands
|
|
111
|
+
* @returns {string}
|
|
112
|
+
*/
|
|
113
|
+
function buildHelp(commands) {
|
|
114
|
+
if (commands.size === 0) {
|
|
115
|
+
return 'No commands available.';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const lines = ['Available commands:', ''];
|
|
119
|
+
|
|
120
|
+
// Find the longest name for alignment
|
|
121
|
+
let maxLen = 0;
|
|
122
|
+
for (const name of commands.keys()) {
|
|
123
|
+
if (name.length > maxLen) maxLen = name.length;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const [name, cmd] of commands) {
|
|
127
|
+
const padding = ' '.repeat(maxLen - name.length + 2);
|
|
128
|
+
lines.push(` ${name}${padding}${cmd.description}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return lines.join('\n');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { loadCommands, validateCommand };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require('node:child_process');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { stopDolt } = require('./worktree');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Forge Clean Command
|
|
10
|
+
* Remove worktrees for merged branches, stopping Dolt servers first.
|
|
11
|
+
* Uses execFileSync (not execSync) to prevent command injection (OWASP A03).
|
|
12
|
+
*
|
|
13
|
+
* @module commands/clean
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Detect the default branch (main, master, develop, trunk).
|
|
18
|
+
* Tries origin/HEAD first, then probes common names.
|
|
19
|
+
* @param {Function} runFile - execFileSync-compatible function
|
|
20
|
+
* @returns {string} Default branch name
|
|
21
|
+
*/
|
|
22
|
+
function getDefaultBranch(runFile) {
|
|
23
|
+
try {
|
|
24
|
+
return runFile('git', ['rev-parse', '--abbrev-ref', 'origin/HEAD'], { stdio: 'pipe' })
|
|
25
|
+
.toString().trim().replace('origin/', '');
|
|
26
|
+
} catch (_e) { /* intentional: origin/HEAD not set, probe common names */ // NOSONAR S2486
|
|
27
|
+
for (const name of ['main', 'master', 'develop', 'trunk']) {
|
|
28
|
+
try {
|
|
29
|
+
runFile('git', ['rev-parse', '--verify', name], { stdio: 'pipe' });
|
|
30
|
+
return name;
|
|
31
|
+
} catch (_e2) { /* intentional: try next branch name */ } // NOSONAR S2486
|
|
32
|
+
}
|
|
33
|
+
return 'main';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parse `git worktree list --porcelain` output into a map of path -> branch.
|
|
39
|
+
* @param {string} output - Raw porcelain output
|
|
40
|
+
* @returns {Map<string, string>} Map of worktree path -> branch name
|
|
41
|
+
*/
|
|
42
|
+
function parseWorktreeList(output) {
|
|
43
|
+
const map = new Map();
|
|
44
|
+
const blocks = output.split('\n\n');
|
|
45
|
+
for (const block of blocks) {
|
|
46
|
+
const lines = block.trim().split('\n');
|
|
47
|
+
let wtPath = null;
|
|
48
|
+
let branch = null;
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
if (line.startsWith('worktree ')) {
|
|
51
|
+
wtPath = line.slice('worktree '.length);
|
|
52
|
+
}
|
|
53
|
+
if (line.startsWith('branch ')) {
|
|
54
|
+
// branch refs/heads/feat/foo -> feat/foo
|
|
55
|
+
branch = line.slice('branch refs/heads/'.length);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (wtPath && branch) {
|
|
59
|
+
map.set(wtPath, branch);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return map;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Clean a single worktree directory if its branch is merged.
|
|
67
|
+
* @param {string} dir - Directory name within .worktrees/
|
|
68
|
+
* @param {Map<string, string>} worktreeMap - Path-to-branch mapping
|
|
69
|
+
* @param {string[]} mergedBranches - List of merged branch names
|
|
70
|
+
* @param {string} worktreesDir - Absolute path to .worktrees/
|
|
71
|
+
* @param {boolean} dryRun - If true, skip actual removal
|
|
72
|
+
* @param {Function} runFile - execFileSync-compatible function
|
|
73
|
+
* @param {object} fsApi - fs module (for DI)
|
|
74
|
+
* @returns {Promise<boolean>} True if the worktree was cleaned (or would be in dry-run)
|
|
75
|
+
*/
|
|
76
|
+
async function cleanWorktree(dir, worktreeMap, mergedBranches, worktreesDir, dryRun, runFile, fsApi) {
|
|
77
|
+
const wtPath = path.resolve(worktreesDir, dir);
|
|
78
|
+
const branch = worktreeMap.get(wtPath);
|
|
79
|
+
|
|
80
|
+
if (branch && mergedBranches.includes(branch)) {
|
|
81
|
+
if (!dryRun) {
|
|
82
|
+
const stopResult = stopDolt(wtPath, { _exec: runFile, _fs: fsApi });
|
|
83
|
+
if (stopResult.stopped) {
|
|
84
|
+
await new Promise(r => setTimeout(r, 500));
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
runFile('git', ['worktree', 'remove', wtPath], { stdio: 'pipe' });
|
|
88
|
+
} catch (_removeErr) { /* intentional: worktree has uncommitted changes, skip */ // NOSONAR S2486
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Main handler for the clean command.
|
|
99
|
+
* @param {string[]} _args - Positional arguments (unused)
|
|
100
|
+
* @param {object} flags - CLI flags
|
|
101
|
+
* @param {string} projectRoot - Project root path
|
|
102
|
+
* @param {object} [opts] - Options for dependency injection
|
|
103
|
+
* @param {Function} [opts._exec] - Override for execFileSync (testing)
|
|
104
|
+
* @param {object} [opts._fs] - Override for fs module (testing)
|
|
105
|
+
* @returns {Promise<{ success: boolean, cleaned: number, active: number, dryRun: boolean }>}
|
|
106
|
+
*/
|
|
107
|
+
async function handler(_args, flags, projectRoot, opts = {}) {
|
|
108
|
+
const runFile = opts._exec || execFileSync;
|
|
109
|
+
const fsApi = opts._fs || fs;
|
|
110
|
+
const dryRun = !!(flags['--dry-run'] || flags.dryRun);
|
|
111
|
+
|
|
112
|
+
const worktreesDir = path.resolve(projectRoot, '.worktrees');
|
|
113
|
+
|
|
114
|
+
// If .worktrees/ doesn't exist, nothing to clean
|
|
115
|
+
if (!fsApi.existsSync(worktreesDir)) {
|
|
116
|
+
return { success: true, cleaned: 0, active: 0, dryRun };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 1. List dirs in .worktrees/
|
|
120
|
+
const entries = fsApi.readdirSync(worktreesDir, { withFileTypes: true });
|
|
121
|
+
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
122
|
+
|
|
123
|
+
if (dirs.length === 0) {
|
|
124
|
+
return { success: true, cleaned: 0, active: 0, dryRun };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 2. Detect default branch, then get merged branches
|
|
128
|
+
const defaultBranch = getDefaultBranch(runFile);
|
|
129
|
+
let mergedBranches;
|
|
130
|
+
try {
|
|
131
|
+
const mergedOutput = runFile('git', ['branch', '--merged', defaultBranch], { stdio: 'pipe' });
|
|
132
|
+
mergedBranches = mergedOutput
|
|
133
|
+
.toString()
|
|
134
|
+
.split('\n')
|
|
135
|
+
.map(b => b.trim().replace(/^\*\s*/, ''))
|
|
136
|
+
.filter(b => b.length > 0);
|
|
137
|
+
} catch (_e) { /* intentional: fallback to empty list */ // NOSONAR S2486
|
|
138
|
+
mergedBranches = [];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 3. Get worktree -> branch mapping from git
|
|
142
|
+
let worktreeMap;
|
|
143
|
+
try {
|
|
144
|
+
const listOutput = runFile('git', ['worktree', 'list', '--porcelain'], { stdio: 'pipe' });
|
|
145
|
+
worktreeMap = parseWorktreeList(listOutput.toString());
|
|
146
|
+
} catch (_e) { /* intentional: fallback to empty map */ // NOSONAR S2486
|
|
147
|
+
worktreeMap = new Map();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 4. For each worktree dir, check if its branch is merged
|
|
151
|
+
let cleaned = 0;
|
|
152
|
+
let active = 0;
|
|
153
|
+
|
|
154
|
+
for (const dir of dirs) {
|
|
155
|
+
const wasCleaned = await cleanWorktree(dir, worktreeMap, mergedBranches, worktreesDir, dryRun, runFile, fsApi);
|
|
156
|
+
if (wasCleaned) {
|
|
157
|
+
cleaned++;
|
|
158
|
+
} else {
|
|
159
|
+
active++;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Prune stale worktree refs to prevent bare repo state
|
|
164
|
+
if (!dryRun && cleaned > 0) {
|
|
165
|
+
try {
|
|
166
|
+
runFile('git', ['worktree', 'prune'], { stdio: 'pipe' });
|
|
167
|
+
} catch (_e) { /* intentional: prune is best-effort cleanup */ } // NOSONAR S2486
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { success: true, cleaned, active, dryRun };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
name: 'clean',
|
|
175
|
+
description: 'Remove worktrees for merged branches (stops Dolt servers)',
|
|
176
|
+
usage: 'forge clean [--dry-run]',
|
|
177
|
+
flags: {
|
|
178
|
+
'--dry-run': 'Show what would be cleaned without removing',
|
|
179
|
+
},
|
|
180
|
+
handler,
|
|
181
|
+
};
|