bmalph 2.7.4 → 2.7.6

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 (73) hide show
  1. package/README.md +87 -34
  2. package/dist/commands/doctor-checks.js +5 -4
  3. package/dist/commands/doctor-checks.js.map +1 -1
  4. package/dist/commands/doctor-runtime-checks.js +104 -86
  5. package/dist/commands/doctor-runtime-checks.js.map +1 -1
  6. package/dist/commands/run.js +4 -0
  7. package/dist/commands/run.js.map +1 -1
  8. package/dist/commands/status.js +12 -3
  9. package/dist/commands/status.js.map +1 -1
  10. package/dist/installer/bmad-assets.js +182 -0
  11. package/dist/installer/bmad-assets.js.map +1 -0
  12. package/dist/installer/commands.js +324 -0
  13. package/dist/installer/commands.js.map +1 -0
  14. package/dist/installer/install.js +42 -0
  15. package/dist/installer/install.js.map +1 -0
  16. package/dist/installer/metadata.js +56 -0
  17. package/dist/installer/metadata.js.map +1 -0
  18. package/dist/installer/project-files.js +169 -0
  19. package/dist/installer/project-files.js.map +1 -0
  20. package/dist/installer/ralph-assets.js +91 -0
  21. package/dist/installer/ralph-assets.js.map +1 -0
  22. package/dist/installer/template-files.js +168 -0
  23. package/dist/installer/template-files.js.map +1 -0
  24. package/dist/installer/types.js +2 -0
  25. package/dist/installer/types.js.map +1 -0
  26. package/dist/installer.js +5 -790
  27. package/dist/installer.js.map +1 -1
  28. package/dist/platform/cursor-runtime-checks.js +81 -0
  29. package/dist/platform/cursor-runtime-checks.js.map +1 -0
  30. package/dist/platform/cursor.js +4 -3
  31. package/dist/platform/cursor.js.map +1 -1
  32. package/dist/platform/detect.js +28 -5
  33. package/dist/platform/detect.js.map +1 -1
  34. package/dist/platform/instructions-snippet.js +18 -0
  35. package/dist/platform/instructions-snippet.js.map +1 -1
  36. package/dist/platform/resolve.js +23 -5
  37. package/dist/platform/resolve.js.map +1 -1
  38. package/dist/run/ralph-process.js +84 -15
  39. package/dist/run/ralph-process.js.map +1 -1
  40. package/dist/transition/artifact-loading.js +91 -0
  41. package/dist/transition/artifact-loading.js.map +1 -0
  42. package/dist/transition/artifact-scan.js +15 -3
  43. package/dist/transition/artifact-scan.js.map +1 -1
  44. package/dist/transition/context-output.js +85 -0
  45. package/dist/transition/context-output.js.map +1 -0
  46. package/dist/transition/fix-plan-sync.js +119 -0
  47. package/dist/transition/fix-plan-sync.js.map +1 -0
  48. package/dist/transition/orchestration.js +25 -362
  49. package/dist/transition/orchestration.js.map +1 -1
  50. package/dist/transition/specs-sync.js +78 -2
  51. package/dist/transition/specs-sync.js.map +1 -1
  52. package/dist/utils/ralph-runtime-state.js +222 -0
  53. package/dist/utils/ralph-runtime-state.js.map +1 -0
  54. package/dist/utils/state.js +17 -16
  55. package/dist/utils/state.js.map +1 -1
  56. package/dist/utils/validate.js +16 -0
  57. package/dist/utils/validate.js.map +1 -1
  58. package/dist/watch/renderer.js +48 -6
  59. package/dist/watch/renderer.js.map +1 -1
  60. package/dist/watch/state-reader.js +79 -44
  61. package/dist/watch/state-reader.js.map +1 -1
  62. package/package.json +1 -1
  63. package/ralph/RALPH-REFERENCE.md +60 -16
  64. package/ralph/drivers/claude-code.sh +25 -0
  65. package/ralph/drivers/codex.sh +11 -0
  66. package/ralph/drivers/copilot.sh +11 -0
  67. package/ralph/drivers/cursor.sh +58 -29
  68. package/ralph/lib/circuit_breaker.sh +3 -3
  69. package/ralph/lib/date_utils.sh +28 -9
  70. package/ralph/lib/response_analyzer.sh +220 -17
  71. package/ralph/ralph_loop.sh +464 -121
  72. package/ralph/templates/PROMPT.md +5 -0
  73. package/ralph/templates/ralphrc.template +14 -4
package/dist/installer.js CHANGED
@@ -1,791 +1,6 @@
1
- import { cp, mkdir, readFile, readdir, rm, chmod, rename } from "node:fs/promises";
2
- import { join, basename, dirname } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { debug, warn } from "./utils/logger.js";
5
- import { formatError, isEnoent } from "./utils/errors.js";
6
- import { exists, atomicWriteFile, parseGitignoreLines, replaceSection, } from "./utils/file-system.js";
7
- import { STATE_DIR, CONFIG_FILE, SKILLS_DIR, SKILLS_PREFIX } from "./utils/constants.js";
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = dirname(__filename);
10
- export async function getPackageVersion() {
11
- const pkgPath = join(__dirname, "..", "package.json");
12
- try {
13
- const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
14
- return pkg.version ?? "unknown";
15
- }
16
- catch (err) {
17
- if (!isEnoent(err)) {
18
- debug(`Failed to read package.json: ${formatError(err)}`);
19
- }
20
- return "unknown";
21
- }
22
- }
23
- export async function getBundledVersions() {
24
- const versionsPath = join(__dirname, "..", "bundled-versions.json");
25
- try {
26
- const versions = JSON.parse(await readFile(versionsPath, "utf-8"));
27
- if (!versions || typeof versions.bmadCommit !== "string") {
28
- throw new Error("Invalid bundled-versions.json structure: missing bmadCommit");
29
- }
30
- return {
31
- bmadCommit: versions.bmadCommit,
32
- };
33
- }
34
- catch (err) {
35
- if (err instanceof Error && err.message.includes("Invalid bundled-versions.json")) {
36
- throw err;
37
- }
38
- throw new Error(`Failed to read bundled-versions.json at ${versionsPath}`, { cause: err });
39
- }
40
- }
41
- export function getBundledBmadDir() {
42
- return join(__dirname, "..", "bmad");
43
- }
44
- export function getBundledRalphDir() {
45
- return join(__dirname, "..", "ralph");
46
- }
47
- export function getSlashCommandsDir() {
48
- return join(__dirname, "..", "slash-commands");
49
- }
50
- const TEMPLATE_PLACEHOLDERS = {
51
- "PROMPT.md": "[YOUR PROJECT NAME]",
52
- "AGENT.md": "pip install -r requirements.txt",
53
- };
54
- async function isTemplateCustomized(filePath, templateName) {
55
- const placeholder = TEMPLATE_PLACEHOLDERS[templateName];
56
- if (!placeholder)
57
- return false;
58
- try {
59
- const content = await readFile(filePath, "utf-8");
60
- return !content.includes(placeholder);
61
- }
62
- catch (err) {
63
- if (isEnoent(err))
64
- return false;
65
- throw err;
66
- }
67
- }
68
- /**
69
- * Lazily loads the default (claude-code) platform to avoid circular imports
70
- * and keep backward compatibility for callers that don't pass a platform.
71
- */
72
- async function getDefaultPlatform() {
73
- const { claudeCodePlatform } = await import("./platform/claude-code.js");
74
- return claudeCodePlatform;
75
- }
76
- /**
77
- * Deliver slash commands based on the platform's command delivery strategy.
78
- *
79
- * - "directory": Copy command files to a directory (e.g., .claude/commands/)
80
- * - "skills": No-op — commands are generated as skills by generateSkills()
81
- * - "index": No-op — commands are discoverable via _bmad/COMMANDS.md
82
- */
83
- async function deliverCommands(projectDir, platform, slashCommandsDir) {
84
- const delivery = platform.commandDelivery;
85
- if (delivery.kind !== "directory") {
86
- return [];
87
- }
88
- const slashFiles = await readdir(slashCommandsDir);
89
- const bundledCommandNames = new Set(slashFiles.filter((f) => f.endsWith(".md")));
90
- const commandsDir = join(projectDir, delivery.dir);
91
- await mkdir(commandsDir, { recursive: true });
92
- // Clean stale bmalph-owned commands before copying (preserve user-created commands)
93
- try {
94
- const existingCommands = await readdir(commandsDir);
95
- for (const file of existingCommands) {
96
- if (file.endsWith(".md") && bundledCommandNames.has(file)) {
97
- await rm(join(commandsDir, file), { force: true });
98
- }
99
- }
100
- }
101
- catch (err) {
102
- if (!isEnoent(err))
103
- throw err;
104
- }
105
- for (const file of bundledCommandNames) {
106
- await cp(join(slashCommandsDir, file), join(commandsDir, file), { dereference: false });
107
- }
108
- return [`${delivery.dir}/`];
109
- }
110
- /**
111
- * Parse a CSV row handling double-quoted fields.
112
- */
113
- function parseCsvRow(row) {
114
- const fields = [];
115
- let current = "";
116
- let inQuotes = false;
117
- for (let i = 0; i < row.length; i++) {
118
- const ch = row[i];
119
- if (inQuotes) {
120
- if (ch === '"') {
121
- if (i + 1 < row.length && row[i + 1] === '"') {
122
- current += '"';
123
- i++;
124
- }
125
- else {
126
- inQuotes = false;
127
- }
128
- }
129
- else {
130
- current += ch;
131
- }
132
- }
133
- else if (ch === '"') {
134
- inQuotes = true;
135
- }
136
- else if (ch === ",") {
137
- fields.push(current);
138
- current = "";
139
- }
140
- else {
141
- current += ch;
142
- }
143
- }
144
- fields.push(current);
145
- return fields;
146
- }
147
- const AGENT_DESCRIPTIONS = {
148
- analyst: "Research, briefs, discovery",
149
- architect: "Technical design, architecture",
150
- pm: "PRDs, epics, stories",
151
- sm: "Sprint planning, status, coordination",
152
- dev: "Implementation, coding",
153
- "ux-designer": "User experience, wireframes",
154
- qa: "Test automation, quality assurance",
155
- "tech-writer": "Documentation, technical writing",
156
- "quick-flow-solo-dev": "Quick one-off tasks, small changes",
157
- };
158
- const BMALPH_COMMANDS = {
159
- bmalph: {
160
- description: "BMAD master agent — navigate phases",
161
- howToRun: "Read and follow the master agent instructions in this file",
162
- },
163
- "bmalph-implement": {
164
- description: "Transition planning artifacts to Ralph format",
165
- howToRun: "Run `bmalph implement`",
166
- },
167
- "bmalph-status": {
168
- description: "Show current phase, Ralph progress, version info",
169
- howToRun: "Run `bmalph status`",
170
- },
171
- "bmalph-upgrade": {
172
- description: "Update bundled assets to current version",
173
- howToRun: "Run `bmalph upgrade`",
174
- },
175
- "bmalph-doctor": {
176
- description: "Check project health and report issues",
177
- howToRun: "Run `bmalph doctor`",
178
- },
179
- "bmalph-watch": {
180
- description: "Launch Ralph live dashboard",
181
- howToRun: "Run `bmalph run`",
182
- },
183
- };
184
- const PHASE_SECTIONS = [
185
- { key: "1-analysis", label: "Phase 1: Analysis" },
186
- { key: "2-planning", label: "Phase 2: Planning" },
187
- { key: "3-solutioning", label: "Phase 3: Solutioning" },
188
- { key: "4-implementation", label: "Phase 4: Implementation" },
189
- { key: "anytime", label: "Utilities" },
190
- ];
191
- // CSV column indices for bmad-help.csv
192
- const CSV_COL_PHASE = 1;
193
- const CSV_COL_NAME = 2;
194
- const CSV_COL_WORKFLOW_FILE = 5;
195
- const CSV_COL_DESCRIPTION = 10;
196
- const FALLBACK_PHASE = "anytime";
197
- /** CLI-pointer bmalph commands are all bmalph-* except the master "bmalph" command. */
198
- function isCliPointer(cmd) {
199
- return cmd.kind === "bmalph" && cmd.name !== "bmalph";
200
- }
201
- /**
202
- * Classify all slash commands by reading CSV metadata and file contents.
203
- * Shared by generateCommandIndex() and generateSkills().
204
- */
205
- export async function classifyCommands(projectDir, slashCmdsDir) {
206
- const helpCsvPath = join(projectDir, "_bmad/_config/bmad-help.csv");
207
- const helpCsv = await readFile(helpCsvPath, "utf-8");
208
- // Parse CSV: build workflow-file → {phase, description} lookup
209
- const csvLines = helpCsv.trimEnd().split(/\r?\n/);
210
- const workflowLookup = new Map();
211
- for (const line of csvLines.slice(1)) {
212
- if (!line.trim())
213
- continue;
214
- const fields = parseCsvRow(line);
215
- const workflowFile = fields[CSV_COL_WORKFLOW_FILE]?.trim();
216
- if (workflowFile) {
217
- workflowLookup.set(workflowFile, {
218
- phase: fields[CSV_COL_PHASE]?.trim() ?? FALLBACK_PHASE,
219
- description: fields[CSV_COL_DESCRIPTION]?.trim() ?? fields[CSV_COL_NAME]?.trim() ?? "",
220
- });
221
- }
222
- }
223
- // Read slash command files
224
- const slashFiles = (await readdir(slashCmdsDir)).filter((f) => f.endsWith(".md")).sort();
225
- const results = [];
226
- for (const file of slashFiles) {
227
- const name = file.replace(/\.md$/, "");
228
- const body = (await readFile(join(slashCmdsDir, file), "utf-8")).trim();
229
- const firstLine = body.split("\n")[0].trim();
230
- // Extract _bmad/ file references from content
231
- const fileRefs = [...body.matchAll(/`(_bmad\/[^`]+)`/g)].map((m) => m[1]);
232
- const agentRef = fileRefs.find((ref) => ref.includes("/agents/"));
233
- const workflowRef = fileRefs.find((ref) => ref.includes("/workflows/") || ref.includes("/tasks/"));
234
- // Classify: bmalph CLI commands
235
- if (name.startsWith("bmalph")) {
236
- const known = BMALPH_COMMANDS[name];
237
- const desc = known?.description ?? name.replace(/-/g, " ");
238
- const howToRun = known?.howToRun ?? `Run \`bmalph ${name.replace("bmalph-", "")}\``;
239
- results.push({
240
- name,
241
- description: desc,
242
- invocation: firstLine,
243
- body,
244
- kind: "bmalph",
245
- howToRun,
246
- });
247
- continue;
248
- }
249
- // Classify: workflow/task commands (matched via CSV)
250
- if (workflowRef && workflowLookup.has(workflowRef)) {
251
- const csv = workflowLookup.get(workflowRef);
252
- results.push({
253
- name,
254
- description: csv.description,
255
- invocation: firstLine,
256
- body,
257
- kind: "workflow",
258
- phase: csv.phase,
259
- });
260
- continue;
261
- }
262
- // Classify: pure agent commands
263
- if (agentRef && !workflowRef) {
264
- results.push({
265
- name,
266
- description: AGENT_DESCRIPTIONS[name] ?? name,
267
- invocation: firstLine,
268
- body,
269
- kind: "agent",
270
- });
271
- continue;
272
- }
273
- // Fallback: unmatched commands go to utilities
274
- results.push({
275
- name,
276
- description: name.replace(/-/g, " "),
277
- invocation: firstLine,
278
- body,
279
- kind: "utility",
280
- phase: FALLBACK_PHASE,
281
- });
282
- }
283
- return results;
284
- }
285
- /**
286
- * Generate _bmad/COMMANDS.md from pre-classified slash commands.
287
- * Provides command discoverability for platforms without native slash command support.
288
- */
289
- export async function generateCommandIndex(projectDir, classified) {
290
- const agents = [];
291
- const phaseGroups = {};
292
- const bmalpEntries = [];
293
- for (const cmd of classified) {
294
- if (cmd.kind === "bmalph") {
295
- bmalpEntries.push({
296
- name: cmd.name,
297
- description: cmd.description,
298
- howToRun: cmd.howToRun,
299
- });
300
- }
301
- else if (cmd.kind === "agent") {
302
- agents.push({ name: cmd.name, description: cmd.description, invocation: cmd.invocation });
303
- }
304
- else if (cmd.kind === "workflow") {
305
- const phase = cmd.phase;
306
- if (!phaseGroups[phase])
307
- phaseGroups[phase] = [];
308
- phaseGroups[phase].push({
309
- name: cmd.name,
310
- description: cmd.description,
311
- invocation: cmd.invocation,
312
- });
313
- }
314
- else {
315
- const phase = cmd.phase ?? FALLBACK_PHASE;
316
- if (!phaseGroups[phase])
317
- phaseGroups[phase] = [];
318
- phaseGroups[phase].push({
319
- name: cmd.name,
320
- description: cmd.description,
321
- invocation: cmd.invocation,
322
- });
323
- }
324
- }
325
- // Build markdown
326
- const sections = ["# BMAD Commands\n\n> Auto-generated by bmalph. Do not edit.\n"];
327
- if (agents.length > 0) {
328
- sections.push(formatCommandTable("Agents", agents));
329
- }
330
- for (const { key, label } of PHASE_SECTIONS) {
331
- const entries = phaseGroups[key];
332
- if (entries && entries.length > 0) {
333
- sections.push(formatCommandTable(label, entries));
334
- }
335
- }
336
- if (bmalpEntries.length > 0) {
337
- sections.push(formatCommandTable("bmalph CLI", bmalpEntries.map((b) => ({
338
- name: b.name,
339
- description: b.description,
340
- invocation: b.howToRun,
341
- })), "How to run"));
342
- }
343
- await atomicWriteFile(join(projectDir, "_bmad/COMMANDS.md"), sections.join("\n"));
344
- }
345
- /**
346
- * Generate Codex Skills from pre-classified slash commands.
347
- * Creates .agents/skills/bmad-<name>/SKILL.md for each non-CLI-pointer command.
348
- */
349
- export async function generateSkills(projectDir, classified) {
350
- const skillsBaseDir = join(projectDir, SKILLS_DIR);
351
- // Cleanup: remove existing bmad-* skill directories
352
- try {
353
- const existingDirs = await readdir(skillsBaseDir);
354
- for (const dir of existingDirs) {
355
- if (dir.startsWith(SKILLS_PREFIX)) {
356
- await rm(join(skillsBaseDir, dir), { recursive: true, force: true });
357
- }
358
- }
359
- }
360
- catch (err) {
361
- if (!isEnoent(err))
362
- throw err;
363
- }
364
- // Generate skills for non-CLI-pointer commands
365
- for (const cmd of classified) {
366
- if (isCliPointer(cmd))
367
- continue;
368
- const skillDir = join(skillsBaseDir, `${SKILLS_PREFIX}${cmd.name}`);
369
- await mkdir(skillDir, { recursive: true });
370
- const skillContent = `---
371
- name: ${cmd.name}
372
- description: >
373
- ${cmd.description}. Use when the user asks about ${cmd.name.replace(/-/g, " ")}.
374
- metadata:
375
- managed-by: bmalph
376
- ---
377
-
378
- ${cmd.body}
379
- `;
380
- await atomicWriteFile(join(skillDir, "SKILL.md"), skillContent);
381
- }
382
- }
383
- function formatCommandTable(heading, entries, thirdCol = "Invocation") {
384
- const lines = [
385
- `## ${heading}\n`,
386
- `| Command | Description | ${thirdCol} |`,
387
- "|---------|-------------|------------|",
388
- ];
389
- for (const e of entries) {
390
- lines.push(`| ${e.name} | ${e.description} | ${e.invocation} |`);
391
- }
392
- return lines.join("\n") + "\n";
393
- }
394
- export async function copyBundledAssets(projectDir, platform) {
395
- const p = platform ?? (await getDefaultPlatform());
396
- const bmadDir = getBundledBmadDir();
397
- const ralphDir = getBundledRalphDir();
398
- const slashCommandsDir = getSlashCommandsDir();
399
- // Validate source directories exist
400
- if (!(await exists(bmadDir))) {
401
- throw new Error(`BMAD source directory not found at ${bmadDir}. Package may be corrupted.`);
402
- }
403
- if (!(await exists(ralphDir))) {
404
- throw new Error(`Ralph source directory not found at ${ralphDir}. Package may be corrupted.`);
405
- }
406
- if (!(await exists(slashCommandsDir))) {
407
- throw new Error(`Slash commands directory not found at ${slashCommandsDir}. Package may be corrupted.`);
408
- }
409
- // Atomic copy: rename-aside pattern to prevent data loss
410
- const bmadDest = join(projectDir, "_bmad");
411
- const bmadOld = join(projectDir, "_bmad.old");
412
- const bmadNew = join(projectDir, "_bmad.new");
413
- // Clean leftover from previous failed attempt
414
- await rm(bmadOld, { recursive: true, force: true });
415
- // Move original aside (tolerate ENOENT on first install)
416
- try {
417
- await rename(bmadDest, bmadOld);
418
- }
419
- catch (err) {
420
- if (!isEnoent(err))
421
- throw err;
422
- debug("No existing _bmad to preserve (first install)");
423
- }
424
- // Stage new content
425
- await rm(bmadNew, { recursive: true, force: true });
426
- await cp(bmadDir, bmadNew, { recursive: true, dereference: false });
427
- // Swap in
428
- try {
429
- await rename(bmadNew, bmadDest);
430
- }
431
- catch (err) {
432
- // Restore original on failure
433
- debug(`Rename failed, restoring original: ${formatError(err)}`);
434
- try {
435
- await rename(bmadOld, bmadDest);
436
- }
437
- catch (restoreErr) {
438
- if (!isEnoent(restoreErr)) {
439
- debug(`Could not restore _bmad.old: ${formatError(restoreErr)}`);
440
- }
441
- }
442
- throw err;
443
- }
444
- // Clean up backup
445
- await rm(bmadOld, { recursive: true, force: true });
446
- // Generate combined manifest from module-help.csv files
447
- await generateManifests(projectDir);
448
- // Classify commands once, reuse for both COMMANDS.md and skills generation
449
- const classified = await classifyCommands(projectDir, slashCommandsDir);
450
- // Generate _bmad/COMMANDS.md for command discoverability
451
- await generateCommandIndex(projectDir, classified);
452
- // Generate Codex Skills for skills-based platforms
453
- if (p.commandDelivery.kind === "skills") {
454
- await generateSkills(projectDir, classified);
455
- }
456
- // Generate _bmad/config.yaml with platform-specific value
457
- const projectName = await deriveProjectName(projectDir);
458
- const escapedName = projectName.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
459
- await atomicWriteFile(join(projectDir, "_bmad/config.yaml"), `# BMAD Configuration - Generated by bmalph
460
- platform: ${p.id}
461
- project_name: "${escapedName}"
462
- output_folder: _bmad-output
463
- user_name: BMad
464
- communication_language: English
465
- document_output_language: English
466
- user_skill_level: intermediate
467
- planning_artifacts: _bmad-output/planning-artifacts
468
- implementation_artifacts: _bmad-output/implementation-artifacts
469
- project_knowledge: docs
470
- modules:
471
- - bmm
472
- `);
473
- // Copy Ralph templates → .ralph/
474
- await mkdir(join(projectDir, ".ralph"), { recursive: true });
475
- // Preserve customized PROMPT.md and @AGENT.md on upgrade
476
- const promptCustomized = await isTemplateCustomized(join(projectDir, ".ralph/PROMPT.md"), "PROMPT.md");
477
- const agentCustomized = await isTemplateCustomized(join(projectDir, ".ralph/@AGENT.md"), "AGENT.md");
478
- if (!promptCustomized) {
479
- await cp(join(ralphDir, "templates/PROMPT.md"), join(projectDir, ".ralph/PROMPT.md"), {
480
- dereference: false,
481
- });
482
- }
483
- if (!agentCustomized) {
484
- await cp(join(ralphDir, "templates/AGENT.md"), join(projectDir, ".ralph/@AGENT.md"), {
485
- dereference: false,
486
- });
487
- }
488
- await cp(join(ralphDir, "RALPH-REFERENCE.md"), join(projectDir, ".ralph/RALPH-REFERENCE.md"), {
489
- dereference: false,
490
- });
491
- // Copy .ralphrc from template (skip if user has customized it)
492
- const ralphrcDest = join(projectDir, ".ralph/.ralphrc");
493
- if (!(await exists(ralphrcDest))) {
494
- // Read template and inject platform driver
495
- let ralphrcContent = await readFile(join(ralphDir, "templates/ralphrc.template"), "utf-8");
496
- // Replace default PLATFORM_DRIVER value with the actual platform id
497
- ralphrcContent = ralphrcContent.replace(/PLATFORM_DRIVER="\$\{PLATFORM_DRIVER:-[^"]*\}"/, `PLATFORM_DRIVER="\${PLATFORM_DRIVER:-${p.id}}"`);
498
- await atomicWriteFile(ralphrcDest, ralphrcContent);
499
- }
500
- // Copy Ralph loop and lib → .ralph/
501
- // Add version marker to ralph_loop.sh
502
- const loopContent = await readFile(join(ralphDir, "ralph_loop.sh"), "utf-8");
503
- const markerLine = `# bmalph-version: ${await getPackageVersion()}`;
504
- // Use .* to handle empty version (edge case) and EOF without newline
505
- const markedContent = loopContent.includes("# bmalph-version:")
506
- ? loopContent.replace(/# bmalph-version:.*/, markerLine)
507
- : loopContent.replace(/^(#!.+\r?\n)/, `$1${markerLine}\n`);
508
- await atomicWriteFile(join(projectDir, ".ralph/ralph_loop.sh"), markedContent);
509
- await chmod(join(projectDir, ".ralph/ralph_loop.sh"), 0o755);
510
- await rm(join(projectDir, ".ralph/lib"), { recursive: true, force: true });
511
- await cp(join(ralphDir, "lib"), join(projectDir, ".ralph/lib"), {
512
- recursive: true,
513
- dereference: false,
514
- });
515
- // Copy Ralph utilities → .ralph/
516
- await cp(join(ralphDir, "ralph_import.sh"), join(projectDir, ".ralph/ralph_import.sh"), {
517
- dereference: false,
518
- });
519
- await chmod(join(projectDir, ".ralph/ralph_import.sh"), 0o755);
520
- await cp(join(ralphDir, "ralph_monitor.sh"), join(projectDir, ".ralph/ralph_monitor.sh"), {
521
- dereference: false,
522
- });
523
- await chmod(join(projectDir, ".ralph/ralph_monitor.sh"), 0o755);
524
- // Copy Ralph drivers → .ralph/drivers/
525
- const driversDir = join(ralphDir, "drivers");
526
- if (await exists(driversDir)) {
527
- const destDriversDir = join(projectDir, ".ralph/drivers");
528
- await rm(destDriversDir, { recursive: true, force: true });
529
- await cp(driversDir, destDriversDir, { recursive: true, dereference: false });
530
- // Make driver scripts executable
531
- try {
532
- const driverFiles = await readdir(destDriversDir);
533
- for (const file of driverFiles) {
534
- if (file.endsWith(".sh")) {
535
- await chmod(join(destDriversDir, file), 0o755);
536
- }
537
- }
538
- }
539
- catch (err) {
540
- debug(`chmod on driver scripts failed (non-fatal): ${formatError(err)}`);
541
- }
542
- }
543
- // Deliver slash commands based on platform strategy
544
- const commandPaths = await deliverCommands(projectDir, p, slashCommandsDir);
545
- // Update .gitignore
546
- await updateGitignore(projectDir);
547
- const updatedPaths = [
548
- "_bmad/",
549
- ".ralph/ralph_loop.sh",
550
- ".ralph/ralph_import.sh",
551
- ".ralph/ralph_monitor.sh",
552
- ".ralph/lib/",
553
- ...(!promptCustomized ? [".ralph/PROMPT.md"] : []),
554
- ...(!agentCustomized ? [".ralph/@AGENT.md"] : []),
555
- ".ralph/RALPH-REFERENCE.md",
556
- ...commandPaths,
557
- ".gitignore",
558
- ];
559
- return { updatedPaths };
560
- }
561
- export async function installProject(projectDir, platform) {
562
- // Create user directories (not overwritten by upgrade)
563
- await mkdir(join(projectDir, STATE_DIR), { recursive: true });
564
- await mkdir(join(projectDir, ".ralph/specs"), { recursive: true });
565
- await mkdir(join(projectDir, ".ralph/logs"), { recursive: true });
566
- await mkdir(join(projectDir, ".ralph/docs/generated"), { recursive: true });
567
- await copyBundledAssets(projectDir, platform);
568
- }
569
- async function deriveProjectName(projectDir) {
570
- try {
571
- const configPath = join(projectDir, CONFIG_FILE);
572
- const raw = await readFile(configPath, "utf-8");
573
- const config = JSON.parse(raw);
574
- if (config.name)
575
- return config.name;
576
- }
577
- catch (err) {
578
- if (!isEnoent(err)) {
579
- warn(`Could not read ${CONFIG_FILE}: ${formatError(err)}`);
580
- }
581
- }
582
- return basename(projectDir);
583
- }
584
- export async function generateManifests(projectDir) {
585
- const configDir = join(projectDir, "_bmad/_config");
586
- await mkdir(configDir, { recursive: true });
587
- const coreHelpPath = join(projectDir, "_bmad/core/module-help.csv");
588
- const bmmHelpPath = join(projectDir, "_bmad/bmm/module-help.csv");
589
- // Validate CSV files exist before reading
590
- if (!(await exists(coreHelpPath))) {
591
- throw new Error(`Core module-help.csv not found at ${coreHelpPath}. BMAD installation may be incomplete.`);
592
- }
593
- if (!(await exists(bmmHelpPath))) {
594
- throw new Error(`BMM module-help.csv not found at ${bmmHelpPath}. BMAD installation may be incomplete.`);
595
- }
596
- const coreContent = await readFile(coreHelpPath, "utf-8");
597
- const bmmContent = await readFile(bmmHelpPath, "utf-8");
598
- // Extract header from core (first line) and data lines from both
599
- const coreLines = coreContent.trimEnd().split(/\r?\n/);
600
- const bmmLines = bmmContent.trimEnd().split(/\r?\n/);
601
- if (!coreLines[0]?.trim()) {
602
- throw new Error(`Core module-help.csv is empty at ${coreHelpPath}`);
603
- }
604
- if (!bmmLines[0]?.trim()) {
605
- throw new Error(`BMM module-help.csv is empty at ${bmmHelpPath}`);
606
- }
607
- const normalize = (line) => line.replace(/,+$/, "");
608
- const header = normalize(coreLines[0]);
609
- const bmmHeader = normalize(bmmLines[0]);
610
- // Validate headers match (warn if mismatch but continue)
611
- if (header && bmmHeader && header !== bmmHeader) {
612
- warn(`CSV header mismatch detected. BMAD modules may have incompatible formats.`);
613
- debug(`CSV header mismatch details - core: "${header.slice(0, 50)}...", bmm: "${bmmHeader.slice(0, 50)}..."`);
614
- }
615
- const coreData = coreLines
616
- .slice(1)
617
- .filter((l) => l.trim())
618
- .map(normalize);
619
- const bmmData = bmmLines
620
- .slice(1)
621
- .filter((l) => l.trim())
622
- .map(normalize);
623
- const combined = [header, ...coreData, ...bmmData].join("\n") + "\n";
624
- await atomicWriteFile(join(configDir, "task-manifest.csv"), combined);
625
- await atomicWriteFile(join(configDir, "workflow-manifest.csv"), combined);
626
- await atomicWriteFile(join(configDir, "bmad-help.csv"), combined);
627
- }
628
- async function updateGitignore(projectDir) {
629
- const gitignorePath = join(projectDir, ".gitignore");
630
- let existing = "";
631
- try {
632
- existing = await readFile(gitignorePath, "utf-8");
633
- }
634
- catch (err) {
635
- if (!isEnoent(err))
636
- throw err;
637
- }
638
- const existingLines = parseGitignoreLines(existing);
639
- const entries = [".ralph/logs/", "_bmad-output/"];
640
- const newEntries = entries.filter((e) => !existingLines.has(e));
641
- if (newEntries.length === 0)
642
- return;
643
- const suffix = existing.length > 0 && !existing.endsWith("\n")
644
- ? "\n" + newEntries.join("\n") + "\n"
645
- : newEntries.join("\n") + "\n";
646
- await atomicWriteFile(gitignorePath, existing + suffix);
647
- }
648
- /**
649
- * Merge the BMAD instructions snippet into the platform's instructions file.
650
- * Creates the file if it doesn't exist, replaces an existing BMAD section on upgrade.
651
- */
652
- export async function mergeInstructionsFile(projectDir, platform) {
653
- const p = platform ?? (await getDefaultPlatform());
654
- const instructionsPath = join(projectDir, p.instructionsFile);
655
- const snippet = p.generateInstructionsSnippet();
656
- const marker = p.instructionsSectionMarker;
657
- // Ensure parent directory exists for nested paths (e.g. .cursor/rules/)
658
- await mkdir(dirname(instructionsPath), { recursive: true });
659
- let existing = "";
660
- try {
661
- existing = await readFile(instructionsPath, "utf-8");
662
- }
663
- catch (err) {
664
- if (!isEnoent(err))
665
- throw err;
666
- }
667
- if (existing.includes(marker)) {
668
- await atomicWriteFile(instructionsPath, replaceSection(existing, marker, "\n" + snippet));
669
- return;
670
- }
671
- await atomicWriteFile(instructionsPath, existing + snippet);
672
- }
673
- export async function isInitialized(projectDir) {
674
- return exists(join(projectDir, CONFIG_FILE));
675
- }
676
- export async function previewInstall(projectDir, platform) {
677
- const p = platform ?? (await getDefaultPlatform());
678
- const wouldCreate = [];
679
- const wouldModify = [];
680
- const wouldSkip = [];
681
- // Directories that would be created
682
- const dirsToCreate = [
683
- `${STATE_DIR}/`,
684
- ".ralph/specs/",
685
- ".ralph/logs/",
686
- ".ralph/docs/generated/",
687
- "_bmad/",
688
- ];
689
- // Add command directory based on delivery strategy
690
- if (p.commandDelivery.kind === "directory") {
691
- dirsToCreate.push(`${p.commandDelivery.dir}/`);
692
- }
693
- else if (p.commandDelivery.kind === "skills") {
694
- dirsToCreate.push(`${SKILLS_DIR}/`);
695
- }
696
- for (const dir of dirsToCreate) {
697
- if (await exists(join(projectDir, dir))) {
698
- if (dir === "_bmad/" ||
699
- (p.commandDelivery.kind === "directory" && dir === `${p.commandDelivery.dir}/`) ||
700
- (p.commandDelivery.kind === "skills" && dir === `${SKILLS_DIR}/`)) {
701
- wouldModify.push(dir);
702
- }
703
- }
704
- else {
705
- wouldCreate.push(dir);
706
- }
707
- }
708
- // Files that would be created/modified
709
- const filesToCheck = [
710
- { path: ".ralph/PROMPT.md", isTemplate: true },
711
- { path: ".ralph/@AGENT.md", isTemplate: true },
712
- { path: ".ralph/ralph_loop.sh", isTemplate: false },
713
- { path: CONFIG_FILE, isTemplate: false },
714
- ];
715
- for (const file of filesToCheck) {
716
- if (await exists(join(projectDir, file.path))) {
717
- if (file.isTemplate) {
718
- wouldModify.push(file.path);
719
- }
720
- }
721
- else {
722
- wouldCreate.push(file.path);
723
- }
724
- }
725
- // .gitignore would be modified if it exists, created otherwise
726
- if (await exists(join(projectDir, ".gitignore"))) {
727
- wouldModify.push(".gitignore");
728
- }
729
- else {
730
- wouldCreate.push(".gitignore");
731
- }
732
- // Instructions file integration check
733
- try {
734
- const content = await readFile(join(projectDir, p.instructionsFile), "utf-8");
735
- if (content.includes(p.instructionsSectionMarker)) {
736
- wouldSkip.push(`${p.instructionsFile} (already integrated)`);
737
- }
738
- else {
739
- wouldModify.push(p.instructionsFile);
740
- }
741
- }
742
- catch (err) {
743
- if (isEnoent(err)) {
744
- wouldCreate.push(p.instructionsFile);
745
- }
746
- else {
747
- throw err;
748
- }
749
- }
750
- return { wouldCreate, wouldModify, wouldSkip };
751
- }
752
- export async function previewUpgrade(projectDir, platform) {
753
- const p = platform ?? (await getDefaultPlatform());
754
- const managedPaths = [
755
- { path: "_bmad/", isDir: true },
756
- { path: ".ralph/ralph_loop.sh", isDir: false },
757
- { path: ".ralph/ralph_import.sh", isDir: false },
758
- { path: ".ralph/ralph_monitor.sh", isDir: false },
759
- { path: ".ralph/lib/", isDir: true },
760
- { path: ".ralph/PROMPT.md", isDir: false, templateName: "PROMPT.md" },
761
- { path: ".ralph/@AGENT.md", isDir: false, templateName: "AGENT.md" },
762
- { path: ".ralph/RALPH-REFERENCE.md", isDir: false },
763
- { path: ".gitignore", isDir: false },
764
- ];
765
- // Add command directory based on delivery strategy
766
- if (p.commandDelivery.kind === "directory") {
767
- managedPaths.push({ path: `${p.commandDelivery.dir}/`, isDir: true });
768
- }
769
- else if (p.commandDelivery.kind === "skills") {
770
- managedPaths.push({ path: `${SKILLS_DIR}/`, isDir: true });
771
- }
772
- const wouldUpdate = [];
773
- const wouldCreate = [];
774
- const wouldPreserve = [];
775
- for (const { path: pathStr, templateName } of managedPaths) {
776
- const fullPath = join(projectDir, pathStr.replace(/\/$/, ""));
777
- if (await exists(fullPath)) {
778
- if (templateName && (await isTemplateCustomized(fullPath, templateName))) {
779
- wouldPreserve.push(pathStr);
780
- }
781
- else {
782
- wouldUpdate.push(pathStr);
783
- }
784
- }
785
- else {
786
- wouldCreate.push(pathStr);
787
- }
788
- }
789
- return { wouldUpdate, wouldCreate, wouldPreserve };
790
- }
1
+ export { getPackageVersion, getBundledVersions, getBundledBmadDir, getBundledRalphDir, getSlashCommandsDir, } from "./installer/metadata.js";
2
+ export { classifyCommands, generateCommandIndex, generateSkills } from "./installer/commands.js";
3
+ export { generateManifests } from "./installer/bmad-assets.js";
4
+ export { mergeInstructionsFile, isInitialized, previewInstall, previewUpgrade, } from "./installer/project-files.js";
5
+ export { copyBundledAssets, installProject } from "./installer/install.js";
791
6
  //# sourceMappingURL=installer.js.map