bmad-method 4.37.0 â 5.0.0-beta.2
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/.github/workflows/promote-to-stable.yml +144 -0
- package/CHANGELOG.md +4 -14
- package/bmad-core/agents/qa.md +37 -18
- package/bmad-core/data/test-levels-framework.md +146 -0
- package/bmad-core/data/test-priorities-matrix.md +172 -0
- package/bmad-core/tasks/nfr-assess.md +343 -0
- package/bmad-core/tasks/qa-gate.md +159 -0
- package/bmad-core/tasks/review-story.md +234 -74
- package/bmad-core/tasks/risk-profile.md +353 -0
- package/bmad-core/tasks/test-design.md +174 -0
- package/bmad-core/tasks/trace-requirements.md +264 -0
- package/bmad-core/templates/qa-gate-tmpl.yaml +102 -0
- package/dist/agents/analyst.txt +20 -26
- package/dist/agents/architect.txt +14 -35
- package/dist/agents/bmad-master.txt +40 -70
- package/dist/agents/bmad-orchestrator.txt +28 -5
- package/dist/agents/dev.txt +0 -14
- package/dist/agents/pm.txt +0 -25
- package/dist/agents/po.txt +0 -18
- package/dist/agents/qa.txt +2079 -135
- package/dist/agents/sm.txt +0 -10
- package/dist/agents/ux-expert.txt +0 -7
- package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-designer.txt +0 -37
- package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-developer.txt +3 -12
- package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-sm.txt +0 -7
- package/dist/expansion-packs/bmad-2d-phaser-game-dev/teams/phaser-2d-nodejs-game-team.txt +44 -90
- package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-architect.txt +14 -49
- package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-designer.txt +0 -46
- package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-developer.txt +0 -15
- package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-sm.txt +0 -17
- package/dist/expansion-packs/bmad-2d-unity-game-dev/teams/unity-2d-game-team.txt +38 -142
- package/dist/expansion-packs/bmad-infrastructure-devops/agents/infra-devops-platform.txt +0 -2
- package/dist/teams/team-all.txt +2181 -261
- package/dist/teams/team-fullstack.txt +43 -57
- package/dist/teams/team-ide-minimal.txt +2064 -125
- package/dist/teams/team-no-ui.txt +43 -57
- package/docs/enhanced-ide-development-workflow.md +220 -15
- package/docs/user-guide.md +271 -18
- package/docs/working-in-the-brownfield.md +264 -31
- package/package.json +1 -1
- package/tools/flattener/main.js +474 -15
- package/tools/flattener/projectRoot.js +182 -23
- package/tools/flattener/stats.helpers.js +331 -0
- package/tools/flattener/stats.js +64 -14
- package/tools/flattener/test-matrix.js +405 -0
- package/tools/installer/bin/bmad.js +33 -32
- package/tools/installer/config/install.config.yaml +11 -1
- package/tools/installer/lib/file-manager.js +1 -1
- package/tools/installer/lib/ide-base-setup.js +1 -1
- package/tools/installer/lib/ide-setup.js +197 -83
- package/tools/installer/lib/installer.js +3 -3
- package/tools/installer/package.json +1 -1
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* deno-lint-ignore-file */
|
|
3
|
+
/*
|
|
4
|
+
Automatic test matrix for project root detection.
|
|
5
|
+
Creates temporary fixtures for various ecosystems and validates findProjectRoot().
|
|
6
|
+
No external options or flags required. Safe to run multiple times.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const os = require("node:os");
|
|
10
|
+
const path = require("node:path");
|
|
11
|
+
const fs = require("fs-extra");
|
|
12
|
+
const { promisify } = require("node:util");
|
|
13
|
+
const { execFile } = require("node:child_process");
|
|
14
|
+
const process = require("node:process");
|
|
15
|
+
const execFileAsync = promisify(execFile);
|
|
16
|
+
|
|
17
|
+
const { findProjectRoot } = require("./projectRoot.js");
|
|
18
|
+
|
|
19
|
+
async function cmdAvailable(cmd) {
|
|
20
|
+
try {
|
|
21
|
+
await execFileAsync(cmd, ["--version"], { timeout: 500, windowsHide: true });
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function testSvnMarker() {
|
|
28
|
+
const root = await mkTmpDir("svn");
|
|
29
|
+
const nested = path.join(root, "proj", "code");
|
|
30
|
+
await fs.ensureDir(nested);
|
|
31
|
+
await fs.ensureDir(path.join(root, ".svn"));
|
|
32
|
+
const found = await findProjectRoot(nested);
|
|
33
|
+
assertEqual(found, root, ".svn marker should be detected");
|
|
34
|
+
return { name: "svn-marker", ok: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function testSymlinkStart() {
|
|
38
|
+
const root = await mkTmpDir("symlink-start");
|
|
39
|
+
const nested = path.join(root, "a", "b");
|
|
40
|
+
await fs.ensureDir(nested);
|
|
41
|
+
await fs.writeFile(path.join(root, ".project-root"), "\n");
|
|
42
|
+
const tmp = await mkTmpDir("symlink-tmp");
|
|
43
|
+
const link = path.join(tmp, "link-to-b");
|
|
44
|
+
try {
|
|
45
|
+
await fs.symlink(nested, link);
|
|
46
|
+
} catch {
|
|
47
|
+
// symlink may not be permitted on some systems; skip
|
|
48
|
+
return { name: "symlink-start", ok: true, skipped: true };
|
|
49
|
+
}
|
|
50
|
+
const found = await findProjectRoot(link);
|
|
51
|
+
assertEqual(found, root, "should resolve symlinked start to real root");
|
|
52
|
+
return { name: "symlink-start", ok: true };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function testSubmoduleLikeInnerGitFile() {
|
|
56
|
+
const root = await mkTmpDir("submodule-like");
|
|
57
|
+
const mid = path.join(root, "mid");
|
|
58
|
+
const leaf = path.join(mid, "leaf");
|
|
59
|
+
await fs.ensureDir(leaf);
|
|
60
|
+
// outer repo
|
|
61
|
+
await fs.ensureDir(path.join(root, ".git"));
|
|
62
|
+
// inner submodule-like .git file
|
|
63
|
+
await fs.writeFile(path.join(mid, ".git"), "gitdir: ../.git/modules/mid\n");
|
|
64
|
+
const found = await findProjectRoot(leaf);
|
|
65
|
+
assertEqual(found, root, "outermost .git should win on tie weight");
|
|
66
|
+
return { name: "submodule-like-gitfile", ok: true };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function mkTmpDir(name) {
|
|
71
|
+
const base = await fs.realpath(os.tmpdir());
|
|
72
|
+
const dir = await fs.mkdtemp(path.join(base, `flattener-${name}-`));
|
|
73
|
+
return dir;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function assertEqual(actual, expected, msg) {
|
|
77
|
+
if (actual !== expected) {
|
|
78
|
+
throw new Error(`${msg}: expected=\"${expected}\" actual=\"${actual}\"`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function testSentinel() {
|
|
83
|
+
const root = await mkTmpDir("sentinel");
|
|
84
|
+
const nested = path.join(root, "a", "b", "c");
|
|
85
|
+
await fs.ensureDir(nested);
|
|
86
|
+
await fs.writeFile(path.join(root, ".project-root"), "\n");
|
|
87
|
+
const found = await findProjectRoot(nested);
|
|
88
|
+
await assertEqual(found, root, "sentinel .project-root should win");
|
|
89
|
+
return { name: "sentinel", ok: true };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function testOtherSentinels() {
|
|
93
|
+
const root = await mkTmpDir("other-sentinels");
|
|
94
|
+
const nested = path.join(root, "x", "y");
|
|
95
|
+
await fs.ensureDir(nested);
|
|
96
|
+
await fs.writeFile(path.join(root, ".workspace-root"), "\n");
|
|
97
|
+
const found1 = await findProjectRoot(nested);
|
|
98
|
+
assertEqual(found1, root, "sentinel .workspace-root should win");
|
|
99
|
+
|
|
100
|
+
await fs.remove(path.join(root, ".workspace-root"));
|
|
101
|
+
await fs.writeFile(path.join(root, ".repo-root"), "\n");
|
|
102
|
+
const found2 = await findProjectRoot(nested);
|
|
103
|
+
assertEqual(found2, root, "sentinel .repo-root should win");
|
|
104
|
+
return { name: "other-sentinels", ok: true };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function testGitCliAndMarker() {
|
|
108
|
+
const hasGit = await cmdAvailable("git");
|
|
109
|
+
if (!hasGit) return { name: "git-cli", ok: true, skipped: true };
|
|
110
|
+
|
|
111
|
+
const root = await mkTmpDir("git");
|
|
112
|
+
const nested = path.join(root, "pkg", "src");
|
|
113
|
+
await fs.ensureDir(nested);
|
|
114
|
+
await execFileAsync("git", ["init"], { cwd: root, timeout: 2000 });
|
|
115
|
+
const found = await findProjectRoot(nested);
|
|
116
|
+
await assertEqual(found, root, "git toplevel should be detected");
|
|
117
|
+
return { name: "git-cli", ok: true };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function testHgMarkerOrCli() {
|
|
121
|
+
// Prefer simple marker test to avoid requiring Mercurial install
|
|
122
|
+
const root = await mkTmpDir("hg");
|
|
123
|
+
const nested = path.join(root, "lib");
|
|
124
|
+
await fs.ensureDir(nested);
|
|
125
|
+
await fs.ensureDir(path.join(root, ".hg"));
|
|
126
|
+
const found = await findProjectRoot(nested);
|
|
127
|
+
await assertEqual(found, root, ".hg marker should be detected");
|
|
128
|
+
return { name: "hg-marker", ok: true };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function testWorkspacePnpm() {
|
|
132
|
+
const root = await mkTmpDir("pnpm-workspace");
|
|
133
|
+
const pkgA = path.join(root, "packages", "a");
|
|
134
|
+
await fs.ensureDir(pkgA);
|
|
135
|
+
await fs.writeFile(path.join(root, "pnpm-workspace.yaml"), "packages:\n - packages/*\n");
|
|
136
|
+
const found = await findProjectRoot(pkgA);
|
|
137
|
+
await assertEqual(found, root, "pnpm-workspace.yaml should be detected");
|
|
138
|
+
return { name: "pnpm-workspace", ok: true };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function testPackageJsonWorkspaces() {
|
|
142
|
+
const root = await mkTmpDir("package-workspaces");
|
|
143
|
+
const pkgA = path.join(root, "packages", "a");
|
|
144
|
+
await fs.ensureDir(pkgA);
|
|
145
|
+
await fs.writeJson(path.join(root, "package.json"), { private: true, workspaces: ["packages/*"] }, { spaces: 2 });
|
|
146
|
+
const found = await findProjectRoot(pkgA);
|
|
147
|
+
await assertEqual(found, root, "package.json workspaces should be detected");
|
|
148
|
+
return { name: "package.json-workspaces", ok: true };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function testLockfiles() {
|
|
152
|
+
const root = await mkTmpDir("lockfiles");
|
|
153
|
+
const nested = path.join(root, "src");
|
|
154
|
+
await fs.ensureDir(nested);
|
|
155
|
+
await fs.writeFile(path.join(root, "yarn.lock"), "\n");
|
|
156
|
+
const found = await findProjectRoot(nested);
|
|
157
|
+
await assertEqual(found, root, "yarn.lock should be detected");
|
|
158
|
+
return { name: "lockfiles", ok: true };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function testLanguageConfigs() {
|
|
162
|
+
const root = await mkTmpDir("lang-configs");
|
|
163
|
+
const nested = path.join(root, "x", "y");
|
|
164
|
+
await fs.ensureDir(nested);
|
|
165
|
+
await fs.writeFile(path.join(root, "pyproject.toml"), "[tool.poetry]\nname='tmp'\n");
|
|
166
|
+
const found = await findProjectRoot(nested);
|
|
167
|
+
await assertEqual(found, root, "pyproject.toml should be detected");
|
|
168
|
+
return { name: "language-configs", ok: true };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function testPreferOuterOnTie() {
|
|
172
|
+
const root = await mkTmpDir("tie");
|
|
173
|
+
const mid = path.join(root, "mid");
|
|
174
|
+
const leaf = path.join(mid, "leaf");
|
|
175
|
+
await fs.ensureDir(leaf);
|
|
176
|
+
// same weight marker at two levels
|
|
177
|
+
await fs.writeFile(path.join(root, "requirements.txt"), "\n");
|
|
178
|
+
await fs.writeFile(path.join(mid, "requirements.txt"), "\n");
|
|
179
|
+
const found = await findProjectRoot(leaf);
|
|
180
|
+
await assertEqual(found, root, "outermost directory should win on equal weight");
|
|
181
|
+
return { name: "prefer-outermost-tie", ok: true };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Additional coverage: Bazel, Nx/Turbo/Rush, Go workspaces, Deno, Java/Scala, PHP, Rust, Nix, Changesets, env markers,
|
|
185
|
+
// and priority interaction between package.json and lockfiles.
|
|
186
|
+
|
|
187
|
+
async function testBazelWorkspace() {
|
|
188
|
+
const root = await mkTmpDir("bazel");
|
|
189
|
+
const nested = path.join(root, "apps", "svc");
|
|
190
|
+
await fs.ensureDir(nested);
|
|
191
|
+
await fs.writeFile(path.join(root, "WORKSPACE"), "workspace(name=\"tmp\")\n");
|
|
192
|
+
const found = await findProjectRoot(nested);
|
|
193
|
+
await assertEqual(found, root, "Bazel WORKSPACE should be detected");
|
|
194
|
+
return { name: "bazel-workspace", ok: true };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function testNx() {
|
|
198
|
+
const root = await mkTmpDir("nx");
|
|
199
|
+
const nested = path.join(root, "apps", "web");
|
|
200
|
+
await fs.ensureDir(nested);
|
|
201
|
+
await fs.writeJson(path.join(root, "nx.json"), { npmScope: "tmp" }, { spaces: 2 });
|
|
202
|
+
const found = await findProjectRoot(nested);
|
|
203
|
+
await assertEqual(found, root, "nx.json should be detected");
|
|
204
|
+
return { name: "nx", ok: true };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function testTurbo() {
|
|
208
|
+
const root = await mkTmpDir("turbo");
|
|
209
|
+
const nested = path.join(root, "packages", "x");
|
|
210
|
+
await fs.ensureDir(nested);
|
|
211
|
+
await fs.writeJson(path.join(root, "turbo.json"), { pipeline: {} }, { spaces: 2 });
|
|
212
|
+
const found = await findProjectRoot(nested);
|
|
213
|
+
await assertEqual(found, root, "turbo.json should be detected");
|
|
214
|
+
return { name: "turbo", ok: true };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function testRush() {
|
|
218
|
+
const root = await mkTmpDir("rush");
|
|
219
|
+
const nested = path.join(root, "apps", "a");
|
|
220
|
+
await fs.ensureDir(nested);
|
|
221
|
+
await fs.writeJson(path.join(root, "rush.json"), { projectFolderMinDepth: 1 }, { spaces: 2 });
|
|
222
|
+
const found = await findProjectRoot(nested);
|
|
223
|
+
await assertEqual(found, root, "rush.json should be detected");
|
|
224
|
+
return { name: "rush", ok: true };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function testGoWorkAndMod() {
|
|
228
|
+
const root = await mkTmpDir("gowork");
|
|
229
|
+
const mod = path.join(root, "modA");
|
|
230
|
+
const nested = path.join(mod, "pkg");
|
|
231
|
+
await fs.ensureDir(nested);
|
|
232
|
+
await fs.writeFile(path.join(root, "go.work"), "go 1.22\nuse ./modA\n");
|
|
233
|
+
await fs.writeFile(path.join(mod, "go.mod"), "module example.com/a\ngo 1.22\n");
|
|
234
|
+
const found = await findProjectRoot(nested);
|
|
235
|
+
await assertEqual(found, root, "go.work should define the workspace root");
|
|
236
|
+
return { name: "go-work", ok: true };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function testDenoJson() {
|
|
240
|
+
const root = await mkTmpDir("deno");
|
|
241
|
+
const nested = path.join(root, "src");
|
|
242
|
+
await fs.ensureDir(nested);
|
|
243
|
+
await fs.writeJson(path.join(root, "deno.json"), { tasks: {} }, { spaces: 2 });
|
|
244
|
+
const found = await findProjectRoot(nested);
|
|
245
|
+
await assertEqual(found, root, "deno.json should be detected");
|
|
246
|
+
return { name: "deno-json", ok: true };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function testGradleSettings() {
|
|
250
|
+
const root = await mkTmpDir("gradle");
|
|
251
|
+
const nested = path.join(root, "app");
|
|
252
|
+
await fs.ensureDir(nested);
|
|
253
|
+
await fs.writeFile(path.join(root, "settings.gradle"), "rootProject.name='tmp'\n");
|
|
254
|
+
const found = await findProjectRoot(nested);
|
|
255
|
+
await assertEqual(found, root, "settings.gradle should be detected");
|
|
256
|
+
return { name: "gradle-settings", ok: true };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function testMavenPom() {
|
|
260
|
+
const root = await mkTmpDir("maven");
|
|
261
|
+
const nested = path.join(root, "module");
|
|
262
|
+
await fs.ensureDir(nested);
|
|
263
|
+
await fs.writeFile(path.join(root, "pom.xml"), "<project></project>\n");
|
|
264
|
+
const found = await findProjectRoot(nested);
|
|
265
|
+
await assertEqual(found, root, "pom.xml should be detected");
|
|
266
|
+
return { name: "maven-pom", ok: true };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function testSbtBuild() {
|
|
270
|
+
const root = await mkTmpDir("sbt");
|
|
271
|
+
const nested = path.join(root, "sub");
|
|
272
|
+
await fs.ensureDir(nested);
|
|
273
|
+
await fs.writeFile(path.join(root, "build.sbt"), "name := \"tmp\"\n");
|
|
274
|
+
const found = await findProjectRoot(nested);
|
|
275
|
+
await assertEqual(found, root, "build.sbt should be detected");
|
|
276
|
+
return { name: "sbt-build", ok: true };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function testComposer() {
|
|
280
|
+
const root = await mkTmpDir("composer");
|
|
281
|
+
const nested = path.join(root, "src");
|
|
282
|
+
await fs.ensureDir(nested);
|
|
283
|
+
await fs.writeJson(path.join(root, "composer.json"), { name: "tmp/pkg" }, { spaces: 2 });
|
|
284
|
+
await fs.writeFile(path.join(root, "composer.lock"), "{}\n");
|
|
285
|
+
const found = await findProjectRoot(nested);
|
|
286
|
+
await assertEqual(found, root, "composer.{json,lock} should be detected");
|
|
287
|
+
return { name: "composer", ok: true };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function testCargo() {
|
|
291
|
+
const root = await mkTmpDir("cargo");
|
|
292
|
+
const nested = path.join(root, "src");
|
|
293
|
+
await fs.ensureDir(nested);
|
|
294
|
+
await fs.writeFile(path.join(root, "Cargo.toml"), "[package]\nname='tmp'\nversion='0.0.0'\n");
|
|
295
|
+
const found = await findProjectRoot(nested);
|
|
296
|
+
await assertEqual(found, root, "Cargo.toml should be detected");
|
|
297
|
+
return { name: "cargo", ok: true };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function testNixFlake() {
|
|
301
|
+
const root = await mkTmpDir("nix");
|
|
302
|
+
const nested = path.join(root, "work");
|
|
303
|
+
await fs.ensureDir(nested);
|
|
304
|
+
await fs.writeFile(path.join(root, "flake.nix"), "{ }\n");
|
|
305
|
+
const found = await findProjectRoot(nested);
|
|
306
|
+
await assertEqual(found, root, "flake.nix should be detected");
|
|
307
|
+
return { name: "nix-flake", ok: true };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function testChangesetConfig() {
|
|
311
|
+
const root = await mkTmpDir("changeset");
|
|
312
|
+
const nested = path.join(root, "pkg");
|
|
313
|
+
await fs.ensureDir(nested);
|
|
314
|
+
await fs.ensureDir(path.join(root, ".changeset"));
|
|
315
|
+
await fs.writeJson(path.join(root, ".changeset", "config.json"), { $schema: "https://unpkg.com/@changesets/config@2.3.1/schema.json" }, { spaces: 2 });
|
|
316
|
+
const found = await findProjectRoot(nested);
|
|
317
|
+
await assertEqual(found, root, ".changeset/config.json should be detected");
|
|
318
|
+
return { name: "changesets", ok: true };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function testEnvCustomMarker() {
|
|
322
|
+
const root = await mkTmpDir("env-marker");
|
|
323
|
+
const nested = path.join(root, "dir");
|
|
324
|
+
await fs.ensureDir(nested);
|
|
325
|
+
await fs.writeFile(path.join(root, "MY_ROOT"), "\n");
|
|
326
|
+
const prev = process.env.PROJECT_ROOT_MARKERS;
|
|
327
|
+
process.env.PROJECT_ROOT_MARKERS = "MY_ROOT";
|
|
328
|
+
try {
|
|
329
|
+
const found = await findProjectRoot(nested);
|
|
330
|
+
await assertEqual(found, root, "custom env marker should be honored");
|
|
331
|
+
} finally {
|
|
332
|
+
if (prev === undefined) delete process.env.PROJECT_ROOT_MARKERS; else process.env.PROJECT_ROOT_MARKERS = prev;
|
|
333
|
+
}
|
|
334
|
+
return { name: "env-custom-marker", ok: true };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function testPackageLowPriorityVsLock() {
|
|
338
|
+
const root = await mkTmpDir("pkg-vs-lock");
|
|
339
|
+
const nested = path.join(root, "nested");
|
|
340
|
+
await fs.ensureDir(path.join(nested, "deep"));
|
|
341
|
+
await fs.writeJson(path.join(nested, "package.json"), { name: "nested" }, { spaces: 2 });
|
|
342
|
+
await fs.writeFile(path.join(root, "yarn.lock"), "\n");
|
|
343
|
+
const found = await findProjectRoot(path.join(nested, "deep"));
|
|
344
|
+
await assertEqual(found, root, "lockfile at root should outrank nested package.json");
|
|
345
|
+
return { name: "package-vs-lock-priority", ok: true };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function run() {
|
|
349
|
+
const tests = [
|
|
350
|
+
testSentinel,
|
|
351
|
+
testOtherSentinels,
|
|
352
|
+
testGitCliAndMarker,
|
|
353
|
+
testHgMarkerOrCli,
|
|
354
|
+
testWorkspacePnpm,
|
|
355
|
+
testPackageJsonWorkspaces,
|
|
356
|
+
testLockfiles,
|
|
357
|
+
testLanguageConfigs,
|
|
358
|
+
testPreferOuterOnTie,
|
|
359
|
+
testBazelWorkspace,
|
|
360
|
+
testNx,
|
|
361
|
+
testTurbo,
|
|
362
|
+
testRush,
|
|
363
|
+
testGoWorkAndMod,
|
|
364
|
+
testDenoJson,
|
|
365
|
+
testGradleSettings,
|
|
366
|
+
testMavenPom,
|
|
367
|
+
testSbtBuild,
|
|
368
|
+
testComposer,
|
|
369
|
+
testCargo,
|
|
370
|
+
testNixFlake,
|
|
371
|
+
testChangesetConfig,
|
|
372
|
+
testEnvCustomMarker,
|
|
373
|
+
testPackageLowPriorityVsLock,
|
|
374
|
+
testSvnMarker,
|
|
375
|
+
testSymlinkStart,
|
|
376
|
+
testSubmoduleLikeInnerGitFile,
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
const results = [];
|
|
380
|
+
for (const t of tests) {
|
|
381
|
+
try {
|
|
382
|
+
const r = await t();
|
|
383
|
+
results.push({ ...r, ok: true });
|
|
384
|
+
console.log(`â ${r.name}${r.skipped ? " (skipped)" : ""}`);
|
|
385
|
+
} catch (err) {
|
|
386
|
+
console.error(`â ${t.name}:`, err && err.message ? err.message : err);
|
|
387
|
+
results.push({ name: t.name, ok: false, error: String(err) });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const failed = results.filter((r) => !r.ok);
|
|
392
|
+
console.log("\nSummary:");
|
|
393
|
+
for (const r of results) {
|
|
394
|
+
console.log(`- ${r.name}: ${r.ok ? "ok" : "FAIL"}${r.skipped ? " (skipped)" : ""}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (failed.length) {
|
|
398
|
+
process.exitCode = 1;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
run().catch((e) => {
|
|
403
|
+
console.error("Fatal error:", e);
|
|
404
|
+
process.exit(1);
|
|
405
|
+
});
|
|
@@ -4,8 +4,8 @@ const { program } = require('commander');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs').promises;
|
|
6
6
|
const yaml = require('js-yaml');
|
|
7
|
-
const chalk = require('chalk');
|
|
8
|
-
const inquirer = require('inquirer');
|
|
7
|
+
const chalk = require('chalk').default || require('chalk');
|
|
8
|
+
const inquirer = require('inquirer').default || require('inquirer');
|
|
9
9
|
const semver = require('semver');
|
|
10
10
|
const https = require('https');
|
|
11
11
|
|
|
@@ -45,7 +45,7 @@ program
|
|
|
45
45
|
.option('-f, --full', 'Install complete BMad Method')
|
|
46
46
|
.option('-x, --expansion-only', 'Install only expansion packs (no bmad-core)')
|
|
47
47
|
.option('-d, --directory <path>', 'Installation directory')
|
|
48
|
-
.option('-i, --ide <ide...>', 'Configure for specific IDE(s) - can specify multiple (cursor, claude-code, windsurf, trae, roo, kilo, cline, gemini, qwen-code, github-copilot, other)')
|
|
48
|
+
.option('-i, --ide <ide...>', 'Configure for specific IDE(s) - can specify multiple (cursor, claude-code, windsurf, trae, roo, kilo, cline, gemini, qwen-code, github-copilot, crush, other)')
|
|
49
49
|
.option('-e, --expansion-packs <packs...>', 'Install specific expansion packs (can specify multiple)')
|
|
50
50
|
.action(async (options) => {
|
|
51
51
|
try {
|
|
@@ -183,17 +183,17 @@ program
|
|
|
183
183
|
});
|
|
184
184
|
|
|
185
185
|
async function promptInstallation() {
|
|
186
|
-
|
|
186
|
+
|
|
187
187
|
// Display ASCII logo
|
|
188
188
|
console.log(chalk.bold.cyan(`
|
|
189
|
-
âââââââ ââââ ââââ ââââââ âââââââ ââââ ââââââââââââââââââââââââ âââ âââââââ âââââââ
|
|
189
|
+
âââââââ ââââ ââââ ââââââ âââââââ ââââ ââââââââââââââââââââââââ âââ âââââââ âââââââ
|
|
190
190
|
âââââââââââââ âââââââââââââââââââââ âââââ âââââââââââââââââââââââââ ââââââââââââââââââââ
|
|
191
191
|
ââââââââââââââââââââââââââââââ ââââââââââââââââââââââââââ âââ âââââââââââ ââââââ âââ
|
|
192
192
|
ââââââââââââââââââââââââââââââ ââââââââââââââââââââââââââ âââ âââââââââââ ââââââ âââ
|
|
193
193
|
âââââââââââ âââ ââââââ âââââââââââ âââ âââ âââââââââââ âââ âââ ââââââââââââââââââââ
|
|
194
|
-
âââââââ âââ ââââââ ââââââââââ âââ âââââââââââ âââ âââ âââ âââââââ âââââââ
|
|
194
|
+
âââââââ âââ ââââââ ââââââââââ âââ âââââââââââ âââ âââ âââ âââââââ âââââââ
|
|
195
195
|
`));
|
|
196
|
-
|
|
196
|
+
|
|
197
197
|
console.log(chalk.bold.magenta('đ Universal AI Agent Framework for Any Domain'));
|
|
198
198
|
console.log(chalk.bold.blue(`⨠Installer v${version}\n`));
|
|
199
199
|
|
|
@@ -218,63 +218,63 @@ async function promptInstallation() {
|
|
|
218
218
|
// Detect existing installations
|
|
219
219
|
const installDir = path.resolve(directory);
|
|
220
220
|
const state = await installer.detectInstallationState(installDir);
|
|
221
|
-
|
|
221
|
+
|
|
222
222
|
// Check for existing expansion packs
|
|
223
223
|
const existingExpansionPacks = state.expansionPacks || {};
|
|
224
|
-
|
|
224
|
+
|
|
225
225
|
// Get available expansion packs
|
|
226
226
|
const availableExpansionPacks = await installer.getAvailableExpansionPacks();
|
|
227
|
-
|
|
227
|
+
|
|
228
228
|
// Build choices list
|
|
229
229
|
const choices = [];
|
|
230
|
-
|
|
230
|
+
|
|
231
231
|
// Load core config to get short-title
|
|
232
232
|
const coreConfigPath = path.join(__dirname, '..', '..', '..', 'bmad-core', 'core-config.yaml');
|
|
233
233
|
const coreConfig = yaml.load(await fs.readFile(coreConfigPath, 'utf8'));
|
|
234
234
|
const coreShortTitle = coreConfig['short-title'] || 'BMad Agile Core System';
|
|
235
|
-
|
|
235
|
+
|
|
236
236
|
// Add BMad core option
|
|
237
237
|
let bmadOptionText;
|
|
238
238
|
if (state.type === 'v4_existing') {
|
|
239
239
|
const currentVersion = state.manifest?.version || 'unknown';
|
|
240
240
|
const newVersion = version; // Always use package.json version
|
|
241
|
-
const versionInfo = currentVersion === newVersion
|
|
241
|
+
const versionInfo = currentVersion === newVersion
|
|
242
242
|
? `(v${currentVersion} - reinstall)`
|
|
243
243
|
: `(v${currentVersion} â v${newVersion})`;
|
|
244
244
|
bmadOptionText = `Update ${coreShortTitle} ${versionInfo} .bmad-core`;
|
|
245
245
|
} else {
|
|
246
246
|
bmadOptionText = `${coreShortTitle} (v${version}) .bmad-core`;
|
|
247
247
|
}
|
|
248
|
-
|
|
248
|
+
|
|
249
249
|
choices.push({
|
|
250
250
|
name: bmadOptionText,
|
|
251
251
|
value: 'bmad-core',
|
|
252
252
|
checked: true
|
|
253
253
|
});
|
|
254
|
-
|
|
254
|
+
|
|
255
255
|
// Add expansion pack options
|
|
256
256
|
for (const pack of availableExpansionPacks) {
|
|
257
257
|
const existing = existingExpansionPacks[pack.id];
|
|
258
258
|
let packOptionText;
|
|
259
|
-
|
|
259
|
+
|
|
260
260
|
if (existing) {
|
|
261
261
|
const currentVersion = existing.manifest?.version || 'unknown';
|
|
262
262
|
const newVersion = pack.version;
|
|
263
|
-
const versionInfo = currentVersion === newVersion
|
|
263
|
+
const versionInfo = currentVersion === newVersion
|
|
264
264
|
? `(v${currentVersion} - reinstall)`
|
|
265
265
|
: `(v${currentVersion} â v${newVersion})`;
|
|
266
266
|
packOptionText = `Update ${pack.shortTitle} ${versionInfo} .${pack.id}`;
|
|
267
267
|
} else {
|
|
268
268
|
packOptionText = `${pack.shortTitle} (v${pack.version}) .${pack.id}`;
|
|
269
269
|
}
|
|
270
|
-
|
|
270
|
+
|
|
271
271
|
choices.push({
|
|
272
272
|
name: packOptionText,
|
|
273
273
|
value: pack.id,
|
|
274
274
|
checked: false
|
|
275
275
|
});
|
|
276
276
|
}
|
|
277
|
-
|
|
277
|
+
|
|
278
278
|
// Ask what to install
|
|
279
279
|
const { selectedItems } = await inquirer.prompt([
|
|
280
280
|
{
|
|
@@ -290,7 +290,7 @@ async function promptInstallation() {
|
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
292
|
]);
|
|
293
|
-
|
|
293
|
+
|
|
294
294
|
// Process selections
|
|
295
295
|
answers.installType = selectedItems.includes('bmad-core') ? 'full' : 'expansion-only';
|
|
296
296
|
answers.expansionPacks = selectedItems.filter(item => item !== 'bmad-core');
|
|
@@ -299,7 +299,7 @@ async function promptInstallation() {
|
|
|
299
299
|
if (selectedItems.includes('bmad-core')) {
|
|
300
300
|
console.log(chalk.cyan('\nđ Document Organization Settings'));
|
|
301
301
|
console.log(chalk.dim('Configure how your project documentation should be organized.\n'));
|
|
302
|
-
|
|
302
|
+
|
|
303
303
|
// Ask about PRD sharding
|
|
304
304
|
const { prdSharded } = await inquirer.prompt([
|
|
305
305
|
{
|
|
@@ -310,7 +310,7 @@ async function promptInstallation() {
|
|
|
310
310
|
}
|
|
311
311
|
]);
|
|
312
312
|
answers.prdSharded = prdSharded;
|
|
313
|
-
|
|
313
|
+
|
|
314
314
|
// Ask about architecture sharding
|
|
315
315
|
const { architectureSharded } = await inquirer.prompt([
|
|
316
316
|
{
|
|
@@ -321,7 +321,7 @@ async function promptInstallation() {
|
|
|
321
321
|
}
|
|
322
322
|
]);
|
|
323
323
|
answers.architectureSharded = architectureSharded;
|
|
324
|
-
|
|
324
|
+
|
|
325
325
|
// Show warning if architecture sharding is disabled
|
|
326
326
|
if (!architectureSharded) {
|
|
327
327
|
console.log(chalk.yellow.bold('\nâ ď¸ IMPORTANT: Architecture Sharding Disabled'));
|
|
@@ -330,7 +330,7 @@ async function promptInstallation() {
|
|
|
330
330
|
console.log(chalk.yellow('as these are used by the dev agent at runtime.'));
|
|
331
331
|
console.log(chalk.yellow('\nAlternatively, you can remove these files from the devLoadAlwaysFiles list'));
|
|
332
332
|
console.log(chalk.yellow('in your core-config.yaml after installation.'));
|
|
333
|
-
|
|
333
|
+
|
|
334
334
|
const { acknowledge } = await inquirer.prompt([
|
|
335
335
|
{
|
|
336
336
|
type: 'confirm',
|
|
@@ -339,7 +339,7 @@ async function promptInstallation() {
|
|
|
339
339
|
default: false
|
|
340
340
|
}
|
|
341
341
|
]);
|
|
342
|
-
|
|
342
|
+
|
|
343
343
|
if (!acknowledge) {
|
|
344
344
|
console.log(chalk.red('Installation cancelled.'));
|
|
345
345
|
process.exit(0);
|
|
@@ -350,14 +350,14 @@ async function promptInstallation() {
|
|
|
350
350
|
// Ask for IDE configuration
|
|
351
351
|
let ides = [];
|
|
352
352
|
let ideSelectionComplete = false;
|
|
353
|
-
|
|
353
|
+
|
|
354
354
|
while (!ideSelectionComplete) {
|
|
355
355
|
console.log(chalk.cyan('\nđ ď¸ IDE Configuration'));
|
|
356
356
|
console.log(chalk.bold.yellow.bgRed(' â ď¸ IMPORTANT: This is a MULTISELECT! Use SPACEBAR to toggle each IDE! '));
|
|
357
357
|
console.log(chalk.bold.magenta('đ¸ Use arrow keys to navigate'));
|
|
358
358
|
console.log(chalk.bold.magenta('đ¸ Use SPACEBAR to select/deselect IDEs'));
|
|
359
359
|
console.log(chalk.bold.magenta('đ¸ Press ENTER when finished selecting\n'));
|
|
360
|
-
|
|
360
|
+
|
|
361
361
|
const ideResponse = await inquirer.prompt([
|
|
362
362
|
{
|
|
363
363
|
type: 'checkbox',
|
|
@@ -373,11 +373,12 @@ async function promptInstallation() {
|
|
|
373
373
|
{ name: 'Cline', value: 'cline' },
|
|
374
374
|
{ name: 'Gemini CLI', value: 'gemini' },
|
|
375
375
|
{ name: 'Qwen Code', value: 'qwen-code' },
|
|
376
|
+
{ name: 'Crush', value: 'crush' },
|
|
376
377
|
{ name: 'Github Copilot', value: 'github-copilot' }
|
|
377
378
|
]
|
|
378
379
|
}
|
|
379
380
|
]);
|
|
380
|
-
|
|
381
|
+
|
|
381
382
|
ides = ideResponse.ides;
|
|
382
383
|
|
|
383
384
|
// Confirm no IDE selection if none selected
|
|
@@ -390,13 +391,13 @@ async function promptInstallation() {
|
|
|
390
391
|
default: false
|
|
391
392
|
}
|
|
392
393
|
]);
|
|
393
|
-
|
|
394
|
+
|
|
394
395
|
if (!confirmNoIde) {
|
|
395
396
|
console.log(chalk.bold.red('\nđ Returning to IDE selection. Remember to use SPACEBAR to select IDEs!\n'));
|
|
396
397
|
continue; // Go back to IDE selection only
|
|
397
398
|
}
|
|
398
399
|
}
|
|
399
|
-
|
|
400
|
+
|
|
400
401
|
ideSelectionComplete = true;
|
|
401
402
|
}
|
|
402
403
|
|
|
@@ -407,7 +408,7 @@ async function promptInstallation() {
|
|
|
407
408
|
if (ides.includes('github-copilot')) {
|
|
408
409
|
console.log(chalk.cyan('\nđ§ GitHub Copilot Configuration'));
|
|
409
410
|
console.log(chalk.dim('BMad works best with specific VS Code settings for optimal agent experience.\n'));
|
|
410
|
-
|
|
411
|
+
|
|
411
412
|
const { configChoice } = await inquirer.prompt([
|
|
412
413
|
{
|
|
413
414
|
type: 'list',
|
|
@@ -430,7 +431,7 @@ async function promptInstallation() {
|
|
|
430
431
|
default: 'defaults'
|
|
431
432
|
}
|
|
432
433
|
]);
|
|
433
|
-
|
|
434
|
+
|
|
434
435
|
answers.githubCopilotConfig = { configChoice };
|
|
435
436
|
}
|
|
436
437
|
|
|
@@ -28,6 +28,16 @@ ide-configurations:
|
|
|
28
28
|
# To use BMad agents in Claude Code:
|
|
29
29
|
# 1. Type /agent-name (e.g., "/dev", "/pm", "/architect")
|
|
30
30
|
# 2. Claude will switch to that agent's persona
|
|
31
|
+
crush:
|
|
32
|
+
name: Crush
|
|
33
|
+
rule-dir: .crush/commands/BMad/
|
|
34
|
+
format: multi-file
|
|
35
|
+
command-suffix: .md
|
|
36
|
+
instructions: |
|
|
37
|
+
# To use BMad agents in Crush:
|
|
38
|
+
# 1. Press CTRL + P and press TAB
|
|
39
|
+
# 2. Select agent or task
|
|
40
|
+
# 3. Crush will switch to that agent's persona / task
|
|
31
41
|
windsurf:
|
|
32
42
|
name: Windsurf
|
|
33
43
|
rule-dir: .windsurf/rules/
|
|
@@ -110,4 +120,4 @@ ide-configurations:
|
|
|
110
120
|
# 1. The installer creates a .qwen/bmad-method/ directory in your project.
|
|
111
121
|
# 2. It concatenates all agent files into a single QWEN.md file.
|
|
112
122
|
# 3. Simply mention the agent in your prompt (e.g., "As *dev, ...").
|
|
113
|
-
# 4. The Qwen Code CLI will automatically have the context for that agent.
|
|
123
|
+
# 4. The Qwen Code CLI will automatically have the context for that agent.
|
|
@@ -2,7 +2,7 @@ const fs = require("fs-extra");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const crypto = require("crypto");
|
|
4
4
|
const yaml = require("js-yaml");
|
|
5
|
-
const chalk = require("chalk");
|
|
5
|
+
const chalk = require("chalk").default || require("chalk");
|
|
6
6
|
const { createReadStream, createWriteStream, promises: fsPromises } = require('fs');
|
|
7
7
|
const { pipeline } = require('stream/promises');
|
|
8
8
|
const resourceLocator = require('./resource-locator');
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const fs = require("fs-extra");
|
|
8
8
|
const yaml = require("js-yaml");
|
|
9
|
-
const chalk = require("chalk");
|
|
9
|
+
const chalk = require("chalk").default || require("chalk");
|
|
10
10
|
const fileManager = require("./file-manager");
|
|
11
11
|
const resourceLocator = require("./resource-locator");
|
|
12
12
|
const { extractYamlFromAgent } = require("../../lib/yaml-utils");
|