DietrichGebert/ponytail · error · Error

${relPath}: ${e.message}

Error message

${relPath}: ${e.message}

What it means

Thrown by readVersion() in scripts/check-versions.js. For each path in VERSION_FILES, the function reads the file (stripping a leading UTF-8 BOM), JSON.parse()s it, and returns .version. Any read or parse failure is caught and re-thrown as a wrapped error prefixed with the relPath so the failing manifest is identifiable. This is a fan-in wrapper: the original cause (ENOENT, SyntaxError, EACCES) is preserved in e.message and only the relPath context is added. Note a missing 'version' field does NOT throw here — it returns undefined and is flagged later by the PINNED_SEMVER check.

Source

Thrown at scripts/check-versions.js:38

// manifests here so a future ecosystem can't drift unnoticed.
const VERSION_FILES = [
  '.claude-plugin/plugin.json',  // Claude Code plugin — what users install
  '.codex-plugin/plugin.json',   // Codex plugin
  '.devin-plugin/plugin.json',   // Devin CLI plugin
  '.github/plugin/plugin.json',  // Copilot plugin
  '.qoder-plugin/plugin.json',   // Qoder plugin
  'gemini-extension.json',       // Gemini CLI extension
  'package.json',                // pi-package / repo root
  'ponytail-mcp/package.json',   // MCP server (private, internal-only)
];

function readVersion(relPath) {
  try {
    // Strip a UTF-8 BOM some Windows editors prepend (breaks JSON.parse).
    const raw = fs.readFileSync(path.join(root, relPath), 'utf8').replace(/^\uFEFF/, '');
    return JSON.parse(raw).version;
  } catch (e) {
    throw new Error(`${relPath}: ${e.message}`);
  }
}

let failed = false;
const versions = VERSION_FILES.map((relPath) => {
  const version = readVersion(relPath);
  if (typeof version !== 'string' || !PINNED_SEMVER.test(version)) {
    console.error(`${relPath}: version must be a pinned X.Y.Z semver, got ${JSON.stringify(version)}`);
    failed = true;
  }
  return [relPath, version];
});

// Every file must declare the same version.
const distinct = [...new Set(versions.map(([, v]) => v))];
if (distinct.length > 1) {
  console.error('Version mismatch — every manifest must share one version:');
  for (const [relPath, version] of versions) console.error(`  ${version}\t${relPath}`);

View on GitHub (pinned to 2ed6c52c9d)

Solutions

  1. Read the relPath in the error message and open that exact file; verify it exists at the repo root.
  2. Validate the JSON: node -e "JSON.parse(require('fs').readFileSync('<relPath>','utf8'))" — the resulting SyntaxError pinpoints the bad token.
  3. Remove any merge-conflict markers (<<<<<< / ======= / >>>>>>), trailing commas, or BOM/HTML, and re-save as clean UTF-8 JSON.
  4. If the file genuinely should not exist yet, either create it with a {"version":"X.Y.Z"} body or remove its entry from VERSION_FILES until it ships.
  5. Re-run node scripts/check-versions.js; expect the PINNED_SEMVER / mismatch checks to take over once parsing succeeds.

Example fix

// before — ponytail-mcp/package.json (merge conflict left in file)
{
  "name": "ponytail-mcp",
<<<<<<< HEAD
  "version": "1.2.0"
=======
  "version": "1.2.1"
>>>>>>> main
}

// after — clean JSON, single pinned version
{
  "name": "ponytail-mcp",
  "version": "1.2.1"
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight: ensure every VERSION_FILES entry exists and parses before readVersion runs
const fs = require('fs'), path = require('path');
for (const rel of VERSION_FILES) {
  const p = path.join(root, rel);
  if (!fs.existsSync(p)) { console.error(`missing: ${rel}`); continue; }
  try { JSON.parse(fs.readFileSync(p, 'utf8').replace(/^\uFEFF/, '')); }
  catch (e) { console.error(`${rel}: ${e.message}`); }
}

Type guard

function isPinnedSemver(v) { return typeof v === 'string' && /^\d+\.\d+\.\d+$/.test(v); }

Try / catch

// Keep the existing relPath-prefixing wrapper; just preserve the cause chain
function readVersion(relPath) {
  try {
    const raw = fs.readFileSync(path.join(root, relPath), 'utf8').replace(/^\uFEFF/, '');
    const v = JSON.parse(raw).version;
    if (!isPinnedSemver(v)) throw new Error(`version must be pinned X.Y.Z, got ${JSON.stringify(v)}`);
    return v;
  } catch (e) {
    const wrapped = new Error(`${relPath}: ${e.message}`);
    wrapped.cause = e;
    throw wrapped;
  }
}

Prevention

When it happens

Trigger: Running node scripts/check-versions.js (typically via npm version / release / CI) when one of VERSION_FILES is missing, unreadable, or contains invalid JSON (merge-conflict markers, trailing comma, unterminated string, HTML error page saved as JSON). Adding a new manifest path to VERSION_FILES before the file exists. A botched merge leaves conflict markers inside package.json.

Common situations: A new manifest (e.g. gemini-extension.json) is added to VERSION_FILES but not yet committed/created. A package.json is corrupted by a merge conflict or a manual edit with a trailing comma. File permissions block reads in CI. A version bump script renames/moves a manifest and forgets to update VERSION_FILES.

Related errors


AI-assisted analysis of DietrichGebert/ponytail@2ed6c52c9d (2026-08-12). Data as JSON: /api/errors/0ed72ea1b0cda0d7. Report an issue: GitHub.