fmode-vision.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  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 SKILL_NAME = 'fmode-vision';
  7. const SOURCE_ROOT = path.resolve(__dirname, '..');
  8. const SKILL_SOURCE = path.join(SOURCE_ROOT, 'skills', SKILL_NAME);
  9. const WORKSPACE_ROOT = process.cwd();
  10. const GLOBAL_TARGET = path.join(os.homedir(), '.claude', 'skills', SKILL_NAME);
  11. const WORKSPACE_TARGET = path.join(WORKSPACE_ROOT, '.claude', 'skills', SKILL_NAME);
  12. const WORKSPACE_SKILLS_ROOT = path.join(WORKSPACE_ROOT, '.claude', 'skills');
  13. const GLOBAL_SKILLS_ROOT = path.join(os.homedir(), '.claude', 'skills');
  14. function expandHome(value) {
  15. return String(value || '').replace(/^~(?=$|[\\/])/, os.homedir());
  16. }
  17. function parseArgs(argv) {
  18. const first = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'install';
  19. const args = { command: first, target: GLOBAL_TARGET, smoke: false, force: false, help: false };
  20. if (first === 'workspace' || first === 'install-workspace') {
  21. args.command = 'install';
  22. args.target = WORKSPACE_TARGET;
  23. }
  24. for (let i = first === argv[0] ? 1 : 0; i < argv.length; i++) {
  25. const token = argv[i];
  26. if (token === '--target' && argv[i + 1]) args.target = argv[++i];
  27. else if (token.startsWith('--target=')) args.target = token.slice('--target='.length);
  28. else if (token === '--workspace') args.target = WORKSPACE_TARGET;
  29. else if (token === '--global') args.target = GLOBAL_TARGET;
  30. else if (token === '--smoke') args.smoke = true;
  31. else if (token === '--force') args.force = true;
  32. else if (token === '--help' || token === '-h') args.help = true;
  33. }
  34. args.target = path.resolve(expandHome(args.target));
  35. return args;
  36. }
  37. function printHelp() {
  38. console.log([
  39. 'fmode-vision skill installer',
  40. '',
  41. 'Usage:',
  42. ' npx fmode-vision@latest workspace [--smoke] # install into ./.claude/skills/fmode-vision',
  43. ' npx fmode-vision@latest install [--smoke] # install into ~/.claude/skills/fmode-vision',
  44. ' npx fmode-vision@latest install --target <dir> [--force]',
  45. ' npx fmode-vision@latest check',
  46. ' npx fmode-vision@latest smoke',
  47. ' npx fmode-vision@latest path',
  48. '',
  49. 'Options:',
  50. ' --workspace Install into ./.claude/skills/fmode-vision',
  51. ' --global Install into ~/.claude/skills/fmode-vision (default)',
  52. ' --target <dir> Install into a custom directory',
  53. ' --force Allow overwriting a custom target',
  54. ' --smoke Run smoke checks after install',
  55. ' --help, -h Show help'
  56. ].join('\n'));
  57. }
  58. function ensureDir(dirPath) { fs.mkdirSync(dirPath, { recursive: true }); }
  59. function isInside(parentDir, childDir) {
  60. const relative = path.relative(path.resolve(parentDir), path.resolve(childDir));
  61. return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
  62. }
  63. function canOverwriteTarget(targetDir, force) {
  64. return force
  65. || path.resolve(targetDir) === path.resolve(GLOBAL_TARGET)
  66. || isInside(WORKSPACE_SKILLS_ROOT, targetDir)
  67. || isInside(GLOBAL_SKILLS_ROOT, targetDir);
  68. }
  69. function copyDirRecursive(source, destination) {
  70. const stat = fs.statSync(source);
  71. if (stat.isDirectory()) {
  72. ensureDir(destination);
  73. for (const child of fs.readdirSync(source)) {
  74. if (child === 'node_modules' || child === 'outputs' || child === '.git') continue;
  75. copyDirRecursive(path.join(source, child), path.join(destination, child));
  76. }
  77. return;
  78. }
  79. ensureDir(path.dirname(destination));
  80. fs.copyFileSync(source, destination);
  81. }
  82. function installSkill(target, force) {
  83. if (!fs.existsSync(SKILL_SOURCE)) {
  84. throw new Error(`Skill source missing: ${SKILL_SOURCE}`);
  85. }
  86. if (fs.existsSync(target)) {
  87. if (!canOverwriteTarget(target, force)) {
  88. throw new Error(`Refusing to overwrite custom target without --force: ${target}`);
  89. }
  90. fs.rmSync(target, { recursive: true, force: true });
  91. }
  92. ensureDir(target);
  93. copyDirRecursive(SKILL_SOURCE, target);
  94. }
  95. function checkSkill(target) {
  96. const required = ['SKILL.md', 'scripts/vision-client.mjs'];
  97. const missing = required.filter(entry => !fs.existsSync(path.join(target, entry)));
  98. if (missing.length) {
  99. throw new Error(`Install target is missing required files: ${missing.join(', ')}`);
  100. }
  101. return { status: 'ok', skill: SKILL_NAME, target, required };
  102. }
  103. function runSmoke() {
  104. const result = spawnSync(process.execPath, ['scripts/smoke.js'], { cwd: SOURCE_ROOT, stdio: 'inherit', shell: false });
  105. if (result.status !== 0) throw new Error('smoke failed');
  106. }
  107. function printNextSteps(target) {
  108. const workspaceMode = isInside(WORKSPACE_SKILLS_ROOT, target);
  109. console.log('');
  110. console.log('Install complete.');
  111. console.log(`Skill installed at: ${target}`);
  112. console.log('');
  113. if (workspaceMode) {
  114. console.log('Project-level skill is ready. Restart the VSCode Claude Code session if it was open.');
  115. } else {
  116. console.log('User-level skill is ready for all Claude Code workspaces.');
  117. }
  118. console.log('');
  119. console.log('Token: set FMODE_API_TOKEN, or ~/.fmode/config.json -> fmodeApiToken, or rely on ANTHROPIC_AUTH_TOKEN.');
  120. console.log('');
  121. console.log('Try this prompt in Claude Code:');
  122. console.log(' 帮我分析这张图片里的关键内容,输出结构化信息。');
  123. }
  124. function main() {
  125. const args = parseArgs(process.argv.slice(2));
  126. if (args.help || args.command === 'help') { printHelp(); return; }
  127. if (args.command === 'path') { console.log(args.target); return; }
  128. if (args.command === 'install') {
  129. installSkill(args.target, args.force);
  130. console.log(JSON.stringify(checkSkill(args.target), null, 2));
  131. if (args.smoke) runSmoke();
  132. printNextSteps(args.target);
  133. return;
  134. }
  135. if (args.command === 'check') { console.log(JSON.stringify(checkSkill(args.target), null, 2)); return; }
  136. if (args.command === 'smoke') { runSmoke(); return; }
  137. printHelp();
  138. process.exitCode = 1;
  139. }
  140. try { main(); }
  141. catch (error) { console.error(`fmode-vision failed: ${error.message}`); process.exit(1); }