Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
124 lines
3.6 KiB
JavaScript
124 lines
3.6 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawn } from "node:child_process";
|
|
import { mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join, resolve } from "node:path";
|
|
|
|
const projectRoot = resolve(import.meta.dirname, "..");
|
|
|
|
async function readPublishedBinName() {
|
|
const packageJsonRaw = await readFile(join(projectRoot, "package.json"), "utf8");
|
|
const packageJson = JSON.parse(packageJsonRaw);
|
|
const binNames = Object.keys(packageJson.bin ?? {});
|
|
|
|
if (binNames.length !== 1) {
|
|
throw new Error(`Expected exactly one published bin entry, found ${binNames.length}`);
|
|
}
|
|
|
|
return binNames[0];
|
|
}
|
|
|
|
function run(command, args, options = {}) {
|
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
const child = spawn(command, args, {
|
|
cwd: projectRoot,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
...options,
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk.toString();
|
|
});
|
|
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk.toString();
|
|
});
|
|
|
|
child.on("error", rejectPromise);
|
|
child.on("exit", (code) => {
|
|
if (code === 0) {
|
|
resolvePromise({ stdout, stderr });
|
|
return;
|
|
}
|
|
|
|
rejectPromise(
|
|
new Error(
|
|
`${command} ${args.join(" ")} failed with code ${code}\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`,
|
|
),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function verifyPackagedBin() {
|
|
const packDir = await mkdtemp(join(tmpdir(), "mcp-template-pack-"));
|
|
const installDir = await mkdtemp(join(tmpdir(), "mcp-template-install-"));
|
|
const binName = await readPublishedBinName();
|
|
|
|
try {
|
|
await run("pnpm", ["pack", "--pack-destination", packDir]);
|
|
const tarballs = (await readdir(packDir)).filter((name) => name.endsWith(".tgz"));
|
|
|
|
if (tarballs.length !== 1) {
|
|
throw new Error(`Expected exactly one tarball, found ${tarballs.length}`);
|
|
}
|
|
|
|
const tarballPath = join(packDir, tarballs[0]);
|
|
await writeFile(join(installDir, "package.json"), '{"name":"publish-bin-check","private":true}\n');
|
|
await run("npm", ["install", tarballPath], { cwd: installDir });
|
|
|
|
const startup = spawn(join(installDir, "node_modules", ".bin", binName), [], {
|
|
cwd: installDir,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
|
|
const startupOutput = await new Promise((resolvePromise, rejectPromise) => {
|
|
let stderr = "";
|
|
const timeout = setTimeout(() => {
|
|
startup.kill("SIGTERM");
|
|
rejectPromise(new Error(`Timed out waiting for packaged bin to start\nSTDERR:\n${stderr}`));
|
|
}, 10000);
|
|
|
|
startup.stderr.on("data", (chunk) => {
|
|
stderr += chunk.toString();
|
|
if (stderr.includes("MCP stdio server started")) {
|
|
clearTimeout(timeout);
|
|
startup.kill("SIGTERM");
|
|
resolvePromise(stderr);
|
|
}
|
|
});
|
|
|
|
startup.on("error", (error) => {
|
|
clearTimeout(timeout);
|
|
rejectPromise(error);
|
|
});
|
|
|
|
startup.on("exit", (code, signal) => {
|
|
if (signal === "SIGTERM") {
|
|
return;
|
|
}
|
|
|
|
clearTimeout(timeout);
|
|
rejectPromise(
|
|
new Error(
|
|
`Packaged bin exited before startup completed (code=${code}, signal=${signal})\nSTDERR:\n${stderr}`,
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
if (!String(startupOutput).includes("MCP stdio server started")) {
|
|
throw new Error("Packaged bin did not emit the expected startup log");
|
|
}
|
|
} finally {
|
|
await rm(packDir, { recursive: true, force: true });
|
|
await rm(installDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
await verifyPackagedBin();
|