#!/usr/bin/env node import { spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; const SOURCE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const INVOCATION_ROOT = path.resolve(process.cwd()); const invocationRelativeToSource = path.relative(SOURCE_ROOT, INVOCATION_ROOT); const invokedFromSource = invocationRelativeToSource === "" || (!invocationRelativeToSource.startsWith("..") && !path.isAbsolute(invocationRelativeToSource)); const WORKSPACE_ROOT = invokedFromSource ? path.dirname(SOURCE_ROOT) : INVOCATION_ROOT; const DEFAULT_TARGET = path.join(os.homedir(), ".claude", "plugins", "fmode-image-set"); const WORKSPACE_TARGET = path.join(WORKSPACE_ROOT, ".claude", "skills", "fmode-image-set"); const EXCLUDED = new Set(["node_modules", "生成结果", "memory", "outputs", ".tmp", ".git", ".env", ".env.local"]); EXCLUDED.add(".claude"); EXCLUDED.add(".codex"); function parseArgs(argv) { const command = argv[0] && !argv[0].startsWith("--") ? argv[0] : "install"; const options = { command, target: command === "workspace" ? WORKSPACE_TARGET : DEFAULT_TARGET, workspace: command === "workspace", smoke: argv.includes("--smoke"), skipInstall: argv.includes("--skip-install"), force: argv.includes("--force"), }; const targetIndex = argv.indexOf("--target"); if (targetIndex >= 0 && argv[targetIndex + 1]) options.target = path.resolve(argv[targetIndex + 1]); return options; } function isInside(parent, candidate) { const relative = path.relative(path.resolve(parent), path.resolve(candidate)); return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative)); } function safeTarget(target, force) { return force || path.resolve(target) === path.resolve(DEFAULT_TARGET) || isInside(path.join(WORKSPACE_ROOT, ".claude", "plugins"), target) || isInside(path.join(WORKSPACE_ROOT, ".claude", "skills"), target); } function copyPackage(source, target) { fs.mkdirSync(target, { recursive: true }); for (const entry of fs.readdirSync(source, { withFileTypes: true })) { if (EXCLUDED.has(entry.name) || entry.name.endsWith(".tgz")) continue; const from = path.join(source, entry.name); const to = path.join(target, entry.name); if (entry.isDirectory()) copyPackage(from, to); else if (entry.isFile()) fs.copyFileSync(from, to); } } function run(command, args, cwd) { 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, stdio: "inherit", encoding: "utf8" }); if (child.status !== 0) throw new Error(`${command} failed with exit ${child.status}`); } function readJsonMaybe(filePath) { if (!fs.existsSync(filePath)) return {}; return JSON.parse(fs.readFileSync(filePath, "utf8")); } function writeWorkspaceActivation(target) { const configPath = path.join(WORKSPACE_ROOT, ".mcp.json"); const existing = readJsonMaybe(configPath); const config = { ...existing, mcpServers: { ...(existing.mcpServers || {}), "fmode-image-set": { command: "node", args: [path.join(target, "mcp", "src", "server.mjs")], cwd: target, }, }, }; fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8"); const packagedSkill = path.join(target, "skills", "fmode-image-set"); copyPackage(packagedSkill, target); } function install(options) { if (fs.existsSync(options.target)) { if (!safeTarget(options.target, options.force)) { throw new Error(`Target exists; use --force only for an intentional custom replacement: ${options.target}`); } fs.rmSync(options.target, { recursive: true, force: true }); } copyPackage(SOURCE_ROOT, options.target); if (options.workspace) writeWorkspaceActivation(options.target); if (!options.skipInstall) run("npm", ["install", "--omit=dev", "--ignore-scripts"], options.target); run(process.execPath, ["install.js", options.smoke ? "--smoke" : "--check", "--skip-install"], options.target); console.log(`Installed Fmode Image Set: ${options.target}`); if (options.workspace) console.log("Restart the Claude Code session so it can discover the new Skill and MCP tools."); } function main() { const options = parseArgs(process.argv.slice(2)); if (options.command === "install" || options.command === "workspace") return install(options); if (options.command === "check") return run(process.execPath, ["install.js", "--check"], SOURCE_ROOT); if (options.command === "smoke") return run(process.execPath, ["install.js", "--smoke", "--skip-install"], SOURCE_ROOT); if (options.command === "path") return console.log(options.target); if (options.command === "run") { return run(process.execPath, ["mcp/src/tools/image-set-run.mjs", ...process.argv.slice(3)], SOURCE_ROOT); } console.log([ "Usage:", " fmode-image-set install [--smoke]", " fmode-image-set workspace [--smoke]", " fmode-image-set check", " fmode-image-set smoke", " fmode-image-set run [options]", " fmode-image-set path", ].join("\n")); } try { main(); } catch (error) { console.error(`fmode-image-set failed: ${error.message}`); process.exit(1); }