fmode-image-set.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. #!/usr/bin/env node
  2. import fs from 'node:fs';
  3. import os from 'node:os';
  4. import path from 'node:path';
  5. import { spawnSync } from 'node:child_process';
  6. import { fileURLToPath } from 'node:url';
  7. import { checkPackage } from '../install.js';
  8. const sourceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
  9. const exclusions = new Set(['node_modules', '.git', '.env', '.env.local', '.claude', '.codex', 'outputs', 'output', '输出', '生成结果', 'memory', '.tmp']);
  10. function inside(parent, child) {
  11. const p = path.resolve(parent).toLowerCase();
  12. const c = path.resolve(child).toLowerCase();
  13. return c === p || c.startsWith(`${p}${path.sep}`);
  14. }
  15. function copyPackage(target, force) {
  16. if (inside(sourceRoot, target)) throw new Error('安装目标不能位于源码包内部。');
  17. if (fs.existsSync(target)) {
  18. if (!force) throw new Error(`目标已存在:${target}。如需覆盖请传入 --force。`);
  19. fs.rmSync(target, { recursive: true, force: true });
  20. }
  21. fs.mkdirSync(path.dirname(target), { recursive: true });
  22. copyTree(sourceRoot, target, true);
  23. // v0.2 removed these providers. Explicit pruning prevents stale runtime files
  24. // when upgrading from an older installation on filesystems with unusual copy behavior.
  25. for (const legacy of ['qwen-vision.mjs', 'jimeng.mjs']) {
  26. const legacyPath = path.join(target, 'mcp', 'src', 'providers', legacy);
  27. fs.rmSync(legacyPath, { force: true });
  28. if (fs.existsSync(legacyPath)) throw new Error(`无法清理旧 Provider,请先重启/关闭 Claude Code 后重试:${legacy}`);
  29. }
  30. }
  31. function copyTree(source, target, isPackageCopy = false) {
  32. const stat = fs.lstatSync(source);
  33. if (stat.isSymbolicLink()) return;
  34. if (stat.isDirectory()) {
  35. fs.mkdirSync(target, { recursive: true });
  36. for (const entry of fs.readdirSync(source)) {
  37. if (isPackageCopy && (exclusions.has(entry) || entry.endsWith('.tgz') || entry.endsWith('.log'))) continue;
  38. copyTree(path.join(source, entry), path.join(target, entry), isPackageCopy);
  39. }
  40. return;
  41. }
  42. fs.copyFileSync(source, target);
  43. }
  44. function readJsonMaybe(file) {
  45. try { return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')) : {}; }
  46. catch { throw new Error(`JSON 无法解析:${file}`); }
  47. }
  48. function writeMcp(workspace, installedRoot) {
  49. const mcpFile = path.join(workspace, '.mcp.json');
  50. const current = readJsonMaybe(mcpFile);
  51. current.mcpServers = current.mcpServers || {};
  52. current.mcpServers['fmode-image-set'] = {
  53. command: 'node', args: [path.join(installedRoot, 'mcp', 'src', 'server.mjs')], cwd: installedRoot,
  54. env: { FMODE_WORKSPACE_ROOT: workspace }
  55. };
  56. fs.writeFileSync(mcpFile, `${JSON.stringify(current, null, 2)}\n`, 'utf8');
  57. JSON.parse(fs.readFileSync(mcpFile, 'utf8'));
  58. fs.writeFileSync(path.join(installedRoot, '.mcp.json'), `${JSON.stringify({ mcpServers: { 'fmode-image-set': current.mcpServers['fmode-image-set'] } }, null, 2)}\n`, 'utf8');
  59. }
  60. function exposeWorkspaceSkills(workspace, installedRoot, force) {
  61. const manifest = readJsonMaybe(path.join(installedRoot, 'skill-package-manifest.json'));
  62. for (const name of manifest.skills) {
  63. const source = path.join(installedRoot, 'skills', name);
  64. const target = path.join(workspace, '.claude', 'skills', name);
  65. if (fs.existsSync(target)) {
  66. if (!force) throw new Error(`Skill 已存在:${target}。使用 --force 覆盖。`);
  67. fs.rmSync(target, { recursive: true, force: true });
  68. }
  69. fs.mkdirSync(path.dirname(target), { recursive: true });
  70. copyTree(source, target, false);
  71. }
  72. }
  73. function runNode(args, cwd) {
  74. const result = spawnSync(process.execPath, args, { cwd, stdio: 'inherit' });
  75. if (result.status !== 0) throw new Error(`命令失败:node ${args.join(' ')}`);
  76. }
  77. function installDependencies(target, skipInstall) {
  78. if (skipInstall) return;
  79. const result = spawnSync('npm', ['ci', '--omit=dev'], { cwd: target, stdio: 'inherit', shell: process.platform === 'win32' });
  80. if (result.status !== 0) throw new Error('依赖安装失败。');
  81. }
  82. function parseFlags(args) {
  83. return { force: args.includes('--force'), smoke: args.includes('--smoke'), skipInstall: args.includes('--skip-install') };
  84. }
  85. async function workspaceInstall(args) {
  86. const flags = parseFlags(args);
  87. const workspace = process.cwd();
  88. const target = path.join(workspace, '.claude', 'plugins', 'fmode-image-set');
  89. copyPackage(target, flags.force);
  90. installDependencies(target, flags.skipInstall);
  91. exposeWorkspaceSkills(workspace, target, flags.force);
  92. writeMcp(workspace, target);
  93. if (flags.smoke) {
  94. runNode(['scripts/smoke-package.mjs'], target);
  95. runNode(['scripts/smoke-mcp.mjs'], target);
  96. }
  97. console.log(`Workspace installed: ${target}`);
  98. console.log('Restart the Claude Code session.');
  99. }
  100. async function globalInstall(args) {
  101. const flags = parseFlags(args);
  102. const target = path.join(os.homedir(), '.claude', 'plugins', 'fmode-image-set');
  103. copyPackage(target, flags.force);
  104. installDependencies(target, flags.skipInstall);
  105. writeMcp(target, target);
  106. if (flags.smoke) runNode(['scripts/acceptance.mjs'], target);
  107. console.log(`Global plugin installed: ${target}`);
  108. }
  109. async function main() {
  110. const [command = 'help', ...args] = process.argv.slice(2);
  111. if (command === 'workspace') return workspaceInstall(args);
  112. if (command === 'install') return globalInstall(args);
  113. if (command === 'check') { const result = checkPackage(sourceRoot); console.log(JSON.stringify(result, null, 2)); process.exitCode = result.ok ? 0 : 1; return; }
  114. if (command === 'smoke') { runNode(['scripts/acceptance.mjs'], sourceRoot); return; }
  115. if (command === 'path') { console.log(sourceRoot); return; }
  116. console.log('Usage: fmode-image-set <workspace|install|check|smoke|path> [--force] [--smoke] [--skip-install]');
  117. }
  118. main().catch(error => { console.error(error.message); process.exit(1); });