forge-workflow 0.0.6 → 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/.cursorrules +149 -0
- package/bin/forge.js +43 -3
- package/lib/agents/README.md +46 -1
- package/lib/agents/cline.plugin.json +11 -4
- package/lib/agents/codex.plugin.json +2 -2
- package/lib/agents/copilot.plugin.json +5 -5
- package/lib/agents/cursor.plugin.json +1 -1
- package/lib/agents/kilocode.plugin.json +1 -1
- package/lib/agents/opencode.plugin.json +7 -4
- package/lib/agents/roo.plugin.json +10 -3
- package/lib/agents-config.js +127 -79
- package/lib/codex-skills.js +50 -0
- package/lib/commands/_issue.js +172 -0
- package/lib/commands/_registry.js +40 -1
- package/lib/commands/claim.js +5 -0
- package/lib/commands/close.js +5 -0
- package/lib/commands/commands-reset.js +147 -0
- package/lib/commands/create.js +5 -0
- package/lib/commands/dev.js +26 -0
- package/lib/commands/issue.js +5 -0
- package/lib/commands/list.js +5 -0
- package/lib/commands/plan.js +18 -0
- package/lib/commands/ready.js +5 -0
- package/lib/commands/setup.js +4295 -0
- package/lib/commands/ship.js +20 -0
- package/lib/commands/show.js +5 -0
- package/lib/commands/status.js +210 -44
- package/lib/commands/sync.js +19 -1
- package/lib/commands/update.js +5 -0
- package/lib/commands/validate.js +13 -0
- package/lib/detect-agent.js +38 -8
- package/lib/detection-utils.js +405 -0
- package/lib/file-utils.js +260 -0
- package/lib/forge-context.js +42 -0
- package/lib/frontmatter.js +79 -0
- package/lib/husky-migration.js +113 -12
- package/lib/lefthook-check.js +27 -6
- package/lib/plugin-manager.js +225 -72
- package/lib/project-discovery.js +39 -5
- package/lib/runtime-health.js +305 -0
- package/lib/shell-utils.js +50 -0
- package/lib/ui-utils.js +43 -0
- package/lib/validation-utils.js +163 -0
- package/lib/workflow/enforce-stage.js +179 -0
- package/lib/workflow/stages.js +201 -0
- package/lib/workflow/state.js +332 -0
- package/opencode.json +67 -0
- package/package.json +15 -5
- package/scripts/beads-context.sh +12 -4
- package/scripts/check-agents.js +103 -0
- package/scripts/lib/eval-runner.js +50 -0
- package/scripts/pr-coordinator.sh +71 -21
- package/scripts/smart-status.sh +21 -11
- package/scripts/sync-commands.js +49 -20
- package/scripts/test.js +16 -1
package/lib/agents-config.js
CHANGED
|
@@ -68,6 +68,25 @@ async function detectProjectMetadata(projectPath) {
|
|
|
68
68
|
return meta;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Write a file unless it already exists, unless overwrite is enabled.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} filePath
|
|
75
|
+
* @param {string} content
|
|
76
|
+
* @param {boolean} overwrite
|
|
77
|
+
* @returns {Promise<void>}
|
|
78
|
+
*/
|
|
79
|
+
async function writeIfNeeded(filePath, content, overwrite = false) {
|
|
80
|
+
const exists = await fs.promises.access(filePath).then(() => true).catch(() => false);
|
|
81
|
+
if (!exists || overwrite) {
|
|
82
|
+
await fs.promises.writeFile(filePath, content, 'utf-8');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createConditionalWriter(overwrite) {
|
|
87
|
+
return (filePath, content) => writeIfNeeded(filePath, content, overwrite);
|
|
88
|
+
}
|
|
89
|
+
|
|
71
90
|
/**
|
|
72
91
|
* Generate AGENTS.md content
|
|
73
92
|
* @param {Object} meta - Project metadata
|
|
@@ -241,7 +260,7 @@ While this universal AGENTS.md works with all agents, you can optionally enable
|
|
|
241
260
|
- **GitHub Copilot**: \`.github/copilot-instructions.md\` + path-specific instructions
|
|
242
261
|
- **Cursor**: \`.cursor/rules/*.mdc\` + native modes
|
|
243
262
|
- **OpenCode**: \`opencode.json\` + custom agents
|
|
244
|
-
- **Kilo**: \`.
|
|
263
|
+
- **Kilo**: \`.kilocode/workflows\`, \`.kilocode/rules\`, \`.kilocode/skills\`
|
|
245
264
|
|
|
246
265
|
Generate with: \`bunx forge setup --agent=<name>\`
|
|
247
266
|
|
|
@@ -277,38 +296,32 @@ async function generateCopilotConfig(projectPath, options = {}) {
|
|
|
277
296
|
await fs.promises.mkdir(instructionsDir, { recursive: true });
|
|
278
297
|
await fs.promises.mkdir(promptsDir, { recursive: true });
|
|
279
298
|
|
|
280
|
-
|
|
281
|
-
const writeIfNeeded = async (filePath, content) => {
|
|
282
|
-
const exists = await fs.promises.access(filePath).then(() => true).catch(() => false);
|
|
283
|
-
if (!exists || overwrite) {
|
|
284
|
-
await fs.promises.writeFile(filePath, content, 'utf-8');
|
|
285
|
-
}
|
|
286
|
-
};
|
|
299
|
+
const writeConfigFile = createConditionalWriter(overwrite);
|
|
287
300
|
|
|
288
301
|
// 1. Create .github/copilot-instructions.md
|
|
289
302
|
const copilotInstructionsPath = path.join(githubDir, 'copilot-instructions.md');
|
|
290
303
|
const copilotInstructionsContent = generateCopilotInstructionsContent(projectMeta);
|
|
291
|
-
await
|
|
304
|
+
await writeConfigFile(copilotInstructionsPath, copilotInstructionsContent);
|
|
292
305
|
|
|
293
306
|
// 2. Create .github/instructions/typescript.instructions.md
|
|
294
307
|
const tsInstructionsPath = path.join(instructionsDir, 'typescript.instructions.md');
|
|
295
308
|
const tsInstructionsContent = generateTypeScriptInstructionsContent();
|
|
296
|
-
await
|
|
309
|
+
await writeConfigFile(tsInstructionsPath, tsInstructionsContent);
|
|
297
310
|
|
|
298
311
|
// 3. Create .github/instructions/testing.instructions.md
|
|
299
312
|
const testInstructionsPath = path.join(instructionsDir, 'testing.instructions.md');
|
|
300
313
|
const testInstructionsContent = generateTestingInstructionsContent();
|
|
301
|
-
await
|
|
314
|
+
await writeConfigFile(testInstructionsPath, testInstructionsContent);
|
|
302
315
|
|
|
303
316
|
// 4. Create .github/prompts/red.prompt.md
|
|
304
317
|
const redPromptPath = path.join(promptsDir, 'red.prompt.md');
|
|
305
318
|
const redPromptContent = generateRedPromptContent();
|
|
306
|
-
await
|
|
319
|
+
await writeConfigFile(redPromptPath, redPromptContent);
|
|
307
320
|
|
|
308
321
|
// 5. Create .github/prompts/green.prompt.md
|
|
309
322
|
const greenPromptPath = path.join(promptsDir, 'green.prompt.md');
|
|
310
323
|
const greenPromptContent = generateGreenPromptContent();
|
|
311
|
-
await
|
|
324
|
+
await writeConfigFile(greenPromptPath, greenPromptContent);
|
|
312
325
|
}
|
|
313
326
|
|
|
314
327
|
/**
|
|
@@ -643,33 +656,27 @@ async function generateCursorConfig(projectPath, options = {}) {
|
|
|
643
656
|
|
|
644
657
|
await fs.promises.mkdir(rulesDir, { recursive: true });
|
|
645
658
|
|
|
646
|
-
|
|
647
|
-
const writeIfNeeded = async (filePath, content) => {
|
|
648
|
-
const exists = await fs.promises.access(filePath).then(() => true).catch(() => false);
|
|
649
|
-
if (!exists || overwrite) {
|
|
650
|
-
await fs.promises.writeFile(filePath, content, 'utf-8');
|
|
651
|
-
}
|
|
652
|
-
};
|
|
659
|
+
const writeConfigFile = createConditionalWriter(overwrite);
|
|
653
660
|
|
|
654
661
|
// 1. Create .cursor/rules/forge-workflow.mdc
|
|
655
662
|
const workflowPath = path.join(rulesDir, 'forge-workflow.mdc');
|
|
656
663
|
const workflowContent = generateCursorWorkflowContent(projectMeta);
|
|
657
|
-
await
|
|
664
|
+
await writeConfigFile(workflowPath, workflowContent);
|
|
658
665
|
|
|
659
666
|
// 2. Create .cursor/rules/tdd-enforcement.mdc
|
|
660
667
|
const tddPath = path.join(rulesDir, 'tdd-enforcement.mdc');
|
|
661
668
|
const tddContent = generateCursorTddContent();
|
|
662
|
-
await
|
|
669
|
+
await writeConfigFile(tddPath, tddContent);
|
|
663
670
|
|
|
664
671
|
// 3. Create .cursor/rules/security-scanning.mdc
|
|
665
672
|
const securityPath = path.join(rulesDir, 'security-scanning.mdc');
|
|
666
673
|
const securityContent = generateCursorSecurityContent();
|
|
667
|
-
await
|
|
674
|
+
await writeConfigFile(securityPath, securityContent);
|
|
668
675
|
|
|
669
676
|
// 4. Create .cursor/rules/documentation.mdc
|
|
670
677
|
const docsPath = path.join(rulesDir, 'documentation.mdc');
|
|
671
678
|
const docsContent = generateCursorDocumentationContent();
|
|
672
|
-
await
|
|
679
|
+
await writeConfigFile(docsPath, docsContent);
|
|
673
680
|
}
|
|
674
681
|
|
|
675
682
|
/**
|
|
@@ -1010,7 +1017,7 @@ Update documentation at relevant stages (not deferred to end):
|
|
|
1010
1017
|
}
|
|
1011
1018
|
|
|
1012
1019
|
/**
|
|
1013
|
-
* Generate Kilo Code configuration
|
|
1020
|
+
* Generate Kilo Code configuration files
|
|
1014
1021
|
* @param {string} projectPath - Path to the project root
|
|
1015
1022
|
* @param {Object} options - Generation options
|
|
1016
1023
|
* @param {boolean} options.overwrite - Whether to overwrite existing files (default: false)
|
|
@@ -1022,29 +1029,45 @@ async function generateKiloConfig(projectPath, options = {}) {
|
|
|
1022
1029
|
// Detect project metadata
|
|
1023
1030
|
const projectMeta = await detectProjectMetadata(projectPath);
|
|
1024
1031
|
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
const
|
|
1029
|
-
if (exists && !overwrite) {
|
|
1030
|
-
return; // Don't overwrite
|
|
1031
|
-
}
|
|
1032
|
+
const kilocodeDir = path.join(projectPath, '.kilocode');
|
|
1033
|
+
const workflowsDir = path.join(kilocodeDir, 'workflows');
|
|
1034
|
+
const rulesDir = path.join(kilocodeDir, 'rules');
|
|
1035
|
+
const skillsDir = path.join(kilocodeDir, 'skills', 'forge-workflow');
|
|
1032
1036
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
+
await fs.promises.mkdir(workflowsDir, { recursive: true });
|
|
1038
|
+
await fs.promises.mkdir(rulesDir, { recursive: true });
|
|
1039
|
+
await fs.promises.mkdir(skillsDir, { recursive: true });
|
|
1040
|
+
|
|
1041
|
+
await writeIfNeeded(
|
|
1042
|
+
path.join(workflowsDir, 'forge-workflow.md'),
|
|
1043
|
+
generateKiloWorkflowContent(projectMeta),
|
|
1044
|
+
overwrite
|
|
1045
|
+
);
|
|
1046
|
+
await writeIfNeeded(
|
|
1047
|
+
path.join(rulesDir, 'workflow.md'),
|
|
1048
|
+
generateKiloRulesContent(projectMeta),
|
|
1049
|
+
overwrite
|
|
1050
|
+
);
|
|
1051
|
+
await writeIfNeeded(
|
|
1052
|
+
path.join(skillsDir, 'SKILL.md'),
|
|
1053
|
+
generateKiloSkillContent(projectMeta),
|
|
1054
|
+
overwrite
|
|
1055
|
+
);
|
|
1037
1056
|
}
|
|
1038
1057
|
|
|
1039
1058
|
/**
|
|
1040
|
-
* Generate .
|
|
1059
|
+
* Generate .kilocode/workflows/forge-workflow.md content
|
|
1041
1060
|
*/
|
|
1042
|
-
function
|
|
1061
|
+
function generateKiloWorkflowContent(meta) {
|
|
1043
1062
|
const packageManager = meta.testCommand?.includes('bun') ? 'bun' : 'npm';
|
|
1044
1063
|
|
|
1045
|
-
return
|
|
1064
|
+
return `---
|
|
1065
|
+
description: "Forge workflow for Kilo Code"
|
|
1066
|
+
---
|
|
1046
1067
|
|
|
1047
|
-
|
|
1068
|
+
# Forge Workflow Framework - Kilo Code
|
|
1069
|
+
|
|
1070
|
+
This project uses the **Forge 7-Stage TDD Workflow** with Kilo Code native workflows.
|
|
1048
1071
|
|
|
1049
1072
|
## Quick Start
|
|
1050
1073
|
|
|
@@ -1054,12 +1077,12 @@ ${meta.testCommand} # Run tests
|
|
|
1054
1077
|
${meta.buildCommand} # Build project
|
|
1055
1078
|
\`\`\`
|
|
1056
1079
|
|
|
1057
|
-
## Kilo
|
|
1080
|
+
## Native Kilo Surface
|
|
1058
1081
|
|
|
1059
|
-
Kilo Code
|
|
1060
|
-
-
|
|
1061
|
-
-
|
|
1062
|
-
-
|
|
1082
|
+
Kilo Code uses native workflows, rules, and skills. Forge generates:
|
|
1083
|
+
- \`.kilocode/workflows/forge-workflow.md\`
|
|
1084
|
+
- \`.kilocode/rules/workflow.md\`
|
|
1085
|
+
- \`.kilocode/skills/forge-workflow/SKILL.md\`
|
|
1063
1086
|
|
|
1064
1087
|
These complement the Forge workflow stages below.
|
|
1065
1088
|
|
|
@@ -1117,36 +1140,67 @@ Final documentation verification
|
|
|
1117
1140
|
- Verify all docs updated
|
|
1118
1141
|
- Check for broken links
|
|
1119
1142
|
- Validate code examples
|
|
1143
|
+
`;
|
|
1144
|
+
}
|
|
1120
1145
|
|
|
1121
|
-
|
|
1146
|
+
/**
|
|
1147
|
+
* Generate .kilocode/rules/workflow.md content
|
|
1148
|
+
*/
|
|
1149
|
+
function generateKiloRulesContent(meta) {
|
|
1150
|
+
const packageManager = meta.testCommand?.includes('bun') ? 'bun' : 'npm';
|
|
1122
1151
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
- Refactor and commit (REFACTOR phase)
|
|
1127
|
-
- No code changes without corresponding tests
|
|
1152
|
+
return `---
|
|
1153
|
+
description: "Forge workflow rules for Kilo Code"
|
|
1154
|
+
---
|
|
1128
1155
|
|
|
1129
|
-
|
|
1156
|
+
# Forge Workflow Rules
|
|
1130
1157
|
|
|
1131
|
-
|
|
1132
|
-
-
|
|
1133
|
-
-
|
|
1134
|
-
-
|
|
1135
|
-
-
|
|
1158
|
+
- Use Forge as the source of truth for stage order and hard stops.
|
|
1159
|
+
- Run \`forge <stage>\` before continuing any workflow stage.
|
|
1160
|
+
- Treat \`.kilocode/workflows\` as the canonical workflow surface.
|
|
1161
|
+
- Keep \`AGENTS.md\` as shared context, not the workflow authority.
|
|
1162
|
+
- If Forge blocks a stage, fix the prerequisite or provide an explicit override payload.
|
|
1136
1163
|
|
|
1137
|
-
##
|
|
1164
|
+
## Environment
|
|
1138
1165
|
|
|
1139
|
-
|
|
1140
|
-
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1166
|
+
- Package manager: ${packageManager}
|
|
1167
|
+
- Language: ${meta.language ? meta.language : 'JavaScript'}
|
|
1168
|
+
`;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Generate .kilocode/skills/forge-workflow/SKILL.md content
|
|
1173
|
+
*/
|
|
1174
|
+
function generateKiloSkillContent(meta) {
|
|
1175
|
+
const packageManager = meta.testCommand?.includes('bun') ? 'bun' : 'npm';
|
|
1176
|
+
|
|
1177
|
+
return `---
|
|
1178
|
+
name: forge-workflow
|
|
1179
|
+
description: Forge stage adapter for Kilo Code
|
|
1180
|
+
---
|
|
1181
|
+
|
|
1182
|
+
# Forge Workflow Skill
|
|
1183
|
+
|
|
1184
|
+
Before starting any stage, invoke \`forge <stage>\` so Forge can enforce stage order, prerequisites, and overrides.
|
|
1185
|
+
If Forge blocks the stage, stop and resolve it first.
|
|
1186
|
+
|
|
1187
|
+
## Workflow Context
|
|
1188
|
+
|
|
1189
|
+
- Package manager: ${packageManager}
|
|
1190
|
+
- Language: ${meta.language ? meta.language : 'JavaScript'}
|
|
1191
|
+
- Canonical workflow surface: \`.kilocode/workflows/forge-workflow.md\`
|
|
1192
|
+
- Shared context: \`AGENTS.md\`
|
|
1193
|
+
|
|
1194
|
+
## Supported Stages
|
|
1195
|
+
|
|
1196
|
+
- \`/status\`
|
|
1197
|
+
- \`/plan\`
|
|
1198
|
+
- \`/dev\`
|
|
1199
|
+
- \`/validate\`
|
|
1200
|
+
- \`/ship\`
|
|
1201
|
+
- \`/review\`
|
|
1202
|
+
- \`/premerge\`
|
|
1203
|
+
- \`/verify\`
|
|
1150
1204
|
`;
|
|
1151
1205
|
}
|
|
1152
1206
|
|
|
@@ -1168,28 +1222,22 @@ async function generateOpenCodeConfig(projectPath, options = {}) {
|
|
|
1168
1222
|
const agentsDir = path.join(opencodeDir, 'agents');
|
|
1169
1223
|
await fs.promises.mkdir(agentsDir, { recursive: true });
|
|
1170
1224
|
|
|
1171
|
-
|
|
1172
|
-
const writeIfNeeded = async (filePath, content) => {
|
|
1173
|
-
const exists = await fs.promises.access(filePath).then(() => true).catch(() => false);
|
|
1174
|
-
if (!exists || overwrite) {
|
|
1175
|
-
await fs.promises.writeFile(filePath, content, 'utf-8');
|
|
1176
|
-
}
|
|
1177
|
-
};
|
|
1225
|
+
const writeConfigFile = createConditionalWriter(overwrite);
|
|
1178
1226
|
|
|
1179
1227
|
// 1. Create opencode.json
|
|
1180
1228
|
const opencodeJsonPath = path.join(projectPath, 'opencode.json');
|
|
1181
1229
|
const opencodeJsonContent = generateOpenCodeJsonContent(projectMeta);
|
|
1182
|
-
await
|
|
1230
|
+
await writeConfigFile(opencodeJsonPath, opencodeJsonContent);
|
|
1183
1231
|
|
|
1184
1232
|
// 2. Create .opencode/agents/plan-review.md
|
|
1185
1233
|
const planAgentPath = path.join(agentsDir, 'plan-review.md');
|
|
1186
1234
|
const planAgentContent = generateOpenCodePlanAgentContent();
|
|
1187
|
-
await
|
|
1235
|
+
await writeConfigFile(planAgentPath, planAgentContent);
|
|
1188
1236
|
|
|
1189
1237
|
// 3. Create .opencode/agents/tdd-build.md
|
|
1190
1238
|
const buildAgentPath = path.join(agentsDir, 'tdd-build.md');
|
|
1191
1239
|
const buildAgentContent = generateOpenCodeBuildAgentContent();
|
|
1192
|
-
await
|
|
1240
|
+
await writeConfigFile(buildAgentPath, buildAgentContent);
|
|
1193
1241
|
}
|
|
1194
1242
|
|
|
1195
1243
|
/**
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
function injectForgeAdapter(content, commandName) {
|
|
5
|
+
const adapter = [
|
|
6
|
+
'> Forge stage adapter',
|
|
7
|
+
'',
|
|
8
|
+
`Before executing this workflow, invoke \`forge ${commandName}\` so Forge can enforce stage order, runtime prerequisites, and override rules.`,
|
|
9
|
+
'If Forge blocks the stage or asks for an explicit override payload, stop and resolve that first.',
|
|
10
|
+
'',
|
|
11
|
+
].join('\n');
|
|
12
|
+
|
|
13
|
+
const match = content.match(/^(---\r?\n[\s\S]*?\r?\n---\r?\n?)([\s\S]*)$/);
|
|
14
|
+
if (!match) {
|
|
15
|
+
return `${adapter}\n${content}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return `${match[1]}\n${adapter}${match[2]}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function listCodexSkillEntries(sourceRoot) {
|
|
22
|
+
const skillsDir = path.join(sourceRoot, '.codex', 'skills');
|
|
23
|
+
|
|
24
|
+
if (!fs.existsSync(skillsDir)) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
29
|
+
.filter((entry) => entry.isDirectory())
|
|
30
|
+
.map((entry) => entry.name)
|
|
31
|
+
.sort()
|
|
32
|
+
.map((commandName) => {
|
|
33
|
+
const sourceFile = path.join(skillsDir, commandName, 'SKILL.md');
|
|
34
|
+
if (!fs.existsSync(sourceFile)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
commandName,
|
|
40
|
+
dir: `.codex/skills/${commandName}/`,
|
|
41
|
+
filename: 'SKILL.md',
|
|
42
|
+
content: injectForgeAdapter(fs.readFileSync(sourceFile, 'utf8'), commandName),
|
|
43
|
+
};
|
|
44
|
+
})
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
listCodexSkillEntries,
|
|
50
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
const { existsSync, readdirSync } = require('node:fs');
|
|
13
13
|
const path = require('node:path');
|
|
14
|
+
const { normalizeStageId } = require('../workflow/stages');
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* @typedef {Object} CommandModule
|
|
@@ -131,4 +132,42 @@ function buildHelp(commands) {
|
|
|
131
132
|
return lines.join('\n');
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
|
|
135
|
+
function isStageCommand(commandName) {
|
|
136
|
+
return normalizeStageId(commandName) !== null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function executeCommand(commands, commandName, args, flags, projectRoot, options = {}) {
|
|
140
|
+
const command = commands.get(commandName);
|
|
141
|
+
if (!command) {
|
|
142
|
+
return { success: false, error: `Unknown command: ${commandName}` };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
if (typeof options.enforceStage === 'function' && isStageCommand(commandName)) {
|
|
147
|
+
const enforcement = await options.enforceStage({
|
|
148
|
+
commandName,
|
|
149
|
+
args,
|
|
150
|
+
flags,
|
|
151
|
+
projectRoot,
|
|
152
|
+
command,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (enforcement?.allowed === false) {
|
|
156
|
+
return {
|
|
157
|
+
success: false,
|
|
158
|
+
error: enforcement.error ?? `Stage ${commandName} is blocked.`,
|
|
159
|
+
enforcement,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return await command.handler(args, flags, projectRoot);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
error: err?.message ?? `Failed to execute ${commandName}`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = { loadCommands, validateCommand, executeCommand };
|