bmad-plus 0.12.2 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +96 -461
- package/package.json +12 -5
- package/readme-international/README.de.md +42 -9
- package/readme-international/README.es.md +43 -10
- package/readme-international/README.fr.md +42 -9
- package/src/bmad-plus/agents/agent-orchestrator/SKILL.md +5 -3
- package/src/bmad-plus/module.yaml +270 -220
- package/src/bmad-plus/packs/pack-dev-studio/README.md +133 -141
- package/src/bmad-plus/packs/pack-dev-studio/SKILL.md +49 -0
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/analyst-agent.md +35 -60
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/document-project.md +59 -59
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/domain-research.md +55 -93
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/market-research.md +58 -93
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/prfaq.md +55 -132
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/product-brief.md +63 -78
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/tech-writer-agent.md +54 -69
- package/src/bmad-plus/packs/pack-dev-studio/categories/analysis/technical-research.md +54 -93
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/architect-agent.md +32 -60
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-architecture.md +67 -71
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/create-epics-stories.md +61 -90
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/generate-project-context.md +56 -78
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/implementation-readiness.md +55 -88
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01-init.md +20 -153
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-01b-continue.md +20 -173
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-02-context.md +14 -220
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-03-starter.md +20 -329
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-04-decisions.md +15 -314
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-05-patterns.md +15 -355
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-06-structure.md +15 -375
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-07-validation.md +14 -357
- package/src/bmad-plus/packs/pack-dev-studio/categories/architecture/steps/step-08-complete.md +13 -78
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/checkpoint-preview.md +52 -65
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-01-gather-context.md +14 -81
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-02-review.md +14 -31
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-03-triage.md +14 -45
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review-steps/step-04-present.md +13 -128
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +61 -87
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/correct-course.md +55 -298
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/create-story.md +52 -426
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-agent.md +48 -69
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story-checklist.md +22 -80
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/dev-story.md +60 -482
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/investigate.md +50 -184
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/qa-e2e-tests.md +57 -173
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/quick-dev.md +56 -108
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/retrospective.md +54 -1509
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-planning.md +54 -296
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/sprint-status.md +41 -283
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-prd.md +58 -18
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/create-ux-design.md +103 -72
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/edit-prd.md +55 -27
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/pm-agent.md +34 -60
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/prd.md +46 -87
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/steps/step-01-init.md +10 -0
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/ux-designer-agent.md +30 -60
- package/src/bmad-plus/packs/pack-dev-studio/categories/planning/validate-prd.md +57 -27
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/advanced-elicitation.md +47 -138
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/adversarial-review.md +48 -34
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/bmad-help.md +51 -68
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/brainstorming.md +46 -3
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/customize.md +68 -109
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/distillator.md +53 -174
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/edge-case-hunter.md +39 -53
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-prose.md +45 -83
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/editorial-review-structure.md +45 -176
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/index-docs.md +45 -63
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/party-mode.md +53 -124
- package/src/bmad-plus/packs/pack-dev-studio/categories/utilities/shard-doc.md +44 -100
- package/src/bmad-plus/packs/pack-dev-studio/dev-studio-orchestrator.md +56 -116
- package/src/bmad-plus/packs/pack-dev-studio/shared/architecture-decision-template.md +49 -12
- package/src/bmad-plus/packs/pack-dev-studio/shared/bwml-spec.md +51 -328
- package/src/bmad-plus/packs/pack-dev-studio/shared/catalog.json +489 -0
- package/src/bmad-plus/packs/pack-dev-studio/shared/execution.md +69 -0
- package/src/bmad-plus/packs/pack-dev-studio/shared/module-help.csv +39 -32
- package/src/bmad-plus/packs/pack-dev-studio/upstream-sync.yaml +85 -14
- package/src/bmad-plus/packs/pack-memory/README.md +6 -0
- package/src/bmad-plus/packs/pack-memory/memory-orchestrator.md +12 -5
- package/src/bmad-plus/packs/pack-seo/SKILL.md +27 -1
- package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +8 -8
- package/src/bmad-plus/packs/pack-seo/scripts/seo_fetch.py +1 -2
- package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +0 -1
- package/src/bmad-plus/packs/pack-seo/seo-chief.md +16 -1
- package/src/bmad-plus/packs/pack-seo/seo-judge.md +12 -0
- package/src/bmad-plus/packs/pack-seo/seo-scout.md +12 -0
- package/src/bmad-plus/skills/bmad-plus-autopilot/SKILL.md +4 -4
- package/src/bmad-plus/skills/bmad-plus-sync/SKILL.md +5 -3
- package/src/bmad-plus/skills/dev-studio/SKILL.md +19 -0
- package/tools/bmad-plus-npx.js +4 -2
- package/tools/build/adapters.config.js +60 -51
- package/tools/build/check-counts.js +52 -54
- package/tools/build/check-install-contract.js +450 -0
- package/tools/build/generate-adapters.js +252 -56
- package/tools/build/generate.js +364 -14
- package/tools/build/generated-adapters/.codex/AGENTS.md +20 -7
- package/tools/build/generated-adapters/.cursor/rules/bmad-plus.mdc +20 -7
- package/tools/build/generated-adapters/.opencode/AGENTS.md +20 -7
- package/tools/build/generated-adapters/AGENTS.md +20 -7
- package/tools/build/generated-adapters/CLAUDE.md +20 -7
- package/tools/build/generated-adapters/CONVENTIONS.md +20 -7
- package/tools/build/generated-adapters/GEMINI.md +20 -7
- package/tools/build/module.template.yaml +82 -0
- package/tools/build/pack-delivery.js +78 -0
- package/tools/cli/bmad-plus-cli.js +16 -1
- package/tools/cli/commands/doctor.js +49 -217
- package/tools/cli/commands/install.js +109 -164
- package/tools/cli/commands/studio.js +68 -0
- package/tools/cli/commands/uninstall.js +173 -65
- package/tools/cli/commands/update-check.js +31 -0
- package/tools/cli/commands/update-policy.js +39 -0
- package/tools/cli/commands/update.js +102 -113
- package/tools/cli/i18n.js +60 -0
- package/tools/cli/lib/ide-config.js +4 -261
- package/tools/cli/lib/install-manifest.js +17 -0
- package/tools/cli/lib/installation-health.js +360 -0
- package/tools/cli/lib/installed-adapters.js +89 -0
- package/tools/cli/lib/npm-runner.js +177 -0
- package/tools/cli/lib/pack-copy.js +62 -66
- package/tools/cli/lib/packs.js +546 -3
- package/tools/cli/lib/python-health.js +233 -0
- package/tools/cli/lib/python-provision.js +2 -2
- package/tools/cli/lib/studio.js +310 -0
- package/tools/cli/lib/update-check.js +153 -0
- package/tools/cli/lib/update-dispatch.js +182 -0
- package/tools/cli/lib/update-policy.js +90 -0
- package/tools/cli/lib/update-transaction.js +334 -0
- package/tools/release/publication-content.js +901 -0
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
const fs = require('node:fs');
|
|
43
43
|
const path = require('node:path');
|
|
44
44
|
const { execFileSync } = require('node:child_process');
|
|
45
|
-
const { loadRegistry, DEFAULT_REGISTRY_PATH } = require('./generate');
|
|
45
|
+
const { loadRegistry, buildDerived, DEFAULT_REGISTRY_PATH } = require('./generate');
|
|
46
46
|
|
|
47
47
|
const REPO_ROOT = path.join(__dirname, '..', '..');
|
|
48
48
|
|
|
@@ -84,12 +84,6 @@ function isFrozenHistoryRow(line) {
|
|
|
84
84
|
|
|
85
85
|
/* ── Derivation ─────────────────────────────────────────────────────────── */
|
|
86
86
|
|
|
87
|
-
/** Sum of list lengths for a given key across a pack's categories. */
|
|
88
|
-
function sumCategories(pack, key) {
|
|
89
|
-
if (!Array.isArray(pack.categories)) return 0;
|
|
90
|
-
return pack.categories.reduce((n, c) => n + (Array.isArray(c[key]) ? c[key].length : 0), 0);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
87
|
/** Count *.md agent files on disk for a packaged pack's categories/ tree. */
|
|
94
88
|
function countAgentFilesOnDisk(packDir) {
|
|
95
89
|
const root = path.join(REPO_ROOT, 'src', 'bmad-plus', 'packs', packDir, 'categories');
|
|
@@ -111,36 +105,28 @@ function countWorkflowFilesOnDisk(packDir) {
|
|
|
111
105
|
return fs.readdirSync(dir).filter((f) => f.endsWith('.md')).length;
|
|
112
106
|
}
|
|
113
107
|
|
|
114
|
-
/** Installer languages, read from the CLI's own i18n module (its SSOT). */
|
|
115
|
-
function deriveLanguages() {
|
|
116
|
-
const { LANGUAGES } = require(path.join(REPO_ROOT, 'tools', 'cli', 'i18n.js'));
|
|
117
|
-
return Object.keys(LANGUAGES);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
108
|
/**
|
|
121
109
|
* Derive every count this gate enforces, from registry.yaml + the filesystem.
|
|
122
110
|
* Cross-checks the registry against the files on disk: if they disagree, the
|
|
123
111
|
* derivation itself is untrustworthy and that is reported as a drift.
|
|
124
112
|
*/
|
|
125
|
-
function deriveTruth(registryPath = DEFAULT_REGISTRY_PATH) {
|
|
126
|
-
const registry = loadRegistry(registryPath);
|
|
127
|
-
const
|
|
113
|
+
function deriveTruth(registryPath = DEFAULT_REGISTRY_PATH, options = {}) {
|
|
114
|
+
const registry = typeof registryPath === 'string' ? loadRegistry(registryPath) : registryPath;
|
|
115
|
+
const derived = buildDerived(registry, options);
|
|
116
|
+
const packs = derived.packs;
|
|
128
117
|
const shield = packs.shield;
|
|
129
118
|
const devStudio = packs['dev-studio'];
|
|
130
|
-
|
|
131
|
-
const shieldAgents =
|
|
132
|
-
const shieldWorkflows =
|
|
133
|
-
const shieldCategories =
|
|
134
|
-
const shieldFrameworks =
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
const devStudioSubAgents = (devStudio.sub_agents || []).length;
|
|
138
|
-
const devStudioWorkflows =
|
|
139
|
-
(devStudio.workflows || []).length + sumCategories(devStudio, 'workflows');
|
|
119
|
+
const { installerAgents, totalAgents } = derived;
|
|
120
|
+
const shieldAgents = shield.categoryAgentCount;
|
|
121
|
+
const shieldWorkflows = shield.workflowCount;
|
|
122
|
+
const shieldCategories = shield.categoryCount;
|
|
123
|
+
const shieldFrameworks = shield.frameworkCount;
|
|
124
|
+
const devStudioSubAgents = devStudio.subAgentCount;
|
|
125
|
+
const devStudioWorkflows = devStudio.workflowCount;
|
|
140
126
|
|
|
141
127
|
const mismatches = [];
|
|
142
|
-
const onDiskAgents = countAgentFilesOnDisk(shield.pack_dir);
|
|
143
|
-
const onDiskWorkflows = countWorkflowFilesOnDisk(shield.pack_dir);
|
|
128
|
+
const onDiskAgents = countAgentFilesOnDisk(registry.packs.shield.pack_dir);
|
|
129
|
+
const onDiskWorkflows = countWorkflowFilesOnDisk(registry.packs.shield.pack_dir);
|
|
144
130
|
if (onDiskAgents !== null && onDiskAgents !== shieldAgents) {
|
|
145
131
|
mismatches.push(
|
|
146
132
|
`registry.yaml declares ${shieldAgents} shield category agents but ${onDiskAgents} agent files exist on disk`
|
|
@@ -152,26 +138,24 @@ function deriveTruth(registryPath = DEFAULT_REGISTRY_PATH) {
|
|
|
152
138
|
);
|
|
153
139
|
}
|
|
154
140
|
|
|
155
|
-
// Every distinct agent a user can talk to: the installer entry points plus
|
|
156
|
-
// the specialists the two packaged orchestrators route to.
|
|
157
|
-
const totalAgents = installerAgents + shieldAgents + devStudioSubAgents;
|
|
158
141
|
// Sizes of the declared per-category agent groups ("5 agents", "6 agents"…),
|
|
159
142
|
// which are legitimate claims wherever a category is described.
|
|
160
|
-
const perCategorySizes = new Set(
|
|
143
|
+
const perCategorySizes = new Set(shield.categoryAgentCounts);
|
|
161
144
|
// Sizes of the per-pack installer rosters ("3 agents (Scout, Chief, Judge)").
|
|
162
|
-
const perPackInstallerSizes = new Set(Object.values(packs).map((p) => p.
|
|
145
|
+
const perPackInstallerSizes = new Set(Object.values(packs).map((p) => p.installerAgentCount));
|
|
163
146
|
|
|
164
147
|
return {
|
|
165
|
-
packCount:
|
|
148
|
+
packCount: derived.packCount,
|
|
166
149
|
installerAgents,
|
|
167
150
|
shieldAgents,
|
|
168
151
|
shieldWorkflows,
|
|
169
152
|
shieldCategories,
|
|
170
153
|
shieldFrameworks,
|
|
154
|
+
shieldReferenceFiles: shield.referenceFiles,
|
|
171
155
|
devStudioSubAgents,
|
|
172
156
|
devStudioWorkflows,
|
|
173
157
|
totalAgents,
|
|
174
|
-
languages:
|
|
158
|
+
languages: derived.languages,
|
|
175
159
|
perCategorySizes,
|
|
176
160
|
// The COMPLETE vocabulary of numbers that may legitimately precede "agents"
|
|
177
161
|
// anywhere in the product's own copy. Default-deny: a value outside this set
|
|
@@ -188,7 +172,7 @@ function deriveTruth(registryPath = DEFAULT_REGISTRY_PATH) {
|
|
|
188
172
|
]),
|
|
189
173
|
// Same idea for the workflow dimension. Anchored lines get graded against
|
|
190
174
|
// the one pack they name; unanchored ones must still be *some* real total.
|
|
191
|
-
workflowCounts: new Set(
|
|
175
|
+
workflowCounts: new Set(Object.values(packs).map((p) => p.workflowCount).filter(Boolean)),
|
|
192
176
|
derivationMismatches: mismatches,
|
|
193
177
|
};
|
|
194
178
|
}
|
|
@@ -228,21 +212,18 @@ const FOREIGN_TABLE_HEADER_RE = /^\s*\|\s*BMAD-METHOD\s*\|/i;
|
|
|
228
212
|
* `\s` already covers U+00A0/U+202F, which the French copy uses before
|
|
229
213
|
* punctuation.
|
|
230
214
|
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* a claim of "27001 agents". A qualifier may still *contain* a hyphen
|
|
235
|
-
* ("multi-role", "GRC-Compliance-"); it just may not START with one.
|
|
215
|
+
* Hyphens allow compounds such as "12-language". The leading token guard in
|
|
216
|
+
* claimRe prevents digits inside identifiers such as `nist-800-53-agent` from
|
|
217
|
+
* becoming counts; the separator itself need not suppress legitimate claims.
|
|
236
218
|
*/
|
|
237
|
-
const GAP = '[\\s
|
|
219
|
+
const GAP = '[\\s*`-]*';
|
|
238
220
|
|
|
239
221
|
/** One qualifier word: hyphenated and accented forms included. */
|
|
240
222
|
const WORD = '[A-Za-zÀ-ÖØ-öø-ÿ][A-Za-zÀ-ÖØ-öø-ÿ-]*';
|
|
241
223
|
|
|
242
224
|
/**
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
* ("5 reasons why you should hire agents").
|
|
225
|
+
* Maximum qualifiers echoed in diagnostics. Longer adjective runs are still
|
|
226
|
+
* graded; clause/dimension boundaries below prevent cross-sentence claims.
|
|
246
227
|
*/
|
|
247
228
|
const MAX_QUALIFIERS = 4;
|
|
248
229
|
|
|
@@ -263,6 +244,10 @@ const NOT_A_QUALIFIER = new Set(
|
|
|
263
244
|
and or plus with across of in on for to the a an
|
|
264
245
|
et ou des les la le du dans sur pour avec y con de en und oder mit für`.split(/\s+/)
|
|
265
246
|
);
|
|
247
|
+
const DIMENSION_WORDS = new Set(
|
|
248
|
+
'agent agents agente agentes agenten workflow workflows pack packs test tests category categories catégorie catégories categoría categorías kategorie kategorien framework frameworks language languages langue langues idioma idiomas sprache sprachen skill skills phase phases level levels reference files'.split(' ')
|
|
249
|
+
);
|
|
250
|
+
const CLAUSE_WORDS = new Set('why you we they should would could must can will'.split(' '));
|
|
266
251
|
|
|
267
252
|
/**
|
|
268
253
|
* Quantity approximators sitting in front of the number. "over 47 agents" is
|
|
@@ -279,14 +264,13 @@ const APPROX_PREFIX_RE = new RegExp(
|
|
|
279
264
|
/**
|
|
280
265
|
* Build a /g/ claim regex for one dimension's noun alternatives.
|
|
281
266
|
*
|
|
282
|
-
* The leading
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* A claim's number is always a whole token.
|
|
267
|
+
* The leading guard forbids a number inside a word, version or identifier.
|
|
268
|
+
* Qualifier tokens require separators, so an unbounded adjective run does not
|
|
269
|
+
* create ambiguous character-by-character partitions when no noun follows.
|
|
286
270
|
*/
|
|
287
271
|
function claimRe(noun) {
|
|
288
272
|
return new RegExp(
|
|
289
|
-
|
|
273
|
+
`(?<![\\w.-])(\\d+)${GAP}(\\+)?${GAP}((?:${WORD}[\\s*\u0060-]+)*?)(${noun})\\b`,
|
|
290
274
|
'gi'
|
|
291
275
|
);
|
|
292
276
|
}
|
|
@@ -298,12 +282,13 @@ const PACKS_RE = claimRe('packs?');
|
|
|
298
282
|
const TESTS_RE = claimRe('tests?');
|
|
299
283
|
const FRAMEWORKS_RE = claimRe('frameworks?');
|
|
300
284
|
const LANGUAGES_RE = claimRe('languages?|langues?|idiomas?|Sprachen?');
|
|
285
|
+
const REFERENCE_FILES_RE = claimRe('reference files?');
|
|
301
286
|
|
|
302
287
|
/**
|
|
303
288
|
* Collect every claim `re` finds on `line`.
|
|
304
289
|
* `re` must be a /g/ regex; it is reset before use so it is safe to reuse.
|
|
305
|
-
*
|
|
306
|
-
*
|
|
290
|
+
* Dimension/clause crossings are discarded. Connectives truncate the echoed
|
|
291
|
+
* qualifiers without discarding a claim such as "38 compliance and GRC agents".
|
|
307
292
|
* @returns {{value:number, plus:boolean, approx:boolean, text:string}[]}
|
|
308
293
|
*/
|
|
309
294
|
function matchAll(re, line) {
|
|
@@ -312,14 +297,16 @@ function matchAll(re, line) {
|
|
|
312
297
|
let m;
|
|
313
298
|
while ((m = re.exec(line)) !== null) {
|
|
314
299
|
const qualifiers = (m[3] || '').split(/[^A-Za-zÀ-ÖØ-öø-ÿ]+/).filter(Boolean);
|
|
315
|
-
if (qualifiers.some((w) =>
|
|
300
|
+
if (qualifiers.some((w) => DIMENSION_WORDS.has(w.toLowerCase()) || CLAUSE_WORDS.has(w.toLowerCase()))) continue;
|
|
301
|
+
const connective = qualifiers.findIndex((w) => NOT_A_QUALIFIER.has(w.toLowerCase()));
|
|
302
|
+
const bounded = qualifiers.slice(0, Math.min(connective < 0 ? qualifiers.length : connective, MAX_QUALIFIERS));
|
|
316
303
|
out.push({
|
|
317
304
|
value: Number(m[1]),
|
|
318
305
|
plus: m[2] === '+',
|
|
319
306
|
approx: APPROX_PREFIX_RE.test(line.slice(0, m.index)),
|
|
320
307
|
// Markers are dropped from the echoed text so a bolded claim reports as
|
|
321
308
|
// `38 compliance agents`, not `38** compliance agents`. file:line locates it.
|
|
322
|
-
text: m[
|
|
309
|
+
text: [m[1] + (m[2] || ''), ...bounded, m[4]].join(' '),
|
|
323
310
|
});
|
|
324
311
|
}
|
|
325
312
|
return out;
|
|
@@ -334,6 +321,11 @@ function matchAll(re, line) {
|
|
|
334
321
|
* @returns {string[]} human-readable drift descriptions (empty when the line is fine)
|
|
335
322
|
*/
|
|
336
323
|
function checkLine(line, truth, { foreignSubject = false } = {}) {
|
|
324
|
+
// In a comparison table only the first cell describes the upstream product.
|
|
325
|
+
if (foreignSubject && line.trimStart().startsWith('|')) {
|
|
326
|
+
const ownCells = line.trim().split('|').slice(2, -1).join(' | ');
|
|
327
|
+
return checkLine(ownCells, truth);
|
|
328
|
+
}
|
|
337
329
|
const problems = [];
|
|
338
330
|
const shieldLine = SHIELD_ANCHOR.test(line);
|
|
339
331
|
const devStudioLine = DEV_STUDIO_ANCHOR.test(line);
|
|
@@ -466,6 +458,12 @@ function checkLine(line, truth, { foreignSubject = false } = {}) {
|
|
|
466
458
|
}
|
|
467
459
|
}
|
|
468
460
|
|
|
461
|
+
for (const { value, plus, approx, text } of matchAll(REFERENCE_FILES_RE, line)) {
|
|
462
|
+
if (value !== truth.shieldReferenceFiles || plus || approx) {
|
|
463
|
+
problems.push(`"${text}" — Shield has ${truth.shieldReferenceFiles} reference files on disk`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
469
467
|
return problems;
|
|
470
468
|
}
|
|
471
469
|
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Verify that real source and npm-package installs deliver the adapter contract.
|
|
4
|
+
* Uses existing dependencies, temporary projects and an isolated brain; no
|
|
5
|
+
* lifecycle scripts, dependency installation or Python provisioning are run.
|
|
6
|
+
*
|
|
7
|
+
* Run: node tools/build/check-install-contract.js
|
|
8
|
+
* Author: Laurent Rochetta
|
|
9
|
+
*/
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const assert = require('node:assert/strict');
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const os = require('node:os');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
const crypto = require('node:crypto');
|
|
17
|
+
const { spawnSync } = require('node:child_process');
|
|
18
|
+
|
|
19
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
20
|
+
const TEMP_PREFIX = 'bmad-install-contract-';
|
|
21
|
+
|
|
22
|
+
function resolveWithin(root, relative) {
|
|
23
|
+
assert(typeof relative === 'string' && relative.length > 0, 'Expected a relative file path');
|
|
24
|
+
assert(
|
|
25
|
+
!path.win32.isAbsolute(relative) && !path.posix.isAbsolute(relative),
|
|
26
|
+
`Absolute path: ${relative}`
|
|
27
|
+
);
|
|
28
|
+
const target = path.resolve(root, relative);
|
|
29
|
+
const fromRoot = path.relative(root, target);
|
|
30
|
+
assert(
|
|
31
|
+
fromRoot &&
|
|
32
|
+
!path.isAbsolute(fromRoot) &&
|
|
33
|
+
!fromRoot.startsWith(`..${path.sep}`) &&
|
|
34
|
+
fromRoot !== '..',
|
|
35
|
+
`Path escapes project: ${relative}`
|
|
36
|
+
);
|
|
37
|
+
return target;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sameSelection(actual, expected, label) {
|
|
41
|
+
assert(Array.isArray(actual), `Manifest ${label} must be an array`);
|
|
42
|
+
assert.deepEqual(
|
|
43
|
+
[...actual].sort(),
|
|
44
|
+
[...expected].sort(),
|
|
45
|
+
`Manifest ${label} differs from requested selection`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Compare the installed bytes, normalizing only Windows/Unix line endings. */
|
|
50
|
+
function verifyInstallation(
|
|
51
|
+
projectDir,
|
|
52
|
+
{ packs, tools, derived, ideConfigs, renderFiles, requireInventory = false }
|
|
53
|
+
) {
|
|
54
|
+
const manifest = JSON.parse(
|
|
55
|
+
fs.readFileSync(path.join(projectDir, '_bmad', '.bmad-plus-install.json'), 'utf8')
|
|
56
|
+
);
|
|
57
|
+
sameSelection(manifest.packs, packs, 'packs');
|
|
58
|
+
sameSelection(manifest.ides, tools, 'ides');
|
|
59
|
+
assert(typeof manifest.user === 'string' && manifest.user, 'Manifest user is missing');
|
|
60
|
+
assert(
|
|
61
|
+
typeof manifest.language === 'string' && manifest.language,
|
|
62
|
+
'Manifest language is missing'
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const files = renderFiles(derived, {
|
|
66
|
+
packs,
|
|
67
|
+
userName: manifest.user,
|
|
68
|
+
language: manifest.language,
|
|
69
|
+
tools: manifest.ides,
|
|
70
|
+
});
|
|
71
|
+
const expectedPaths = new Set(files.map(({ file }) => file));
|
|
72
|
+
assert.equal(expectedPaths.size, files.length, 'Renderer returned duplicate output paths');
|
|
73
|
+
assert.equal(
|
|
74
|
+
derived.targets.spine,
|
|
75
|
+
'AGENTS.md',
|
|
76
|
+
'Install contract requires the root AGENTS.md spine'
|
|
77
|
+
);
|
|
78
|
+
assert(
|
|
79
|
+
expectedPaths.has(derived.targets.spine),
|
|
80
|
+
'Renderer must include the root AGENTS.md spine'
|
|
81
|
+
);
|
|
82
|
+
for (const tool of tools) {
|
|
83
|
+
assert(expectedPaths.has(ideConfigs[tool].configFile), `Renderer omitted the ${tool} adapter`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const { file, content } of files) {
|
|
87
|
+
const target = resolveWithin(projectDir, file);
|
|
88
|
+
assert(fs.existsSync(target), `Missing installed file: ${file}`);
|
|
89
|
+
const actual = fs.readFileSync(target, 'utf8').replace(/\r\n/g, '\n');
|
|
90
|
+
const expected = content.replace(/\r\n/g, '\n');
|
|
91
|
+
assert(actual === expected, `Installed adapter differs from renderer: ${file}`);
|
|
92
|
+
}
|
|
93
|
+
for (const { configFile } of Object.values(ideConfigs)) {
|
|
94
|
+
assert(
|
|
95
|
+
expectedPaths.has(configFile) || !fs.existsSync(resolveWithin(projectDir, configFile)),
|
|
96
|
+
`Unselected adapter was installed: ${configFile}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (requireInventory) {
|
|
100
|
+
assert.equal(manifest.fileInventory?.schemaVersion, 1, 'Missing file inventory');
|
|
101
|
+
const { collectManagedFiles } = require('../cli/lib/update-transaction');
|
|
102
|
+
const managedFiles = collectManagedFiles({ projectDir, packs });
|
|
103
|
+
assert.deepEqual(
|
|
104
|
+
Object.keys(manifest.fileInventory.files).sort(),
|
|
105
|
+
managedFiles.map((entry) => entry.file).sort(),
|
|
106
|
+
'Inventory does not cover exactly the selected framework files'
|
|
107
|
+
);
|
|
108
|
+
for (const { file } of managedFiles) {
|
|
109
|
+
const actualHash = crypto
|
|
110
|
+
.createHash('sha256')
|
|
111
|
+
.update(fs.readFileSync(resolveWithin(projectDir, file)))
|
|
112
|
+
.digest('hex');
|
|
113
|
+
assert.equal(
|
|
114
|
+
manifest.fileInventory.files[file],
|
|
115
|
+
actualHash,
|
|
116
|
+
`Inventory differs from installed bytes: ${file}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return files.length;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function verifyDiagnostics(report, { packs, tools, derived }) {
|
|
124
|
+
assert.equal(report.schemaVersion, 1, 'Unsupported diagnostic report');
|
|
125
|
+
assert.equal(report.versions.installed, derived.product.version);
|
|
126
|
+
assert.equal(report.versions.cli, derived.product.version);
|
|
127
|
+
assert.equal(report.versions.published, null, 'Doctor must not invent npm freshness evidence');
|
|
128
|
+
sameSelection(
|
|
129
|
+
report.packs.map(({ id }) => id),
|
|
130
|
+
packs,
|
|
131
|
+
'diagnostic packs'
|
|
132
|
+
);
|
|
133
|
+
sameSelection(
|
|
134
|
+
report.integrations.map(({ tool }) => tool),
|
|
135
|
+
tools,
|
|
136
|
+
'diagnostic integrations'
|
|
137
|
+
);
|
|
138
|
+
for (const integration of report.integrations) {
|
|
139
|
+
assert.deepEqual(
|
|
140
|
+
integration,
|
|
141
|
+
{
|
|
142
|
+
tool: integration.tool,
|
|
143
|
+
...derived.diagnostics.integrations[integration.tool],
|
|
144
|
+
ownership: 'unchanged',
|
|
145
|
+
hostVerified: false,
|
|
146
|
+
},
|
|
147
|
+
'An installed adapter must not imply verified host execution'
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const expectedErrors = [];
|
|
151
|
+
assert.deepEqual(
|
|
152
|
+
report.checks
|
|
153
|
+
.filter(({ status }) => status === 'error')
|
|
154
|
+
.map(({ code, path: file }) => `${code}:${file}`)
|
|
155
|
+
.sort(),
|
|
156
|
+
expectedErrors.sort(),
|
|
157
|
+
'Every advertised pack must install without delivery errors'
|
|
158
|
+
);
|
|
159
|
+
assert.equal(report.summary.errors, expectedErrors.length);
|
|
160
|
+
assert.equal(report.inventory.status, 'complete');
|
|
161
|
+
assert(report.inventory.trackedFiles > 0, 'Doctor did not inspect the ownership inventory');
|
|
162
|
+
assert.equal(
|
|
163
|
+
report.checks.filter(({ code }) => code === 'file.unchanged').length,
|
|
164
|
+
report.inventory.trackedFiles,
|
|
165
|
+
'Every freshly installed managed file must have matching ownership'
|
|
166
|
+
);
|
|
167
|
+
assert(!report.checks.some(({ code }) => ['file.modified', 'file.unowned'].includes(code)));
|
|
168
|
+
for (const runtime of report.runtimes.filter(({ runtime }) => runtime === 'python')) {
|
|
169
|
+
assert.equal(
|
|
170
|
+
runtime.imports,
|
|
171
|
+
'not-verified',
|
|
172
|
+
'Presence of requirements cannot prove Python imports'
|
|
173
|
+
);
|
|
174
|
+
assert.equal(runtime.environment, 'missing', 'Contract checks must not provision Python');
|
|
175
|
+
}
|
|
176
|
+
const expectedStatus = expectedErrors.length
|
|
177
|
+
? 'error'
|
|
178
|
+
: report.summary.warnings
|
|
179
|
+
? 'warning'
|
|
180
|
+
: 'ok';
|
|
181
|
+
assert.equal(report.status, expectedStatus);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function run(command, args, { cwd, env, timeout = 60000, expectedExitCode = 0 }) {
|
|
185
|
+
const result = spawnSync(command, args, {
|
|
186
|
+
cwd,
|
|
187
|
+
env,
|
|
188
|
+
timeout,
|
|
189
|
+
encoding: 'utf8',
|
|
190
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
191
|
+
windowsHide: true,
|
|
192
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
193
|
+
});
|
|
194
|
+
if (result.error || result.status !== expectedExitCode) {
|
|
195
|
+
const reason = result.error
|
|
196
|
+
? result.error.message
|
|
197
|
+
: `exit ${result.status}, signal ${result.signal || 'none'}`;
|
|
198
|
+
throw new Error(
|
|
199
|
+
`${path.basename(command)} ${args.join(' ')}: ${reason}\n${result.stdout || ''}${result.stderr || ''}`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
return result.stdout;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Invoke npm without a shell, including Windows where npm.cmd is not executable. */
|
|
206
|
+
function npmCommand(env) {
|
|
207
|
+
const candidates = [
|
|
208
|
+
env.npm_execpath,
|
|
209
|
+
path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
210
|
+
...(env.PATH || env.Path || '')
|
|
211
|
+
.split(path.delimiter)
|
|
212
|
+
.map((dir) => path.join(dir, 'node_modules', 'npm', 'bin', 'npm-cli.js')),
|
|
213
|
+
];
|
|
214
|
+
const cli = candidates.find(
|
|
215
|
+
(candidate) => candidate && /npm-cli\.js$/i.test(candidate) && fs.existsSync(candidate)
|
|
216
|
+
);
|
|
217
|
+
if (cli) return { command: process.execPath, args: [cli] };
|
|
218
|
+
assert(process.platform !== 'win32', 'Could not locate npm-cli.js beside Node or on PATH');
|
|
219
|
+
return { command: 'npm', args: [] };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function packAndExtract(tempRoot, env) {
|
|
223
|
+
const destination = path.join(tempRoot, 'distribution');
|
|
224
|
+
fs.mkdirSync(destination);
|
|
225
|
+
const npm = npmCommand(env);
|
|
226
|
+
const output = run(
|
|
227
|
+
npm.command,
|
|
228
|
+
[
|
|
229
|
+
...npm.args,
|
|
230
|
+
'pack',
|
|
231
|
+
'--ignore-scripts',
|
|
232
|
+
'--offline',
|
|
233
|
+
'--json',
|
|
234
|
+
'--pack-destination',
|
|
235
|
+
destination,
|
|
236
|
+
'--cache',
|
|
237
|
+
path.join(tempRoot, 'npm-cache'),
|
|
238
|
+
],
|
|
239
|
+
{ cwd: REPO_ROOT, env, timeout: 120000 }
|
|
240
|
+
);
|
|
241
|
+
const packed = JSON.parse(output);
|
|
242
|
+
assert(Array.isArray(packed) && packed.length === 1, 'npm pack must produce exactly one package');
|
|
243
|
+
const archive = resolveWithin(destination, packed[0].filename);
|
|
244
|
+
const entries = run('tar', ['-tzf', archive], { cwd: destination, env }).trim().split(/\r?\n/);
|
|
245
|
+
for (const entry of entries) {
|
|
246
|
+
assert(entry.startsWith('package/'), `Unexpected npm archive entry: ${entry}`);
|
|
247
|
+
resolveWithin(destination, entry);
|
|
248
|
+
}
|
|
249
|
+
run('tar', ['-xzf', archive, '-C', destination], { cwd: destination, env });
|
|
250
|
+
const packageRoot = path.join(destination, 'package');
|
|
251
|
+
assert(
|
|
252
|
+
!fs.existsSync(path.join(packageRoot, 'registry.yaml')),
|
|
253
|
+
'npm contract must work without registry.yaml'
|
|
254
|
+
);
|
|
255
|
+
assert(fs.statSync(packageRoot).isDirectory(), 'npm package directory is missing');
|
|
256
|
+
return packageRoot;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function runMatrix(packageRoot, variant, tempRoot, env, reference) {
|
|
260
|
+
const allPacks = Object.keys(reference.packDefinitions).filter(
|
|
261
|
+
(id) => !reference.packDefinitions[id].disabled
|
|
262
|
+
);
|
|
263
|
+
const allTools = Object.keys(reference.ideConfigs);
|
|
264
|
+
assert(allTools.length > 0, 'IDE_CONFIGS must expose at least one target');
|
|
265
|
+
const selections = [
|
|
266
|
+
{ argument: 'core', packs: ['core'] },
|
|
267
|
+
{ argument: 'core,osint', packs: ['core', 'osint'] },
|
|
268
|
+
{ argument: 'core,memory', packs: ['core', 'memory'] },
|
|
269
|
+
{ argument: 'all', packs: allPacks },
|
|
270
|
+
];
|
|
271
|
+
const toolSelections = [...allTools.map((tool) => [tool]), allTools];
|
|
272
|
+
let installations = 0;
|
|
273
|
+
let files = 0;
|
|
274
|
+
for (const selection of selections) {
|
|
275
|
+
for (const tools of toolSelections) {
|
|
276
|
+
const label = `${variant}/${selection.argument}/${tools.join(',')}`;
|
|
277
|
+
const projectDir = path.join(tempRoot, `${variant}-project-${++installations}`);
|
|
278
|
+
fs.mkdirSync(projectDir);
|
|
279
|
+
try {
|
|
280
|
+
run(
|
|
281
|
+
process.execPath,
|
|
282
|
+
[
|
|
283
|
+
path.join(packageRoot, 'tools', 'cli', 'bmad-plus-cli.js'),
|
|
284
|
+
'install',
|
|
285
|
+
'--yes',
|
|
286
|
+
'--lang',
|
|
287
|
+
'en',
|
|
288
|
+
'--directory',
|
|
289
|
+
projectDir,
|
|
290
|
+
'--packs',
|
|
291
|
+
selection.argument,
|
|
292
|
+
'--tools',
|
|
293
|
+
tools.join(','),
|
|
294
|
+
],
|
|
295
|
+
{ cwd: projectDir, env }
|
|
296
|
+
);
|
|
297
|
+
files += verifyInstallation(projectDir, { ...reference, packs: selection.packs, tools });
|
|
298
|
+
const expectedExitCode = 0;
|
|
299
|
+
const diagnosis = run(
|
|
300
|
+
process.execPath,
|
|
301
|
+
[
|
|
302
|
+
path.join(packageRoot, 'tools', 'cli', 'bmad-plus-cli.js'),
|
|
303
|
+
'doctor',
|
|
304
|
+
'--json',
|
|
305
|
+
'--directory',
|
|
306
|
+
projectDir,
|
|
307
|
+
],
|
|
308
|
+
{ cwd: projectDir, env, expectedExitCode }
|
|
309
|
+
);
|
|
310
|
+
verifyDiagnostics(JSON.parse(diagnosis), {
|
|
311
|
+
packs: selection.packs,
|
|
312
|
+
tools,
|
|
313
|
+
derived: reference.derived,
|
|
314
|
+
variant,
|
|
315
|
+
});
|
|
316
|
+
if (selection.packs.includes('dev-studio')) {
|
|
317
|
+
const list = JSON.parse(
|
|
318
|
+
run(
|
|
319
|
+
process.execPath,
|
|
320
|
+
[
|
|
321
|
+
path.join(packageRoot, 'tools/cli/bmad-plus-cli.js'),
|
|
322
|
+
'studio',
|
|
323
|
+
'list',
|
|
324
|
+
'--directory',
|
|
325
|
+
projectDir,
|
|
326
|
+
'--json',
|
|
327
|
+
],
|
|
328
|
+
{ cwd: projectDir, env }
|
|
329
|
+
)
|
|
330
|
+
);
|
|
331
|
+
assert.equal(list.workflows.length, reference.derived.packs['dev-studio'].workflowCount);
|
|
332
|
+
const prepared = JSON.parse(
|
|
333
|
+
run(
|
|
334
|
+
process.execPath,
|
|
335
|
+
[
|
|
336
|
+
path.join(packageRoot, 'tools/cli/bmad-plus-cli.js'),
|
|
337
|
+
'studio',
|
|
338
|
+
'prepare',
|
|
339
|
+
'create-ux-design',
|
|
340
|
+
'--request',
|
|
341
|
+
'Design the sign-in flow',
|
|
342
|
+
'--directory',
|
|
343
|
+
projectDir,
|
|
344
|
+
'--json',
|
|
345
|
+
],
|
|
346
|
+
{ cwd: projectDir, env }
|
|
347
|
+
)
|
|
348
|
+
);
|
|
349
|
+
assert.equal(prepared.status, 'ready');
|
|
350
|
+
assert.equal(prepared.executed, false);
|
|
351
|
+
assert(prepared.instructions.some((item) => item.path.endsWith('/create-ux-design.md')));
|
|
352
|
+
assert(!fs.existsSync(path.join(projectDir, prepared.outputPath)));
|
|
353
|
+
}
|
|
354
|
+
if (tools.length === allTools.length) {
|
|
355
|
+
const output = run(
|
|
356
|
+
process.execPath,
|
|
357
|
+
[
|
|
358
|
+
path.join(packageRoot, 'tools', 'cli', 'bmad-plus-cli.js'),
|
|
359
|
+
'update-check',
|
|
360
|
+
'--offline',
|
|
361
|
+
'--json',
|
|
362
|
+
'--directory',
|
|
363
|
+
projectDir,
|
|
364
|
+
],
|
|
365
|
+
{ cwd: projectDir, env }
|
|
366
|
+
);
|
|
367
|
+
const checked = JSON.parse(output);
|
|
368
|
+
assert.equal(
|
|
369
|
+
checked.status,
|
|
370
|
+
'unknown',
|
|
371
|
+
'Offline check must not claim the latest release'
|
|
372
|
+
);
|
|
373
|
+
assert.equal(checked.updateAvailable, null);
|
|
374
|
+
assert.equal(checked.canAutoApply, false);
|
|
375
|
+
}
|
|
376
|
+
} catch (error) {
|
|
377
|
+
throw new Error(`${label}: ${error.message}`, { cause: error });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
console.log(
|
|
381
|
+
`OK ${variant}: ${selection.argument} across ${toolSelections.length} tool selections`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
return { installations, files };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function main() {
|
|
388
|
+
const { PACKS, DERIVED } = require('../cli/lib/packs');
|
|
389
|
+
const { IDE_CONFIGS } = require('../cli/lib/ide-config');
|
|
390
|
+
const { generateUserFiles } = require('./generate-adapters');
|
|
391
|
+
assert(
|
|
392
|
+
DERIVED && typeof generateUserFiles === 'function',
|
|
393
|
+
'Generated install contract exports are missing'
|
|
394
|
+
);
|
|
395
|
+
const reference = {
|
|
396
|
+
packDefinitions: PACKS,
|
|
397
|
+
derived: DERIVED,
|
|
398
|
+
ideConfigs: IDE_CONFIGS,
|
|
399
|
+
renderFiles: generateUserFiles,
|
|
400
|
+
requireInventory: true,
|
|
401
|
+
};
|
|
402
|
+
const tempParent = fs.realpathSync(os.tmpdir());
|
|
403
|
+
const tempRoot = fs.mkdtempSync(path.join(tempParent, TEMP_PREFIX));
|
|
404
|
+
try {
|
|
405
|
+
const brain = path.join(tempRoot, 'brain');
|
|
406
|
+
fs.mkdirSync(brain);
|
|
407
|
+
const env = {
|
|
408
|
+
...process.env,
|
|
409
|
+
BMAD_PLUS_BRAIN: brain,
|
|
410
|
+
NODE_PATH: path.join(REPO_ROOT, 'node_modules'),
|
|
411
|
+
NO_COLOR: '1',
|
|
412
|
+
NPM_CONFIG_UPDATE_NOTIFIER: 'false',
|
|
413
|
+
};
|
|
414
|
+
const source = runMatrix(REPO_ROOT, 'source', tempRoot, env, reference);
|
|
415
|
+
const packageRoot = packAndExtract(tempRoot, env);
|
|
416
|
+
const { checkPythonDelivery } = require('./pack-delivery');
|
|
417
|
+
const { loadRegistry } = require('./generate');
|
|
418
|
+
assert.deepEqual(
|
|
419
|
+
checkPythonDelivery(loadRegistry(), {
|
|
420
|
+
packageRoot,
|
|
421
|
+
packageJson: JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')),
|
|
422
|
+
}),
|
|
423
|
+
[],
|
|
424
|
+
'Declared Python resources must exist in the real npm archive'
|
|
425
|
+
);
|
|
426
|
+
const packaged = runMatrix(packageRoot, 'npm', tempRoot, env, reference);
|
|
427
|
+
console.log(
|
|
428
|
+
`Install contract passed: ${source.installations + packaged.installations} installs and doctor JSON reports, ${source.files + packaged.files} files compared (source + npm without registry.yaml).`
|
|
429
|
+
);
|
|
430
|
+
return 0;
|
|
431
|
+
} finally {
|
|
432
|
+
// Delete only this mkdtemp-owned direct child of the verified temp directory.
|
|
433
|
+
assert(
|
|
434
|
+
path.dirname(tempRoot) === tempParent && path.basename(tempRoot).startsWith(TEMP_PREFIX)
|
|
435
|
+
);
|
|
436
|
+
assert(fs.realpathSync(tempRoot) === tempRoot && !fs.lstatSync(tempRoot).isSymbolicLink());
|
|
437
|
+
fs.rmSync(tempRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (require.main === module) {
|
|
442
|
+
try {
|
|
443
|
+
process.exitCode = main();
|
|
444
|
+
} catch (error) {
|
|
445
|
+
console.error(`Install contract failed: ${error.message}`);
|
|
446
|
+
process.exitCode = 1;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
module.exports = { main, resolveWithin, verifyInstallation };
|