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.
Files changed (53) hide show
  1. package/README.md +111 -30
  2. package/bundled-versions.json +1 -2
  3. package/dist/cli.js +3 -2
  4. package/dist/commands/check-updates.js +5 -27
  5. package/dist/commands/doctor.d.ts +14 -2
  6. package/dist/commands/doctor.js +99 -56
  7. package/dist/commands/init.d.ts +1 -0
  8. package/dist/commands/init.js +75 -9
  9. package/dist/commands/upgrade.js +8 -5
  10. package/dist/installer.d.ts +16 -5
  11. package/dist/installer.js +245 -128
  12. package/dist/platform/aider.d.ts +2 -0
  13. package/dist/platform/aider.js +71 -0
  14. package/dist/platform/claude-code.d.ts +2 -0
  15. package/dist/platform/claude-code.js +88 -0
  16. package/dist/platform/codex.d.ts +2 -0
  17. package/dist/platform/codex.js +67 -0
  18. package/dist/platform/copilot.d.ts +2 -0
  19. package/dist/platform/copilot.js +71 -0
  20. package/dist/platform/cursor.d.ts +2 -0
  21. package/dist/platform/cursor.js +71 -0
  22. package/dist/platform/detect.d.ts +7 -0
  23. package/dist/platform/detect.js +23 -0
  24. package/dist/platform/index.d.ts +4 -0
  25. package/dist/platform/index.js +3 -0
  26. package/dist/platform/registry.d.ts +4 -0
  27. package/dist/platform/registry.js +27 -0
  28. package/dist/platform/resolve.d.ts +8 -0
  29. package/dist/platform/resolve.js +24 -0
  30. package/dist/platform/types.d.ts +41 -0
  31. package/dist/platform/types.js +7 -0
  32. package/dist/platform/windsurf.d.ts +2 -0
  33. package/dist/platform/windsurf.js +71 -0
  34. package/dist/transition/artifacts.js +1 -1
  35. package/dist/transition/fix-plan.d.ts +1 -1
  36. package/dist/transition/fix-plan.js +3 -2
  37. package/dist/transition/orchestration.js +1 -1
  38. package/dist/transition/specs-changelog.js +4 -1
  39. package/dist/transition/specs-index.js +2 -3
  40. package/dist/utils/config.d.ts +2 -1
  41. package/dist/utils/errors.js +3 -0
  42. package/dist/utils/github.d.ts +0 -1
  43. package/dist/utils/github.js +1 -18
  44. package/dist/utils/json.js +2 -2
  45. package/dist/utils/state.js +7 -1
  46. package/dist/utils/validate.d.ts +1 -0
  47. package/dist/utils/validate.js +35 -4
  48. package/package.json +4 -4
  49. package/ralph/drivers/claude-code.sh +118 -0
  50. package/ralph/drivers/codex.sh +81 -0
  51. package/ralph/ralph_import.sh +11 -0
  52. package/ralph/ralph_loop.sh +37 -64
  53. 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
- typeof versions.bmadCommit !== "string" ||
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}: ${formatError(err)}`);
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
- export async function copyBundledAssets(projectDir) {
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: write to temp dir, then swap with old
159
+ // Atomic copy: rename-aside pattern to prevent data loss
66
160
  const bmadDest = join(projectDir, "_bmad");
67
- const bmadTemp = join(projectDir, "_bmad.new");
68
- await rm(bmadTemp, { recursive: true, force: true });
69
- await cp(bmadDir, bmadTemp, { recursive: true, dereference: false });
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 rm(bmadDest, { recursive: true, force: true });
72
- await rename(bmadTemp, bmadDest);
167
+ await rename(bmadDest, bmadOld);
73
168
  }
74
169
  catch (err) {
75
- // If rename fails, attempt to restore from temp
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(bmadTemp, bmadDest);
185
+ await rename(bmadOld, bmadDest);
78
186
  }
79
- catch {
80
- // Best effort restore failed
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: claude-code
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
- await cp(join(ralphDir, "templates/PROMPT.md"), join(projectDir, ".ralph/PROMPT.md"), {
106
- dereference: false,
107
- });
108
- await cp(join(ralphDir, "templates/AGENT.md"), join(projectDir, ".ralph/@AGENT.md"), {
109
- dereference: false,
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
- await cp(join(ralphDir, "templates/ralphrc.template"), ralphrcDest, { dereference: false });
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
- // Install all slash commands → .claude/commands/
144
- // Clean stale bmalph-owned commands before copying (preserve user-created commands)
145
- const commandsDir = join(projectDir, ".claude/commands");
146
- await mkdir(commandsDir, { recursive: true });
147
- const slashFiles = await readdir(slashCommandsDir);
148
- const bundledCommandNames = new Set(slashFiles.filter((f) => f.endsWith(".md")));
149
- try {
150
- const existingCommands = await readdir(commandsDir);
151
- for (const file of existingCommands) {
152
- if (file.endsWith(".md") && bundledCommandNames.has(file)) {
153
- await rm(join(commandsDir, file), { force: true });
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
- catch (err) {
158
- if (!isEnoent(err))
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
- return {
167
- updatedPaths: [
168
- "_bmad/",
169
- ".ralph/ralph_loop.sh",
170
- ".ralph/ralph_import.sh",
171
- ".ralph/ralph_monitor.sh",
172
- ".ralph/lib/",
173
- ".ralph/PROMPT.md",
174
- ".ralph/@AGENT.md",
175
- ".ralph/RALPH-REFERENCE.md",
176
- ".claude/commands/",
177
- ".gitignore",
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
- export async function mergeClaudeMd(projectDir) {
273
- const claudeMdPath = join(projectDir, "CLAUDE.md");
274
- const snippet = `
275
- ## BMAD-METHOD Integration
276
-
277
- Use \`/bmalph\` to navigate phases. Use \`/bmad-help\` to discover all commands. Use \`/bmalph-status\` for a quick overview.
278
-
279
- ### Phases
280
-
281
- | Phase | Focus | Key Commands |
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(claudeMdPath, "utf-8");
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("## BMAD-METHOD Integration")) {
413
+ if (existing.includes(marker)) {
324
414
  // Replace stale section with current content, preserving content after it
325
- const sectionStart = existing.indexOf("## BMAD-METHOD Integration");
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 BMAD section start
329
- const nextHeadingMatch = afterSection.match(/\n## (?!BMAD-METHOD Integration)/);
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(claudeMdPath, before.trimEnd() + "\n" + snippet + after);
422
+ await atomicWriteFile(instructionsPath, before.trimEnd() + "\n" + snippet + after);
332
423
  return;
333
424
  }
334
- await atomicWriteFile(claudeMdPath, existing + snippet);
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 previewInstall(projectDir) {
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/" || dir === ".claude/commands/") {
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
- // CLAUDE.md integration check
492
+ // Instructions file integration check
387
493
  try {
388
- const claudeMd = await readFile(join(projectDir, "CLAUDE.md"), "utf-8");
389
- if (claudeMd.includes("## BMAD-METHOD Integration")) {
390
- wouldSkip.push("CLAUDE.md (already integrated)");
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("CLAUDE.md");
499
+ wouldModify.push(p.instructionsFile);
394
500
  }
395
501
  }
396
502
  catch (err) {
397
503
  if (isEnoent(err)) {
398
- wouldCreate.push("CLAUDE.md");
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
- for (const { path: p } of managedPaths) {
422
- if (await exists(join(projectDir, p.replace(/\/$/, "")))) {
423
- wouldUpdate.push(p);
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(p);
543
+ wouldCreate.push(pathStr);
427
544
  }
428
545
  }
429
- return { wouldUpdate, wouldCreate };
546
+ return { wouldUpdate, wouldCreate, wouldPreserve };
430
547
  }
@@ -0,0 +1,2 @@
1
+ import type { Platform } from "./types.js";
2
+ export declare const aiderPlatform: Platform;
@@ -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
+ };
@@ -0,0 +1,2 @@
1
+ import type { Platform } from "./types.js";
2
+ export declare const claudeCodePlatform: Platform;