#!/usr/bin/env node /** * Validates zone/data/manifest.json for Phase 3 manifest v2. * Exit 0 on success, 1 on validation errors (prints to stderr). */ import { readFileSync } from 'node:fs'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const manifestPath = resolve(root, 'data/manifest.json'); const errors = []; function fail(msg) { errors.push(msg); } function requireArray(name, value) { if (!Array.isArray(value) || value.length === 0) { fail(`${name} must be a non-empty array`); return false; } return true; } function requireObject(name, value) { if (value == null || typeof value !== 'object' || Array.isArray(value)) { fail(`${name} must be an object`); return false; } return true; } let manifest; try { const raw = readFileSync(manifestPath, 'utf8'); manifest = JSON.parse(raw); } catch (e) { console.error(`Cannot read manifest: ${manifestPath}: ${e.message}`); process.exit(1); } if (manifest.version !== 2) { fail(`version must be 2 (got ${manifest.version})`); } requireObject('dashboard', manifest.dashboard); requireArray('links', manifest.links); requireArray('builds', manifest.builds); requireObject('status', manifest.status); requireObject('integrations', manifest.integrations); requireArray('widgets', manifest.widgets); const widgetTypes = new Set([ 'queue', 'badge', 'timesheet', 'log', 'buildStatus', 'authoringDetail', ]); const widgetIds = new Set(); for (const w of manifest.widgets || []) { if (!w.id) fail('widget missing id'); else if (widgetIds.has(w.id)) fail(`duplicate widget id: ${w.id}`); else widgetIds.add(w.id); if (!w.type || !widgetTypes.has(w.type)) { fail(`widget ${w.id || '?'} has invalid type: ${w.type}`); } if (!w.title) fail(`widget ${w.id} missing title`); if (w.type === 'log' && !w.logKey) { fail(`widget ${w.id} type log requires logKey`); } if (w.type === 'buildStatus' && !w.lastRunKey) { fail(`widget ${w.id} type buildStatus requires lastRunKey`); } } for (const b of manifest.builds || []) { if (!b.id) fail('build missing id'); if (!b.lastRunKey) fail(`build ${b.id} missing lastRunKey`); } if (errors.length) { console.error('Manifest validation failed:'); for (const e of errors) console.error(` - ${e}`); process.exit(1); } console.log(`OK: ${manifestPath} (v2, ${manifest.widgets.length} widgets)`);