install.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. #!/usr/bin/env node
  2. const fs = require('fs');
  3. const os = require('os');
  4. const path = require('path');
  5. const { spawnSync } = require('child_process');
  6. const ROOT = __dirname;
  7. const PLUGIN_NAME = 'tihao';
  8. const SKILL_NAME = 'tihao';
  9. const SKILL_NAMES = ['tihao', 'fmode-image-analysis'];
  10. const LEGACY_PLUGIN_NAMES = ['tihao-sourcing'];
  11. const LEGACY_SKILL_NAMES = ['tihao-creator-sourcing'];
  12. function main() {
  13. const args = new Set(process.argv.slice(2));
  14. if (args.has('--check')) {
  15. console.log('tihao package is readable.');
  16. return;
  17. }
  18. const workspaceMode = args.has('--workspace') || process.argv.includes('workspace');
  19. const baseDir = workspaceMode
  20. ? path.join(process.cwd(), '.claude')
  21. : path.join(os.homedir(), '.claude');
  22. const pluginTarget = path.join(baseDir, 'plugins', PLUGIN_NAME);
  23. const primarySkillTarget = path.join(baseDir, 'skills', SKILL_NAME);
  24. copyDir(ROOT, pluginTarget, shouldCopyPackageFile);
  25. for (const skillName of SKILL_NAMES) {
  26. copyDir(path.join(ROOT, 'skills', skillName), path.join(baseDir, 'skills', skillName), () => true);
  27. }
  28. removeLegacyPlugins(baseDir, pluginTarget);
  29. removeLegacySkills(baseDir, primarySkillTarget);
  30. ensureRuntimeDeps(pluginTarget);
  31. writeWorkspaceMcp(process.cwd(), pluginTarget, workspaceMode);
  32. console.log(`Installed ${PLUGIN_NAME} plugin to ${pluginTarget}`);
  33. for (const skillName of SKILL_NAMES) {
  34. console.log(`Installed ${skillName} skill to ${path.join(baseDir, 'skills', skillName)}`);
  35. }
  36. if (args.has('--smoke')) {
  37. const result = spawnSync(process.execPath, [path.join(pluginTarget, 'scripts/smoke-package.js')], {
  38. cwd: pluginTarget,
  39. stdio: 'inherit'
  40. });
  41. process.exit(result.status || 0);
  42. }
  43. }
  44. function removeLegacyPlugins(baseDir, currentPluginTarget) {
  45. const pluginsRoot = path.resolve(baseDir, 'plugins');
  46. const resolvedCurrent = path.resolve(currentPluginTarget);
  47. for (const legacyName of LEGACY_PLUGIN_NAMES) {
  48. const legacyTarget = path.resolve(pluginsRoot, legacyName);
  49. const relativeLegacy = path.relative(pluginsRoot, legacyTarget);
  50. if (legacyTarget === resolvedCurrent) continue;
  51. if (relativeLegacy.startsWith('..') || path.isAbsolute(relativeLegacy)) continue;
  52. removeDirRecursive(legacyTarget);
  53. }
  54. }
  55. function removeLegacySkills(baseDir, currentSkillTarget) {
  56. const skillsRoot = path.resolve(baseDir, 'skills');
  57. const resolvedCurrent = path.resolve(currentSkillTarget);
  58. for (const legacyName of LEGACY_SKILL_NAMES) {
  59. const legacyTarget = path.resolve(skillsRoot, legacyName);
  60. const relativeLegacy = path.relative(skillsRoot, legacyTarget);
  61. if (legacyTarget === resolvedCurrent) continue;
  62. if (relativeLegacy.startsWith('..') || path.isAbsolute(relativeLegacy)) continue;
  63. removeDirRecursive(legacyTarget);
  64. }
  65. }
  66. function removeDirRecursive(target) {
  67. if (!fs.existsSync(target)) return;
  68. try {
  69. fs.rmSync(target, { recursive: true, force: true });
  70. } catch (error) {
  71. if (!fs.existsSync(target)) return;
  72. }
  73. if (!fs.existsSync(target)) return;
  74. for (const entry of fs.readdirSync(target, { withFileTypes: true })) {
  75. const child = path.join(target, entry.name);
  76. if (entry.isDirectory() && !entry.isSymbolicLink()) {
  77. removeDirRecursive(child);
  78. } else {
  79. fs.unlinkSync(child);
  80. }
  81. }
  82. fs.rmdirSync(target);
  83. }
  84. function shouldCopyPackageFile(source) {
  85. const rel = path.relative(ROOT, source).replace(/\\/g, '/');
  86. if (!rel) return true;
  87. if (rel.startsWith('node_modules/')) return false;
  88. if (rel.startsWith('outputs/')) return false;
  89. if (rel.startsWith('.claude/')) return false;
  90. if (rel === '.env' || rel === '.env.local' || rel === '.npmrc') return false;
  91. return true;
  92. }
  93. function copyDir(source, target, predicate) {
  94. if (!fs.existsSync(source)) return;
  95. fs.mkdirSync(target, { recursive: true });
  96. for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
  97. const sourcePath = path.join(source, entry.name);
  98. if (!predicate(sourcePath)) continue;
  99. const targetPath = path.join(target, entry.name);
  100. if (entry.isDirectory()) {
  101. copyDir(sourcePath, targetPath, predicate);
  102. } else if (entry.isFile()) {
  103. fs.mkdirSync(path.dirname(targetPath), { recursive: true });
  104. fs.copyFileSync(sourcePath, targetPath);
  105. }
  106. }
  107. }
  108. function writeWorkspaceMcp(cwd, pluginTarget, workspaceMode) {
  109. if (!workspaceMode) return;
  110. const mcpFile = path.join(cwd, '.mcp.json');
  111. const existing = fs.existsSync(mcpFile) ? safeJson(fs.readFileSync(mcpFile, 'utf8')) : {};
  112. const mcpServers = { ...(existing.mcpServers || {}) };
  113. for (const legacyName of LEGACY_PLUGIN_NAMES) {
  114. delete mcpServers[legacyName];
  115. }
  116. mcpServers[PLUGIN_NAME] = {
  117. command: 'node',
  118. args: [path.join(pluginTarget, 'mcp/src/server.js')]
  119. };
  120. const next = {
  121. ...existing,
  122. mcpServers
  123. };
  124. fs.writeFileSync(mcpFile, JSON.stringify(next, null, 2), 'utf8');
  125. }
  126. function ensureRuntimeDeps(pluginTarget) {
  127. const sdkPath = path.join(pluginTarget, 'node_modules', '@modelcontextprotocol', 'sdk');
  128. if (fs.existsSync(sdkPath)) return;
  129. const sourceNodeModules = path.join(ROOT, 'node_modules');
  130. if (fs.existsSync(sourceNodeModules)) {
  131. copyDir(sourceNodeModules, path.join(pluginTarget, 'node_modules'), () => true);
  132. if (fs.existsSync(sdkPath)) return;
  133. }
  134. const npmCommand = resolveNpmCommand(['install', '--omit=dev']);
  135. const result = spawnSync(npmCommand.command, npmCommand.args, {
  136. cwd: pluginTarget,
  137. stdio: 'inherit',
  138. shell: npmCommand.shell
  139. });
  140. if (result.status !== 0) {
  141. throw new Error('Failed to install runtime dependencies for copied plugin.');
  142. }
  143. }
  144. function resolveNpmCommand(args) {
  145. if (process.env.npm_execpath) {
  146. return { command: process.execPath, args: [process.env.npm_execpath, ...args], shell: false };
  147. }
  148. if (process.platform === 'win32') {
  149. return { command: 'npm.cmd', args, shell: true };
  150. }
  151. return { command: 'npm', args, shell: false };
  152. }
  153. function safeJson(raw) {
  154. try {
  155. return JSON.parse(raw);
  156. } catch {
  157. return {};
  158. }
  159. }
  160. main();