93 lines
2.5 KiB
JavaScript
Executable File
93 lines
2.5 KiB
JavaScript
Executable File
#!/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)`);
|