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
- Read the relPath in the error message and open that exact file; verify it exists at the repo root.
- Validate the JSON: node -e "JSON.parse(require('fs').readFileSync('<relPath>','utf8'))" — the resulting SyntaxError pinpoints the bad token.
- Remove any merge-conflict markers (<<<<<< / ======= / >>>>>>), trailing commas, or BOM/HTML, and re-save as clean UTF-8 JSON.
- 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.
- 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
- Run check-versions in pre-commit and CI so a corrupted manifest blocks the commit, not the release.
- When adding a manifest to VERSION_FILES, commit the file in the same change.
- Never hand-edit package.json during a merge — resolve conflicts in an editor that validates JSON on save.
- Treat a BOM-aware parse (strip /^\uFEFF/) as the single source of truth; do not re-add BOM stripping ad hoc.
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
- description for ${name} must be one line, no quotes, under 1
- skills/${name}/SKILL.md has no frontmatter
AI-assisted analysis of DietrichGebert/ponytail@2ed6c52c9d (2026-08-12).
Data as JSON: /api/errors/0ed72ea1b0cda0d7.
Report an issue: GitHub.