forge-workflow 0.0.7 → 0.0.8
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/bin/forge.js +7 -0
- package/lib/commands/_issue.js +172 -0
- package/lib/commands/claim.js +5 -0
- package/lib/commands/close.js +5 -0
- package/lib/commands/create.js +5 -0
- package/lib/commands/issue.js +5 -0
- package/lib/commands/list.js +5 -0
- package/lib/commands/ready.js +5 -0
- package/lib/commands/show.js +5 -0
- package/lib/commands/sync.js +3 -1
- package/lib/commands/update.js +5 -0
- package/package.json +1 -1
- package/scripts/lib/eval-runner.js +50 -0
package/bin/forge.js
CHANGED
|
@@ -2541,6 +2541,13 @@ function parseFlags() {
|
|
|
2541
2541
|
sync: false, // Scaffold Beads GitHub sync workflows (--sync)
|
|
2542
2542
|
};
|
|
2543
2543
|
|
|
2544
|
+
// Issue passthrough commands delegate all flags to bd.
|
|
2545
|
+
// Skip global parsing so flags like --type, -p, --help reach the handler intact.
|
|
2546
|
+
const issuePassthroughCommands = ['create', 'update', 'claim', 'close', 'show', 'list', 'ready', 'issue'];
|
|
2547
|
+
if (issuePassthroughCommands.includes(args[0])) {
|
|
2548
|
+
return flags;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2544
2551
|
for (let i = 0; i < args.length;) {
|
|
2545
2552
|
const arg = args[i];
|
|
2546
2553
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execFileSync } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
const SUBCOMMANDS = {
|
|
6
|
+
create: {
|
|
7
|
+
description: 'Create a Beads issue via Forge',
|
|
8
|
+
usage: 'forge create [title] [bd-create-flags]',
|
|
9
|
+
helpCommand: 'create',
|
|
10
|
+
buildBdArgs: (args) => ['create', ...args],
|
|
11
|
+
},
|
|
12
|
+
update: {
|
|
13
|
+
description: 'Update a Beads issue via Forge',
|
|
14
|
+
usage: 'forge update <id...> [bd-update-flags]',
|
|
15
|
+
helpCommand: 'update',
|
|
16
|
+
buildBdArgs: (args) => ['update', ...args],
|
|
17
|
+
},
|
|
18
|
+
claim: {
|
|
19
|
+
description: 'Claim a Beads issue via Forge',
|
|
20
|
+
usage: 'forge claim <id> [bd-update-flags]',
|
|
21
|
+
helpCommand: 'update',
|
|
22
|
+
buildBdArgs: (args) => {
|
|
23
|
+
const [issueId, ...rest] = args;
|
|
24
|
+
if (!issueId) {
|
|
25
|
+
return { error: 'Missing issue id. Usage: forge claim <id> [bd-update-flags]' };
|
|
26
|
+
}
|
|
27
|
+
return ['update', issueId, '--claim', ...rest];
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
close: {
|
|
31
|
+
description: 'Close a Beads issue via Forge',
|
|
32
|
+
usage: 'forge close <id...> [bd-close-flags]',
|
|
33
|
+
helpCommand: 'close',
|
|
34
|
+
buildBdArgs: (args) => ['close', ...args],
|
|
35
|
+
},
|
|
36
|
+
show: {
|
|
37
|
+
description: 'Show a Beads issue via Forge',
|
|
38
|
+
usage: 'forge show <id> [bd-show-flags]',
|
|
39
|
+
helpCommand: 'show',
|
|
40
|
+
buildBdArgs: (args) => ['show', ...args],
|
|
41
|
+
},
|
|
42
|
+
list: {
|
|
43
|
+
description: 'List Beads issues via Forge',
|
|
44
|
+
usage: 'forge list [bd-list-flags]',
|
|
45
|
+
helpCommand: 'list',
|
|
46
|
+
buildBdArgs: (args) => ['list', ...args],
|
|
47
|
+
},
|
|
48
|
+
ready: {
|
|
49
|
+
description: 'Show ready Beads issues via Forge',
|
|
50
|
+
usage: 'forge ready [bd-ready-flags]',
|
|
51
|
+
helpCommand: 'ready',
|
|
52
|
+
buildBdArgs: (args) => ['ready', ...args],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function normalizeArgs(args = []) {
|
|
57
|
+
return args.filter(arg => arg !== '--');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getExecOptions(projectRoot) {
|
|
61
|
+
return {
|
|
62
|
+
cwd: projectRoot,
|
|
63
|
+
stdio: 'inherit',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function formatIssueHelp() {
|
|
68
|
+
const lines = [
|
|
69
|
+
'Usage: forge issue <subcommand> [...]',
|
|
70
|
+
'',
|
|
71
|
+
'Supported subcommands:',
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
for (const [name, spec] of Object.entries(SUBCOMMANDS)) {
|
|
75
|
+
lines.push(` ${name.padEnd(6)} ${spec.description}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
lines.push('');
|
|
79
|
+
lines.push('Examples:');
|
|
80
|
+
lines.push(' forge create --title "Add feature" --type feature');
|
|
81
|
+
lines.push(' forge claim forge-abc');
|
|
82
|
+
lines.push(' forge update forge-abc --priority 1');
|
|
83
|
+
lines.push(' forge close forge-abc --reason "Done"');
|
|
84
|
+
lines.push(' forge issue show forge-abc --json');
|
|
85
|
+
|
|
86
|
+
return lines.join('\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function extractErrorMessage(error) {
|
|
90
|
+
if (error?.code === 'ENOENT') {
|
|
91
|
+
return 'Beads (bd) command not found. Install or initialize Beads before using Forge issue commands.';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// With stdio: 'inherit', error.stderr and error.stdout are always null.
|
|
95
|
+
// Only error.message is available for diagnostics.
|
|
96
|
+
return error?.message?.trim() || 'Beads command failed';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildBdArgs(subcommand, rawArgs) {
|
|
100
|
+
const spec = SUBCOMMANDS[subcommand];
|
|
101
|
+
if (!spec) {
|
|
102
|
+
return { error: `Unknown issue subcommand '${subcommand}'.\n\n${formatIssueHelp()}` };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const args = normalizeArgs(rawArgs);
|
|
106
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
107
|
+
return [spec.helpCommand, '--help'];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return spec.buildBdArgs(args);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function runIssueSubcommand(subcommand, args, projectRoot, opts = {}) {
|
|
114
|
+
const exec = opts._exec || execFileSync;
|
|
115
|
+
const bdArgs = buildBdArgs(subcommand, args);
|
|
116
|
+
|
|
117
|
+
if (!Array.isArray(bdArgs)) {
|
|
118
|
+
return { success: false, error: bdArgs.error };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
exec('bd', bdArgs, getExecOptions(projectRoot));
|
|
123
|
+
return { success: true, subcommand };
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return {
|
|
126
|
+
success: false,
|
|
127
|
+
error: extractErrorMessage(error),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function makeAliasCommand(subcommand) {
|
|
133
|
+
const spec = SUBCOMMANDS[subcommand];
|
|
134
|
+
if (!spec) {
|
|
135
|
+
throw new Error(`Unknown issue subcommand '${subcommand}'`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
name: subcommand,
|
|
140
|
+
description: spec.description,
|
|
141
|
+
usage: spec.usage,
|
|
142
|
+
flags: {},
|
|
143
|
+
handler: async (args, _flags, projectRoot, opts = {}) =>
|
|
144
|
+
runIssueSubcommand(subcommand, args, projectRoot, opts),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function createIssueCommand() {
|
|
149
|
+
return {
|
|
150
|
+
name: 'issue',
|
|
151
|
+
description: 'Manage Beads issues through the Forge command surface',
|
|
152
|
+
usage: 'forge issue <create|update|claim|close|show|list|ready> [...]',
|
|
153
|
+
flags: {},
|
|
154
|
+
handler: async (args, _flags, projectRoot, opts = {}) => {
|
|
155
|
+
const [subcommand, ...rest] = normalizeArgs(args);
|
|
156
|
+
|
|
157
|
+
if (!subcommand || subcommand === '--help' || subcommand === '-h') {
|
|
158
|
+
return { success: true, output: formatIssueHelp() };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return runIssueSubcommand(subcommand, rest, projectRoot, opts);
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = {
|
|
167
|
+
SUBCOMMANDS,
|
|
168
|
+
buildBdArgs,
|
|
169
|
+
createIssueCommand,
|
|
170
|
+
makeAliasCommand,
|
|
171
|
+
runIssueSubcommand,
|
|
172
|
+
};
|
package/lib/commands/sync.js
CHANGED
|
@@ -7,7 +7,9 @@ function isRecoverableBeadsSyncError(error) {
|
|
|
7
7
|
return (
|
|
8
8
|
message.includes('failed to open database') ||
|
|
9
9
|
message.includes('database not found') ||
|
|
10
|
-
message.includes('no beads configuration found')
|
|
10
|
+
message.includes('no beads configuration found') ||
|
|
11
|
+
message.includes('remote \'origin\' not found') ||
|
|
12
|
+
message.includes('remote "origin" not found')
|
|
11
13
|
);
|
|
12
14
|
}
|
|
13
15
|
|
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
12
13
|
const { execSync } = require('node:child_process');
|
|
13
14
|
|
|
14
15
|
// ── active worktree tracking (cleanup on crash) ─────────────────────
|
|
@@ -17,6 +18,17 @@ const { execSync } = require('node:child_process');
|
|
|
17
18
|
// Note: execSync is safe here — all paths are internally generated, never user input.
|
|
18
19
|
const activeEvalWorktrees = new Map(); // path -> branch
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Force-remove a directory that git worktree remove may leave behind (Windows).
|
|
23
|
+
*/
|
|
24
|
+
function forceRemoveDir(dirPath) {
|
|
25
|
+
try {
|
|
26
|
+
if (fs.existsSync(dirPath)) {
|
|
27
|
+
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
} catch (_err) { /* best-effort */ }
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
function cleanupActiveWorktrees() {
|
|
21
33
|
if (activeEvalWorktrees.size === 0) return;
|
|
22
34
|
let repoRoot;
|
|
@@ -25,6 +37,7 @@ function cleanupActiveWorktrees() {
|
|
|
25
37
|
try {
|
|
26
38
|
execSync(`git worktree remove --force "${wtPath}"`, { cwd: repoRoot, stdio: 'pipe' });
|
|
27
39
|
} catch (_err) { /* already removed */ }
|
|
40
|
+
forceRemoveDir(wtPath);
|
|
28
41
|
if (branch && branch.startsWith('eval-')) {
|
|
29
42
|
try {
|
|
30
43
|
execSync(`git branch -D "${branch}"`, { cwd: repoRoot, stdio: 'pipe' });
|
|
@@ -35,6 +48,37 @@ function cleanupActiveWorktrees() {
|
|
|
35
48
|
activeEvalWorktrees.clear();
|
|
36
49
|
}
|
|
37
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Remove stale eval-* directories left behind by crashed runs.
|
|
53
|
+
* Git has already forgotten them (worktree prune), but the directories persist on Windows.
|
|
54
|
+
*/
|
|
55
|
+
function cleanupStaleEvalWorktrees() {
|
|
56
|
+
try {
|
|
57
|
+
const worktreesDir = getWorktreesDir();
|
|
58
|
+
if (!fs.existsSync(worktreesDir)) return;
|
|
59
|
+
|
|
60
|
+
// Get the list of paths git still knows about
|
|
61
|
+
const repoRoot = getRepoRoot();
|
|
62
|
+
const knownRaw = execSync('git worktree list --porcelain', {
|
|
63
|
+
cwd: repoRoot, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
64
|
+
});
|
|
65
|
+
const knownPaths = new Set(
|
|
66
|
+
knownRaw.split('\n')
|
|
67
|
+
.filter((l) => l.startsWith('worktree '))
|
|
68
|
+
.map((l) => l.slice('worktree '.length).replace(/\\/g, '/'))
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const entries = fs.readdirSync(worktreesDir);
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (!entry.startsWith('eval-')) continue;
|
|
74
|
+
const fullPath = path.join(worktreesDir, entry).replace(/\\/g, '/');
|
|
75
|
+
if (!knownPaths.has(fullPath)) {
|
|
76
|
+
forceRemoveDir(path.join(worktreesDir, entry));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (_err) { /* best-effort — don't block eval creation */ }
|
|
80
|
+
}
|
|
81
|
+
|
|
38
82
|
process.on('exit', cleanupActiveWorktrees);
|
|
39
83
|
process.on('SIGINT', () => {
|
|
40
84
|
const hadWork = activeEvalWorktrees.size > 0;
|
|
@@ -78,6 +122,9 @@ function getWorktreesDir() {
|
|
|
78
122
|
* @returns {Promise<{ path: string, branch: string }>}
|
|
79
123
|
*/
|
|
80
124
|
async function createEvalWorktree() {
|
|
125
|
+
// Self-heal: remove stale eval dirs from previous crashed runs
|
|
126
|
+
cleanupStaleEvalWorktrees();
|
|
127
|
+
|
|
81
128
|
const timestamp = Date.now();
|
|
82
129
|
const pid = process.pid;
|
|
83
130
|
const name = `eval-${timestamp}-${pid}`;
|
|
@@ -128,6 +175,9 @@ async function destroyEvalWorktree(worktreePath) {
|
|
|
128
175
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
129
176
|
});
|
|
130
177
|
|
|
178
|
+
// Windows: git worktree remove often leaves the directory behind
|
|
179
|
+
forceRemoveDir(worktreePath);
|
|
180
|
+
|
|
131
181
|
// Prune to clean up references
|
|
132
182
|
execSync('git worktree prune', {
|
|
133
183
|
cwd: repoRoot,
|