Files
charleswiltgen__axiom/scripts/pre-deploy.ts
T
Charles Wiltgen 4fd38fbb9a refactor: migrate scripts from Node to Deno runtime
Update shebangs, import prefixes (node:fs, node:child_process), and
package.json scripts to use Deno. Replace pre-deploy.js with
pre-deploy.ts for native TypeScript support.
2026-03-19 09:24:17 -07:00

556 lines
16 KiB
TypeScript

#!/usr/bin/env -S deno run --allow-read --allow-run --allow-env
/**
* Axiom Pre-Deploy Validation Suite
*
* Comprehensive validation that thousands of developers depend on.
* Run before every deploy: `deno task predeploy`
*
* Phase 1: Static validation (fast, no builds)
* Phase 2: Build validation (slower, requires tools)
*/
import fs from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";
const root = path.resolve(import.meta.dirname!, "..");
const pluginDir = path.join(root, ".claude-plugin/plugins/axiom");
let totalErrors = 0;
let totalWarnings = 0;
const errors: string[] = [];
const warnings: string[] = [];
function error(check: string, msg: string): void {
totalErrors++;
errors.push(` ✗ [${check}] ${msg}`);
}
function warn(check: string, msg: string): void {
totalWarnings++;
warnings.push(` ⚠ [${check}] ${msg}`);
}
function heading(title: string): void {
console.log(`\n── ${title} ──`);
}
interface Frontmatter {
[key: string]: string;
}
function parseFrontmatter(content: string): Frontmatter | null {
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) return null;
const fm: Frontmatter = {};
for (const line of match[1].split("\n")) {
const kv = line.match(/^(\w[\w-]*):\s*(.+)/);
if (kv) {
let val = kv[2].trim();
if (
(val.startsWith('"') && val.endsWith('"')) ||
(val.startsWith("'") && val.endsWith("'"))
) {
val = val.slice(1, -1);
}
fm[kv[1]] = val;
}
}
return fm;
}
interface PluginManifest {
version: string;
skills?: { name: string; description: string }[];
commands?: string[];
}
interface MarketplaceManifest {
plugins?: { name: string; version: string }[];
}
// ── Phase 1: Static Validation ──
heading("1. Plugin Validate (Claude Code)");
try {
execSync("claude plugin validate .", {
cwd: root,
stdio: "pipe",
encoding: "utf8",
});
console.log(" ✓ claude plugin validate passed");
} catch (e: unknown) {
// Deno's node-compat execSync error shape
const err = e as Error & { status?: number; stderr?: string; stdout?: string; code?: string };
const detail = err.stderr || err.stdout || err.message || `exit code ${err.status}`;
error("plugin-validate", `claude plugin validate failed:\n${detail}`);
}
heading("2. JSON Syntax");
let claudeCode: PluginManifest | undefined;
let marketplace: MarketplaceManifest | undefined;
try {
claudeCode = JSON.parse(
fs.readFileSync(path.join(pluginDir, "claude-code.json"), "utf8"),
);
console.log(" ✓ claude-code.json valid");
} catch (e: unknown) {
error("json", `claude-code.json parse error: ${(e as Error).message}`);
}
try {
marketplace = JSON.parse(
fs.readFileSync(
path.join(root, ".claude-plugin/marketplace.json"),
"utf8",
),
);
console.log(" ✓ marketplace.json valid");
} catch (e: unknown) {
error("json", `marketplace.json parse error: ${(e as Error).message}`);
}
heading("3. Character Budget");
if (claudeCode) {
let total = 0;
const oversize: string[] = [];
for (const skill of claudeCode.skills || []) {
total += skill.description.length;
if (skill.description.length > 300) {
oversize.push(`${skill.name} (${skill.description.length} chars)`);
}
}
if (total > 15000) {
error(
"budget",
`Total ${total}/15,000 chars — EXCEEDS BUDGET (skills invisible to Claude)`,
);
} else if (total > 14000) {
warn("budget", `Total ${total}/15,000 chars — dangerously close to budget`);
} else {
console.log(
` ✓ Budget OK: ${total}/15,000 chars (${15000 - total} headroom)`,
);
}
for (const s of oversize) {
warn("budget", `Router description over 300 chars: ${s}`);
}
}
heading("4. Manifest ↔ Filesystem Sync");
if (claudeCode) {
const skillsDir = path.join(pluginDir, "skills");
for (const skill of claudeCode.skills || []) {
const skillPath = path.join(skillsDir, skill.name, "SKILL.md");
if (!fs.existsSync(skillPath)) {
error(
"manifest-sync",
`Manifest skill "${skill.name}" has no SKILL.md at ${skill.name}/SKILL.md`,
);
}
}
console.log(
`${claudeCode.skills!.length} manifest skills checked against filesystem`,
);
for (const cmdPath of claudeCode.commands || []) {
const resolved = path.join(pluginDir, cmdPath);
if (!fs.existsSync(resolved)) {
error(
"manifest-sync",
`Manifest command path "${cmdPath}" does not exist`,
);
}
}
console.log(
`${claudeCode.commands!.length} manifest commands checked against filesystem`,
);
}
heading("5. Skill Integrity");
const allSkillNames = new Set<string>();
let skillFilesChecked = 0;
function checkSkillsIn(dir: string): void {
if (!fs.existsSync(dir)) return;
for (const name of fs.readdirSync(dir)) {
const fullPath = path.join(dir, name);
const stat = fs.statSync(fullPath, { throwIfNoEntry: false });
if (!stat?.isDirectory()) continue;
const skillFile = path.join(fullPath, "SKILL.md");
if (fs.existsSync(skillFile)) {
skillFilesChecked++;
const content = fs.readFileSync(skillFile, "utf8");
if (content.trim().length < 50) {
error(
"skill-integrity",
`${name}/SKILL.md is effectively empty (${content.trim().length} chars)`,
);
}
const fm = parseFrontmatter(content);
if (!fm) {
error("skill-integrity", `${name}/SKILL.md has no YAML frontmatter`);
} else {
if (!fm.name)
error(
"skill-integrity",
`${name}/SKILL.md missing required frontmatter field: name`,
);
if (!fm.description)
error(
"skill-integrity",
`${name}/SKILL.md missing required frontmatter field: description`,
);
if (!fm.license)
warn("skill-integrity", `${name}/SKILL.md missing license field`);
if (fm.name && fm.name !== name) {
warn(
"skill-integrity",
`${name}/SKILL.md frontmatter name "${fm.name}" doesn't match directory "${name}"`,
);
}
}
if (allSkillNames.has(name)) {
error("skill-integrity", `Duplicate skill name: "${name}"`);
}
allSkillNames.add(name);
}
checkSkillsIn(fullPath);
}
}
checkSkillsIn(path.join(pluginDir, "skills"));
console.log(
`${skillFilesChecked} skill files checked (frontmatter, duplicates, emptiness)`,
);
heading("6. Agent Integrity");
const agentsDir = path.join(pluginDir, "agents");
let agentFilesChecked = 0;
const allAgentNames = new Set<string>();
if (fs.existsSync(agentsDir)) {
for (const file of fs.readdirSync(agentsDir)) {
if (!file.endsWith(".md")) continue;
agentFilesChecked++;
const content = fs.readFileSync(path.join(agentsDir, file), "utf8");
const fm = parseFrontmatter(content);
if (!fm) {
error("agent-integrity", `${file} has no YAML frontmatter`);
} else {
if (!fm.description && !fm.name) {
error(
"agent-integrity",
`${file} missing both name and description in frontmatter`,
);
}
}
const body = content.replace(/^---[\s\S]*?---\n?/, "").trim();
if (body.length < 20) {
error("agent-integrity", `${file} has effectively empty body`);
}
const agentName = file.replace(".md", "");
if (allAgentNames.has(agentName)) {
error("agent-integrity", `Duplicate agent name: "${agentName}"`);
}
allAgentNames.add(agentName);
}
console.log(`${agentFilesChecked} agent files checked`);
}
heading("7. Command Integrity");
const commandsDir = path.join(pluginDir, "commands");
let commandFilesChecked = 0;
if (fs.existsSync(commandsDir)) {
for (const file of fs.readdirSync(commandsDir)) {
if (!file.endsWith(".md")) continue;
commandFilesChecked++;
const content = fs.readFileSync(path.join(commandsDir, file), "utf8");
const fm = parseFrontmatter(content);
if (!fm) {
error("command-integrity", `${file} has no YAML frontmatter`);
} else {
if (!fm.description) {
warn("command-integrity", `${file} missing description in frontmatter`);
}
}
}
console.log(`${commandFilesChecked} command files checked`);
}
heading("8. Version Consistency");
const versions: Record<string, string> = {};
if (claudeCode) versions["claude-code.json"] = claudeCode.version;
if (marketplace) {
const plugin = marketplace.plugins?.find((p) => p.name === "axiom");
if (plugin) versions["marketplace.json"] = plugin.version;
else warn("version", "axiom plugin not found in marketplace.json");
}
const metadataPath = path.join(pluginDir, "hooks/metadata.txt");
if (fs.existsSync(metadataPath)) {
const lines = fs.readFileSync(metadataPath, "utf8").trim().split("\n");
versions["metadata.txt"] = lines[0];
}
const configPath = path.join(root, "docs/.vitepress/config.ts");
if (fs.existsSync(configPath)) {
const configContent = fs.readFileSync(configPath, "utf8");
const vMatch = configContent.match(/• v(\d+\.\d+\.\d+)/);
if (vMatch) versions["config.ts"] = vMatch[1];
else warn("version", "Version not found in docs/.vitepress/config.ts footer");
}
const mcpPkgPath = path.join(root, "mcp-server/package.json");
if (fs.existsSync(mcpPkgPath)) {
const mcpPkg = JSON.parse(fs.readFileSync(mcpPkgPath, "utf8"));
versions["mcp-server/package.json"] = mcpPkg.version;
}
const versionValues = Object.values(versions);
const allSame = versionValues.every((v) => v === versionValues[0]);
if (allSame && versionValues.length > 0) {
console.log(
` ✓ All ${versionValues.length} files report version ${versionValues[0]}`,
);
} else {
error("version", "Version mismatch across files:");
for (const [file, ver] of Object.entries(versions)) {
const mark = ver === versionValues[0] ? " " : "→ ";
errors.push(` ${mark}${file}: ${ver}`);
}
}
heading("9. Metadata Accuracy");
if (fs.existsSync(metadataPath)) {
const lines = fs.readFileSync(metadataPath, "utf8").trim().split("\n");
const metaSkills = parseInt(lines[1], 10);
const metaAgents = parseInt(lines[2], 10);
const metaCommands = parseInt(lines[3], 10);
if (metaSkills !== skillFilesChecked) {
error(
"metadata",
`metadata.txt says ${metaSkills} skills, filesystem has ${skillFilesChecked}`,
);
} else {
console.log(` ✓ Skill count matches: ${metaSkills}`);
}
if (metaAgents !== agentFilesChecked) {
error(
"metadata",
`metadata.txt says ${metaAgents} agents, filesystem has ${agentFilesChecked}`,
);
} else {
console.log(` ✓ Agent count matches: ${metaAgents}`);
}
if (metaCommands !== commandFilesChecked) {
error(
"metadata",
`metadata.txt says ${metaCommands} commands, filesystem has ${commandFilesChecked}`,
);
} else {
console.log(` ✓ Command count matches: ${metaCommands}`);
}
}
heading("10. Router Cross-References");
let crossRefChecked = 0;
let brokenRefs = 0;
const routerSkillNames = (claudeCode?.skills || []).map((s) => s.name);
for (const routerName of routerSkillNames) {
const routerPath = path.join(pluginDir, "skills", routerName, "SKILL.md");
if (!fs.existsSync(routerPath)) continue;
const content = fs.readFileSync(routerPath, "utf8");
const refs = content.matchAll(/\/skill (axiom-[\w-]+)/g);
for (const ref of refs) {
crossRefChecked++;
const targetName = ref[1];
if (!allSkillNames.has(targetName)) {
error(
"cross-ref",
`Router "${routerName}" references non-existent skill "${targetName}"`,
);
brokenRefs++;
}
}
}
if (brokenRefs === 0) {
console.log(
`${crossRefChecked} cross-references validated across ${routerSkillNames.length} routers`,
);
}
heading("11. Hook Scripts");
try {
execSync("command -v shellcheck", { stdio: "pipe" });
try {
execSync(`shellcheck ${path.join(pluginDir, "hooks")}/*.sh`, {
stdio: "pipe",
cwd: root,
});
console.log(" ✓ Shell scripts pass shellcheck");
} catch (e: unknown) {
const err = e as { stdout?: Buffer; stderr?: Buffer };
error(
"hooks",
`shellcheck failures:\n${err.stdout?.toString() || err.stderr?.toString()}`,
);
}
} catch {
warn(
"hooks",
"shellcheck not installed (brew install shellcheck) — skipping hook lint",
);
}
// ── Phase 1 Summary ──
heading("Phase 1 Summary (Static)");
console.log(
` Skills: ${skillFilesChecked} | Agents: ${agentFilesChecked} | Commands: ${commandFilesChecked}`,
);
console.log(
` Cross-refs: ${crossRefChecked} | Routers: ${routerSkillNames.length}`,
);
if (errors.length > 0) {
console.log(`\n ERRORS (${totalErrors}):`);
for (const e of errors) console.log(e);
}
if (warnings.length > 0) {
console.log(`\n WARNINGS (${totalWarnings}):`);
for (const w of warnings) console.log(w);
}
if (totalErrors > 0) {
console.log(
`\n✗ Phase 1 FAILED with ${totalErrors} error(s). Fix before deploying.`,
);
Deno.exit(1);
}
console.log("\n✓ Phase 1 PASSED — static validation clean\n");
// ── Phase 2: Build Validation ──
heading("Phase 2: Build Validation");
if (Deno.args.includes("--static")) {
console.log(" ⊘ Skipped (--static flag)");
Deno.exit(0);
}
heading("12. MCP Server Tests");
try {
execSync("pnpm test", {
cwd: path.join(root, "mcp-server"),
stdio: "pipe",
timeout: 60000,
});
console.log(" ✓ MCP server tests pass");
} catch (e: unknown) {
const err = e as { stdout?: Buffer; stderr?: Buffer };
const output = err.stdout?.toString() || err.stderr?.toString() || "";
const summary = output.match(/Tests\s+\d+.*|FAIL.*|✗.*/gm);
error(
"mcp-tests",
`MCP server tests failed${summary ? ":\n " + summary.join("\n ") : ""}`,
);
console.log("\n✗ Phase 2 FAILED. Fix MCP tests before deploying.");
Deno.exit(1);
}
heading("13. MCP Bundle Build");
try {
execSync("pnpm run build:bundle", {
cwd: path.join(root, "mcp-server"),
stdio: "pipe",
timeout: 120000,
});
const bundlePath = path.join(root, "mcp-server/dist/bundle.json");
if (fs.existsSync(bundlePath)) {
const bundleSize = fs.statSync(bundlePath).size;
if (bundleSize < 1000) {
error(
"mcp-bundle",
`bundle.json suspiciously small (${bundleSize} bytes)`,
);
} else {
const bundleKB = Math.round(bundleSize / 1024);
console.log(` ✓ MCP bundle built successfully (${bundleKB} KB)`);
}
} else {
error("mcp-bundle", "bundle.json not generated");
}
} catch (e: unknown) {
const err = e as { stderr?: Buffer };
error(
"mcp-bundle",
`MCP bundle build failed: ${err.stderr?.toString()?.slice(0, 200) || "unknown error"}`,
);
console.log("\n✗ Phase 2 FAILED. Fix MCP bundle before deploying.");
Deno.exit(1);
}
heading("14. VitePress Build");
try {
execSync("npm run docs:build", {
cwd: root,
stdio: "pipe",
timeout: 120000,
});
console.log(" ✓ VitePress build succeeds (dead links validated)");
} catch (e: unknown) {
const err = e as { stdout?: Buffer; stderr?: Buffer };
const output = err.stdout?.toString() || err.stderr?.toString() || "";
const deadLinks = output.match(/dead link.*|404.*|DEAD_LINKS.*/gim);
error(
"vitepress",
`VitePress build failed${deadLinks ? ":\n " + deadLinks.slice(0, 5).join("\n ") : ""}`,
);
console.log("\n✗ Phase 2 FAILED. Fix VitePress build before deploying.");
Deno.exit(1);
}
// ── Final Summary ──
heading("Final Summary");
console.log(
` Phase 1: ✓ Static validation (${skillFilesChecked} skills, ${agentFilesChecked} agents, ${commandFilesChecked} commands)`,
);
console.log(" Phase 2: ✓ Build validation (MCP tests + bundle + VitePress)");
if (totalWarnings > 0) {
console.log(`\n ${totalWarnings} warning(s) — review above`);
}
console.log("\n✓ ALL CHECKS PASSED — safe to deploy\n");