| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132 |
- #!/usr/bin/env node
- import fs from 'node:fs';
- import os from 'node:os';
- import path from 'node:path';
- import { spawnSync } from 'node:child_process';
- import { fileURLToPath } from 'node:url';
- import { checkPackage } from '../install.js';
- const sourceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
- const exclusions = new Set(['node_modules', '.git', '.env', '.env.local', '.claude', '.codex', 'outputs', 'output', '输出', '生成结果', 'memory', '.tmp']);
- function inside(parent, child) {
- const p = path.resolve(parent).toLowerCase();
- const c = path.resolve(child).toLowerCase();
- return c === p || c.startsWith(`${p}${path.sep}`);
- }
- function copyPackage(target, force) {
- if (inside(sourceRoot, target)) throw new Error('安装目标不能位于源码包内部。');
- if (fs.existsSync(target)) {
- if (!force) throw new Error(`目标已存在:${target}。如需覆盖请传入 --force。`);
- fs.rmSync(target, { recursive: true, force: true });
- }
- fs.mkdirSync(path.dirname(target), { recursive: true });
- copyTree(sourceRoot, target, true);
- // v0.2 removed these providers. Explicit pruning prevents stale runtime files
- // when upgrading from an older installation on filesystems with unusual copy behavior.
- for (const legacy of ['qwen-vision.mjs', 'jimeng.mjs']) {
- const legacyPath = path.join(target, 'mcp', 'src', 'providers', legacy);
- fs.rmSync(legacyPath, { force: true });
- if (fs.existsSync(legacyPath)) throw new Error(`无法清理旧 Provider,请先重启/关闭 Claude Code 后重试:${legacy}`);
- }
- }
- function copyTree(source, target, isPackageCopy = false) {
- const stat = fs.lstatSync(source);
- if (stat.isSymbolicLink()) return;
- if (stat.isDirectory()) {
- fs.mkdirSync(target, { recursive: true });
- for (const entry of fs.readdirSync(source)) {
- if (isPackageCopy && (exclusions.has(entry) || entry.endsWith('.tgz') || entry.endsWith('.log'))) continue;
- copyTree(path.join(source, entry), path.join(target, entry), isPackageCopy);
- }
- return;
- }
- fs.copyFileSync(source, target);
- }
- function readJsonMaybe(file) {
- try { return fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')) : {}; }
- catch { throw new Error(`JSON 无法解析:${file}`); }
- }
- function writeMcp(workspace, installedRoot) {
- const mcpFile = path.join(workspace, '.mcp.json');
- const current = readJsonMaybe(mcpFile);
- current.mcpServers = current.mcpServers || {};
- current.mcpServers['fmode-image-set'] = {
- command: 'node', args: [path.join(installedRoot, 'mcp', 'src', 'server.mjs')], cwd: installedRoot,
- env: { FMODE_WORKSPACE_ROOT: workspace }
- };
- fs.writeFileSync(mcpFile, `${JSON.stringify(current, null, 2)}\n`, 'utf8');
- JSON.parse(fs.readFileSync(mcpFile, 'utf8'));
- fs.writeFileSync(path.join(installedRoot, '.mcp.json'), `${JSON.stringify({ mcpServers: { 'fmode-image-set': current.mcpServers['fmode-image-set'] } }, null, 2)}\n`, 'utf8');
- }
- function exposeWorkspaceSkills(workspace, installedRoot, force) {
- const manifest = readJsonMaybe(path.join(installedRoot, 'skill-package-manifest.json'));
- for (const name of manifest.skills) {
- const source = path.join(installedRoot, 'skills', name);
- const target = path.join(workspace, '.claude', 'skills', name);
- if (fs.existsSync(target)) {
- if (!force) throw new Error(`Skill 已存在:${target}。使用 --force 覆盖。`);
- fs.rmSync(target, { recursive: true, force: true });
- }
- fs.mkdirSync(path.dirname(target), { recursive: true });
- copyTree(source, target, false);
- }
- }
- function runNode(args, cwd) {
- const result = spawnSync(process.execPath, args, { cwd, stdio: 'inherit' });
- if (result.status !== 0) throw new Error(`命令失败:node ${args.join(' ')}`);
- }
- function installDependencies(target, skipInstall) {
- if (skipInstall) return;
- const result = spawnSync('npm', ['ci', '--omit=dev'], { cwd: target, stdio: 'inherit', shell: process.platform === 'win32' });
- if (result.status !== 0) throw new Error('依赖安装失败。');
- }
- function parseFlags(args) {
- return { force: args.includes('--force'), smoke: args.includes('--smoke'), skipInstall: args.includes('--skip-install') };
- }
- async function workspaceInstall(args) {
- const flags = parseFlags(args);
- const workspace = process.cwd();
- const target = path.join(workspace, '.claude', 'plugins', 'fmode-image-set');
- copyPackage(target, flags.force);
- installDependencies(target, flags.skipInstall);
- exposeWorkspaceSkills(workspace, target, flags.force);
- writeMcp(workspace, target);
- if (flags.smoke) {
- runNode(['scripts/smoke-package.mjs'], target);
- runNode(['scripts/smoke-mcp.mjs'], target);
- }
- console.log(`Workspace installed: ${target}`);
- console.log('Restart the Claude Code session.');
- }
- async function globalInstall(args) {
- const flags = parseFlags(args);
- const target = path.join(os.homedir(), '.claude', 'plugins', 'fmode-image-set');
- copyPackage(target, flags.force);
- installDependencies(target, flags.skipInstall);
- writeMcp(target, target);
- if (flags.smoke) runNode(['scripts/acceptance.mjs'], target);
- console.log(`Global plugin installed: ${target}`);
- }
- async function main() {
- const [command = 'help', ...args] = process.argv.slice(2);
- if (command === 'workspace') return workspaceInstall(args);
- if (command === 'install') return globalInstall(args);
- if (command === 'check') { const result = checkPackage(sourceRoot); console.log(JSON.stringify(result, null, 2)); process.exitCode = result.ok ? 0 : 1; return; }
- if (command === 'smoke') { runNode(['scripts/acceptance.mjs'], sourceRoot); return; }
- if (command === 'path') { console.log(sourceRoot); return; }
- console.log('Usage: fmode-image-set <workspace|install|check|smoke|path> [--force] [--smoke] [--skip-install]');
- }
- main().catch(error => { console.error(error.message); process.exit(1); });
|