Files
claude-engineering-plugin/plugins/compound-engineering/skills/excalidraw-png-export/scripts/export_png.mjs
John Lamb 0b26ab8fe6 Merge upstream origin/main with local fork additions preserved
Accept upstream's ce-review pipeline rewrite (6-stage persona-based
architecture with structured JSON, confidence gating, three execution
modes). Retire 4 overlapping review agents (security-sentinel,
performance-oracle, data-migration-expert, data-integrity-guardian)
replaced by upstream equivalents. Add 5 local review agents as
conditional personas in the persona catalog (kieran-python, tiangolo-
fastapi, kieran-typescript, julik-frontend-races, architecture-
strategist).

Accept upstream skill renames (file-todos→todo-create, resolve_todo_
parallel→todo-resolve), port local Assessment and worktree constraint
additions to new files. Merge best-practices-researcher with upstream
platform-agnostic discovery + local FastAPI mappings. Remove Rails/Ruby
skills (dhh-rails-style, andrew-kane-gem-writer, dspy-ruby) per fork's
FastAPI pivot.

Component counts: 36 agents, 48 skills, 7 commands.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:28:22 -05:00

91 lines
2.9 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Export an Excalidraw JSON file to PNG using Playwright + the official Excalidraw library.
*
* Usage: node export_png.mjs <input.excalidraw> [output.png]
*
* All rendering happens locally. Diagram data never leaves the machine.
* The Excalidraw JS library is fetched from esm.sh CDN (code only, not user data).
*/
import { createRequire } from "module";
import { readFileSync, writeFileSync, copyFileSync } from "fs";
import { createServer } from "http";
import { join, extname, dirname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const RUNTIME_DIR = join(__dirname, ".export-runtime");
const HTML_PATH = join(__dirname, "export.html");
// Resolve playwright from the runtime directory, not the script's location
const require = createRequire(join(RUNTIME_DIR, "node_modules", "playwright", "index.mjs"));
const { chromium } = await import(join(RUNTIME_DIR, "node_modules", "playwright", "index.mjs"));
const inputPath = process.argv[2];
if (!inputPath) {
console.error("Usage: node export_png.mjs <input.excalidraw> [output.png]");
process.exit(1);
}
const outputPath = process.argv[3] || inputPath.replace(/\.excalidraw$/, ".png");
// Set up a temp serving directory
const SERVE_DIR = join(__dirname, ".export-tmp");
const { mkdirSync, rmSync } = await import("fs");
mkdirSync(SERVE_DIR, { recursive: true });
copyFileSync(HTML_PATH, join(SERVE_DIR, "export.html"));
copyFileSync(inputPath, join(SERVE_DIR, "diagram.excalidraw"));
const MIME = {
".html": "text/html",
".json": "application/json",
".excalidraw": "application/json",
};
const server = createServer((req, res) => {
const file = join(SERVE_DIR, req.url === "/" ? "export.html" : req.url);
try {
const data = readFileSync(file);
res.writeHead(200, { "Content-Type": MIME[extname(file)] || "application/octet-stream" });
res.end(data);
} catch {
res.writeHead(404);
res.end("Not found");
}
});
server.listen(0, "127.0.0.1", async () => {
const port = server.address().port;
let browser;
try {
browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
page.on("pageerror", err => console.error("Page error:", err.message));
await page.goto(`http://127.0.0.1:${port}`);
await page.waitForFunction(
() => document.title.startsWith("READY") || document.title.startsWith("ERROR"),
{ timeout: 30000 }
);
const title = await page.title();
if (title.startsWith("ERROR")) {
console.error("Export failed:", title);
process.exit(1);
}
const dataUrl = await page.evaluate(() => window.__PNG_DATA__);
const base64 = dataUrl.replace(/^data:image\/png;base64,/, "");
writeFileSync(outputPath, Buffer.from(base64, "base64"));
console.log(outputPath);
} finally {
if (browser) await browser.close();
server.close();
rmSync(SERVE_DIR, { recursive: true, force: true });
}
});