|
| 1 | +#!/usr/bin/env node |
| 2 | +// Verifies the three places that enumerate "which servers this suite ships" |
| 3 | +// agree with each other: |
| 4 | +// 1. README.md's "The Suite" table (the human-facing source of truth) |
| 5 | +// 2. tests/check-packages.js's NPM_PACKAGES / PYPI_PACKAGES (the registry |
| 6 | +// existence check) |
| 7 | +// 3. setup.sh's npm install loop (the one-shot installer) |
| 8 | +// |
| 9 | +// These three lists are maintained by hand in three different file formats. |
| 10 | +// Each one looks correct read in isolation — the bug only shows up when you |
| 11 | +// check that they actually agree. A server added to the README table with no |
| 12 | +// matching entry in check-packages.js silently stops being verified; a |
| 13 | +// server missing from setup.sh's install loop silently stops being |
| 14 | +// installed. Exit 0 = all three agree. Exit 1 = drift detected. |
| 15 | + |
| 16 | +const fs = require("fs"); |
| 17 | +const path = require("path"); |
| 18 | + |
| 19 | +const ROOT = path.join(__dirname, ".."); |
| 20 | +const README_PATH = path.join(ROOT, "README.md"); |
| 21 | +const CHECK_PACKAGES_PATH = path.join(ROOT, "tests", "check-packages.js"); |
| 22 | +const SETUP_SH_PATH = path.join(ROOT, "setup.sh"); |
| 23 | + |
| 24 | +// Hand-picked anchor: the exact suite as of this task's spec. If this list |
| 25 | +// itself needs to change, that's a deliberate suite change, not drift. |
| 26 | +const EXPECTED_NPM_PACKAGES = [ |
| 27 | + "mcp-google-ads", |
| 28 | + "mcp-bing-ads", |
| 29 | + "mcp-linkedin-ads", |
| 30 | + "mcp-reddit-ads", |
| 31 | + "mcp-ga4", |
| 32 | + "mcp-google-gsc", |
| 33 | + "mcp-gtm-ga4", |
| 34 | +]; |
| 35 | +const EXPECTED_PYPI_PACKAGES = ["meta-ads-mcp"]; |
| 36 | + |
| 37 | +function readmeInstallCommands(readme) { |
| 38 | + const npm = [...readme.matchAll(/`npm install (mcp-[a-z0-9-]+)`/g)].map((m) => m[1]); |
| 39 | + const pip = [...readme.matchAll(/`pip install ([a-z0-9-]+)`/g)].map((m) => m[1]); |
| 40 | + return { npm, pip }; |
| 41 | +} |
| 42 | + |
| 43 | +function checkPackagesArray(source, varName) { |
| 44 | + const re = new RegExp(`const ${varName} = \\[([^\\]]*)\\]`, "s"); |
| 45 | + const match = source.match(re); |
| 46 | + if (!match) throw new Error(`Could not find ${varName} in check-packages.js`); |
| 47 | + return [...match[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]); |
| 48 | +} |
| 49 | + |
| 50 | +function setupShNpmPackages(source) { |
| 51 | + const match = source.match(/for pkg in ([a-z0-9 -]+); do/); |
| 52 | + if (!match) throw new Error("Could not find npm install loop in setup.sh"); |
| 53 | + return match[1].trim().split(/\s+/); |
| 54 | +} |
| 55 | + |
| 56 | +function sorted(arr) { |
| 57 | + return [...arr].sort(); |
| 58 | +} |
| 59 | + |
| 60 | +let failures = 0; |
| 61 | +function assertSameMembers(label, actual, expected) { |
| 62 | + const a = sorted(actual); |
| 63 | + const e = sorted(expected); |
| 64 | + const ok = a.length === e.length && a.every((v, i) => v === e[i]); |
| 65 | + if (ok) { |
| 66 | + console.log(` ✓ ${label}`); |
| 67 | + } else { |
| 68 | + console.log(` ✗ ${label}`); |
| 69 | + console.log(` expected: ${JSON.stringify(e)}`); |
| 70 | + console.log(` actual: ${JSON.stringify(a)}`); |
| 71 | + failures++; |
| 72 | + } |
| 73 | +} |
| 74 | + |
| 75 | +const readme = fs.readFileSync(README_PATH, "utf8"); |
| 76 | +const checkPackagesSrc = fs.readFileSync(CHECK_PACKAGES_PATH, "utf8"); |
| 77 | +const setupShSrc = fs.readFileSync(SETUP_SH_PATH, "utf8"); |
| 78 | + |
| 79 | +const { npm: readmeNpm, pip: readmePip } = readmeInstallCommands(readme); |
| 80 | +const scriptNpm = checkPackagesArray(checkPackagesSrc, "NPM_PACKAGES"); |
| 81 | +const scriptPypi = checkPackagesArray(checkPackagesSrc, "PYPI_PACKAGES"); |
| 82 | +const setupNpm = setupShNpmPackages(setupShSrc); |
| 83 | + |
| 84 | +console.log(""); |
| 85 | +console.log("Suite manifest sync (README ↔ check-packages.js ↔ setup.sh)"); |
| 86 | +console.log("─".repeat(60)); |
| 87 | + |
| 88 | +// Anchors: README itself must list exactly the expected suite. |
| 89 | +assertSameMembers("README table lists the expected 7 npm packages", readmeNpm, EXPECTED_NPM_PACKAGES); |
| 90 | +assertSameMembers("README table lists the expected PyPI package", readmePip, EXPECTED_PYPI_PACKAGES); |
| 91 | + |
| 92 | +// Joins: the two mirrors must agree with the README, not just look plausible on their own. |
| 93 | +assertSameMembers("check-packages.js NPM_PACKAGES matches README", scriptNpm, readmeNpm); |
| 94 | +assertSameMembers("check-packages.js PYPI_PACKAGES matches README", scriptPypi, readmePip); |
| 95 | +assertSameMembers("setup.sh install loop matches README's npm packages", setupNpm, readmeNpm); |
| 96 | + |
| 97 | +console.log(""); |
| 98 | +if (failures > 0) { |
| 99 | + console.error(`${failures} manifest mismatch(es) found.`); |
| 100 | + process.exit(1); |
| 101 | +} else { |
| 102 | + console.log("All manifests agree."); |
| 103 | +} |
0 commit comments