install.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. #!/usr/bin/env node
  2. import { spawnSync } from "node:child_process";
  3. import fs from "node:fs";
  4. import path from "node:path";
  5. import { fileURLToPath } from "node:url";
  6. const ROOT = path.dirname(fileURLToPath(import.meta.url));
  7. const MIN_NODE_MAJOR = 20;
  8. const REQUIRED_FILES = [
  9. "package.json",
  10. ".claude-plugin/plugin.json",
  11. ".mcp.json",
  12. "skill-package-manifest.json",
  13. "skills/fmode-image-set/SKILL.md",
  14. "mcp/src/server.mjs",
  15. "mcp/src/tools/image-set-run.mjs",
  16. "mcp/src/tools/image-set-validate.mjs",
  17. "mcp/src/features/image-set/image-set-runner.mjs",
  18. "mcp/src/features/image-set/product-vision-client.mjs",
  19. "mcp/src/features/image-set/validate-plan.mjs",
  20. "mcp/src/providers/jimeng-v4-client.mjs",
  21. ];
  22. function parseArgs(argv) {
  23. return {
  24. check: argv.includes("--check"),
  25. smoke: argv.includes("--smoke"),
  26. skipInstall: argv.includes("--skip-install"),
  27. help: argv.includes("--help") || argv.includes("-h"),
  28. };
  29. }
  30. function run(command, args, cwd = ROOT) {
  31. const useCmd = process.platform === "win32" && command === "npm";
  32. const executable = useCmd ? "cmd.exe" : command;
  33. const finalArgs = useCmd ? ["/d", "/s", "/c", "npm", ...args] : args;
  34. const child = spawnSync(executable, finalArgs, {
  35. cwd,
  36. encoding: "utf8",
  37. stdio: "inherit",
  38. maxBuffer: 1024 * 1024 * 100,
  39. });
  40. if (child.status !== 0) {
  41. throw new Error(`${command} ${args.join(" ")} failed with exit ${child.status}`);
  42. }
  43. }
  44. function readJson(relativePath) {
  45. return JSON.parse(fs.readFileSync(path.join(ROOT, relativePath), "utf8"));
  46. }
  47. function writeMcpConfig(rootDir = ROOT) {
  48. const config = {
  49. mcpServers: {
  50. "fmode-image-set": {
  51. command: "node",
  52. args: [path.join(rootDir, "mcp", "src", "server.mjs")],
  53. cwd: rootDir,
  54. },
  55. },
  56. };
  57. fs.writeFileSync(path.join(rootDir, ".mcp.json"), `${JSON.stringify(config, null, 2)}\n`, "utf8");
  58. }
  59. export function checkPackage() {
  60. const major = Number(process.versions.node.split(".")[0]);
  61. if (!Number.isFinite(major) || major < MIN_NODE_MAJOR) {
  62. throw new Error(`Node.js ${MIN_NODE_MAJOR}+ is required. Current version: ${process.version}`);
  63. }
  64. const missing = REQUIRED_FILES.filter((relativePath) => !fs.existsSync(path.join(ROOT, relativePath)));
  65. if (missing.length) throw new Error(`Missing required files: ${missing.join(", ")}`);
  66. const packageJson = readJson("package.json");
  67. const pluginJson = readJson(".claude-plugin/plugin.json");
  68. const manifest = readJson("skill-package-manifest.json");
  69. const serverText = fs.readFileSync(path.join(ROOT, "mcp", "src", "server.mjs"), "utf8");
  70. const serverVersion = serverText.match(/const VERSION = "([^"]+)"/)?.[1] || "";
  71. const versions = new Set([packageJson.version, pluginJson.version, manifest.version, serverVersion]);
  72. if (versions.size !== 1) throw new Error("package, plugin, and skill manifest versions must match");
  73. if (pluginJson.name !== "fmode-image-set" || manifest.name !== "fmode-image-set") {
  74. throw new Error("plugin and skill manifest names must be fmode-image-set");
  75. }
  76. if (!manifest.skills.includes("fmode-image-set")) throw new Error("entry skill is missing from package manifest");
  77. return { version: packageJson.version, files: REQUIRED_FILES.length };
  78. }
  79. async function main() {
  80. const options = parseArgs(process.argv.slice(2));
  81. if (options.help) {
  82. console.log("Usage: node install.js [--check] [--smoke] [--skip-install]");
  83. return;
  84. }
  85. const checked = checkPackage();
  86. writeMcpConfig(ROOT);
  87. console.log(`Fmode Image Set ${checked.version}`);
  88. console.log(`Required files: OK (${checked.files})`);
  89. if (!options.check && !options.skipInstall) {
  90. run("npm", ["install", "--omit=dev", "--ignore-scripts"]);
  91. }
  92. if (options.smoke) {
  93. run(process.execPath, ["scripts/smoke-package.mjs"]);
  94. run(process.execPath, ["scripts/smoke-mcp.mjs"]);
  95. }
  96. }
  97. main().catch((error) => {
  98. console.error(`Install failed: ${error.message}`);
  99. process.exit(1);
  100. });