bmalph 2.2.0 → 2.3.0
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/README.md +111 -30
- package/bundled-versions.json +1 -2
- package/dist/cli.js +3 -2
- package/dist/commands/check-updates.js +5 -27
- package/dist/commands/doctor.d.ts +14 -2
- package/dist/commands/doctor.js +99 -56
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +75 -9
- package/dist/commands/upgrade.js +8 -5
- package/dist/installer.d.ts +16 -5
- package/dist/installer.js +245 -128
- package/dist/platform/aider.d.ts +2 -0
- package/dist/platform/aider.js +71 -0
- package/dist/platform/claude-code.d.ts +2 -0
- package/dist/platform/claude-code.js +88 -0
- package/dist/platform/codex.d.ts +2 -0
- package/dist/platform/codex.js +67 -0
- package/dist/platform/copilot.d.ts +2 -0
- package/dist/platform/copilot.js +71 -0
- package/dist/platform/cursor.d.ts +2 -0
- package/dist/platform/cursor.js +71 -0
- package/dist/platform/detect.d.ts +7 -0
- package/dist/platform/detect.js +23 -0
- package/dist/platform/index.d.ts +4 -0
- package/dist/platform/index.js +3 -0
- package/dist/platform/registry.d.ts +4 -0
- package/dist/platform/registry.js +27 -0
- package/dist/platform/resolve.d.ts +8 -0
- package/dist/platform/resolve.js +24 -0
- package/dist/platform/types.d.ts +41 -0
- package/dist/platform/types.js +7 -0
- package/dist/platform/windsurf.d.ts +2 -0
- package/dist/platform/windsurf.js +71 -0
- package/dist/transition/artifacts.js +1 -1
- package/dist/transition/fix-plan.d.ts +1 -1
- package/dist/transition/fix-plan.js +3 -2
- package/dist/transition/orchestration.js +1 -1
- package/dist/transition/specs-changelog.js +4 -1
- package/dist/transition/specs-index.js +2 -3
- package/dist/utils/config.d.ts +2 -1
- package/dist/utils/errors.js +3 -0
- package/dist/utils/github.d.ts +0 -1
- package/dist/utils/github.js +1 -18
- package/dist/utils/json.js +2 -2
- package/dist/utils/state.js +7 -1
- package/dist/utils/validate.d.ts +1 -0
- package/dist/utils/validate.js +35 -4
- package/package.json +4 -4
- package/ralph/drivers/claude-code.sh +118 -0
- package/ralph/drivers/codex.sh +81 -0
- package/ralph/ralph_import.sh +11 -0
- package/ralph/ralph_loop.sh +37 -64
- package/ralph/templates/ralphrc.template +7 -0
package/dist/installer.js
CHANGED
|
@@ -22,21 +22,18 @@ export function getBundledVersions() {
|
|
|
22
22
|
const versionsPath = join(__dirname, "..", "bundled-versions.json");
|
|
23
23
|
try {
|
|
24
24
|
const versions = JSON.parse(readFileSync(versionsPath, "utf-8"));
|
|
25
|
-
if (!versions ||
|
|
26
|
-
|
|
27
|
-
typeof versions.ralphCommit !== "string") {
|
|
28
|
-
throw new Error("Invalid bundled-versions.json structure: missing bmadCommit or ralphCommit");
|
|
25
|
+
if (!versions || typeof versions.bmadCommit !== "string") {
|
|
26
|
+
throw new Error("Invalid bundled-versions.json structure: missing bmadCommit");
|
|
29
27
|
}
|
|
30
28
|
return {
|
|
31
29
|
bmadCommit: versions.bmadCommit,
|
|
32
|
-
ralphCommit: versions.ralphCommit,
|
|
33
30
|
};
|
|
34
31
|
}
|
|
35
32
|
catch (err) {
|
|
36
33
|
if (err instanceof Error && err.message.includes("Invalid bundled-versions.json")) {
|
|
37
34
|
throw err;
|
|
38
35
|
}
|
|
39
|
-
throw new Error(`Failed to read bundled-versions.json at ${versionsPath}:
|
|
36
|
+
throw new Error(`Failed to read bundled-versions.json at ${versionsPath}`, { cause: err });
|
|
40
37
|
}
|
|
41
38
|
}
|
|
42
39
|
export function getBundledBmadDir() {
|
|
@@ -48,7 +45,104 @@ export function getBundledRalphDir() {
|
|
|
48
45
|
export function getSlashCommandsDir() {
|
|
49
46
|
return join(__dirname, "..", "slash-commands");
|
|
50
47
|
}
|
|
51
|
-
|
|
48
|
+
const TEMPLATE_PLACEHOLDERS = {
|
|
49
|
+
"PROMPT.md": "[YOUR PROJECT NAME]",
|
|
50
|
+
"AGENT.md": "pip install -r requirements.txt",
|
|
51
|
+
};
|
|
52
|
+
async function isTemplateCustomized(filePath, templateName) {
|
|
53
|
+
const placeholder = TEMPLATE_PLACEHOLDERS[templateName];
|
|
54
|
+
if (!placeholder)
|
|
55
|
+
return false;
|
|
56
|
+
try {
|
|
57
|
+
const content = await readFile(filePath, "utf-8");
|
|
58
|
+
return !content.includes(placeholder);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
if (isEnoent(err))
|
|
62
|
+
return false;
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Lazily loads the default (claude-code) platform to avoid circular imports
|
|
68
|
+
* and keep backward compatibility for callers that don't pass a platform.
|
|
69
|
+
*/
|
|
70
|
+
async function getDefaultPlatform() {
|
|
71
|
+
const { claudeCodePlatform } = await import("./platform/claude-code.js");
|
|
72
|
+
return claudeCodePlatform;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Deliver slash commands based on the platform's command delivery strategy.
|
|
76
|
+
*
|
|
77
|
+
* - "directory": Copy command files to a directory (e.g., .claude/commands/)
|
|
78
|
+
* - "inline": Merge command content as sections in the instructions file
|
|
79
|
+
* - "none": Skip command delivery entirely
|
|
80
|
+
*/
|
|
81
|
+
async function deliverCommands(projectDir, platform, slashCommandsDir) {
|
|
82
|
+
const delivery = platform.commandDelivery;
|
|
83
|
+
if (delivery.kind === "none") {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
const slashFiles = await readdir(slashCommandsDir);
|
|
87
|
+
const bundledCommandNames = new Set(slashFiles.filter((f) => f.endsWith(".md")));
|
|
88
|
+
if (delivery.kind === "directory") {
|
|
89
|
+
const commandsDir = join(projectDir, delivery.dir);
|
|
90
|
+
await mkdir(commandsDir, { recursive: true });
|
|
91
|
+
// Clean stale bmalph-owned commands before copying (preserve user-created commands)
|
|
92
|
+
try {
|
|
93
|
+
const existingCommands = await readdir(commandsDir);
|
|
94
|
+
for (const file of existingCommands) {
|
|
95
|
+
if (file.endsWith(".md") && bundledCommandNames.has(file)) {
|
|
96
|
+
await rm(join(commandsDir, file), { force: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (!isEnoent(err))
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
for (const file of bundledCommandNames) {
|
|
105
|
+
await cp(join(slashCommandsDir, file), join(commandsDir, file), { dereference: false });
|
|
106
|
+
}
|
|
107
|
+
return [`${delivery.dir}/`];
|
|
108
|
+
}
|
|
109
|
+
if (delivery.kind === "inline") {
|
|
110
|
+
// Merge command content as sections in the instructions file
|
|
111
|
+
const instructionsPath = join(projectDir, platform.instructionsFile);
|
|
112
|
+
let existing = "";
|
|
113
|
+
try {
|
|
114
|
+
existing = await readFile(instructionsPath, "utf-8");
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (!isEnoent(err))
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
const commandSections = [];
|
|
121
|
+
for (const file of [...bundledCommandNames].sort()) {
|
|
122
|
+
const commandName = file.replace(/\.md$/, "");
|
|
123
|
+
const content = await readFile(join(slashCommandsDir, file), "utf-8");
|
|
124
|
+
commandSections.push(`### Command: ${commandName}\n\n${content.trim()}`);
|
|
125
|
+
}
|
|
126
|
+
const inlineMarker = "## BMAD Commands";
|
|
127
|
+
const commandsBlock = `\n${inlineMarker}\n\n${commandSections.join("\n\n---\n\n")}\n`;
|
|
128
|
+
if (existing.includes(inlineMarker)) {
|
|
129
|
+
// Replace existing commands section
|
|
130
|
+
const sectionStart = existing.indexOf(inlineMarker);
|
|
131
|
+
const before = existing.slice(0, sectionStart);
|
|
132
|
+
const afterSection = existing.slice(sectionStart);
|
|
133
|
+
const nextHeadingMatch = afterSection.match(/\n## (?!BMAD Commands)/);
|
|
134
|
+
const after = nextHeadingMatch ? afterSection.slice(nextHeadingMatch.index) : "";
|
|
135
|
+
await atomicWriteFile(instructionsPath, before.trimEnd() + commandsBlock + after);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
await atomicWriteFile(instructionsPath, existing + commandsBlock);
|
|
139
|
+
}
|
|
140
|
+
return [platform.instructionsFile];
|
|
141
|
+
}
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
export async function copyBundledAssets(projectDir, platform) {
|
|
145
|
+
const p = platform ?? (await getDefaultPlatform());
|
|
52
146
|
const bmadDir = getBundledBmadDir();
|
|
53
147
|
const ralphDir = getBundledRalphDir();
|
|
54
148
|
const slashCommandsDir = getSlashCommandsDir();
|
|
@@ -62,32 +156,50 @@ export async function copyBundledAssets(projectDir) {
|
|
|
62
156
|
if (!(await exists(slashCommandsDir))) {
|
|
63
157
|
throw new Error(`Slash commands directory not found at ${slashCommandsDir}. Package may be corrupted.`);
|
|
64
158
|
}
|
|
65
|
-
// Atomic copy:
|
|
159
|
+
// Atomic copy: rename-aside pattern to prevent data loss
|
|
66
160
|
const bmadDest = join(projectDir, "_bmad");
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
161
|
+
const bmadOld = join(projectDir, "_bmad.old");
|
|
162
|
+
const bmadNew = join(projectDir, "_bmad.new");
|
|
163
|
+
// Clean leftover from previous failed attempt
|
|
164
|
+
await rm(bmadOld, { recursive: true, force: true });
|
|
165
|
+
// Move original aside (tolerate ENOENT on first install)
|
|
70
166
|
try {
|
|
71
|
-
await
|
|
72
|
-
await rename(bmadTemp, bmadDest);
|
|
167
|
+
await rename(bmadDest, bmadOld);
|
|
73
168
|
}
|
|
74
169
|
catch (err) {
|
|
75
|
-
|
|
170
|
+
if (!isEnoent(err))
|
|
171
|
+
throw err;
|
|
172
|
+
debug("No existing _bmad to preserve (first install)");
|
|
173
|
+
}
|
|
174
|
+
// Stage new content
|
|
175
|
+
await rm(bmadNew, { recursive: true, force: true });
|
|
176
|
+
await cp(bmadDir, bmadNew, { recursive: true, dereference: false });
|
|
177
|
+
// Swap in
|
|
178
|
+
try {
|
|
179
|
+
await rename(bmadNew, bmadDest);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
// Restore original on failure
|
|
183
|
+
debug(`Rename failed, restoring original: ${formatError(err)}`);
|
|
76
184
|
try {
|
|
77
|
-
await rename(
|
|
185
|
+
await rename(bmadOld, bmadDest);
|
|
78
186
|
}
|
|
79
|
-
catch {
|
|
80
|
-
|
|
187
|
+
catch (restoreErr) {
|
|
188
|
+
if (!isEnoent(restoreErr)) {
|
|
189
|
+
debug(`Could not restore _bmad.old: ${formatError(restoreErr)}`);
|
|
190
|
+
}
|
|
81
191
|
}
|
|
82
192
|
throw err;
|
|
83
193
|
}
|
|
194
|
+
// Clean up backup
|
|
195
|
+
await rm(bmadOld, { recursive: true, force: true });
|
|
84
196
|
// Generate combined manifest from module-help.csv files
|
|
85
197
|
await generateManifests(projectDir);
|
|
86
|
-
// Generate _bmad/config.yaml
|
|
198
|
+
// Generate _bmad/config.yaml with platform-specific value
|
|
87
199
|
const projectName = await deriveProjectName(projectDir);
|
|
88
200
|
const escapedName = projectName.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
89
201
|
await atomicWriteFile(join(projectDir, "_bmad/config.yaml"), `# BMAD Configuration - Generated by bmalph
|
|
90
|
-
platform:
|
|
202
|
+
platform: ${p.id}
|
|
91
203
|
project_name: "${escapedName}"
|
|
92
204
|
output_folder: _bmad-output
|
|
93
205
|
user_name: BMad
|
|
@@ -102,19 +214,30 @@ modules:
|
|
|
102
214
|
`);
|
|
103
215
|
// Copy Ralph templates → .ralph/
|
|
104
216
|
await mkdir(join(projectDir, ".ralph"), { recursive: true });
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
217
|
+
// Preserve customized PROMPT.md and @AGENT.md on upgrade
|
|
218
|
+
const promptCustomized = await isTemplateCustomized(join(projectDir, ".ralph/PROMPT.md"), "PROMPT.md");
|
|
219
|
+
const agentCustomized = await isTemplateCustomized(join(projectDir, ".ralph/@AGENT.md"), "AGENT.md");
|
|
220
|
+
if (!promptCustomized) {
|
|
221
|
+
await cp(join(ralphDir, "templates/PROMPT.md"), join(projectDir, ".ralph/PROMPT.md"), {
|
|
222
|
+
dereference: false,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (!agentCustomized) {
|
|
226
|
+
await cp(join(ralphDir, "templates/AGENT.md"), join(projectDir, ".ralph/@AGENT.md"), {
|
|
227
|
+
dereference: false,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
111
230
|
await cp(join(ralphDir, "RALPH-REFERENCE.md"), join(projectDir, ".ralph/RALPH-REFERENCE.md"), {
|
|
112
231
|
dereference: false,
|
|
113
232
|
});
|
|
114
233
|
// Copy .ralphrc from template (skip if user has customized it)
|
|
115
234
|
const ralphrcDest = join(projectDir, ".ralph/.ralphrc");
|
|
116
235
|
if (!(await exists(ralphrcDest))) {
|
|
117
|
-
|
|
236
|
+
// Read template and inject platform driver
|
|
237
|
+
let ralphrcContent = await readFile(join(ralphDir, "templates/ralphrc.template"), "utf-8");
|
|
238
|
+
// Replace default PLATFORM_DRIVER value with the actual platform id
|
|
239
|
+
ralphrcContent = ralphrcContent.replace(/PLATFORM_DRIVER="\$\{PLATFORM_DRIVER:-[^"]*\}"/, `PLATFORM_DRIVER="\${PLATFORM_DRIVER:-${p.id}}"`);
|
|
240
|
+
await atomicWriteFile(ralphrcDest, ralphrcContent);
|
|
118
241
|
}
|
|
119
242
|
// Copy Ralph loop and lib → .ralph/
|
|
120
243
|
// Add version marker to ralph_loop.sh
|
|
@@ -140,51 +263,50 @@ modules:
|
|
|
140
263
|
dereference: false,
|
|
141
264
|
});
|
|
142
265
|
await chmod(join(projectDir, ".ralph/ralph_monitor.sh"), 0o755);
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
266
|
+
// Copy Ralph drivers → .ralph/drivers/
|
|
267
|
+
const driversDir = join(ralphDir, "drivers");
|
|
268
|
+
if (await exists(driversDir)) {
|
|
269
|
+
const destDriversDir = join(projectDir, ".ralph/drivers");
|
|
270
|
+
await rm(destDriversDir, { recursive: true, force: true });
|
|
271
|
+
await cp(driversDir, destDriversDir, { recursive: true, dereference: false });
|
|
272
|
+
// Make driver scripts executable
|
|
273
|
+
try {
|
|
274
|
+
const driverFiles = await readdir(destDriversDir);
|
|
275
|
+
for (const file of driverFiles) {
|
|
276
|
+
if (file.endsWith(".sh")) {
|
|
277
|
+
await chmod(join(destDriversDir, file), 0o755);
|
|
278
|
+
}
|
|
154
279
|
}
|
|
155
280
|
}
|
|
281
|
+
catch {
|
|
282
|
+
// Non-fatal if chmod fails
|
|
283
|
+
}
|
|
156
284
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
throw err;
|
|
160
|
-
}
|
|
161
|
-
for (const file of bundledCommandNames) {
|
|
162
|
-
await cp(join(slashCommandsDir, file), join(commandsDir, file), { dereference: false });
|
|
163
|
-
}
|
|
285
|
+
// Deliver slash commands based on platform strategy
|
|
286
|
+
const commandPaths = await deliverCommands(projectDir, p, slashCommandsDir);
|
|
164
287
|
// Update .gitignore
|
|
165
288
|
await updateGitignore(projectDir);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
};
|
|
289
|
+
const updatedPaths = [
|
|
290
|
+
"_bmad/",
|
|
291
|
+
".ralph/ralph_loop.sh",
|
|
292
|
+
".ralph/ralph_import.sh",
|
|
293
|
+
".ralph/ralph_monitor.sh",
|
|
294
|
+
".ralph/lib/",
|
|
295
|
+
...(!promptCustomized ? [".ralph/PROMPT.md"] : []),
|
|
296
|
+
...(!agentCustomized ? [".ralph/@AGENT.md"] : []),
|
|
297
|
+
".ralph/RALPH-REFERENCE.md",
|
|
298
|
+
...commandPaths,
|
|
299
|
+
".gitignore",
|
|
300
|
+
];
|
|
301
|
+
return { updatedPaths };
|
|
180
302
|
}
|
|
181
|
-
export async function installProject(projectDir) {
|
|
303
|
+
export async function installProject(projectDir, platform) {
|
|
182
304
|
// Create user directories (not overwritten by upgrade)
|
|
183
305
|
await mkdir(join(projectDir, STATE_DIR), { recursive: true });
|
|
184
306
|
await mkdir(join(projectDir, ".ralph/specs"), { recursive: true });
|
|
185
307
|
await mkdir(join(projectDir, ".ralph/logs"), { recursive: true });
|
|
186
308
|
await mkdir(join(projectDir, ".ralph/docs/generated"), { recursive: true });
|
|
187
|
-
await copyBundledAssets(projectDir);
|
|
309
|
+
await copyBundledAssets(projectDir, platform);
|
|
188
310
|
}
|
|
189
311
|
async function deriveProjectName(projectDir) {
|
|
190
312
|
try {
|
|
@@ -269,74 +391,54 @@ async function updateGitignore(projectDir) {
|
|
|
269
391
|
: newEntries.join("\n") + "\n";
|
|
270
392
|
await atomicWriteFile(gitignorePath, existing + suffix);
|
|
271
393
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
| 1. Analysis | Understand the problem | \`/create-brief\`, \`/brainstorm-project\`, \`/market-research\` |
|
|
284
|
-
| 2. Planning | Define the solution | \`/create-prd\`, \`/create-ux\` |
|
|
285
|
-
| 3. Solutioning | Design the architecture | \`/create-architecture\`, \`/create-epics-stories\`, \`/implementation-readiness\` |
|
|
286
|
-
| 4. Implementation | Build it | \`/sprint-planning\`, \`/create-story\`, then \`/bmalph-implement\` for Ralph |
|
|
287
|
-
|
|
288
|
-
### Workflow
|
|
289
|
-
|
|
290
|
-
1. Work through Phases 1-3 using BMAD agents and workflows (interactive, command-driven)
|
|
291
|
-
2. Run \`/bmalph-implement\` to transition planning artifacts into Ralph format, then start Ralph
|
|
292
|
-
|
|
293
|
-
### Management Commands
|
|
294
|
-
|
|
295
|
-
| Command | Description |
|
|
296
|
-
|---------|-------------|
|
|
297
|
-
| \`/bmalph-status\` | Show current phase, Ralph progress, version info |
|
|
298
|
-
| \`/bmalph-implement\` | Transition planning artifacts → prepare Ralph loop |
|
|
299
|
-
| \`/bmalph-upgrade\` | Update bundled assets to match current bmalph version |
|
|
300
|
-
| \`/bmalph-doctor\` | Check project health and report issues |
|
|
301
|
-
| \`/bmalph-reset\` | Reset state (soft or hard reset with confirmation) |
|
|
302
|
-
|
|
303
|
-
### Available Agents
|
|
304
|
-
|
|
305
|
-
| Command | Agent | Role |
|
|
306
|
-
|---------|-------|------|
|
|
307
|
-
| \`/analyst\` | Analyst | Research, briefs, discovery |
|
|
308
|
-
| \`/architect\` | Architect | Technical design, architecture |
|
|
309
|
-
| \`/pm\` | Product Manager | PRDs, epics, stories |
|
|
310
|
-
| \`/sm\` | Scrum Master | Sprint planning, status, coordination |
|
|
311
|
-
| \`/dev\` | Developer | Implementation, coding |
|
|
312
|
-
| \`/ux-designer\` | UX Designer | User experience, wireframes |
|
|
313
|
-
| \`/qa\` | QA Engineer | Test automation, quality assurance |
|
|
314
|
-
`;
|
|
394
|
+
/**
|
|
395
|
+
* Merge the BMAD instructions snippet into the platform's instructions file.
|
|
396
|
+
* Creates the file if it doesn't exist, replaces an existing BMAD section on upgrade.
|
|
397
|
+
*/
|
|
398
|
+
export async function mergeInstructionsFile(projectDir, platform) {
|
|
399
|
+
const p = platform ?? (await getDefaultPlatform());
|
|
400
|
+
const instructionsPath = join(projectDir, p.instructionsFile);
|
|
401
|
+
const snippet = p.generateInstructionsSnippet();
|
|
402
|
+
const marker = p.instructionsSectionMarker;
|
|
403
|
+
// Ensure parent directory exists for nested paths (e.g. .cursor/rules/)
|
|
404
|
+
await mkdir(dirname(instructionsPath), { recursive: true });
|
|
315
405
|
let existing = "";
|
|
316
406
|
try {
|
|
317
|
-
existing = await readFile(
|
|
407
|
+
existing = await readFile(instructionsPath, "utf-8");
|
|
318
408
|
}
|
|
319
409
|
catch (err) {
|
|
320
410
|
if (!isEnoent(err))
|
|
321
411
|
throw err;
|
|
322
412
|
}
|
|
323
|
-
if (existing.includes(
|
|
413
|
+
if (existing.includes(marker)) {
|
|
324
414
|
// Replace stale section with current content, preserving content after it
|
|
325
|
-
const sectionStart = existing.indexOf(
|
|
415
|
+
const sectionStart = existing.indexOf(marker);
|
|
326
416
|
const before = existing.slice(0, sectionStart);
|
|
327
417
|
const afterSection = existing.slice(sectionStart);
|
|
328
|
-
// Find the next level-2 heading after the
|
|
329
|
-
const
|
|
418
|
+
// Find the next level-2 heading after the section start
|
|
419
|
+
const markerEscaped = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
420
|
+
const nextHeadingMatch = afterSection.match(new RegExp(`\\n## (?!${markerEscaped.slice(3)})`));
|
|
330
421
|
const after = nextHeadingMatch ? afterSection.slice(nextHeadingMatch.index) : "";
|
|
331
|
-
await atomicWriteFile(
|
|
422
|
+
await atomicWriteFile(instructionsPath, before.trimEnd() + "\n" + snippet + after);
|
|
332
423
|
return;
|
|
333
424
|
}
|
|
334
|
-
await atomicWriteFile(
|
|
425
|
+
await atomicWriteFile(instructionsPath, existing + snippet);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* @deprecated Use `mergeInstructionsFile(projectDir)` instead.
|
|
429
|
+
* Kept for backward compatibility during migration.
|
|
430
|
+
*/
|
|
431
|
+
export async function mergeClaudeMd(projectDir) {
|
|
432
|
+
return mergeInstructionsFile(projectDir);
|
|
335
433
|
}
|
|
336
434
|
export async function isInitialized(projectDir) {
|
|
337
435
|
return exists(join(projectDir, CONFIG_FILE));
|
|
338
436
|
}
|
|
339
|
-
export async function
|
|
437
|
+
export async function hasExistingBmadDir(projectDir) {
|
|
438
|
+
return exists(join(projectDir, "_bmad"));
|
|
439
|
+
}
|
|
440
|
+
export async function previewInstall(projectDir, platform) {
|
|
441
|
+
const p = platform ?? (await getDefaultPlatform());
|
|
340
442
|
const wouldCreate = [];
|
|
341
443
|
const wouldModify = [];
|
|
342
444
|
const wouldSkip = [];
|
|
@@ -347,11 +449,15 @@ export async function previewInstall(projectDir) {
|
|
|
347
449
|
".ralph/logs/",
|
|
348
450
|
".ralph/docs/generated/",
|
|
349
451
|
"_bmad/",
|
|
350
|
-
".claude/commands/",
|
|
351
452
|
];
|
|
453
|
+
// Add command directory only for directory-based delivery
|
|
454
|
+
if (p.commandDelivery.kind === "directory") {
|
|
455
|
+
dirsToCreate.push(`${p.commandDelivery.dir}/`);
|
|
456
|
+
}
|
|
352
457
|
for (const dir of dirsToCreate) {
|
|
353
458
|
if (await exists(join(projectDir, dir))) {
|
|
354
|
-
if (dir === "_bmad/" ||
|
|
459
|
+
if (dir === "_bmad/" ||
|
|
460
|
+
(p.commandDelivery.kind === "directory" && dir === `${p.commandDelivery.dir}/`)) {
|
|
355
461
|
wouldModify.push(dir);
|
|
356
462
|
}
|
|
357
463
|
}
|
|
@@ -383,19 +489,19 @@ export async function previewInstall(projectDir) {
|
|
|
383
489
|
else {
|
|
384
490
|
wouldCreate.push(".gitignore");
|
|
385
491
|
}
|
|
386
|
-
//
|
|
492
|
+
// Instructions file integration check
|
|
387
493
|
try {
|
|
388
|
-
const
|
|
389
|
-
if (
|
|
390
|
-
wouldSkip.push(
|
|
494
|
+
const content = await readFile(join(projectDir, p.instructionsFile), "utf-8");
|
|
495
|
+
if (content.includes(p.instructionsSectionMarker)) {
|
|
496
|
+
wouldSkip.push(`${p.instructionsFile} (already integrated)`);
|
|
391
497
|
}
|
|
392
498
|
else {
|
|
393
|
-
wouldModify.push(
|
|
499
|
+
wouldModify.push(p.instructionsFile);
|
|
394
500
|
}
|
|
395
501
|
}
|
|
396
502
|
catch (err) {
|
|
397
503
|
if (isEnoent(err)) {
|
|
398
|
-
wouldCreate.push(
|
|
504
|
+
wouldCreate.push(p.instructionsFile);
|
|
399
505
|
}
|
|
400
506
|
else {
|
|
401
507
|
throw err;
|
|
@@ -403,28 +509,39 @@ export async function previewInstall(projectDir) {
|
|
|
403
509
|
}
|
|
404
510
|
return { wouldCreate, wouldModify, wouldSkip };
|
|
405
511
|
}
|
|
406
|
-
export async function previewUpgrade(projectDir) {
|
|
512
|
+
export async function previewUpgrade(projectDir, platform) {
|
|
513
|
+
const p = platform ?? (await getDefaultPlatform());
|
|
407
514
|
const managedPaths = [
|
|
408
515
|
{ path: "_bmad/", isDir: true },
|
|
409
516
|
{ path: ".ralph/ralph_loop.sh", isDir: false },
|
|
410
517
|
{ path: ".ralph/ralph_import.sh", isDir: false },
|
|
411
518
|
{ path: ".ralph/ralph_monitor.sh", isDir: false },
|
|
412
519
|
{ path: ".ralph/lib/", isDir: true },
|
|
413
|
-
{ path: ".ralph/PROMPT.md", isDir: false },
|
|
414
|
-
{ path: ".ralph/@AGENT.md", isDir: false },
|
|
520
|
+
{ path: ".ralph/PROMPT.md", isDir: false, templateName: "PROMPT.md" },
|
|
521
|
+
{ path: ".ralph/@AGENT.md", isDir: false, templateName: "AGENT.md" },
|
|
415
522
|
{ path: ".ralph/RALPH-REFERENCE.md", isDir: false },
|
|
416
|
-
{ path: ".claude/commands/", isDir: true },
|
|
417
523
|
{ path: ".gitignore", isDir: false },
|
|
418
524
|
];
|
|
525
|
+
// Add command directory only for directory-based delivery
|
|
526
|
+
if (p.commandDelivery.kind === "directory") {
|
|
527
|
+
managedPaths.push({ path: `${p.commandDelivery.dir}/`, isDir: true });
|
|
528
|
+
}
|
|
419
529
|
const wouldUpdate = [];
|
|
420
530
|
const wouldCreate = [];
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
531
|
+
const wouldPreserve = [];
|
|
532
|
+
for (const { path: pathStr, templateName } of managedPaths) {
|
|
533
|
+
const fullPath = join(projectDir, pathStr.replace(/\/$/, ""));
|
|
534
|
+
if (await exists(fullPath)) {
|
|
535
|
+
if (templateName && (await isTemplateCustomized(fullPath, templateName))) {
|
|
536
|
+
wouldPreserve.push(pathStr);
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
wouldUpdate.push(pathStr);
|
|
540
|
+
}
|
|
424
541
|
}
|
|
425
542
|
else {
|
|
426
|
-
wouldCreate.push(
|
|
543
|
+
wouldCreate.push(pathStr);
|
|
427
544
|
}
|
|
428
545
|
}
|
|
429
|
-
return { wouldUpdate, wouldCreate };
|
|
546
|
+
return { wouldUpdate, wouldCreate, wouldPreserve };
|
|
430
547
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { isEnoent, formatError } from "../utils/errors.js";
|
|
4
|
+
export const aiderPlatform = {
|
|
5
|
+
id: "aider",
|
|
6
|
+
displayName: "Aider",
|
|
7
|
+
tier: "instructions-only",
|
|
8
|
+
instructionsFile: "CONVENTIONS.md",
|
|
9
|
+
commandDelivery: { kind: "none" },
|
|
10
|
+
instructionsSectionMarker: "## BMAD-METHOD Integration",
|
|
11
|
+
generateInstructionsSnippet: () => `
|
|
12
|
+
## BMAD-METHOD Integration
|
|
13
|
+
|
|
14
|
+
Ask the BMAD master agent to navigate phases. Ask for help to discover all available agents and workflows.
|
|
15
|
+
|
|
16
|
+
### Phases
|
|
17
|
+
|
|
18
|
+
| Phase | Focus | Key Agents |
|
|
19
|
+
|-------|-------|-----------|
|
|
20
|
+
| 1. Analysis | Understand the problem | Analyst agent |
|
|
21
|
+
| 2. Planning | Define the solution | Product Manager agent |
|
|
22
|
+
| 3. Solutioning | Design the architecture | Architect agent |
|
|
23
|
+
|
|
24
|
+
### Workflow
|
|
25
|
+
|
|
26
|
+
Work through Phases 1-3 using BMAD agents and workflows interactively.
|
|
27
|
+
|
|
28
|
+
> **Note:** Ralph (Phase 4 — autonomous implementation) is not supported on this platform.
|
|
29
|
+
|
|
30
|
+
### Available Agents
|
|
31
|
+
|
|
32
|
+
| Agent | Role |
|
|
33
|
+
|-------|------|
|
|
34
|
+
| Analyst | Research, briefs, discovery |
|
|
35
|
+
| Architect | Technical design, architecture |
|
|
36
|
+
| Product Manager | PRDs, epics, stories |
|
|
37
|
+
| Scrum Master | Sprint planning, status, coordination |
|
|
38
|
+
| Developer | Implementation, coding |
|
|
39
|
+
| UX Designer | User experience, wireframes |
|
|
40
|
+
| QA Engineer | Test automation, quality assurance |
|
|
41
|
+
`,
|
|
42
|
+
getDoctorChecks: () => [
|
|
43
|
+
{
|
|
44
|
+
id: "instructions-file",
|
|
45
|
+
label: "CONVENTIONS.md contains BMAD snippet",
|
|
46
|
+
check: async (projectDir) => {
|
|
47
|
+
try {
|
|
48
|
+
const content = await readFile(join(projectDir, "CONVENTIONS.md"), "utf-8");
|
|
49
|
+
if (content.includes("BMAD-METHOD Integration")) {
|
|
50
|
+
return { passed: true };
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
passed: false,
|
|
54
|
+
detail: "missing BMAD-METHOD Integration section",
|
|
55
|
+
hint: "Run: bmalph init",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (isEnoent(err)) {
|
|
60
|
+
return {
|
|
61
|
+
passed: false,
|
|
62
|
+
detail: "CONVENTIONS.md not found",
|
|
63
|
+
hint: "Run: bmalph init",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return { passed: false, detail: formatError(err), hint: "Check file permissions" };
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
};
|