fmode-image-set.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. #!/usr/bin/env node
  2. import { spawnSync } from "node:child_process";
  3. import fs from "node:fs";
  4. import os from "node:os";
  5. import path from "node:path";
  6. import { fileURLToPath } from "node:url";
  7. const SOURCE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
  8. const INVOCATION_ROOT = path.resolve(process.cwd());
  9. const invocationRelativeToSource = path.relative(SOURCE_ROOT, INVOCATION_ROOT);
  10. const invokedFromSource = invocationRelativeToSource === ""
  11. || (!invocationRelativeToSource.startsWith("..") && !path.isAbsolute(invocationRelativeToSource));
  12. const WORKSPACE_ROOT = invokedFromSource ? path.dirname(SOURCE_ROOT) : INVOCATION_ROOT;
  13. const DEFAULT_TARGET = path.join(os.homedir(), ".claude", "plugins", "fmode-image-set");
  14. const WORKSPACE_TARGET = path.join(WORKSPACE_ROOT, ".claude", "skills", "fmode-image-set");
  15. const EXCLUDED = new Set(["node_modules", "生成结果", "memory", "outputs", ".tmp", ".git", ".env", ".env.local"]);
  16. EXCLUDED.add(".claude");
  17. EXCLUDED.add(".codex");
  18. function parseArgs(argv) {
  19. const command = argv[0] && !argv[0].startsWith("--") ? argv[0] : "install";
  20. const options = {
  21. command,
  22. target: command === "workspace" ? WORKSPACE_TARGET : DEFAULT_TARGET,
  23. workspace: command === "workspace",
  24. smoke: argv.includes("--smoke"),
  25. skipInstall: argv.includes("--skip-install"),
  26. force: argv.includes("--force"),
  27. };
  28. const targetIndex = argv.indexOf("--target");
  29. if (targetIndex >= 0 && argv[targetIndex + 1]) options.target = path.resolve(argv[targetIndex + 1]);
  30. return options;
  31. }
  32. function isInside(parent, candidate) {
  33. const relative = path.relative(path.resolve(parent), path.resolve(candidate));
  34. return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
  35. }
  36. function safeTarget(target, force) {
  37. return force
  38. || path.resolve(target) === path.resolve(DEFAULT_TARGET)
  39. || isInside(path.join(WORKSPACE_ROOT, ".claude", "plugins"), target)
  40. || isInside(path.join(WORKSPACE_ROOT, ".claude", "skills"), target);
  41. }
  42. function copyPackage(source, target) {
  43. fs.mkdirSync(target, { recursive: true });
  44. for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
  45. if (EXCLUDED.has(entry.name) || entry.name.endsWith(".tgz")) continue;
  46. const from = path.join(source, entry.name);
  47. const to = path.join(target, entry.name);
  48. if (entry.isDirectory()) copyPackage(from, to);
  49. else if (entry.isFile()) fs.copyFileSync(from, to);
  50. }
  51. }
  52. function run(command, args, cwd) {
  53. const useCmd = process.platform === "win32" && command === "npm";
  54. const executable = useCmd ? "cmd.exe" : command;
  55. const finalArgs = useCmd ? ["/d", "/s", "/c", "npm", ...args] : args;
  56. const child = spawnSync(executable, finalArgs, { cwd, stdio: "inherit", encoding: "utf8" });
  57. if (child.status !== 0) throw new Error(`${command} failed with exit ${child.status}`);
  58. }
  59. function readJsonMaybe(filePath) {
  60. if (!fs.existsSync(filePath)) return {};
  61. return JSON.parse(fs.readFileSync(filePath, "utf8"));
  62. }
  63. function writeWorkspaceActivation(target) {
  64. const configPath = path.join(WORKSPACE_ROOT, ".mcp.json");
  65. const existing = readJsonMaybe(configPath);
  66. const config = {
  67. ...existing,
  68. mcpServers: {
  69. ...(existing.mcpServers || {}),
  70. "fmode-image-set": {
  71. command: "node",
  72. args: [path.join(target, "mcp", "src", "server.mjs")],
  73. cwd: target,
  74. },
  75. },
  76. };
  77. fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
  78. const packagedSkill = path.join(target, "skills", "fmode-image-set");
  79. copyPackage(packagedSkill, target);
  80. }
  81. function install(options) {
  82. if (fs.existsSync(options.target)) {
  83. if (!safeTarget(options.target, options.force)) {
  84. throw new Error(`Target exists; use --force only for an intentional custom replacement: ${options.target}`);
  85. }
  86. fs.rmSync(options.target, { recursive: true, force: true });
  87. }
  88. copyPackage(SOURCE_ROOT, options.target);
  89. if (options.workspace) writeWorkspaceActivation(options.target);
  90. if (!options.skipInstall) run("npm", ["install", "--omit=dev", "--ignore-scripts"], options.target);
  91. run(process.execPath, ["install.js", options.smoke ? "--smoke" : "--check", "--skip-install"], options.target);
  92. console.log(`Installed Fmode Image Set: ${options.target}`);
  93. if (options.workspace) console.log("Restart the Claude Code session so it can discover the new Skill and MCP tools.");
  94. }
  95. function main() {
  96. const options = parseArgs(process.argv.slice(2));
  97. if (options.command === "install" || options.command === "workspace") return install(options);
  98. if (options.command === "check") return run(process.execPath, ["install.js", "--check"], SOURCE_ROOT);
  99. if (options.command === "smoke") return run(process.execPath, ["install.js", "--smoke", "--skip-install"], SOURCE_ROOT);
  100. if (options.command === "path") return console.log(options.target);
  101. if (options.command === "run") {
  102. return run(process.execPath, ["mcp/src/tools/image-set-run.mjs", ...process.argv.slice(3)], SOURCE_ROOT);
  103. }
  104. console.log([
  105. "Usage:",
  106. " fmode-image-set install [--smoke]",
  107. " fmode-image-set workspace [--smoke]",
  108. " fmode-image-set check",
  109. " fmode-image-set smoke",
  110. " fmode-image-set run <start|execute|resume|status> [options]",
  111. " fmode-image-set path",
  112. ].join("\n"));
  113. }
  114. try {
  115. main();
  116. } catch (error) {
  117. console.error(`fmode-image-set failed: ${error.message}`);
  118. process.exit(1);
  119. }