bmad-method 5.0.0-beta.2 → 5.0.1
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/ISSUE_TEMPLATE/bug_report.md +3 -3
- package/.github/ISSUE_TEMPLATE/feature_request.md +3 -3
- package/.github/workflows/discord.yaml +11 -2
- package/.github/workflows/format-check.yaml +42 -0
- package/.github/workflows/manual-release.yaml +173 -0
- package/.husky/pre-commit +3 -0
- package/.vscode/settings.json +26 -1
- package/CHANGELOG.md +0 -11
- package/README.md +2 -0
- package/bmad-core/agent-teams/team-all.yaml +1 -1
- package/bmad-core/agents/bmad-orchestrator.md +1 -1
- package/bmad-core/agents/dev.md +4 -4
- package/bmad-core/data/bmad-kb.md +1 -1
- package/bmad-core/data/test-levels-framework.md +12 -12
- package/bmad-core/tasks/facilitate-brainstorming-session.md +1 -1
- package/bmad-core/tasks/nfr-assess.md +10 -10
- package/bmad-core/tasks/qa-gate.md +23 -23
- package/bmad-core/tasks/review-story.md +18 -18
- package/bmad-core/tasks/risk-profile.md +25 -25
- package/bmad-core/tasks/test-design.md +9 -9
- package/bmad-core/tasks/trace-requirements.md +21 -21
- package/bmad-core/templates/architecture-tmpl.yaml +49 -49
- package/bmad-core/templates/brainstorming-output-tmpl.yaml +5 -5
- package/bmad-core/templates/brownfield-architecture-tmpl.yaml +31 -31
- package/bmad-core/templates/brownfield-prd-tmpl.yaml +13 -13
- package/bmad-core/templates/competitor-analysis-tmpl.yaml +19 -6
- package/bmad-core/templates/front-end-architecture-tmpl.yaml +21 -9
- package/bmad-core/templates/front-end-spec-tmpl.yaml +24 -24
- package/bmad-core/templates/fullstack-architecture-tmpl.yaml +122 -104
- package/bmad-core/templates/market-research-tmpl.yaml +2 -2
- package/bmad-core/templates/prd-tmpl.yaml +9 -9
- package/bmad-core/templates/project-brief-tmpl.yaml +4 -4
- package/bmad-core/templates/qa-gate-tmpl.yaml +9 -9
- package/bmad-core/templates/story-tmpl.yaml +12 -12
- package/bmad-core/workflows/brownfield-fullstack.yaml +9 -9
- package/bmad-core/workflows/brownfield-service.yaml +1 -1
- package/bmad-core/workflows/brownfield-ui.yaml +1 -1
- package/bmad-core/workflows/greenfield-fullstack.yaml +1 -1
- package/bmad-core/workflows/greenfield-service.yaml +1 -1
- package/bmad-core/workflows/greenfield-ui.yaml +1 -1
- package/common/utils/bmad-doc-template.md +5 -5
- package/dist/agents/analyst.txt +28 -15
- package/dist/agents/architect.txt +220 -190
- package/dist/agents/bmad-master.txt +298 -255
- package/dist/agents/bmad-orchestrator.txt +1 -1
- package/dist/agents/pm.txt +20 -20
- package/dist/agents/po.txt +11 -11
- package/dist/agents/qa.txt +275 -618
- package/dist/agents/sm.txt +11 -11
- package/dist/agents/ux-expert.txt +23 -23
- package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-designer.txt +109 -109
- package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-developer.txt +75 -77
- package/dist/expansion-packs/bmad-2d-phaser-game-dev/agents/game-sm.txt +41 -41
- package/dist/expansion-packs/bmad-2d-phaser-game-dev/teams/phaser-2d-nodejs-game-team.txt +483 -474
- package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-architect.txt +1 -1
- package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-designer.txt +149 -149
- package/dist/expansion-packs/bmad-2d-unity-game-dev/agents/game-sm.txt +20 -20
- package/dist/expansion-packs/bmad-2d-unity-game-dev/teams/unity-2d-game-team.txt +371 -358
- package/dist/expansion-packs/bmad-infrastructure-devops/agents/infra-devops-platform.txt +25 -25
- package/dist/teams/team-all.txt +581 -881
- package/dist/teams/team-fullstack.txt +316 -273
- package/dist/teams/team-ide-minimal.txt +276 -619
- package/dist/teams/team-no-ui.txt +281 -238
- package/docs/versioning-and-releases.md +114 -44
- package/eslint.config.mjs +119 -0
- package/expansion-packs/Complete AI Agent System - Blank Templates & Google Cloud Setup/PART 1 - Google Cloud Vertex AI Setup Documentation/1.4 Deployment Configuration/1.4.2 - cloudbuild.yaml +26 -26
- package/expansion-packs/bmad-2d-phaser-game-dev/agents/game-developer.md +4 -4
- package/expansion-packs/bmad-2d-phaser-game-dev/agents/game-sm.md +1 -1
- package/expansion-packs/bmad-2d-phaser-game-dev/data/development-guidelines.md +26 -28
- package/expansion-packs/bmad-2d-phaser-game-dev/templates/game-architecture-tmpl.yaml +50 -50
- package/expansion-packs/bmad-2d-phaser-game-dev/templates/game-brief-tmpl.yaml +23 -23
- package/expansion-packs/bmad-2d-phaser-game-dev/templates/game-design-doc-tmpl.yaml +24 -24
- package/expansion-packs/bmad-2d-phaser-game-dev/templates/game-story-tmpl.yaml +42 -42
- package/expansion-packs/bmad-2d-phaser-game-dev/templates/level-design-doc-tmpl.yaml +65 -65
- package/expansion-packs/bmad-2d-phaser-game-dev/workflows/game-dev-greenfield.yaml +5 -5
- package/expansion-packs/bmad-2d-phaser-game-dev/workflows/game-prototype.yaml +1 -1
- package/expansion-packs/bmad-2d-unity-game-dev/agents/game-developer.md +3 -3
- package/expansion-packs/bmad-2d-unity-game-dev/data/bmad-kb.md +1 -1
- package/expansion-packs/bmad-2d-unity-game-dev/templates/game-brief-tmpl.yaml +23 -23
- package/expansion-packs/bmad-2d-unity-game-dev/templates/game-design-doc-tmpl.yaml +63 -63
- package/expansion-packs/bmad-2d-unity-game-dev/templates/game-story-tmpl.yaml +20 -20
- package/expansion-packs/bmad-2d-unity-game-dev/templates/level-design-doc-tmpl.yaml +65 -65
- package/expansion-packs/bmad-2d-unity-game-dev/workflows/game-dev-greenfield.yaml +5 -5
- package/expansion-packs/bmad-2d-unity-game-dev/workflows/game-prototype.yaml +1 -1
- package/expansion-packs/bmad-infrastructure-devops/templates/infrastructure-architecture-tmpl.yaml +20 -20
- package/expansion-packs/bmad-infrastructure-devops/templates/infrastructure-platform-from-arch-tmpl.yaml +7 -7
- package/package.json +62 -39
- package/prettier.config.mjs +32 -0
- package/release_notes.md +30 -0
- package/tools/bmad-npx-wrapper.js +10 -10
- package/tools/builders/web-builder.js +124 -130
- package/tools/bump-all-versions.js +42 -33
- package/tools/bump-expansion-version.js +23 -16
- package/tools/cli.js +10 -12
- package/tools/flattener/aggregate.js +10 -10
- package/tools/flattener/binary.js +44 -17
- package/tools/flattener/discovery.js +19 -18
- package/tools/flattener/files.js +6 -6
- package/tools/flattener/ignoreRules.js +125 -125
- package/tools/flattener/main.js +201 -304
- package/tools/flattener/projectRoot.js +75 -73
- package/tools/flattener/prompts.js +9 -9
- package/tools/flattener/stats.helpers.js +131 -67
- package/tools/flattener/stats.js +3 -3
- package/tools/flattener/test-matrix.js +201 -193
- package/tools/flattener/xml.js +33 -31
- package/tools/installer/bin/bmad.js +130 -89
- package/tools/installer/config/ide-agent-config.yaml +1 -1
- package/tools/installer/config/install.config.yaml +2 -2
- package/tools/installer/lib/config-loader.js +46 -42
- package/tools/installer/lib/file-manager.js +91 -113
- package/tools/installer/lib/ide-base-setup.js +57 -56
- package/tools/installer/lib/ide-setup.js +375 -343
- package/tools/installer/lib/installer.js +875 -714
- package/tools/installer/lib/memory-profiler.js +54 -53
- package/tools/installer/lib/module-manager.js +19 -15
- package/tools/installer/lib/resource-locator.js +26 -28
- package/tools/installer/package.json +19 -19
- package/tools/lib/dependency-resolver.js +26 -30
- package/tools/lib/yaml-utils.js +7 -7
- package/tools/preview-release-notes.js +66 -0
- package/tools/shared/bannerArt.js +3 -3
- package/tools/sync-installer-version.js +7 -9
- package/tools/update-expansion-version.js +14 -15
- package/tools/upgraders/v3-to-v4-upgrader.js +203 -294
- package/tools/version-bump.js +41 -26
- package/tools/yaml-format.js +56 -43
- package/.github/workflows/promote-to-stable.yml +0 -144
- package/.github/workflows/release.yaml +0 -60
- package/.releaserc.json +0 -21
- package/tools/semantic-release-sync-installer.js +0 -30
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
/* deno-lint-ignore-file */
|
|
3
2
|
/*
|
|
4
3
|
Automatic test matrix for project root detection.
|
|
@@ -6,65 +5,65 @@
|
|
|
6
5
|
No external options or flags required. Safe to run multiple times.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
|
-
const os = require(
|
|
10
|
-
const path = require(
|
|
11
|
-
const fs = require(
|
|
12
|
-
const { promisify } = require(
|
|
13
|
-
const { execFile } = require(
|
|
14
|
-
const process = require(
|
|
8
|
+
const os = require('node:os');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const fs = require('fs-extra');
|
|
11
|
+
const { promisify } = require('node:util');
|
|
12
|
+
const { execFile } = require('node:child_process');
|
|
13
|
+
const process = require('node:process');
|
|
15
14
|
const execFileAsync = promisify(execFile);
|
|
16
15
|
|
|
17
|
-
const { findProjectRoot } = require(
|
|
16
|
+
const { findProjectRoot } = require('./projectRoot.js');
|
|
18
17
|
|
|
19
18
|
async function cmdAvailable(cmd) {
|
|
20
19
|
try {
|
|
21
|
-
await execFileAsync(cmd, [
|
|
20
|
+
await execFileAsync(cmd, ['--version'], { timeout: 500, windowsHide: true });
|
|
22
21
|
return true;
|
|
23
22
|
} catch {
|
|
24
23
|
return false;
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
async function testSvnMarker() {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
26
|
+
async function testSvnMarker() {
|
|
27
|
+
const root = await mkTmpDir('svn');
|
|
28
|
+
const nested = path.join(root, 'proj', 'code');
|
|
29
|
+
await fs.ensureDir(nested);
|
|
30
|
+
await fs.ensureDir(path.join(root, '.svn'));
|
|
31
|
+
const found = await findProjectRoot(nested);
|
|
32
|
+
assertEqual(found, root, '.svn marker should be detected');
|
|
33
|
+
return { name: 'svn-marker', ok: true };
|
|
34
|
+
}
|
|
36
35
|
|
|
37
|
-
async function testSymlinkStart() {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
async function testSymlinkStart() {
|
|
37
|
+
const root = await mkTmpDir('symlink-start');
|
|
38
|
+
const nested = path.join(root, 'a', 'b');
|
|
39
|
+
await fs.ensureDir(nested);
|
|
40
|
+
await fs.writeFile(path.join(root, '.project-root'), '\n');
|
|
41
|
+
const tmp = await mkTmpDir('symlink-tmp');
|
|
42
|
+
const link = path.join(tmp, 'link-to-b');
|
|
43
|
+
try {
|
|
44
|
+
await fs.symlink(nested, link);
|
|
45
|
+
} catch {
|
|
46
|
+
// symlink may not be permitted on some systems; skip
|
|
47
|
+
return { name: 'symlink-start', ok: true, skipped: true };
|
|
48
|
+
}
|
|
49
|
+
const found = await findProjectRoot(link);
|
|
50
|
+
assertEqual(found, root, 'should resolve symlinked start to real root');
|
|
51
|
+
return { name: 'symlink-start', ok: true };
|
|
49
52
|
}
|
|
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
53
|
|
|
55
|
-
async function testSubmoduleLikeInnerGitFile() {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
54
|
+
async function testSubmoduleLikeInnerGitFile() {
|
|
55
|
+
const root = await mkTmpDir('submodule-like');
|
|
56
|
+
const mid = path.join(root, 'mid');
|
|
57
|
+
const leaf = path.join(mid, 'leaf');
|
|
58
|
+
await fs.ensureDir(leaf);
|
|
59
|
+
// outer repo
|
|
60
|
+
await fs.ensureDir(path.join(root, '.git'));
|
|
61
|
+
// inner submodule-like .git file
|
|
62
|
+
await fs.writeFile(path.join(mid, '.git'), 'gitdir: ../.git/modules/mid\n');
|
|
63
|
+
const found = await findProjectRoot(leaf);
|
|
64
|
+
assertEqual(found, root, 'outermost .git should win on tie weight');
|
|
65
|
+
return { name: 'submodule-like-gitfile', ok: true };
|
|
66
|
+
}
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
async function mkTmpDir(name) {
|
|
@@ -75,274 +74,283 @@ async function mkTmpDir(name) {
|
|
|
75
74
|
|
|
76
75
|
function assertEqual(actual, expected, msg) {
|
|
77
76
|
if (actual !== expected) {
|
|
78
|
-
throw new Error(`${msg}: expected
|
|
77
|
+
throw new Error(`${msg}: expected="${expected}" actual="${actual}"`);
|
|
79
78
|
}
|
|
80
79
|
}
|
|
81
80
|
|
|
82
81
|
async function testSentinel() {
|
|
83
|
-
const root = await mkTmpDir(
|
|
84
|
-
const nested = path.join(root,
|
|
82
|
+
const root = await mkTmpDir('sentinel');
|
|
83
|
+
const nested = path.join(root, 'a', 'b', 'c');
|
|
85
84
|
await fs.ensureDir(nested);
|
|
86
|
-
await fs.writeFile(path.join(root,
|
|
85
|
+
await fs.writeFile(path.join(root, '.project-root'), '\n');
|
|
87
86
|
const found = await findProjectRoot(nested);
|
|
88
|
-
await assertEqual(found, root,
|
|
89
|
-
return { name:
|
|
87
|
+
await assertEqual(found, root, 'sentinel .project-root should win');
|
|
88
|
+
return { name: 'sentinel', ok: true };
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
async function testOtherSentinels() {
|
|
93
|
-
const root = await mkTmpDir(
|
|
94
|
-
const nested = path.join(root,
|
|
92
|
+
const root = await mkTmpDir('other-sentinels');
|
|
93
|
+
const nested = path.join(root, 'x', 'y');
|
|
95
94
|
await fs.ensureDir(nested);
|
|
96
|
-
await fs.writeFile(path.join(root,
|
|
95
|
+
await fs.writeFile(path.join(root, '.workspace-root'), '\n');
|
|
97
96
|
const found1 = await findProjectRoot(nested);
|
|
98
|
-
assertEqual(found1, root,
|
|
97
|
+
assertEqual(found1, root, 'sentinel .workspace-root should win');
|
|
99
98
|
|
|
100
|
-
await fs.remove(path.join(root,
|
|
101
|
-
await fs.writeFile(path.join(root,
|
|
99
|
+
await fs.remove(path.join(root, '.workspace-root'));
|
|
100
|
+
await fs.writeFile(path.join(root, '.repo-root'), '\n');
|
|
102
101
|
const found2 = await findProjectRoot(nested);
|
|
103
|
-
assertEqual(found2, root,
|
|
104
|
-
return { name:
|
|
102
|
+
assertEqual(found2, root, 'sentinel .repo-root should win');
|
|
103
|
+
return { name: 'other-sentinels', ok: true };
|
|
105
104
|
}
|
|
106
105
|
|
|
107
106
|
async function testGitCliAndMarker() {
|
|
108
|
-
const hasGit = await cmdAvailable(
|
|
109
|
-
if (!hasGit) return { name:
|
|
107
|
+
const hasGit = await cmdAvailable('git');
|
|
108
|
+
if (!hasGit) return { name: 'git-cli', ok: true, skipped: true };
|
|
110
109
|
|
|
111
|
-
const root = await mkTmpDir(
|
|
112
|
-
const nested = path.join(root,
|
|
110
|
+
const root = await mkTmpDir('git');
|
|
111
|
+
const nested = path.join(root, 'pkg', 'src');
|
|
113
112
|
await fs.ensureDir(nested);
|
|
114
|
-
await execFileAsync(
|
|
113
|
+
await execFileAsync('git', ['init'], { cwd: root, timeout: 2000 });
|
|
115
114
|
const found = await findProjectRoot(nested);
|
|
116
|
-
await assertEqual(found, root,
|
|
117
|
-
return { name:
|
|
115
|
+
await assertEqual(found, root, 'git toplevel should be detected');
|
|
116
|
+
return { name: 'git-cli', ok: true };
|
|
118
117
|
}
|
|
119
118
|
|
|
120
119
|
async function testHgMarkerOrCli() {
|
|
121
120
|
// Prefer simple marker test to avoid requiring Mercurial install
|
|
122
|
-
const root = await mkTmpDir(
|
|
123
|
-
const nested = path.join(root,
|
|
121
|
+
const root = await mkTmpDir('hg');
|
|
122
|
+
const nested = path.join(root, 'lib');
|
|
124
123
|
await fs.ensureDir(nested);
|
|
125
|
-
await fs.ensureDir(path.join(root,
|
|
124
|
+
await fs.ensureDir(path.join(root, '.hg'));
|
|
126
125
|
const found = await findProjectRoot(nested);
|
|
127
|
-
await assertEqual(found, root,
|
|
128
|
-
return { name:
|
|
126
|
+
await assertEqual(found, root, '.hg marker should be detected');
|
|
127
|
+
return { name: 'hg-marker', ok: true };
|
|
129
128
|
}
|
|
130
129
|
|
|
131
130
|
async function testWorkspacePnpm() {
|
|
132
|
-
const root = await mkTmpDir(
|
|
133
|
-
const pkgA = path.join(root,
|
|
131
|
+
const root = await mkTmpDir('pnpm-workspace');
|
|
132
|
+
const pkgA = path.join(root, 'packages', 'a');
|
|
134
133
|
await fs.ensureDir(pkgA);
|
|
135
|
-
await fs.writeFile(path.join(root,
|
|
134
|
+
await fs.writeFile(path.join(root, 'pnpm-workspace.yaml'), 'packages:\n - packages/*\n');
|
|
136
135
|
const found = await findProjectRoot(pkgA);
|
|
137
|
-
await assertEqual(found, root,
|
|
138
|
-
return { name:
|
|
136
|
+
await assertEqual(found, root, 'pnpm-workspace.yaml should be detected');
|
|
137
|
+
return { name: 'pnpm-workspace', ok: true };
|
|
139
138
|
}
|
|
140
139
|
|
|
141
140
|
async function testPackageJsonWorkspaces() {
|
|
142
|
-
const root = await mkTmpDir(
|
|
143
|
-
const pkgA = path.join(root,
|
|
141
|
+
const root = await mkTmpDir('package-workspaces');
|
|
142
|
+
const pkgA = path.join(root, 'packages', 'a');
|
|
144
143
|
await fs.ensureDir(pkgA);
|
|
145
|
-
await fs.writeJson(
|
|
144
|
+
await fs.writeJson(
|
|
145
|
+
path.join(root, 'package.json'),
|
|
146
|
+
{ private: true, workspaces: ['packages/*'] },
|
|
147
|
+
{ spaces: 2 },
|
|
148
|
+
);
|
|
146
149
|
const found = await findProjectRoot(pkgA);
|
|
147
|
-
await assertEqual(found, root,
|
|
148
|
-
return { name:
|
|
150
|
+
await assertEqual(found, root, 'package.json workspaces should be detected');
|
|
151
|
+
return { name: 'package.json-workspaces', ok: true };
|
|
149
152
|
}
|
|
150
153
|
|
|
151
154
|
async function testLockfiles() {
|
|
152
|
-
const root = await mkTmpDir(
|
|
153
|
-
const nested = path.join(root,
|
|
155
|
+
const root = await mkTmpDir('lockfiles');
|
|
156
|
+
const nested = path.join(root, 'src');
|
|
154
157
|
await fs.ensureDir(nested);
|
|
155
|
-
await fs.writeFile(path.join(root,
|
|
158
|
+
await fs.writeFile(path.join(root, 'yarn.lock'), '\n');
|
|
156
159
|
const found = await findProjectRoot(nested);
|
|
157
|
-
await assertEqual(found, root,
|
|
158
|
-
return { name:
|
|
160
|
+
await assertEqual(found, root, 'yarn.lock should be detected');
|
|
161
|
+
return { name: 'lockfiles', ok: true };
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
async function testLanguageConfigs() {
|
|
162
|
-
const root = await mkTmpDir(
|
|
163
|
-
const nested = path.join(root,
|
|
165
|
+
const root = await mkTmpDir('lang-configs');
|
|
166
|
+
const nested = path.join(root, 'x', 'y');
|
|
164
167
|
await fs.ensureDir(nested);
|
|
165
|
-
await fs.writeFile(path.join(root,
|
|
168
|
+
await fs.writeFile(path.join(root, 'pyproject.toml'), "[tool.poetry]\nname='tmp'\n");
|
|
166
169
|
const found = await findProjectRoot(nested);
|
|
167
|
-
await assertEqual(found, root,
|
|
168
|
-
return { name:
|
|
170
|
+
await assertEqual(found, root, 'pyproject.toml should be detected');
|
|
171
|
+
return { name: 'language-configs', ok: true };
|
|
169
172
|
}
|
|
170
173
|
|
|
171
174
|
async function testPreferOuterOnTie() {
|
|
172
|
-
const root = await mkTmpDir(
|
|
173
|
-
const mid = path.join(root,
|
|
174
|
-
const leaf = path.join(mid,
|
|
175
|
+
const root = await mkTmpDir('tie');
|
|
176
|
+
const mid = path.join(root, 'mid');
|
|
177
|
+
const leaf = path.join(mid, 'leaf');
|
|
175
178
|
await fs.ensureDir(leaf);
|
|
176
179
|
// same weight marker at two levels
|
|
177
|
-
await fs.writeFile(path.join(root,
|
|
178
|
-
await fs.writeFile(path.join(mid,
|
|
180
|
+
await fs.writeFile(path.join(root, 'requirements.txt'), '\n');
|
|
181
|
+
await fs.writeFile(path.join(mid, 'requirements.txt'), '\n');
|
|
179
182
|
const found = await findProjectRoot(leaf);
|
|
180
|
-
await assertEqual(found, root,
|
|
181
|
-
return { name:
|
|
183
|
+
await assertEqual(found, root, 'outermost directory should win on equal weight');
|
|
184
|
+
return { name: 'prefer-outermost-tie', ok: true };
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
// Additional coverage: Bazel, Nx/Turbo/Rush, Go workspaces, Deno, Java/Scala, PHP, Rust, Nix, Changesets, env markers,
|
|
185
188
|
// and priority interaction between package.json and lockfiles.
|
|
186
189
|
|
|
187
190
|
async function testBazelWorkspace() {
|
|
188
|
-
const root = await mkTmpDir(
|
|
189
|
-
const nested = path.join(root,
|
|
191
|
+
const root = await mkTmpDir('bazel');
|
|
192
|
+
const nested = path.join(root, 'apps', 'svc');
|
|
190
193
|
await fs.ensureDir(nested);
|
|
191
|
-
await fs.writeFile(path.join(root,
|
|
194
|
+
await fs.writeFile(path.join(root, 'WORKSPACE'), 'workspace(name="tmp")\n');
|
|
192
195
|
const found = await findProjectRoot(nested);
|
|
193
|
-
await assertEqual(found, root,
|
|
194
|
-
return { name:
|
|
196
|
+
await assertEqual(found, root, 'Bazel WORKSPACE should be detected');
|
|
197
|
+
return { name: 'bazel-workspace', ok: true };
|
|
195
198
|
}
|
|
196
199
|
|
|
197
200
|
async function testNx() {
|
|
198
|
-
const root = await mkTmpDir(
|
|
199
|
-
const nested = path.join(root,
|
|
201
|
+
const root = await mkTmpDir('nx');
|
|
202
|
+
const nested = path.join(root, 'apps', 'web');
|
|
200
203
|
await fs.ensureDir(nested);
|
|
201
|
-
await fs.writeJson(path.join(root,
|
|
204
|
+
await fs.writeJson(path.join(root, 'nx.json'), { npmScope: 'tmp' }, { spaces: 2 });
|
|
202
205
|
const found = await findProjectRoot(nested);
|
|
203
|
-
await assertEqual(found, root,
|
|
204
|
-
return { name:
|
|
206
|
+
await assertEqual(found, root, 'nx.json should be detected');
|
|
207
|
+
return { name: 'nx', ok: true };
|
|
205
208
|
}
|
|
206
209
|
|
|
207
210
|
async function testTurbo() {
|
|
208
|
-
const root = await mkTmpDir(
|
|
209
|
-
const nested = path.join(root,
|
|
211
|
+
const root = await mkTmpDir('turbo');
|
|
212
|
+
const nested = path.join(root, 'packages', 'x');
|
|
210
213
|
await fs.ensureDir(nested);
|
|
211
|
-
await fs.writeJson(path.join(root,
|
|
214
|
+
await fs.writeJson(path.join(root, 'turbo.json'), { pipeline: {} }, { spaces: 2 });
|
|
212
215
|
const found = await findProjectRoot(nested);
|
|
213
|
-
await assertEqual(found, root,
|
|
214
|
-
return { name:
|
|
216
|
+
await assertEqual(found, root, 'turbo.json should be detected');
|
|
217
|
+
return { name: 'turbo', ok: true };
|
|
215
218
|
}
|
|
216
219
|
|
|
217
220
|
async function testRush() {
|
|
218
|
-
const root = await mkTmpDir(
|
|
219
|
-
const nested = path.join(root,
|
|
221
|
+
const root = await mkTmpDir('rush');
|
|
222
|
+
const nested = path.join(root, 'apps', 'a');
|
|
220
223
|
await fs.ensureDir(nested);
|
|
221
|
-
await fs.writeJson(path.join(root,
|
|
224
|
+
await fs.writeJson(path.join(root, 'rush.json'), { projectFolderMinDepth: 1 }, { spaces: 2 });
|
|
222
225
|
const found = await findProjectRoot(nested);
|
|
223
|
-
await assertEqual(found, root,
|
|
224
|
-
return { name:
|
|
226
|
+
await assertEqual(found, root, 'rush.json should be detected');
|
|
227
|
+
return { name: 'rush', ok: true };
|
|
225
228
|
}
|
|
226
229
|
|
|
227
230
|
async function testGoWorkAndMod() {
|
|
228
|
-
const root = await mkTmpDir(
|
|
229
|
-
const mod = path.join(root,
|
|
230
|
-
const nested = path.join(mod,
|
|
231
|
+
const root = await mkTmpDir('gowork');
|
|
232
|
+
const mod = path.join(root, 'modA');
|
|
233
|
+
const nested = path.join(mod, 'pkg');
|
|
231
234
|
await fs.ensureDir(nested);
|
|
232
|
-
await fs.writeFile(path.join(root,
|
|
233
|
-
await fs.writeFile(path.join(mod,
|
|
235
|
+
await fs.writeFile(path.join(root, 'go.work'), 'go 1.22\nuse ./modA\n');
|
|
236
|
+
await fs.writeFile(path.join(mod, 'go.mod'), 'module example.com/a\ngo 1.22\n');
|
|
234
237
|
const found = await findProjectRoot(nested);
|
|
235
|
-
await assertEqual(found, root,
|
|
236
|
-
return { name:
|
|
238
|
+
await assertEqual(found, root, 'go.work should define the workspace root');
|
|
239
|
+
return { name: 'go-work', ok: true };
|
|
237
240
|
}
|
|
238
241
|
|
|
239
242
|
async function testDenoJson() {
|
|
240
|
-
const root = await mkTmpDir(
|
|
241
|
-
const nested = path.join(root,
|
|
243
|
+
const root = await mkTmpDir('deno');
|
|
244
|
+
const nested = path.join(root, 'src');
|
|
242
245
|
await fs.ensureDir(nested);
|
|
243
|
-
await fs.writeJson(path.join(root,
|
|
246
|
+
await fs.writeJson(path.join(root, 'deno.json'), { tasks: {} }, { spaces: 2 });
|
|
244
247
|
const found = await findProjectRoot(nested);
|
|
245
|
-
await assertEqual(found, root,
|
|
246
|
-
return { name:
|
|
248
|
+
await assertEqual(found, root, 'deno.json should be detected');
|
|
249
|
+
return { name: 'deno-json', ok: true };
|
|
247
250
|
}
|
|
248
251
|
|
|
249
252
|
async function testGradleSettings() {
|
|
250
|
-
const root = await mkTmpDir(
|
|
251
|
-
const nested = path.join(root,
|
|
253
|
+
const root = await mkTmpDir('gradle');
|
|
254
|
+
const nested = path.join(root, 'app');
|
|
252
255
|
await fs.ensureDir(nested);
|
|
253
|
-
await fs.writeFile(path.join(root,
|
|
256
|
+
await fs.writeFile(path.join(root, 'settings.gradle'), "rootProject.name='tmp'\n");
|
|
254
257
|
const found = await findProjectRoot(nested);
|
|
255
|
-
await assertEqual(found, root,
|
|
256
|
-
return { name:
|
|
258
|
+
await assertEqual(found, root, 'settings.gradle should be detected');
|
|
259
|
+
return { name: 'gradle-settings', ok: true };
|
|
257
260
|
}
|
|
258
261
|
|
|
259
262
|
async function testMavenPom() {
|
|
260
|
-
const root = await mkTmpDir(
|
|
261
|
-
const nested = path.join(root,
|
|
263
|
+
const root = await mkTmpDir('maven');
|
|
264
|
+
const nested = path.join(root, 'module');
|
|
262
265
|
await fs.ensureDir(nested);
|
|
263
|
-
await fs.writeFile(path.join(root,
|
|
266
|
+
await fs.writeFile(path.join(root, 'pom.xml'), '<project></project>\n');
|
|
264
267
|
const found = await findProjectRoot(nested);
|
|
265
|
-
await assertEqual(found, root,
|
|
266
|
-
return { name:
|
|
268
|
+
await assertEqual(found, root, 'pom.xml should be detected');
|
|
269
|
+
return { name: 'maven-pom', ok: true };
|
|
267
270
|
}
|
|
268
271
|
|
|
269
272
|
async function testSbtBuild() {
|
|
270
|
-
const root = await mkTmpDir(
|
|
271
|
-
const nested = path.join(root,
|
|
273
|
+
const root = await mkTmpDir('sbt');
|
|
274
|
+
const nested = path.join(root, 'sub');
|
|
272
275
|
await fs.ensureDir(nested);
|
|
273
|
-
await fs.writeFile(path.join(root,
|
|
276
|
+
await fs.writeFile(path.join(root, 'build.sbt'), 'name := "tmp"\n');
|
|
274
277
|
const found = await findProjectRoot(nested);
|
|
275
|
-
await assertEqual(found, root,
|
|
276
|
-
return { name:
|
|
278
|
+
await assertEqual(found, root, 'build.sbt should be detected');
|
|
279
|
+
return { name: 'sbt-build', ok: true };
|
|
277
280
|
}
|
|
278
281
|
|
|
279
282
|
async function testComposer() {
|
|
280
|
-
const root = await mkTmpDir(
|
|
281
|
-
const nested = path.join(root,
|
|
283
|
+
const root = await mkTmpDir('composer');
|
|
284
|
+
const nested = path.join(root, 'src');
|
|
282
285
|
await fs.ensureDir(nested);
|
|
283
|
-
await fs.writeJson(path.join(root,
|
|
284
|
-
await fs.writeFile(path.join(root,
|
|
286
|
+
await fs.writeJson(path.join(root, 'composer.json'), { name: 'tmp/pkg' }, { spaces: 2 });
|
|
287
|
+
await fs.writeFile(path.join(root, 'composer.lock'), '{}\n');
|
|
285
288
|
const found = await findProjectRoot(nested);
|
|
286
|
-
await assertEqual(found, root,
|
|
287
|
-
return { name:
|
|
289
|
+
await assertEqual(found, root, 'composer.{json,lock} should be detected');
|
|
290
|
+
return { name: 'composer', ok: true };
|
|
288
291
|
}
|
|
289
292
|
|
|
290
293
|
async function testCargo() {
|
|
291
|
-
const root = await mkTmpDir(
|
|
292
|
-
const nested = path.join(root,
|
|
294
|
+
const root = await mkTmpDir('cargo');
|
|
295
|
+
const nested = path.join(root, 'src');
|
|
293
296
|
await fs.ensureDir(nested);
|
|
294
|
-
await fs.writeFile(path.join(root,
|
|
297
|
+
await fs.writeFile(path.join(root, 'Cargo.toml'), "[package]\nname='tmp'\nversion='0.0.0'\n");
|
|
295
298
|
const found = await findProjectRoot(nested);
|
|
296
|
-
await assertEqual(found, root,
|
|
297
|
-
return { name:
|
|
299
|
+
await assertEqual(found, root, 'Cargo.toml should be detected');
|
|
300
|
+
return { name: 'cargo', ok: true };
|
|
298
301
|
}
|
|
299
302
|
|
|
300
303
|
async function testNixFlake() {
|
|
301
|
-
const root = await mkTmpDir(
|
|
302
|
-
const nested = path.join(root,
|
|
304
|
+
const root = await mkTmpDir('nix');
|
|
305
|
+
const nested = path.join(root, 'work');
|
|
303
306
|
await fs.ensureDir(nested);
|
|
304
|
-
await fs.writeFile(path.join(root,
|
|
307
|
+
await fs.writeFile(path.join(root, 'flake.nix'), '{ }\n');
|
|
305
308
|
const found = await findProjectRoot(nested);
|
|
306
|
-
await assertEqual(found, root,
|
|
307
|
-
return { name:
|
|
309
|
+
await assertEqual(found, root, 'flake.nix should be detected');
|
|
310
|
+
return { name: 'nix-flake', ok: true };
|
|
308
311
|
}
|
|
309
312
|
|
|
310
313
|
async function testChangesetConfig() {
|
|
311
|
-
const root = await mkTmpDir(
|
|
312
|
-
const nested = path.join(root,
|
|
314
|
+
const root = await mkTmpDir('changeset');
|
|
315
|
+
const nested = path.join(root, 'pkg');
|
|
313
316
|
await fs.ensureDir(nested);
|
|
314
|
-
await fs.ensureDir(path.join(root,
|
|
315
|
-
await fs.writeJson(
|
|
317
|
+
await fs.ensureDir(path.join(root, '.changeset'));
|
|
318
|
+
await fs.writeJson(
|
|
319
|
+
path.join(root, '.changeset', 'config.json'),
|
|
320
|
+
{ $schema: 'https://unpkg.com/@changesets/config@2.3.1/schema.json' },
|
|
321
|
+
{ spaces: 2 },
|
|
322
|
+
);
|
|
316
323
|
const found = await findProjectRoot(nested);
|
|
317
|
-
await assertEqual(found, root,
|
|
318
|
-
return { name:
|
|
324
|
+
await assertEqual(found, root, '.changeset/config.json should be detected');
|
|
325
|
+
return { name: 'changesets', ok: true };
|
|
319
326
|
}
|
|
320
327
|
|
|
321
328
|
async function testEnvCustomMarker() {
|
|
322
|
-
const root = await mkTmpDir(
|
|
323
|
-
const nested = path.join(root,
|
|
329
|
+
const root = await mkTmpDir('env-marker');
|
|
330
|
+
const nested = path.join(root, 'dir');
|
|
324
331
|
await fs.ensureDir(nested);
|
|
325
|
-
await fs.writeFile(path.join(root,
|
|
332
|
+
await fs.writeFile(path.join(root, 'MY_ROOT'), '\n');
|
|
326
333
|
const prev = process.env.PROJECT_ROOT_MARKERS;
|
|
327
|
-
process.env.PROJECT_ROOT_MARKERS =
|
|
334
|
+
process.env.PROJECT_ROOT_MARKERS = 'MY_ROOT';
|
|
328
335
|
try {
|
|
329
336
|
const found = await findProjectRoot(nested);
|
|
330
|
-
await assertEqual(found, root,
|
|
337
|
+
await assertEqual(found, root, 'custom env marker should be honored');
|
|
331
338
|
} finally {
|
|
332
|
-
if (prev === undefined) delete process.env.PROJECT_ROOT_MARKERS;
|
|
339
|
+
if (prev === undefined) delete process.env.PROJECT_ROOT_MARKERS;
|
|
340
|
+
else process.env.PROJECT_ROOT_MARKERS = prev;
|
|
333
341
|
}
|
|
334
|
-
return { name:
|
|
342
|
+
return { name: 'env-custom-marker', ok: true };
|
|
335
343
|
}
|
|
336
344
|
|
|
337
345
|
async function testPackageLowPriorityVsLock() {
|
|
338
|
-
const root = await mkTmpDir(
|
|
339
|
-
const nested = path.join(root,
|
|
340
|
-
await fs.ensureDir(path.join(nested,
|
|
341
|
-
await fs.writeJson(path.join(nested,
|
|
342
|
-
await fs.writeFile(path.join(root,
|
|
343
|
-
const found = await findProjectRoot(path.join(nested,
|
|
344
|
-
await assertEqual(found, root,
|
|
345
|
-
return { name:
|
|
346
|
+
const root = await mkTmpDir('pkg-vs-lock');
|
|
347
|
+
const nested = path.join(root, 'nested');
|
|
348
|
+
await fs.ensureDir(path.join(nested, 'deep'));
|
|
349
|
+
await fs.writeJson(path.join(nested, 'package.json'), { name: 'nested' }, { spaces: 2 });
|
|
350
|
+
await fs.writeFile(path.join(root, 'yarn.lock'), '\n');
|
|
351
|
+
const found = await findProjectRoot(path.join(nested, 'deep'));
|
|
352
|
+
await assertEqual(found, root, 'lockfile at root should outrank nested package.json');
|
|
353
|
+
return { name: 'package-vs-lock-priority', ok: true };
|
|
346
354
|
}
|
|
347
355
|
|
|
348
356
|
async function run() {
|
|
@@ -381,25 +389,25 @@ async function run() {
|
|
|
381
389
|
try {
|
|
382
390
|
const r = await t();
|
|
383
391
|
results.push({ ...r, ok: true });
|
|
384
|
-
console.log(`✔ ${r.name}${r.skipped ?
|
|
385
|
-
} catch (
|
|
386
|
-
console.error(`✖ ${t.name}:`,
|
|
387
|
-
results.push({ name: t.name, ok: false, error: String(
|
|
392
|
+
console.log(`✔ ${r.name}${r.skipped ? ' (skipped)' : ''}`);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
console.error(`✖ ${t.name}:`, error && error.message ? error.message : error);
|
|
395
|
+
results.push({ name: t.name, ok: false, error: String(error) });
|
|
388
396
|
}
|
|
389
397
|
}
|
|
390
398
|
|
|
391
399
|
const failed = results.filter((r) => !r.ok);
|
|
392
|
-
console.log(
|
|
400
|
+
console.log('\nSummary:');
|
|
393
401
|
for (const r of results) {
|
|
394
|
-
console.log(`- ${r.name}: ${r.ok ?
|
|
402
|
+
console.log(`- ${r.name}: ${r.ok ? 'ok' : 'FAIL'}${r.skipped ? ' (skipped)' : ''}`);
|
|
395
403
|
}
|
|
396
404
|
|
|
397
|
-
if (failed.length) {
|
|
405
|
+
if (failed.length > 0) {
|
|
398
406
|
process.exitCode = 1;
|
|
399
407
|
}
|
|
400
408
|
}
|
|
401
409
|
|
|
402
|
-
run().catch((
|
|
403
|
-
console.error(
|
|
410
|
+
run().catch((error) => {
|
|
411
|
+
console.error('Fatal error:', error);
|
|
404
412
|
process.exit(1);
|
|
405
413
|
});
|