aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
95 lines (81 loc) • 3.58 kB
JavaScript
/**
* Install-size budget gate.
*
* Runs `npm pack --dry-run --json`, reads the reported packed + unpacked
* sizes, and fails with a non-zero exit code if either exceeds the budget.
*
* Budgets are set intentionally above current measured values so we catch
* new regressions rather than baselining today's number. They can be
* overridden via env for specific CI needs:
*
* AIWG_PACK_PACKED_BUDGET_KB — default 12000 (12 MB)
* AIWG_PACK_UNPACKED_BUDGET_KB — default 40000 (40 MB)
* AIWG_PACK_FILES_BUDGET — default 4000 (file count)
*
* The 5 MB install-size target in Phase 6 (#923) is aspirational and
* requires a bundler migration — tracked separately. This gate prevents
* the package from ballooning in the meantime.
*
* Usage: node tools/cli/check-install-size.mjs [--verbose]
*/
import { execSync } from 'child_process';
const PACKED_BUDGET_KB = parseIntEnv('AIWG_PACK_PACKED_BUDGET_KB', 12_000);
const UNPACKED_BUDGET_KB = parseIntEnv('AIWG_PACK_UNPACKED_BUDGET_KB', 40_000);
const FILES_BUDGET = parseIntEnv('AIWG_PACK_FILES_BUDGET', 4_000);
const verbose = process.argv.includes('--verbose');
function parseIntEnv(name, def) {
const raw = process.env[name];
const n = raw ? parseInt(raw, 10) : NaN;
return Number.isFinite(n) && n > 0 ? n : def;
}
function fmtKb(bytes) {
return `${(bytes / 1024).toFixed(1)} KB`;
}
function fmtMb(bytes) {
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
}
try {
// `npm pack --dry-run --json` returns an array with one entry per tarball.
// We use the first entry (single package).
const raw = execSync('npm pack --dry-run --json', { stdio: ['ignore', 'pipe', 'pipe'] });
const out = JSON.parse(raw.toString());
const pkg = Array.isArray(out) ? out[0] : out;
if (!pkg || typeof pkg.size !== 'number') {
console.error('check-install-size: unexpected npm pack output');
process.exit(1);
}
const packedKB = pkg.size / 1024;
const unpackedKB = pkg.unpackedSize / 1024;
const fileCount = Array.isArray(pkg.files) ? pkg.files.length : (pkg.fileCount ?? 0);
if (verbose) {
console.log(`Package: ${pkg.name}@${pkg.version}`);
console.log(`Packed: ${fmtKb(pkg.size)} (budget ${PACKED_BUDGET_KB} KB)`);
console.log(`Unpacked: ${fmtMb(pkg.unpackedSize)} (budget ${UNPACKED_BUDGET_KB / 1024} MB)`);
console.log(`File count: ${fileCount} (budget ${FILES_BUDGET})`);
console.log('');
}
const failures = [];
if (packedKB > PACKED_BUDGET_KB) {
failures.push(`Packed size ${fmtKb(pkg.size)} exceeds budget of ${PACKED_BUDGET_KB} KB`);
}
if (unpackedKB > UNPACKED_BUDGET_KB) {
failures.push(`Unpacked size ${fmtMb(pkg.unpackedSize)} exceeds budget of ${UNPACKED_BUDGET_KB / 1024} MB`);
}
if (fileCount > FILES_BUDGET) {
failures.push(`File count ${fileCount} exceeds budget of ${FILES_BUDGET}`);
}
if (failures.length > 0) {
console.error('check-install-size: FAILED');
for (const f of failures) console.error(` - ${f}`);
console.error('');
console.error('Budgets are set above current measured values to catch regressions.');
console.error('If the increase is intentional, bump AIWG_PACK_*_BUDGET* and document why.');
process.exit(1);
}
console.log(`check-install-size: OK (${fmtKb(pkg.size)} packed / ${fmtMb(pkg.unpackedSize)} unpacked / ${fileCount} files)`);
} catch (err) {
console.error('check-install-size: failed to measure package size');
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}