hub/apps/matrix reads apps/list and each app's config on every server (the read token first, the admin token once if a server refuses) and returns per app and server the version, available update and a config hash, plus whether versions and configs match and how many JSON leaf paths differ. Config texts never leave the hub. In hub mode the Apps section has the server selector: All servers shows the matrix with filters for differing versions, differing configs and updates; one server shows its list without write actions. The dev mock takes --variant a|b with differing apps. Co-Authored-By: Claude Opus 5.5 <[email protected]>
39 lines
1.4 KiB
TypeScript
39 lines
1.4 KiB
TypeScript
/*
|
|
A small structural diff of two JSON values: objects by key, arrays by index. A leaf that
|
|
differs, a key present on one side only, and a change of type each give one entry.
|
|
*/
|
|
export interface JsonDifference {
|
|
path: string
|
|
a: unknown
|
|
b: unknown
|
|
}
|
|
|
|
const kind = (v: unknown): string => (v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v)
|
|
|
|
export function jsonDiff(a: unknown, b: unknown, path = ''): JsonDifference[] {
|
|
const ka = kind(a)
|
|
if (ka !== kind(b)) return [{ path, a, b }]
|
|
if (ka === 'array') {
|
|
const [xs, ys] = [a as unknown[], b as unknown[]]
|
|
const out: JsonDifference[] = []
|
|
for (let i = 0; i < Math.max(xs.length, ys.length); i++) {
|
|
const here = `${path}[${i}]`
|
|
if (i >= xs.length || i >= ys.length) out.push({ path: here, a: xs[i], b: ys[i] })
|
|
else out.push(...jsonDiff(xs[i], ys[i], here))
|
|
}
|
|
return out
|
|
}
|
|
if (ka === 'object') {
|
|
const [x, y] = [a as Record<string, unknown>, b as Record<string, unknown>]
|
|
const out: JsonDifference[] = []
|
|
for (const key of new Set([...Object.keys(x), ...Object.keys(y)])) {
|
|
const here = path === '' ? key : `${path}.${key}`
|
|
const [inX, inY] = [Object.hasOwn(x, key), Object.hasOwn(y, key)]
|
|
if (inX && inY) out.push(...jsonDiff(x[key], y[key], here))
|
|
else out.push({ path: here, a: x[key], b: y[key] })
|
|
}
|
|
return out
|
|
}
|
|
return a === b || (Number.isNaN(a) && Number.isNaN(b)) ? [] : [{ path, a, b }]
|
|
}
|