#!/usr/bin/env node const fs = require('fs'); const os = require('os'); const path = require('path'); const { spawnSync } = require('child_process'); const ROOT = __dirname; const PLUGIN_NAME = 'tihao'; const SKILL_NAME = 'tihao'; const SKILL_NAMES = ['tihao', 'fmode-image-analysis']; const LEGACY_PLUGIN_NAMES = ['tihao-sourcing']; const LEGACY_SKILL_NAMES = ['tihao-creator-sourcing']; function main() { const args = new Set(process.argv.slice(2)); if (args.has('--check')) { console.log('tihao package is readable.'); return; } const workspaceMode = args.has('--workspace') || process.argv.includes('workspace'); const baseDir = workspaceMode ? path.join(process.cwd(), '.claude') : path.join(os.homedir(), '.claude'); const pluginTarget = path.join(baseDir, 'plugins', PLUGIN_NAME); const primarySkillTarget = path.join(baseDir, 'skills', SKILL_NAME); copyDir(ROOT, pluginTarget, shouldCopyPackageFile); for (const skillName of SKILL_NAMES) { copyDir(path.join(ROOT, 'skills', skillName), path.join(baseDir, 'skills', skillName), () => true); } removeLegacyPlugins(baseDir, pluginTarget); removeLegacySkills(baseDir, primarySkillTarget); ensureRuntimeDeps(pluginTarget); writeWorkspaceMcp(process.cwd(), pluginTarget, workspaceMode); console.log(`Installed ${PLUGIN_NAME} plugin to ${pluginTarget}`); for (const skillName of SKILL_NAMES) { console.log(`Installed ${skillName} skill to ${path.join(baseDir, 'skills', skillName)}`); } if (args.has('--smoke')) { const result = spawnSync(process.execPath, [path.join(pluginTarget, 'scripts/smoke-package.js')], { cwd: pluginTarget, stdio: 'inherit' }); process.exit(result.status || 0); } } function removeLegacyPlugins(baseDir, currentPluginTarget) { const pluginsRoot = path.resolve(baseDir, 'plugins'); const resolvedCurrent = path.resolve(currentPluginTarget); for (const legacyName of LEGACY_PLUGIN_NAMES) { const legacyTarget = path.resolve(pluginsRoot, legacyName); const relativeLegacy = path.relative(pluginsRoot, legacyTarget); if (legacyTarget === resolvedCurrent) continue; if (relativeLegacy.startsWith('..') || path.isAbsolute(relativeLegacy)) continue; removeDirRecursive(legacyTarget); } } function removeLegacySkills(baseDir, currentSkillTarget) { const skillsRoot = path.resolve(baseDir, 'skills'); const resolvedCurrent = path.resolve(currentSkillTarget); for (const legacyName of LEGACY_SKILL_NAMES) { const legacyTarget = path.resolve(skillsRoot, legacyName); const relativeLegacy = path.relative(skillsRoot, legacyTarget); if (legacyTarget === resolvedCurrent) continue; if (relativeLegacy.startsWith('..') || path.isAbsolute(relativeLegacy)) continue; removeDirRecursive(legacyTarget); } } function removeDirRecursive(target) { if (!fs.existsSync(target)) return; try { fs.rmSync(target, { recursive: true, force: true }); } catch (error) { if (!fs.existsSync(target)) return; } if (!fs.existsSync(target)) return; for (const entry of fs.readdirSync(target, { withFileTypes: true })) { const child = path.join(target, entry.name); if (entry.isDirectory() && !entry.isSymbolicLink()) { removeDirRecursive(child); } else { fs.unlinkSync(child); } } fs.rmdirSync(target); } function shouldCopyPackageFile(source) { const rel = path.relative(ROOT, source).replace(/\\/g, '/'); if (!rel) return true; if (rel.startsWith('node_modules/')) return false; if (rel.startsWith('outputs/')) return false; if (rel.startsWith('.claude/')) return false; if (rel === '.env' || rel === '.env.local' || rel === '.npmrc') return false; return true; } function copyDir(source, target, predicate) { if (!fs.existsSync(source)) return; fs.mkdirSync(target, { recursive: true }); for (const entry of fs.readdirSync(source, { withFileTypes: true })) { const sourcePath = path.join(source, entry.name); if (!predicate(sourcePath)) continue; const targetPath = path.join(target, entry.name); if (entry.isDirectory()) { copyDir(sourcePath, targetPath, predicate); } else if (entry.isFile()) { fs.mkdirSync(path.dirname(targetPath), { recursive: true }); fs.copyFileSync(sourcePath, targetPath); } } } function writeWorkspaceMcp(cwd, pluginTarget, workspaceMode) { if (!workspaceMode) return; const mcpFile = path.join(cwd, '.mcp.json'); const existing = fs.existsSync(mcpFile) ? safeJson(fs.readFileSync(mcpFile, 'utf8')) : {}; const mcpServers = { ...(existing.mcpServers || {}) }; for (const legacyName of LEGACY_PLUGIN_NAMES) { delete mcpServers[legacyName]; } mcpServers[PLUGIN_NAME] = { command: 'node', args: [path.join(pluginTarget, 'mcp/src/server.js')] }; const next = { ...existing, mcpServers }; fs.writeFileSync(mcpFile, JSON.stringify(next, null, 2), 'utf8'); } function ensureRuntimeDeps(pluginTarget) { const sdkPath = path.join(pluginTarget, 'node_modules', '@modelcontextprotocol', 'sdk'); if (fs.existsSync(sdkPath)) return; const sourceNodeModules = path.join(ROOT, 'node_modules'); if (fs.existsSync(sourceNodeModules)) { copyDir(sourceNodeModules, path.join(pluginTarget, 'node_modules'), () => true); if (fs.existsSync(sdkPath)) return; } const npmCommand = resolveNpmCommand(['install', '--omit=dev']); const result = spawnSync(npmCommand.command, npmCommand.args, { cwd: pluginTarget, stdio: 'inherit', shell: npmCommand.shell }); if (result.status !== 0) { throw new Error('Failed to install runtime dependencies for copied plugin.'); } } function resolveNpmCommand(args) { if (process.env.npm_execpath) { return { command: process.execPath, args: [process.env.npm_execpath, ...args], shell: false }; } if (process.platform === 'win32') { return { command: 'npm.cmd', args, shell: true }; } return { command: 'npm', args, shell: false }; } function safeJson(raw) { try { return JSON.parse(raw); } catch { return {}; } } main();