forge-workflow 0.0.9 → 0.0.10
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 +2 -2
- package/.claude/commands/plan.md +2 -2
- package/.claude/commands/ship.md +2 -2
- package/.claude/commands/status.md +4 -4
- package/.cline/workflows/dev.md +2 -2
- package/.cline/workflows/plan.md +2 -2
- package/.cline/workflows/ship.md +2 -2
- package/.cline/workflows/status.md +4 -4
- package/.codex/skills/dev/SKILL.md +2 -2
- package/.codex/skills/plan/SKILL.md +2 -2
- package/.codex/skills/ship/SKILL.md +2 -2
- package/.codex/skills/status/SKILL.md +4 -4
- package/.cursor/commands/dev.md +2 -2
- package/.cursor/commands/plan.md +2 -2
- package/.cursor/commands/ship.md +2 -2
- package/.cursor/commands/status.md +4 -4
- package/.github/prompts/dev.prompt.md +2 -2
- package/.github/prompts/plan.prompt.md +2 -2
- package/.github/prompts/ship.prompt.md +2 -2
- package/.github/prompts/status.prompt.md +4 -4
- package/.github/workflows/beads-to-github.yml +43 -10
- package/.github/workflows/github-to-beads.yml +10 -7
- package/.kilocode/workflows/dev.md +2 -2
- package/.kilocode/workflows/plan.md +2 -2
- package/.kilocode/workflows/ship.md +2 -2
- package/.kilocode/workflows/status.md +4 -4
- package/.opencode/commands/dev.md +2 -2
- package/.opencode/commands/plan.md +2 -2
- package/.opencode/commands/ship.md +2 -2
- package/.opencode/commands/status.md +4 -4
- package/.roo/commands/dev.md +2 -2
- package/.roo/commands/plan.md +2 -2
- package/.roo/commands/ship.md +2 -2
- package/.roo/commands/status.md +4 -4
- package/AGENTS.md +1 -0
- package/CLAUDE.md +12 -0
- package/bin/forge.js +10 -5
- package/docs/BEADS_GITHUB_SYNC.md +26 -0
- package/docs/TOOLCHAIN.md +130 -148
- package/lib/beads-bootstrap.js +225 -0
- package/lib/beads-health-check.js +55 -10
- package/lib/beads-setup.js +104 -28
- package/lib/beads-sync-scaffold.js +11 -6
- package/lib/commands/_issue.js +11 -1
- package/lib/commands/issues.js +49 -0
- package/lib/commands/recommend.js +22 -1
- package/lib/commands/setup.js +16 -10
- package/lib/commands/status.js +181 -0
- package/lib/commands/team.js +11 -1
- package/lib/commands/test.js +37 -2
- package/lib/commands/validate.js +14 -8
- package/lib/commands/worktree.js +27 -54
- package/lib/dep-guard/keyword-ripple.js +184 -0
- package/lib/detect-worktree.js +9 -10
- package/lib/forge-issues.js +326 -0
- package/lib/issue-sync/authority.js +100 -0
- package/lib/issue-sync/github-pull.js +184 -0
- package/lib/issue-sync/import-primitives.js +98 -0
- package/lib/issue-sync/legacy-link-bridge.js +436 -0
- package/lib/issue-sync/link-store.js +292 -0
- package/lib/issue-sync/project-github.js +123 -0
- package/lib/issue-sync/reconcile.js +195 -0
- package/lib/issue-sync/schema.js +126 -0
- package/lib/lefthook-check.js +5 -2
- package/lib/project-memory.js +564 -0
- package/lib/runtime-health.js +100 -12
- package/lib/smart-status/conflicts.js +205 -0
- package/lib/smart-status/scoring.js +177 -0
- package/lib/status/beads-snapshot.js +102 -0
- package/lib/status/presenter.js +65 -0
- package/lib/workflow/enforce-stage.js +3 -1
- package/lib/workflow/state-manager.js +164 -8
- package/package.json +12 -4
- package/scripts/beads-context.sh +124 -5
- package/scripts/beads-context.test.js +21 -4
- package/scripts/beads-migrate-to-dolt.sh +7 -0
- package/scripts/beads-upgrade-smoke.sh +263 -0
- package/scripts/behavioral-judge.sh +115 -11
- package/scripts/benchmark.js +349 -63
- package/scripts/dep-guard-analyze.js +52 -17
- package/scripts/dep-guard-keyword-ripple.js +29 -0
- package/scripts/dep-guard-render-review.js +86 -0
- package/scripts/dep-guard.sh +45 -232
- package/scripts/forge-team/lib/sync-github.sh +160 -28
- package/scripts/forge-team/tests/sync-github.test.sh +195 -58
- package/scripts/github-beads-sync/index.mjs +122 -98
- package/scripts/github-beads-sync/mapping.mjs +54 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +2 -2
- package/scripts/github-beads-sync/reverse-sync.mjs +31 -7
- package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
- package/scripts/preflight.sh +181 -0
- package/scripts/smart-status-score.js +31 -0
- package/scripts/smart-status-sessions.js +51 -0
- package/scripts/smart-status.sh +74 -329
- package/scripts/sync-agentic-workflow.js +48 -0
- package/scripts/test-ci-shard.js +244 -0
- package/scripts/test-dashboard.js +188 -52
- package/scripts/test-full-suite.js +186 -0
- package/scripts/test-profile.js +278 -0
- package/scripts/test.js +219 -28
- package/scripts/validate.js +143 -0
- package/scripts/validate.sh +18 -1
|
@@ -0,0 +1,225 @@
|
|
|
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 { safeBeadsInit } = require('./beads-setup');
|
|
7
|
+
|
|
8
|
+
function isIgnorableFsError(error) {
|
|
9
|
+
return ['EACCES', 'ENOENT', 'ENOTDIR', 'EPERM'].includes(error?.code);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isRecoverableCommandError(error) {
|
|
13
|
+
return error?.code === 'ENOENT' || typeof error?.status === 'number';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function hasBackupJsonl(backupDir, fsApi) {
|
|
17
|
+
try {
|
|
18
|
+
return fsApi.readdirSync(backupDir).some((entry) => entry.endsWith('.jsonl'));
|
|
19
|
+
} catch (err) {
|
|
20
|
+
if (isIgnorableFsError(err)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveMainWorktree(projectRoot, exec) {
|
|
28
|
+
try {
|
|
29
|
+
const commonDir = exec('git', ['rev-parse', '--git-common-dir'], {
|
|
30
|
+
cwd: projectRoot,
|
|
31
|
+
encoding: 'utf8',
|
|
32
|
+
stdio: 'pipe'
|
|
33
|
+
}).trim();
|
|
34
|
+
return path.resolve(projectRoot, commonDir, '..');
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (isRecoverableCommandError(err)) {
|
|
37
|
+
return projectRoot;
|
|
38
|
+
}
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isRecoverableBeadsError(error) {
|
|
44
|
+
const message = error?.message ?? String(error ?? '');
|
|
45
|
+
return (
|
|
46
|
+
/database(?:\s+[^\s]+)?\s+not found/i.test(message) ||
|
|
47
|
+
message.includes('failed to open database') ||
|
|
48
|
+
message.includes('no beads configuration found')
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readMetadataDatabaseName(projectRoot, fsApi) {
|
|
53
|
+
const metadataPath = path.resolve(projectRoot, '.beads', 'metadata.json');
|
|
54
|
+
if (!fsApi.existsSync(metadataPath)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const metadata = JSON.parse(fsApi.readFileSync(metadataPath, 'utf8'));
|
|
60
|
+
if (typeof metadata.dolt_database === 'string' && metadata.dolt_database.trim()) {
|
|
61
|
+
return metadata.dolt_database.trim();
|
|
62
|
+
}
|
|
63
|
+
} catch (err) {
|
|
64
|
+
if (err instanceof SyntaxError || isIgnorableFsError(err)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function stageExistingBeadsDest(beadsDest, fsApi) {
|
|
74
|
+
if (!fsApi.existsSync(beadsDest)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const stagedDest = `${beadsDest}.bootstrap-backup`;
|
|
79
|
+
if (fsApi.existsSync(stagedDest) && typeof fsApi.rmSync === 'function') {
|
|
80
|
+
fsApi.rmSync(stagedDest, { recursive: true, force: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typeof fsApi.renameSync === 'function') {
|
|
84
|
+
fsApi.renameSync(beadsDest, stagedDest);
|
|
85
|
+
return stagedDest;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (typeof fsApi.rmSync === 'function') {
|
|
89
|
+
fsApi.rmSync(beadsDest, { recursive: true, force: true });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function restoreStagedBeadsDest(beadsDest, stagedDest, fsApi) {
|
|
96
|
+
if (!stagedDest || fsApi.existsSync(beadsDest) || typeof fsApi.renameSync !== 'function') {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fsApi.renameSync(stagedDest, beadsDest);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function cleanupStagedBeadsDest(stagedDest, fsApi) {
|
|
104
|
+
if (!stagedDest || !fsApi.existsSync(stagedDest) || typeof fsApi.rmSync !== 'function') {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fsApi.rmSync(stagedDest, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function tryLinkBeadsSource(beadsSource, beadsDest, platform, fsApi) {
|
|
112
|
+
if (!fsApi.existsSync(beadsSource)) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const stagedDest = stageExistingBeadsDest(beadsDest, fsApi);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
if (platform === 'win32') {
|
|
120
|
+
fsApi.symlinkSync(beadsSource, beadsDest, 'junction');
|
|
121
|
+
} else {
|
|
122
|
+
fsApi.symlinkSync(beadsSource, beadsDest);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
cleanupStagedBeadsDest(stagedDest, fsApi);
|
|
126
|
+
return true;
|
|
127
|
+
} catch (symlinkErr) {
|
|
128
|
+
restoreStagedBeadsDest(beadsDest, stagedDest, fsApi);
|
|
129
|
+
if (symlinkErr.code !== 'EPERM') {
|
|
130
|
+
throw symlinkErr;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buildInitArgs(mainProjectRoot, projectRoot, fsApi) {
|
|
137
|
+
const metadataDatabaseName = readMetadataDatabaseName(mainProjectRoot, fsApi)
|
|
138
|
+
|| readMetadataDatabaseName(projectRoot, fsApi);
|
|
139
|
+
const initArgs = ['init', '--force'];
|
|
140
|
+
if (metadataDatabaseName) {
|
|
141
|
+
initArgs.push('--database', metadataDatabaseName);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return initArgs;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function runFreshInit(projectRoot, mainProjectRoot, beadsSource, initArgs, exec, runSafeBeadsInit, fsApi) {
|
|
148
|
+
const safeInitResult = runSafeBeadsInit(projectRoot, {
|
|
149
|
+
prefix: path.basename(mainProjectRoot),
|
|
150
|
+
execBdInit: (root) => {
|
|
151
|
+
exec('bd', initArgs, { cwd: root, stdio: 'pipe' });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!safeInitResult.success) {
|
|
156
|
+
const initMessage = safeInitResult.errors?.[0]
|
|
157
|
+
|| safeInitResult.warnings?.[0]
|
|
158
|
+
|| 'unknown safeBeadsInit failure';
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
strategy: 'fresh-init-failed',
|
|
162
|
+
warning: `Beads bootstrap fresh init failed: ${initMessage}`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (safeInitResult.skipped) {
|
|
167
|
+
return {
|
|
168
|
+
success: true,
|
|
169
|
+
strategy: 'existing-state',
|
|
170
|
+
warning: 'Beads bootstrap reused existing initialized state'
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const backupDir = path.resolve(beadsSource, 'backup');
|
|
175
|
+
if (hasBackupJsonl(backupDir, fsApi)) {
|
|
176
|
+
try {
|
|
177
|
+
exec('bd', ['backup', 'restore', backupDir], { cwd: projectRoot, stdio: 'pipe' });
|
|
178
|
+
return {
|
|
179
|
+
success: true,
|
|
180
|
+
strategy: 'backup-restore',
|
|
181
|
+
warning: 'Beads bootstrap restored from backup'
|
|
182
|
+
};
|
|
183
|
+
} catch (restoreErr) {
|
|
184
|
+
return {
|
|
185
|
+
success: true,
|
|
186
|
+
strategy: 'fresh-init',
|
|
187
|
+
warning: `Beads bootstrap initialized fresh after backup restore failed: ${restoreErr.message}`
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
success: true,
|
|
194
|
+
strategy: 'fresh-init',
|
|
195
|
+
warning: 'Beads bootstrap initialized fresh (no backup found)'
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function bootstrapBeads(projectRoot, options = {}) {
|
|
200
|
+
const exec = options._exec || execFileSync;
|
|
201
|
+
const fsApi = options._fs || fs;
|
|
202
|
+
const platform = options._platform || process.platform;
|
|
203
|
+
const runSafeBeadsInit = options._safeBeadsInit || safeBeadsInit;
|
|
204
|
+
const mainProjectRoot = options.mainProjectRoot || resolveMainWorktree(projectRoot, exec);
|
|
205
|
+
const beadsSource = path.resolve(mainProjectRoot, '.beads');
|
|
206
|
+
const beadsDest = path.resolve(projectRoot, '.beads');
|
|
207
|
+
|
|
208
|
+
if (beadsSource === beadsDest) {
|
|
209
|
+
return { success: true, strategy: 'in-place', warning: null };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (tryLinkBeadsSource(beadsSource, beadsDest, platform, fsApi)) {
|
|
213
|
+
return { success: true, strategy: 'linked', warning: null };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const initArgs = buildInitArgs(mainProjectRoot, projectRoot, fsApi);
|
|
217
|
+
return runFreshInit(projectRoot, mainProjectRoot, beadsSource, initArgs, exec, runSafeBeadsInit, fsApi);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = {
|
|
221
|
+
bootstrapBeads,
|
|
222
|
+
hasBackupJsonl,
|
|
223
|
+
isRecoverableBeadsError,
|
|
224
|
+
resolveMainWorktree,
|
|
225
|
+
};
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
const { execFileSync } = require('node:child_process');
|
|
11
11
|
const fs = require('node:fs');
|
|
12
12
|
const path = require('node:path');
|
|
13
|
+
const { bootstrapBeads, isRecoverableBeadsError } = require('./beads-bootstrap');
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* @typedef {Object} HealthCheckResult
|
|
@@ -49,6 +50,46 @@ function cleanupTestIssue(jsonlPath, issueId) {
|
|
|
49
50
|
fs.writeFileSync(jsonlPath, filtered.length > 0 ? filtered.join('\n') + '\n' : '');
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
function appendWarning(existingWarning, nextWarning) {
|
|
54
|
+
return existingWarning ? `${existingWarning}; ${nextWarning}` : nextWarning;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createHealthCheckIssue(exec, execOpts) {
|
|
58
|
+
return exec('bd', [
|
|
59
|
+
'create',
|
|
60
|
+
'--title=Setup verification',
|
|
61
|
+
'--type=task',
|
|
62
|
+
'--priority=4'
|
|
63
|
+
], execOpts);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createIssueWithRecovery(projectRoot, exec, execOpts, bootstrap) {
|
|
67
|
+
try {
|
|
68
|
+
return { output: createHealthCheckIssue(exec, execOpts), warning: null };
|
|
69
|
+
} catch (createErr) {
|
|
70
|
+
if (!isRecoverableBeadsError(createErr)) {
|
|
71
|
+
throw createErr;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const bootstrapResult = bootstrap(projectRoot);
|
|
75
|
+
if (!bootstrapResult?.success) {
|
|
76
|
+
const extra = bootstrapResult?.warning ? `; ${bootstrapResult.warning}` : '';
|
|
77
|
+
return {
|
|
78
|
+
error: `${createErr.message}${extra}`
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const bootstrapWarning = bootstrapResult.warning
|
|
83
|
+
? `bootstrap warning: ${bootstrapResult.warning}`
|
|
84
|
+
: `bootstrap warning: recovered via ${bootstrapResult.strategy}`;
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
output: createHealthCheckIssue(exec, execOpts),
|
|
88
|
+
warning: bootstrapWarning
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
52
93
|
/**
|
|
53
94
|
* Run a Beads health check smoke test.
|
|
54
95
|
*
|
|
@@ -62,6 +103,7 @@ function cleanupTestIssue(jsonlPath, issueId) {
|
|
|
62
103
|
*/
|
|
63
104
|
function beadsHealthCheck(projectRoot, options = {}) {
|
|
64
105
|
const exec = options._exec || execFileSync;
|
|
106
|
+
const bootstrap = options._bootstrap || ((root) => bootstrapBeads(root, { _exec: exec }));
|
|
65
107
|
const execOpts = { cwd: projectRoot, encoding: 'utf8' };
|
|
66
108
|
|
|
67
109
|
/** @type {string|undefined} */
|
|
@@ -71,14 +113,18 @@ function beadsHealthCheck(projectRoot, options = {}) {
|
|
|
71
113
|
/** @type {string|null} */
|
|
72
114
|
let issueId;
|
|
73
115
|
try {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
116
|
+
const createResult = createIssueWithRecovery(projectRoot, exec, execOpts, bootstrap);
|
|
117
|
+
if (createResult.error) {
|
|
118
|
+
return {
|
|
119
|
+
healthy: false,
|
|
120
|
+
failedStep: 'create',
|
|
121
|
+
error: createResult.error
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
warning = createResult.warning || warning;
|
|
80
126
|
|
|
81
|
-
issueId = parseIssueId(
|
|
127
|
+
issueId = parseIssueId(createResult.output);
|
|
82
128
|
if (!issueId) {
|
|
83
129
|
return {
|
|
84
130
|
healthy: false,
|
|
@@ -120,7 +166,7 @@ function beadsHealthCheck(projectRoot, options = {}) {
|
|
|
120
166
|
exec('bd', ['sync'], execOpts);
|
|
121
167
|
} catch (syncErr) {
|
|
122
168
|
// Sync failure is non-fatal (no remote configured is common)
|
|
123
|
-
warning = `bd sync warning: ${syncErr.message}
|
|
169
|
+
warning = appendWarning(warning, `bd sync warning: ${syncErr.message}`);
|
|
124
170
|
}
|
|
125
171
|
|
|
126
172
|
// Step d: Remove the test issue from JSONL
|
|
@@ -128,8 +174,7 @@ function beadsHealthCheck(projectRoot, options = {}) {
|
|
|
128
174
|
cleanupTestIssue(path.join(projectRoot, '.beads', 'issues.jsonl'), issueId);
|
|
129
175
|
} catch (cleanupErr) {
|
|
130
176
|
// Cleanup failure is non-fatal
|
|
131
|
-
|
|
132
|
-
warning = warning ? `${warning}; ${cleanupWarning}` : cleanupWarning;
|
|
177
|
+
warning = appendWarning(warning, `cleanup warning: ${cleanupErr.message}`);
|
|
133
178
|
}
|
|
134
179
|
|
|
135
180
|
return {
|
package/lib/beads-setup.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Utilities for configuring Beads during `forge setup`.
|
|
3
3
|
*
|
|
4
4
|
* Handles prefix sanitization, config/gitignore generation,
|
|
5
|
-
* initialization detection, and
|
|
5
|
+
* initialization detection, and legacy JSONL compatibility shims.
|
|
6
6
|
*
|
|
7
7
|
* @module beads-setup
|
|
8
8
|
*/
|
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
const fs = require('node:fs');
|
|
11
11
|
const path = require('node:path');
|
|
12
12
|
const { spawnSync } = require('node:child_process');
|
|
13
|
+
const { secureExecFileSync } = require('./shell-utils');
|
|
14
|
+
|
|
15
|
+
function isIgnorableFsError(error) {
|
|
16
|
+
return ['EACCES', 'ENOENT', 'ENOTDIR', 'EPERM'].includes(error?.code);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isRecoverableCommandError(error) {
|
|
20
|
+
return error?.code === 'ENOENT' || typeof error?.status === 'number';
|
|
21
|
+
}
|
|
13
22
|
|
|
14
23
|
/**
|
|
15
24
|
* Resolve a command to its full path to avoid relying on inherited PATH.
|
|
@@ -24,7 +33,10 @@ function resolveCommand(command) {
|
|
|
24
33
|
if (result.status === 0 && result.stdout) {
|
|
25
34
|
return result.stdout.trim().split(/\r?\n/)[0].trim();
|
|
26
35
|
}
|
|
27
|
-
} catch (
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (!isRecoverableCommandError(error)) {
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
28
40
|
// Expected: command resolution may fail if 'which'/'where.exe' is unavailable — fall back to bare command name
|
|
29
41
|
}
|
|
30
42
|
return command;
|
|
@@ -108,10 +120,15 @@ function writeBeadsGitignore(projectRoot) {
|
|
|
108
120
|
/**
|
|
109
121
|
* Check whether Beads is properly initialized in a project.
|
|
110
122
|
*
|
|
111
|
-
* Returns `true`
|
|
123
|
+
* Returns `true` when all of the following are present:
|
|
112
124
|
* - `.beads/` directory exists
|
|
113
125
|
* - `.beads/config.yaml` exists and contains an `issue-prefix` key
|
|
114
|
-
* -
|
|
126
|
+
* - the config points to either:
|
|
127
|
+
* - a legacy SQLite tracker with legacy data markers present, or
|
|
128
|
+
* - a Dolt-backed tracker with at least one post-`bd init` marker present
|
|
129
|
+
*
|
|
130
|
+
* Legacy SQLite-backed Beads installs also count as initialized so `forge setup`
|
|
131
|
+
* does not run `bd init` over an existing tracker before migration.
|
|
115
132
|
*
|
|
116
133
|
* @param {string} projectRoot - Absolute path to the project root
|
|
117
134
|
* @returns {boolean} `true` if Beads is fully initialized
|
|
@@ -119,34 +136,74 @@ function writeBeadsGitignore(projectRoot) {
|
|
|
119
136
|
function isBeadsInitialized(projectRoot) {
|
|
120
137
|
const beadsDir = path.join(projectRoot, '.beads');
|
|
121
138
|
const configPath = path.join(beadsDir, 'config.yaml');
|
|
122
|
-
const jsonlPath = path.join(beadsDir, 'issues.jsonl');
|
|
123
139
|
|
|
124
140
|
if (!fs.existsSync(beadsDir)) return false;
|
|
125
141
|
if (!fs.existsSync(configPath)) return false;
|
|
126
|
-
if (!fs.existsSync(jsonlPath)) return false;
|
|
127
142
|
|
|
128
143
|
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
129
144
|
if (!configContent.includes('issue-prefix:')) return false;
|
|
130
145
|
|
|
131
|
-
|
|
146
|
+
const hasLegacyState = [
|
|
147
|
+
'issues.jsonl',
|
|
148
|
+
'backup',
|
|
149
|
+
'bd.db',
|
|
150
|
+
'beads.db',
|
|
151
|
+
'db.sqlite'
|
|
152
|
+
].some((entry) => fs.existsSync(path.join(beadsDir, entry)));
|
|
153
|
+
const hasDoltMetadata = fs.existsSync(path.join(beadsDir, 'metadata.json'));
|
|
154
|
+
const hasDoltCompanionState = [
|
|
155
|
+
'README.md',
|
|
156
|
+
'hooks',
|
|
157
|
+
'redirect'
|
|
158
|
+
].some((entry) => fs.existsSync(path.join(beadsDir, entry)));
|
|
159
|
+
const hasDoltState = hasDoltMetadata && hasDoltCompanionState;
|
|
160
|
+
|
|
161
|
+
if (configContent.includes('backend: sqlite')) {
|
|
162
|
+
return hasLegacyState;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (configContent.includes('backend: dolt')) {
|
|
166
|
+
return hasDoltState;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return hasLegacyState || hasDoltState;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function readBeadsDatabaseName(projectRoot) {
|
|
173
|
+
const metadataPath = path.join(projectRoot, '.beads', 'metadata.json');
|
|
174
|
+
if (!fs.existsSync(metadataPath)) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
180
|
+
if (typeof metadata.dolt_database === 'string' && metadata.dolt_database.trim()) {
|
|
181
|
+
return metadata.dolt_database.trim();
|
|
182
|
+
}
|
|
183
|
+
if (typeof metadata.database === 'string' && metadata.database.trim()) {
|
|
184
|
+
return metadata.database.trim();
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (error instanceof SyntaxError || isIgnorableFsError(error)) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return null;
|
|
132
194
|
}
|
|
133
195
|
|
|
134
196
|
/**
|
|
135
|
-
*
|
|
197
|
+
* Legacy compatibility shim for the removed JSONL pre-seed workaround.
|
|
136
198
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
199
|
+
* Beads latest no longer requires `.beads/issues.jsonl` during init, so
|
|
200
|
+
* this helper intentionally does nothing. It remains exported because some
|
|
201
|
+
* older tests and setup paths still import it.
|
|
139
202
|
*
|
|
140
|
-
* @param {string}
|
|
203
|
+
* @param {string} _projectRoot - Absolute path to the project root
|
|
141
204
|
*/
|
|
142
|
-
function preSeedJsonl(
|
|
143
|
-
|
|
144
|
-
fs.mkdirSync(beadsDir, { recursive: true });
|
|
145
|
-
|
|
146
|
-
const jsonlPath = path.join(beadsDir, 'issues.jsonl');
|
|
147
|
-
if (!fs.existsSync(jsonlPath)) {
|
|
148
|
-
fs.writeFileSync(jsonlPath, '', 'utf8');
|
|
149
|
-
}
|
|
205
|
+
function preSeedJsonl(_projectRoot) {
|
|
206
|
+
// Legacy no-op: Dolt-backed Beads no longer requires JSONL pre-seeding.
|
|
150
207
|
}
|
|
151
208
|
|
|
152
209
|
// ---------------------------------------------------------------------------
|
|
@@ -212,6 +269,31 @@ function restoreDirectory(dirPath, snapshot) {
|
|
|
212
269
|
}
|
|
213
270
|
}
|
|
214
271
|
|
|
272
|
+
function resolveHooksDir(projectRoot) {
|
|
273
|
+
const fallbackHooksDir = path.join(projectRoot, '.git', 'hooks');
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const gitHooksPath = secureExecFileSync(resolveCommand('git'), ['rev-parse', '--git-path', 'hooks'], {
|
|
277
|
+
cwd: projectRoot,
|
|
278
|
+
encoding: 'utf8',
|
|
279
|
+
stdio: 'pipe'
|
|
280
|
+
}).trim();
|
|
281
|
+
|
|
282
|
+
if (!gitHooksPath) {
|
|
283
|
+
return fallbackHooksDir;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return path.isAbsolute(gitHooksPath)
|
|
287
|
+
? gitHooksPath
|
|
288
|
+
: path.resolve(projectRoot, gitHooksPath);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
if (isRecoverableCommandError(error)) {
|
|
291
|
+
return fallbackHooksDir;
|
|
292
|
+
}
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
215
297
|
/**
|
|
216
298
|
* Default bd init executor — calls `execFileSync('bd', ['init'])`.
|
|
217
299
|
*
|
|
@@ -237,7 +319,7 @@ function defaultExecBdInit(projectRoot) {
|
|
|
237
319
|
* e. Run bd init (via execBdInit callback or default execFileSync)
|
|
238
320
|
* f. Restore .git/hooks/ from snapshot
|
|
239
321
|
* g. Restore lefthook hooks if callback provided
|
|
240
|
-
* h.
|
|
322
|
+
* h. Skip the removed issues.jsonl pre-seed workaround
|
|
241
323
|
*
|
|
242
324
|
* @param {string} projectRoot - Absolute path to the project root
|
|
243
325
|
* @param {Object} [options={}] - Configuration options
|
|
@@ -279,7 +361,7 @@ function safeBeadsInit(projectRoot, options = {}) {
|
|
|
279
361
|
}
|
|
280
362
|
|
|
281
363
|
// (d) Snapshot current .git/hooks/ directory
|
|
282
|
-
const hooksDir =
|
|
364
|
+
const hooksDir = resolveHooksDir(projectRoot);
|
|
283
365
|
const hooksSnapshot = snapshotDirectory(hooksDir);
|
|
284
366
|
|
|
285
367
|
// (e) Run bd init — wrapped in try/catch
|
|
@@ -316,13 +398,6 @@ function safeBeadsInit(projectRoot, options = {}) {
|
|
|
316
398
|
}
|
|
317
399
|
}
|
|
318
400
|
|
|
319
|
-
// (h) Pre-seed JSONL if empty
|
|
320
|
-
try {
|
|
321
|
-
preSeedJsonl(projectRoot);
|
|
322
|
-
} catch (err) {
|
|
323
|
-
warnings.push(`Failed to pre-seed issues.jsonl: ${err.message}`);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
401
|
return {
|
|
327
402
|
success: true,
|
|
328
403
|
skipped: false,
|
|
@@ -336,6 +411,7 @@ module.exports = {
|
|
|
336
411
|
writeBeadsConfig,
|
|
337
412
|
writeBeadsGitignore,
|
|
338
413
|
isBeadsInitialized,
|
|
414
|
+
readBeadsDatabaseName,
|
|
339
415
|
preSeedJsonl,
|
|
340
416
|
safeBeadsInit
|
|
341
417
|
};
|
|
@@ -4,6 +4,8 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const { execFileSync } = require('node:child_process');
|
|
6
6
|
|
|
7
|
+
const DEFAULT_BEADS_VERSION = '1.0.0';
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Detect the default branch of the repository.
|
|
9
11
|
*
|
|
@@ -60,7 +62,7 @@ function detectDefaultBranch(projectRoot, options = {}) {
|
|
|
60
62
|
*
|
|
61
63
|
* Strategy (in order):
|
|
62
64
|
* 1. `bd --version` -> parse version string (e.g. "beads version 0.52.0" -> "0.52.0")
|
|
63
|
-
* 2. Fall back to
|
|
65
|
+
* 2. Fall back to the current repo baseline release
|
|
64
66
|
*
|
|
65
67
|
* @param {object} [options] - Options object.
|
|
66
68
|
* @param {Function} [options._exec] - Injected execFileSync for testing.
|
|
@@ -83,7 +85,7 @@ function detectBeadsVersion(options = {}) {
|
|
|
83
85
|
// Expected: 'bd --version' fails when bd CLI is not installed — fall through to default version
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
return
|
|
88
|
+
return DEFAULT_BEADS_VERSION;
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
/**
|
|
@@ -126,10 +128,12 @@ function templateWorkflows(workflowDir, branch, beadsVersion, createdFiles = [])
|
|
|
126
128
|
/branches:\s*\[master\]/g,
|
|
127
129
|
`branches: [${branch}]`
|
|
128
130
|
);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
for (const version of ['0.49.1', DEFAULT_BEADS_VERSION, '__FORGE_BEADS_VERSION__']) {
|
|
132
|
+
content = content.replaceAll(
|
|
133
|
+
`BD_VERSION="${version}"`,
|
|
134
|
+
`BD_VERSION="${beadsVersion}"`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
133
137
|
// Only write if content actually changed
|
|
134
138
|
if (content !== original) {
|
|
135
139
|
fs.writeFileSync(filePath, content);
|
|
@@ -253,6 +257,7 @@ function scaffoldBeadsSync(projectRoot, packageDir, _options = {}) {
|
|
|
253
257
|
}
|
|
254
258
|
|
|
255
259
|
module.exports = {
|
|
260
|
+
DEFAULT_BEADS_VERSION,
|
|
256
261
|
detectDefaultBranch,
|
|
257
262
|
detectBeadsVersion,
|
|
258
263
|
templateWorkflows,
|
package/lib/commands/_issue.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { execFileSync } = require('node:child_process');
|
|
4
|
+
const { runIssueOperation } = require('../forge-issues');
|
|
4
5
|
|
|
5
6
|
const SUBCOMMANDS = {
|
|
6
7
|
create: {
|
|
@@ -110,14 +111,23 @@ function buildBdArgs(subcommand, rawArgs) {
|
|
|
110
111
|
return spec.buildBdArgs(args);
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
const WRITE_SUBCOMMANDS = new Set(['create', 'update', 'claim', 'close']);
|
|
115
|
+
|
|
113
116
|
async function runIssueSubcommand(subcommand, args, projectRoot, opts = {}) {
|
|
114
|
-
const exec = opts._exec || execFileSync;
|
|
115
117
|
const bdArgs = buildBdArgs(subcommand, args);
|
|
116
118
|
|
|
117
119
|
if (!Array.isArray(bdArgs)) {
|
|
118
120
|
return { success: false, error: bdArgs.error };
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
if (WRITE_SUBCOMMANDS.has(subcommand)) {
|
|
124
|
+
const runner = opts.runIssueOperation || runIssueOperation;
|
|
125
|
+
const operation = subcommand === 'claim' ? 'update' : subcommand;
|
|
126
|
+
return runner(operation, bdArgs.slice(1), projectRoot, opts);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const exec = opts._exec || execFileSync;
|
|
130
|
+
|
|
121
131
|
try {
|
|
122
132
|
exec('bd', bdArgs, getExecOptions(projectRoot));
|
|
123
133
|
return { success: true, subcommand };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { runIssueOperation } = require('../forge-issues');
|
|
4
|
+
|
|
5
|
+
const SUPPORTED_SUBCOMMANDS = ['create', 'list', 'show', 'close', 'update'];
|
|
6
|
+
|
|
7
|
+
function normalizeArgs(args = []) {
|
|
8
|
+
return args.filter(arg => arg !== '--');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function formatHelp() {
|
|
12
|
+
return [
|
|
13
|
+
'Usage: forge issues <subcommand> [...]',
|
|
14
|
+
'',
|
|
15
|
+
'Supported subcommands:',
|
|
16
|
+
' create Create an issue through Forge',
|
|
17
|
+
' list List issues through Forge',
|
|
18
|
+
' show Show a single issue through Forge',
|
|
19
|
+
' close Close an issue through Forge',
|
|
20
|
+
' update Update an issue through Forge',
|
|
21
|
+
].join('\n');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
name: 'issues',
|
|
26
|
+
description: 'Manage issues through the Forge authority layer',
|
|
27
|
+
usage: 'forge issues <subcommand> [...]',
|
|
28
|
+
flags: {},
|
|
29
|
+
handler: async (args, _flags, projectRoot, opts = {}) => {
|
|
30
|
+
const [subcommand, ...rest] = normalizeArgs(args);
|
|
31
|
+
|
|
32
|
+
if (!subcommand || subcommand === '--help' || subcommand === '-h') {
|
|
33
|
+
return {
|
|
34
|
+
success: true,
|
|
35
|
+
output: formatHelp(),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!SUPPORTED_SUBCOMMANDS.includes(subcommand)) {
|
|
40
|
+
return {
|
|
41
|
+
success: false,
|
|
42
|
+
error: `Unknown issue subcommand '${subcommand}'.\n\n${formatHelp()}`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const runner = opts.runIssueOperation || runIssueOperation;
|
|
47
|
+
return runner(subcommand, rest, projectRoot, opts);
|
|
48
|
+
},
|
|
49
|
+
};
|