#!/usr/bin/env node import { spawnSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const ROOT = path.dirname(fileURLToPath(import.meta.url)); const MIN_NODE_MAJOR = 20; const REQUIRED_FILES = [ "package.json", ".claude-plugin/plugin.json", ".mcp.json", "skill-package-manifest.json", "skills/fmode-image-set/SKILL.md", "mcp/src/server.mjs", "mcp/src/tools/image-set-run.mjs", "mcp/src/tools/image-set-validate.mjs", "mcp/src/features/image-set/image-set-runner.mjs", "mcp/src/features/image-set/product-vision-client.mjs", "mcp/src/features/image-set/validate-plan.mjs", "mcp/src/providers/jimeng-v4-client.mjs", ]; function parseArgs(argv) { return { check: argv.includes("--check"), smoke: argv.includes("--smoke"), skipInstall: argv.includes("--skip-install"), help: argv.includes("--help") || argv.includes("-h"), }; } function run(command, args, cwd = ROOT) { const useCmd = process.platform === "win32" && command === "npm"; const executable = useCmd ? "cmd.exe" : command; const finalArgs = useCmd ? ["/d", "/s", "/c", "npm", ...args] : args; const child = spawnSync(executable, finalArgs, { cwd, encoding: "utf8", stdio: "inherit", maxBuffer: 1024 * 1024 * 100, }); if (child.status !== 0) { throw new Error(`${command} ${args.join(" ")} failed with exit ${child.status}`); } } function readJson(relativePath) { return JSON.parse(fs.readFileSync(path.join(ROOT, relativePath), "utf8")); } function writeMcpConfig(rootDir = ROOT) { const config = { mcpServers: { "fmode-image-set": { command: "node", args: [path.join(rootDir, "mcp", "src", "server.mjs")], cwd: rootDir, }, }, }; fs.writeFileSync(path.join(rootDir, ".mcp.json"), `${JSON.stringify(config, null, 2)}\n`, "utf8"); } export function checkPackage() { const major = Number(process.versions.node.split(".")[0]); if (!Number.isFinite(major) || major < MIN_NODE_MAJOR) { throw new Error(`Node.js ${MIN_NODE_MAJOR}+ is required. Current version: ${process.version}`); } const missing = REQUIRED_FILES.filter((relativePath) => !fs.existsSync(path.join(ROOT, relativePath))); if (missing.length) throw new Error(`Missing required files: ${missing.join(", ")}`); const packageJson = readJson("package.json"); const pluginJson = readJson(".claude-plugin/plugin.json"); const manifest = readJson("skill-package-manifest.json"); const serverText = fs.readFileSync(path.join(ROOT, "mcp", "src", "server.mjs"), "utf8"); const serverVersion = serverText.match(/const VERSION = "([^"]+)"/)?.[1] || ""; const versions = new Set([packageJson.version, pluginJson.version, manifest.version, serverVersion]); if (versions.size !== 1) throw new Error("package, plugin, and skill manifest versions must match"); if (pluginJson.name !== "fmode-image-set" || manifest.name !== "fmode-image-set") { throw new Error("plugin and skill manifest names must be fmode-image-set"); } if (!manifest.skills.includes("fmode-image-set")) throw new Error("entry skill is missing from package manifest"); return { version: packageJson.version, files: REQUIRED_FILES.length }; } async function main() { const options = parseArgs(process.argv.slice(2)); if (options.help) { console.log("Usage: node install.js [--check] [--smoke] [--skip-install]"); return; } const checked = checkPackage(); writeMcpConfig(ROOT); console.log(`Fmode Image Set ${checked.version}`); console.log(`Required files: OK (${checked.files})`); if (!options.check && !options.skipInstall) { run("npm", ["install", "--omit=dev", "--ignore-scripts"]); } if (options.smoke) { run(process.execPath, ["scripts/smoke-package.mjs"]); run(process.execPath, ["scripts/smoke-mcp.mjs"]); } } main().catch((error) => { console.error(`Install failed: ${error.message}`); process.exit(1); });