Files
t3mp3st/scripts/sync-bt6-maintainer.mjs
T
Joseph Magly afc9dad1b2 chore(aiwg): adopt shared BT6 maintainer workflow (#148)
* docs(policy): allow low-impact contributor courtesy cleanup

* chore(aiwg): migrate to shared BT6 maintainer plugin

* chore: reconcile delivery validation
2026-08-02 17:17:28 -04:00

134 lines
5.1 KiB
JavaScript

#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import {
cpSync,
existsSync,
mkdtempSync,
readFileSync,
readdirSync,
renameSync,
rmSync,
statSync,
} from 'node:fs';
import { createHash } from 'node:crypto';
import { basename, dirname, join, relative, resolve } from 'node:path';
const projectRoot = resolve(import.meta.dirname, '..');
const lockPath = join(projectRoot, '.aiwg', 'bt6-maintainer.lock.json');
const lock = JSON.parse(readFileSync(lockPath, 'utf8'));
const checkOnly = process.argv.includes('--check');
function run(command, args, options = {}) {
return execFileSync(command, args, {
cwd: projectRoot,
encoding: 'utf8',
stdio: options.capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
});
}
function capture(command, args) {
return run(command, args, { capture: true });
}
function filesUnder(root, current = root) {
const files = [];
for (const entry of readdirSync(current, { withFileTypes: true })) {
const path = join(current, entry.name);
if (entry.isDirectory()) files.push(...filesUnder(root, path));
if (entry.isFile()) files.push(relative(root, path));
}
return files.sort();
}
function digest(path) {
return createHash('sha256').update(readFileSync(path)).digest('hex');
}
function compareTrees(expectedRoot, actualRoot) {
const expectedFiles = filesUnder(expectedRoot);
const actualFiles = existsSync(actualRoot) ? filesUnder(actualRoot) : [];
const missing = expectedFiles.filter((path) => !actualFiles.includes(path));
const extra = actualFiles.filter((path) => !expectedFiles.includes(path));
const changed = expectedFiles.filter(
(path) => actualFiles.includes(path) && digest(join(expectedRoot, path)) !== digest(join(actualRoot, path)),
);
return { equal: missing.length === 0 && extra.length === 0 && changed.length === 0, missing, extra, changed };
}
function assertContained(parent, child, label) {
const parentPath = resolve(parent);
const childPath = resolve(child);
if (childPath !== parentPath && !childPath.startsWith(`${parentPath}/`)) {
throw new Error(`${label} escapes ${parentPath}: ${childPath}`);
}
}
const installRef = `${lock.source}@${lock.ref}`;
run('aiwg', ['install', installRef, '--refresh']);
const info = capture('aiwg', ['packages', 'info', lock.registryKey]);
const plainInfo = info.replace(/\x1b\[[0-9;]*m/g, '');
const cacheMatch = plainInfo.match(/^\s*Cache:\s*(.+)$/m);
if (!cacheMatch) throw new Error(`Unable to resolve AIWG package cache for ${lock.registryKey}`);
const cacheRoot = resolve(cacheMatch[1].trim());
const cachedCommit = capture('git', ['-C', cacheRoot, 'rev-parse', 'HEAD']).trim();
const cachedTree = capture('git', ['-C', cacheRoot, 'rev-parse', `HEAD:${lock.sourceWrapperPath}`]).trim();
if (cachedCommit !== lock.commit) {
throw new Error(`Pinned commit mismatch: expected ${lock.commit}, received ${cachedCommit}`);
}
if (cachedTree !== lock.wrapperTree) {
throw new Error(`Pinned wrapper tree mismatch: expected ${lock.wrapperTree}, received ${cachedTree}`);
}
const sourceWrapper = join(cacheRoot, lock.sourceWrapperPath);
assertContained(cacheRoot, sourceWrapper, 'source wrapper');
const targetWrapper = join(projectRoot, lock.wrapperPath);
assertContained(join(projectRoot, '.aiwg', 'plugins'), targetWrapper, 'target wrapper');
const manifest = JSON.parse(readFileSync(join(sourceWrapper, 'manifest.json'), 'utf8'));
if (manifest.id !== lock.package || manifest.type !== 'plugin' || manifest.version !== lock.version) {
throw new Error(`Wrapper identity mismatch for ${lock.package}@${lock.version}`);
}
const before = compareTrees(sourceWrapper, targetWrapper);
if (checkOnly) {
if (!before.equal) {
throw new Error(`Vendored wrapper differs: ${JSON.stringify(before)}`);
}
console.log(`bt6-maintainer ${lock.version} matches ${lock.commit} (${lock.wrapperTree})`);
process.exit(0);
}
const pluginsRoot = dirname(targetWrapper);
const stage = mkdtempSync(join(pluginsRoot, '.bt6-maintainer-stage-'));
const stagedWrapper = join(stage, basename(targetWrapper));
cpSync(sourceWrapper, stagedWrapper, { recursive: true, errorOnExist: true });
const backupRoot = join(projectRoot, '.aiwg', 'working', 'bt6-maintainer-sync-backup');
rmSync(backupRoot, { recursive: true, force: true });
if (existsSync(targetWrapper)) {
cpSync(targetWrapper, backupRoot, { recursive: true });
rmSync(targetWrapper, { recursive: true, force: true });
}
try {
renameSync(stagedWrapper, targetWrapper);
rmSync(stage, { recursive: true, force: true });
run('aiwg', ['use', lock.package]);
run('aiwg', ['use', lock.package, '--provider', 'codex']);
run('aiwg', ['index', 'build']);
run('aiwg', ['doctor', '--project-local']);
const after = compareTrees(sourceWrapper, targetWrapper);
if (!after.equal) throw new Error(`Post-deploy wrapper differs: ${JSON.stringify(after)}`);
rmSync(backupRoot, { recursive: true, force: true });
console.log(`Synchronized ${lock.package} ${lock.version} from ${lock.commit}`);
} catch (error) {
rmSync(targetWrapper, { recursive: true, force: true });
if (existsSync(backupRoot) && statSync(backupRoot).isDirectory()) {
renameSync(backupRoot, targetWrapper);
}
throw error;
}