bmalph 2.7.5 → 2.7.7
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 +20 -5
- package/dist/commands/doctor-runtime-checks.js +104 -86
- package/dist/commands/doctor-runtime-checks.js.map +1 -1
- package/dist/commands/run.js +11 -2
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/watch.js +5 -0
- package/dist/commands/watch.js.map +1 -1
- package/dist/installer/bmad-assets.js +182 -0
- package/dist/installer/bmad-assets.js.map +1 -0
- package/dist/installer/commands.js +324 -0
- package/dist/installer/commands.js.map +1 -0
- package/dist/installer/install.js +42 -0
- package/dist/installer/install.js.map +1 -0
- package/dist/installer/metadata.js +56 -0
- package/dist/installer/metadata.js.map +1 -0
- package/dist/installer/project-files.js +169 -0
- package/dist/installer/project-files.js.map +1 -0
- package/dist/installer/ralph-assets.js +91 -0
- package/dist/installer/ralph-assets.js.map +1 -0
- package/dist/installer/template-files.js +187 -0
- package/dist/installer/template-files.js.map +1 -0
- package/dist/installer/types.js +2 -0
- package/dist/installer/types.js.map +1 -0
- package/dist/installer.js +5 -843
- package/dist/installer.js.map +1 -1
- package/dist/run/run-dashboard.js +20 -6
- package/dist/run/run-dashboard.js.map +1 -1
- package/dist/transition/artifact-loading.js +91 -0
- package/dist/transition/artifact-loading.js.map +1 -0
- package/dist/transition/context-output.js +85 -0
- package/dist/transition/context-output.js.map +1 -0
- package/dist/transition/context.js +11 -3
- package/dist/transition/context.js.map +1 -1
- package/dist/transition/fix-plan-sync.js +119 -0
- package/dist/transition/fix-plan-sync.js.map +1 -0
- package/dist/transition/orchestration.js +25 -362
- package/dist/transition/orchestration.js.map +1 -1
- package/dist/transition/specs-sync.js +78 -2
- package/dist/transition/specs-sync.js.map +1 -1
- package/dist/utils/ralph-runtime-state.js +222 -0
- package/dist/utils/ralph-runtime-state.js.map +1 -0
- package/dist/utils/state.js +17 -16
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/validate.js +16 -0
- package/dist/utils/validate.js.map +1 -1
- package/dist/watch/dashboard.js +25 -21
- package/dist/watch/dashboard.js.map +1 -1
- package/dist/watch/frame-writer.js +83 -0
- package/dist/watch/frame-writer.js.map +1 -0
- package/dist/watch/renderer.js +214 -49
- package/dist/watch/renderer.js.map +1 -1
- package/dist/watch/state-reader.js +87 -44
- package/dist/watch/state-reader.js.map +1 -1
- package/package.json +1 -1
- package/ralph/RALPH-REFERENCE.md +34 -14
- package/ralph/drivers/claude-code.sh +27 -0
- package/ralph/drivers/codex.sh +11 -0
- package/ralph/drivers/copilot.sh +11 -0
- package/ralph/drivers/cursor.sh +11 -0
- package/ralph/lib/circuit_breaker.sh +3 -3
- package/ralph/lib/date_utils.sh +28 -9
- package/ralph/lib/enable_core.sh +10 -2
- package/ralph/lib/response_analyzer.sh +252 -40
- package/ralph/ralph_import.sh +9 -1
- package/ralph/ralph_loop.sh +548 -128
- package/ralph/templates/PROMPT.md +20 -5
- package/ralph/templates/ralphrc.template +14 -4
package/dist/installer.js
CHANGED
|
@@ -1,844 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
async function prepareBmadSwap(projectDir, bundledBmadDir) {
|
|
395
|
-
const dest = join(projectDir, "_bmad");
|
|
396
|
-
const backup = join(projectDir, "_bmad.old");
|
|
397
|
-
const staged = join(projectDir, "_bmad.new");
|
|
398
|
-
const destExists = await exists(dest);
|
|
399
|
-
const backupExists = await exists(backup);
|
|
400
|
-
let hasBackup = false;
|
|
401
|
-
if (destExists && backupExists) {
|
|
402
|
-
throw new Error("Found both _bmad and _bmad.old from a previous failed install or upgrade. " +
|
|
403
|
-
"Restore or remove one of them before retrying.");
|
|
404
|
-
}
|
|
405
|
-
if (backupExists) {
|
|
406
|
-
hasBackup = true;
|
|
407
|
-
debug("Found existing _bmad.old from previous failed rollback, preserving backup");
|
|
408
|
-
}
|
|
409
|
-
else if (destExists) {
|
|
410
|
-
try {
|
|
411
|
-
await rename(dest, backup);
|
|
412
|
-
hasBackup = true;
|
|
413
|
-
}
|
|
414
|
-
catch (err) {
|
|
415
|
-
if (!isEnoent(err))
|
|
416
|
-
throw err;
|
|
417
|
-
debug("_bmad disappeared before it could be preserved, continuing without backup");
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
else {
|
|
421
|
-
debug("No existing _bmad to preserve (first install)");
|
|
422
|
-
}
|
|
423
|
-
// Stage new content.
|
|
424
|
-
await rm(staged, { recursive: true, force: true });
|
|
425
|
-
await cp(bundledBmadDir, staged, { recursive: true, dereference: false });
|
|
426
|
-
return { dest, backup, staged, hasBackup };
|
|
427
|
-
}
|
|
428
|
-
async function finalizeBmadInstall(projectDir, slashCommandsDir, platform) {
|
|
429
|
-
await generateManifests(projectDir);
|
|
430
|
-
const classified = await classifyCommands(projectDir, slashCommandsDir);
|
|
431
|
-
await generateCommandIndex(projectDir, classified);
|
|
432
|
-
const projectName = await deriveProjectName(projectDir);
|
|
433
|
-
const escapedName = projectName.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
434
|
-
await atomicWriteFile(join(projectDir, "_bmad/config.yaml"), `# BMAD Configuration - Generated by bmalph
|
|
435
|
-
platform: ${platform.id}
|
|
436
|
-
project_name: "${escapedName}"
|
|
437
|
-
output_folder: _bmad-output
|
|
438
|
-
user_name: BMad
|
|
439
|
-
communication_language: English
|
|
440
|
-
document_output_language: English
|
|
441
|
-
user_skill_level: intermediate
|
|
442
|
-
planning_artifacts: _bmad-output/planning-artifacts
|
|
443
|
-
implementation_artifacts: _bmad-output/implementation-artifacts
|
|
444
|
-
project_knowledge: docs
|
|
445
|
-
modules:
|
|
446
|
-
- bmm
|
|
447
|
-
`);
|
|
448
|
-
return classified;
|
|
449
|
-
}
|
|
450
|
-
async function rollbackBmadFinalization(swap, error) {
|
|
451
|
-
debug(`BMAD finalization failed after swap: ${formatError(error)}`);
|
|
452
|
-
try {
|
|
453
|
-
await rm(swap.dest, { recursive: true, force: true });
|
|
454
|
-
if (swap.hasBackup) {
|
|
455
|
-
await rename(swap.backup, swap.dest);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
catch (rollbackErr) {
|
|
459
|
-
throw new Error("BMAD finalization failed after swap and rollback also failed. " +
|
|
460
|
-
`Original error: ${formatError(error)}. ` +
|
|
461
|
-
`Rollback error: ${formatError(rollbackErr)}`, {
|
|
462
|
-
cause: rollbackErr,
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
if (swap.hasBackup) {
|
|
466
|
-
throw new Error("BMAD finalization failed after swap; previous BMAD installation was restored.", {
|
|
467
|
-
cause: error,
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
throw new Error("BMAD finalization failed after swap; incomplete BMAD installation was cleaned up.", {
|
|
471
|
-
cause: error,
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
|
-
async function commitBmadSwap(swap) {
|
|
475
|
-
if (!swap.hasBackup)
|
|
476
|
-
return;
|
|
477
|
-
await rm(swap.backup, { recursive: true, force: true });
|
|
478
|
-
}
|
|
479
|
-
export async function copyBundledAssets(projectDir, platform) {
|
|
480
|
-
const p = platform ?? (await getDefaultPlatform());
|
|
481
|
-
const bmadDir = getBundledBmadDir();
|
|
482
|
-
const ralphDir = getBundledRalphDir();
|
|
483
|
-
const slashCommandsDir = getSlashCommandsDir();
|
|
484
|
-
// Validate source directories exist
|
|
485
|
-
if (!(await exists(bmadDir))) {
|
|
486
|
-
throw new Error(`BMAD source directory not found at ${bmadDir}. Package may be corrupted.`);
|
|
487
|
-
}
|
|
488
|
-
if (!(await exists(ralphDir))) {
|
|
489
|
-
throw new Error(`Ralph source directory not found at ${ralphDir}. Package may be corrupted.`);
|
|
490
|
-
}
|
|
491
|
-
if (!(await exists(slashCommandsDir))) {
|
|
492
|
-
throw new Error(`Slash commands directory not found at ${slashCommandsDir}. Package may be corrupted.`);
|
|
493
|
-
}
|
|
494
|
-
// Atomic copy: rename-aside pattern to prevent data loss.
|
|
495
|
-
const bmadSwap = await prepareBmadSwap(projectDir, bmadDir);
|
|
496
|
-
// Swap in.
|
|
497
|
-
try {
|
|
498
|
-
await rename(bmadSwap.staged, bmadSwap.dest);
|
|
499
|
-
}
|
|
500
|
-
catch (err) {
|
|
501
|
-
// Restore original on failure.
|
|
502
|
-
debug(`Rename failed, restoring original: ${formatError(err)}`);
|
|
503
|
-
try {
|
|
504
|
-
await rename(bmadSwap.backup, bmadSwap.dest);
|
|
505
|
-
}
|
|
506
|
-
catch (restoreErr) {
|
|
507
|
-
if (!isEnoent(restoreErr)) {
|
|
508
|
-
debug(`Could not restore _bmad.old: ${formatError(restoreErr)}`);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
throw err;
|
|
512
|
-
}
|
|
513
|
-
const classified = await (async () => {
|
|
514
|
-
try {
|
|
515
|
-
return await finalizeBmadInstall(projectDir, slashCommandsDir, p);
|
|
516
|
-
}
|
|
517
|
-
catch (err) {
|
|
518
|
-
return await rollbackBmadFinalization(bmadSwap, err);
|
|
519
|
-
}
|
|
520
|
-
})();
|
|
521
|
-
await commitBmadSwap(bmadSwap);
|
|
522
|
-
// Generate Codex Skills for skills-based platforms.
|
|
523
|
-
if (p.commandDelivery.kind === "skills") {
|
|
524
|
-
await generateSkills(projectDir, classified);
|
|
525
|
-
}
|
|
526
|
-
// Copy Ralph templates → .ralph/
|
|
527
|
-
await mkdir(join(projectDir, ".ralph"), { recursive: true });
|
|
528
|
-
// Preserve customized PROMPT.md and @AGENT.md on upgrade
|
|
529
|
-
const promptCustomized = await isTemplateCustomized(join(projectDir, ".ralph/PROMPT.md"), "PROMPT.md");
|
|
530
|
-
const agentCustomized = await isTemplateCustomized(join(projectDir, ".ralph/@AGENT.md"), "AGENT.md");
|
|
531
|
-
if (!promptCustomized) {
|
|
532
|
-
await cp(join(ralphDir, "templates/PROMPT.md"), join(projectDir, ".ralph/PROMPT.md"), {
|
|
533
|
-
dereference: false,
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
if (!agentCustomized) {
|
|
537
|
-
await cp(join(ralphDir, "templates/AGENT.md"), join(projectDir, ".ralph/@AGENT.md"), {
|
|
538
|
-
dereference: false,
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
await cp(join(ralphDir, "RALPH-REFERENCE.md"), join(projectDir, ".ralph/RALPH-REFERENCE.md"), {
|
|
542
|
-
dereference: false,
|
|
543
|
-
});
|
|
544
|
-
// Copy .ralphrc from template (skip if user has customized it)
|
|
545
|
-
const ralphrcDest = join(projectDir, ".ralph/.ralphrc");
|
|
546
|
-
if (!(await exists(ralphrcDest))) {
|
|
547
|
-
// Read template and inject platform driver
|
|
548
|
-
let ralphrcContent = await readFile(join(ralphDir, "templates/ralphrc.template"), "utf-8");
|
|
549
|
-
// Replace default PLATFORM_DRIVER value with the actual platform id
|
|
550
|
-
ralphrcContent = ralphrcContent.replace(/PLATFORM_DRIVER="\$\{PLATFORM_DRIVER:-[^"]*\}"/, `PLATFORM_DRIVER="\${PLATFORM_DRIVER:-${p.id}}"`);
|
|
551
|
-
await atomicWriteFile(ralphrcDest, ralphrcContent);
|
|
552
|
-
}
|
|
553
|
-
// Copy Ralph loop and lib → .ralph/
|
|
554
|
-
// Add version marker to ralph_loop.sh
|
|
555
|
-
const loopContent = await readFile(join(ralphDir, "ralph_loop.sh"), "utf-8");
|
|
556
|
-
const markerLine = `# bmalph-version: ${await getPackageVersion()}`;
|
|
557
|
-
// Use .* to handle empty version (edge case) and EOF without newline
|
|
558
|
-
const markedContent = loopContent.includes("# bmalph-version:")
|
|
559
|
-
? loopContent.replace(/# bmalph-version:.*/, markerLine)
|
|
560
|
-
: loopContent.replace(/^(#!.+\r?\n)/, `$1${markerLine}\n`);
|
|
561
|
-
await atomicWriteFile(join(projectDir, ".ralph/ralph_loop.sh"), markedContent);
|
|
562
|
-
await chmod(join(projectDir, ".ralph/ralph_loop.sh"), 0o755);
|
|
563
|
-
await rm(join(projectDir, ".ralph/lib"), { recursive: true, force: true });
|
|
564
|
-
await cp(join(ralphDir, "lib"), join(projectDir, ".ralph/lib"), {
|
|
565
|
-
recursive: true,
|
|
566
|
-
dereference: false,
|
|
567
|
-
});
|
|
568
|
-
// Copy Ralph utilities → .ralph/
|
|
569
|
-
await cp(join(ralphDir, "ralph_import.sh"), join(projectDir, ".ralph/ralph_import.sh"), {
|
|
570
|
-
dereference: false,
|
|
571
|
-
});
|
|
572
|
-
await chmod(join(projectDir, ".ralph/ralph_import.sh"), 0o755);
|
|
573
|
-
await cp(join(ralphDir, "ralph_monitor.sh"), join(projectDir, ".ralph/ralph_monitor.sh"), {
|
|
574
|
-
dereference: false,
|
|
575
|
-
});
|
|
576
|
-
await chmod(join(projectDir, ".ralph/ralph_monitor.sh"), 0o755);
|
|
577
|
-
// Copy Ralph drivers → .ralph/drivers/
|
|
578
|
-
const driversDir = join(ralphDir, "drivers");
|
|
579
|
-
if (await exists(driversDir)) {
|
|
580
|
-
const destDriversDir = join(projectDir, ".ralph/drivers");
|
|
581
|
-
await rm(destDriversDir, { recursive: true, force: true });
|
|
582
|
-
await cp(driversDir, destDriversDir, { recursive: true, dereference: false });
|
|
583
|
-
// Make driver scripts executable
|
|
584
|
-
try {
|
|
585
|
-
const driverFiles = await readdir(destDriversDir);
|
|
586
|
-
for (const file of driverFiles) {
|
|
587
|
-
if (file.endsWith(".sh")) {
|
|
588
|
-
await chmod(join(destDriversDir, file), 0o755);
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
catch (err) {
|
|
593
|
-
debug(`chmod on driver scripts failed (non-fatal): ${formatError(err)}`);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
// Deliver slash commands based on platform strategy
|
|
597
|
-
const commandPaths = await deliverCommands(projectDir, p, slashCommandsDir);
|
|
598
|
-
// Update .gitignore
|
|
599
|
-
await updateGitignore(projectDir);
|
|
600
|
-
const updatedPaths = [
|
|
601
|
-
"_bmad/",
|
|
602
|
-
".ralph/ralph_loop.sh",
|
|
603
|
-
".ralph/ralph_import.sh",
|
|
604
|
-
".ralph/ralph_monitor.sh",
|
|
605
|
-
".ralph/lib/",
|
|
606
|
-
...(!promptCustomized ? [".ralph/PROMPT.md"] : []),
|
|
607
|
-
...(!agentCustomized ? [".ralph/@AGENT.md"] : []),
|
|
608
|
-
".ralph/RALPH-REFERENCE.md",
|
|
609
|
-
...commandPaths,
|
|
610
|
-
".gitignore",
|
|
611
|
-
];
|
|
612
|
-
return { updatedPaths };
|
|
613
|
-
}
|
|
614
|
-
export async function installProject(projectDir, platform) {
|
|
615
|
-
// Create user directories (not overwritten by upgrade)
|
|
616
|
-
await mkdir(join(projectDir, STATE_DIR), { recursive: true });
|
|
617
|
-
await mkdir(join(projectDir, ".ralph/specs"), { recursive: true });
|
|
618
|
-
await mkdir(join(projectDir, ".ralph/logs"), { recursive: true });
|
|
619
|
-
await mkdir(join(projectDir, ".ralph/docs/generated"), { recursive: true });
|
|
620
|
-
await copyBundledAssets(projectDir, platform);
|
|
621
|
-
}
|
|
622
|
-
async function deriveProjectName(projectDir) {
|
|
623
|
-
try {
|
|
624
|
-
const configPath = join(projectDir, CONFIG_FILE);
|
|
625
|
-
const raw = await readFile(configPath, "utf-8");
|
|
626
|
-
const config = JSON.parse(raw);
|
|
627
|
-
if (config.name)
|
|
628
|
-
return config.name;
|
|
629
|
-
}
|
|
630
|
-
catch (err) {
|
|
631
|
-
if (!isEnoent(err)) {
|
|
632
|
-
warn(`Could not read ${CONFIG_FILE}: ${formatError(err)}`);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
return basename(projectDir);
|
|
636
|
-
}
|
|
637
|
-
export async function generateManifests(projectDir) {
|
|
638
|
-
const configDir = join(projectDir, "_bmad/_config");
|
|
639
|
-
await mkdir(configDir, { recursive: true });
|
|
640
|
-
const coreHelpPath = join(projectDir, "_bmad/core/module-help.csv");
|
|
641
|
-
const bmmHelpPath = join(projectDir, "_bmad/bmm/module-help.csv");
|
|
642
|
-
// Validate CSV files exist before reading
|
|
643
|
-
if (!(await exists(coreHelpPath))) {
|
|
644
|
-
throw new Error(`Core module-help.csv not found at ${coreHelpPath}. BMAD installation may be incomplete.`);
|
|
645
|
-
}
|
|
646
|
-
if (!(await exists(bmmHelpPath))) {
|
|
647
|
-
throw new Error(`BMM module-help.csv not found at ${bmmHelpPath}. BMAD installation may be incomplete.`);
|
|
648
|
-
}
|
|
649
|
-
const coreContent = await readFile(coreHelpPath, "utf-8");
|
|
650
|
-
const bmmContent = await readFile(bmmHelpPath, "utf-8");
|
|
651
|
-
// Extract header from core (first line) and data lines from both
|
|
652
|
-
const coreLines = coreContent.trimEnd().split(/\r?\n/);
|
|
653
|
-
const bmmLines = bmmContent.trimEnd().split(/\r?\n/);
|
|
654
|
-
if (!coreLines[0]?.trim()) {
|
|
655
|
-
throw new Error(`Core module-help.csv is empty at ${coreHelpPath}`);
|
|
656
|
-
}
|
|
657
|
-
if (!bmmLines[0]?.trim()) {
|
|
658
|
-
throw new Error(`BMM module-help.csv is empty at ${bmmHelpPath}`);
|
|
659
|
-
}
|
|
660
|
-
const normalize = (line) => line.replace(/,+$/, "");
|
|
661
|
-
const header = normalize(coreLines[0]);
|
|
662
|
-
const bmmHeader = normalize(bmmLines[0]);
|
|
663
|
-
// Validate headers match (warn if mismatch but continue)
|
|
664
|
-
if (header && bmmHeader && header !== bmmHeader) {
|
|
665
|
-
warn(`CSV header mismatch detected. BMAD modules may have incompatible formats.`);
|
|
666
|
-
debug(`CSV header mismatch details - core: "${header.slice(0, 50)}...", bmm: "${bmmHeader.slice(0, 50)}..."`);
|
|
667
|
-
}
|
|
668
|
-
const coreData = coreLines
|
|
669
|
-
.slice(1)
|
|
670
|
-
.filter((l) => l.trim())
|
|
671
|
-
.map(normalize);
|
|
672
|
-
const bmmData = bmmLines
|
|
673
|
-
.slice(1)
|
|
674
|
-
.filter((l) => l.trim())
|
|
675
|
-
.map(normalize);
|
|
676
|
-
const combined = [header, ...coreData, ...bmmData].join("\n") + "\n";
|
|
677
|
-
await atomicWriteFile(join(configDir, "task-manifest.csv"), combined);
|
|
678
|
-
await atomicWriteFile(join(configDir, "workflow-manifest.csv"), combined);
|
|
679
|
-
await atomicWriteFile(join(configDir, "bmad-help.csv"), combined);
|
|
680
|
-
}
|
|
681
|
-
async function updateGitignore(projectDir) {
|
|
682
|
-
const gitignorePath = join(projectDir, ".gitignore");
|
|
683
|
-
let existing = "";
|
|
684
|
-
try {
|
|
685
|
-
existing = await readFile(gitignorePath, "utf-8");
|
|
686
|
-
}
|
|
687
|
-
catch (err) {
|
|
688
|
-
if (!isEnoent(err))
|
|
689
|
-
throw err;
|
|
690
|
-
}
|
|
691
|
-
const existingLines = parseGitignoreLines(existing);
|
|
692
|
-
const entries = [".ralph/logs/", "_bmad-output/"];
|
|
693
|
-
const newEntries = entries.filter((e) => !existingLines.has(e));
|
|
694
|
-
if (newEntries.length === 0)
|
|
695
|
-
return;
|
|
696
|
-
const suffix = existing.length > 0 && !existing.endsWith("\n")
|
|
697
|
-
? "\n" + newEntries.join("\n") + "\n"
|
|
698
|
-
: newEntries.join("\n") + "\n";
|
|
699
|
-
await atomicWriteFile(gitignorePath, existing + suffix);
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Merge the BMAD instructions snippet into the platform's instructions file.
|
|
703
|
-
* Creates the file if it doesn't exist, replaces an existing BMAD section on upgrade.
|
|
704
|
-
*/
|
|
705
|
-
export async function mergeInstructionsFile(projectDir, platform) {
|
|
706
|
-
const p = platform ?? (await getDefaultPlatform());
|
|
707
|
-
const instructionsPath = join(projectDir, p.instructionsFile);
|
|
708
|
-
const snippet = p.generateInstructionsSnippet();
|
|
709
|
-
const marker = p.instructionsSectionMarker;
|
|
710
|
-
// Ensure parent directory exists for nested paths (e.g. .cursor/rules/)
|
|
711
|
-
await mkdir(dirname(instructionsPath), { recursive: true });
|
|
712
|
-
let existing = "";
|
|
713
|
-
try {
|
|
714
|
-
existing = await readFile(instructionsPath, "utf-8");
|
|
715
|
-
}
|
|
716
|
-
catch (err) {
|
|
717
|
-
if (!isEnoent(err))
|
|
718
|
-
throw err;
|
|
719
|
-
}
|
|
720
|
-
if (existing.includes(marker)) {
|
|
721
|
-
await atomicWriteFile(instructionsPath, replaceSection(existing, marker, "\n" + snippet));
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
await atomicWriteFile(instructionsPath, existing + snippet);
|
|
725
|
-
}
|
|
726
|
-
export async function isInitialized(projectDir) {
|
|
727
|
-
return exists(join(projectDir, CONFIG_FILE));
|
|
728
|
-
}
|
|
729
|
-
export async function previewInstall(projectDir, platform) {
|
|
730
|
-
const p = platform ?? (await getDefaultPlatform());
|
|
731
|
-
const wouldCreate = [];
|
|
732
|
-
const wouldModify = [];
|
|
733
|
-
const wouldSkip = [];
|
|
734
|
-
// Directories that would be created
|
|
735
|
-
const dirsToCreate = [
|
|
736
|
-
`${STATE_DIR}/`,
|
|
737
|
-
".ralph/specs/",
|
|
738
|
-
".ralph/logs/",
|
|
739
|
-
".ralph/docs/generated/",
|
|
740
|
-
"_bmad/",
|
|
741
|
-
];
|
|
742
|
-
// Add command directory based on delivery strategy
|
|
743
|
-
if (p.commandDelivery.kind === "directory") {
|
|
744
|
-
dirsToCreate.push(`${p.commandDelivery.dir}/`);
|
|
745
|
-
}
|
|
746
|
-
else if (p.commandDelivery.kind === "skills") {
|
|
747
|
-
dirsToCreate.push(`${SKILLS_DIR}/`);
|
|
748
|
-
}
|
|
749
|
-
for (const dir of dirsToCreate) {
|
|
750
|
-
if (await exists(join(projectDir, dir))) {
|
|
751
|
-
if (dir === "_bmad/" ||
|
|
752
|
-
(p.commandDelivery.kind === "directory" && dir === `${p.commandDelivery.dir}/`) ||
|
|
753
|
-
(p.commandDelivery.kind === "skills" && dir === `${SKILLS_DIR}/`)) {
|
|
754
|
-
wouldModify.push(dir);
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
else {
|
|
758
|
-
wouldCreate.push(dir);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
// Files that would be created/modified
|
|
762
|
-
const filesToCheck = [
|
|
763
|
-
{ path: ".ralph/PROMPT.md", isTemplate: true },
|
|
764
|
-
{ path: ".ralph/@AGENT.md", isTemplate: true },
|
|
765
|
-
{ path: ".ralph/ralph_loop.sh", isTemplate: false },
|
|
766
|
-
{ path: CONFIG_FILE, isTemplate: false },
|
|
767
|
-
];
|
|
768
|
-
for (const file of filesToCheck) {
|
|
769
|
-
if (await exists(join(projectDir, file.path))) {
|
|
770
|
-
if (file.isTemplate) {
|
|
771
|
-
wouldModify.push(file.path);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
else {
|
|
775
|
-
wouldCreate.push(file.path);
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
// .gitignore would be modified if it exists, created otherwise
|
|
779
|
-
if (await exists(join(projectDir, ".gitignore"))) {
|
|
780
|
-
wouldModify.push(".gitignore");
|
|
781
|
-
}
|
|
782
|
-
else {
|
|
783
|
-
wouldCreate.push(".gitignore");
|
|
784
|
-
}
|
|
785
|
-
// Instructions file integration check
|
|
786
|
-
try {
|
|
787
|
-
const content = await readFile(join(projectDir, p.instructionsFile), "utf-8");
|
|
788
|
-
if (content.includes(p.instructionsSectionMarker)) {
|
|
789
|
-
wouldSkip.push(`${p.instructionsFile} (already integrated)`);
|
|
790
|
-
}
|
|
791
|
-
else {
|
|
792
|
-
wouldModify.push(p.instructionsFile);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
catch (err) {
|
|
796
|
-
if (isEnoent(err)) {
|
|
797
|
-
wouldCreate.push(p.instructionsFile);
|
|
798
|
-
}
|
|
799
|
-
else {
|
|
800
|
-
throw err;
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
return { wouldCreate, wouldModify, wouldSkip };
|
|
804
|
-
}
|
|
805
|
-
export async function previewUpgrade(projectDir, platform) {
|
|
806
|
-
const p = platform ?? (await getDefaultPlatform());
|
|
807
|
-
const managedPaths = [
|
|
808
|
-
{ path: "_bmad/", isDir: true },
|
|
809
|
-
{ path: ".ralph/ralph_loop.sh", isDir: false },
|
|
810
|
-
{ path: ".ralph/ralph_import.sh", isDir: false },
|
|
811
|
-
{ path: ".ralph/ralph_monitor.sh", isDir: false },
|
|
812
|
-
{ path: ".ralph/lib/", isDir: true },
|
|
813
|
-
{ path: ".ralph/PROMPT.md", isDir: false, templateName: "PROMPT.md" },
|
|
814
|
-
{ path: ".ralph/@AGENT.md", isDir: false, templateName: "AGENT.md" },
|
|
815
|
-
{ path: ".ralph/RALPH-REFERENCE.md", isDir: false },
|
|
816
|
-
{ path: ".gitignore", isDir: false },
|
|
817
|
-
];
|
|
818
|
-
// Add command directory based on delivery strategy
|
|
819
|
-
if (p.commandDelivery.kind === "directory") {
|
|
820
|
-
managedPaths.push({ path: `${p.commandDelivery.dir}/`, isDir: true });
|
|
821
|
-
}
|
|
822
|
-
else if (p.commandDelivery.kind === "skills") {
|
|
823
|
-
managedPaths.push({ path: `${SKILLS_DIR}/`, isDir: true });
|
|
824
|
-
}
|
|
825
|
-
const wouldUpdate = [];
|
|
826
|
-
const wouldCreate = [];
|
|
827
|
-
const wouldPreserve = [];
|
|
828
|
-
for (const { path: pathStr, templateName } of managedPaths) {
|
|
829
|
-
const fullPath = join(projectDir, pathStr.replace(/\/$/, ""));
|
|
830
|
-
if (await exists(fullPath)) {
|
|
831
|
-
if (templateName && (await isTemplateCustomized(fullPath, templateName))) {
|
|
832
|
-
wouldPreserve.push(pathStr);
|
|
833
|
-
}
|
|
834
|
-
else {
|
|
835
|
-
wouldUpdate.push(pathStr);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
else {
|
|
839
|
-
wouldCreate.push(pathStr);
|
|
840
|
-
}
|
|
841
|
-
}
|
|
842
|
-
return { wouldUpdate, wouldCreate, wouldPreserve };
|
|
843
|
-
}
|
|
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";
|
|
844
6
|
//# sourceMappingURL=installer.js.map
|