skill-image.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. #!/usr/bin/env node
  2. // Copyright (c) 未来飞马
  3. //
  4. // This Source Code Form is subject to the terms of the Mozilla Public
  5. // License, v. 2.0. If a copy of the MPL was not distributed with this
  6. // file, You can obtain one at https://mozilla.org/MPL/2.0/.
  7. //
  8. // Trademark Notice:
  9. // The MPL-2.0 license grants copyright permissions for source code only.
  10. // It does NOT grant any rights to use trademarks including "未来飞马",
  11. // "Harness Loop", "RSI", and associated slogan "让AI进化提前发生,让AI落地快人一步".
  12. // Any use of these trademarks requires separate written permission.
  13. const fs = require('fs');
  14. const os = require('os');
  15. const path = require('path');
  16. const { spawnSync } = require('child_process');
  17. const SKILL_NAME = 'skill-image';
  18. const SOURCE_ROOT = path.resolve(__dirname, '..');
  19. const SKILL_SOURCE = path.join(SOURCE_ROOT, 'skills', SKILL_NAME);
  20. const LIB = path.join(SOURCE_ROOT, 'lib', 'skill-image.mjs');
  21. const WORKSPACE_ROOT = process.cwd();
  22. const GLOBAL_TARGET = path.join(os.homedir(), '.claude', 'skills', SKILL_NAME);
  23. const WORKSPACE_TARGET = path.join(WORKSPACE_ROOT, '.claude', 'skills', SKILL_NAME);
  24. const WORKSPACE_SKILLS_ROOT = path.join(WORKSPACE_ROOT, '.claude', 'skills');
  25. const GLOBAL_SKILLS_ROOT = path.join(os.homedir(), '.claude', 'skills');
  26. const SUBCOMMANDS = ['install', 'install-workspace', 'workspace', 'check', 'path', 'smoke', 'help'];
  27. function expandHome(value) {
  28. return String(value || '').replace(/^~(?=$|[\\/])/, os.homedir());
  29. }
  30. function parseArgs(argv) {
  31. const first = argv[0];
  32. if (!first || !SUBCOMMANDS.includes(first)) {
  33. // No subcommand: passthrough to the image generator (--arch/--app/--scene/...).
  34. return { command: 'generate', passthrough: argv };
  35. }
  36. const args = { command: first, target: GLOBAL_TARGET, smoke: false, force: false, help: false };
  37. if (first === 'workspace' || first === 'install-workspace') {
  38. args.command = 'install';
  39. args.target = WORKSPACE_TARGET;
  40. }
  41. for (let i = 1; i < argv.length; i++) {
  42. const token = argv[i];
  43. if (token === '--target' && argv[i + 1]) args.target = argv[++i];
  44. else if (token.startsWith('--target=')) args.target = token.slice('--target='.length);
  45. else if (token === '--workspace') args.target = WORKSPACE_TARGET;
  46. else if (token === '--global') args.target = GLOBAL_TARGET;
  47. else if (token === '--smoke') args.smoke = true;
  48. else if (token === '--force') args.force = true;
  49. else if (token === '--help' || token === '-h') args.help = true;
  50. }
  51. args.target = path.resolve(expandHome(args.target));
  52. return args;
  53. }
  54. function printHelp() {
  55. console.log([
  56. 'skill-image — AI 图像生成器 + FmodeCode / FmodeCode 技能安装器',
  57. '',
  58. 'Usage:',
  59. ' node <技能目录>/bin/skill-image.js --arch "prompt" [name] # 架构图 ¥0.5',
  60. ' node <技能目录>/bin/skill-image.js --app "prompt" [name] # 应用界面 ¥0.5',
  61. ' node <技能目录>/bin/skill-image.js --explode "prompt" [name] # 爆炸图 ¥0.5',
  62. ' node <技能目录>/bin/skill-image.js --product "prompt" [name] # 产品设计图 ¥0.5',
  63. ' node <技能目录>/bin/skill-image.js --scene "prompt" [name] # 场景插图 ¥0.3',
  64. ' node <技能目录>/bin/skill-image.js --detail "prompt" [name] # 细节图 ¥0.3',
  65. ' node <技能目录>/bin/skill-image.js --slide "prompt" [name] # 整页PPT ¥0.5',
  66. '',
  67. ' node <技能目录>/bin/skill-image.js workspace [--smoke] # install into ./.claude/skills/skill-image',
  68. ' node <技能目录>/bin/skill-image.js install [--smoke] # install into ~/.claude/skills/skill-image',
  69. ' node <技能目录>/bin/skill-image.js install --target <dir> [--force]',
  70. ' node <技能目录>/bin/skill-image.js check',
  71. ' node <技能目录>/bin/skill-image.js smoke',
  72. ' node <技能目录>/bin/skill-image.js path',
  73. '',
  74. 'Options:',
  75. ' --workspace Install into ./.claude/skills/skill-image',
  76. ' --global Install into ~/.claude/skills/skill-image (default)',
  77. ' --target <dir> Install into a custom directory',
  78. ' --force Allow overwriting a custom target',
  79. ' --smoke Run smoke checks after install',
  80. ' --help, -h Show help'
  81. ].join('\n'));
  82. }
  83. function ensureDir(dirPath) { fs.mkdirSync(dirPath, { recursive: true }); }
  84. function isInside(parentDir, childDir) {
  85. const relative = path.relative(path.resolve(parentDir), path.resolve(childDir));
  86. return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
  87. }
  88. function canOverwriteTarget(targetDir, force) {
  89. return force
  90. || path.resolve(targetDir) === path.resolve(GLOBAL_TARGET)
  91. || isInside(WORKSPACE_SKILLS_ROOT, targetDir)
  92. || isInside(GLOBAL_SKILLS_ROOT, targetDir);
  93. }
  94. function copyDirRecursive(source, destination) {
  95. const stat = fs.statSync(source);
  96. if (stat.isDirectory()) {
  97. ensureDir(destination);
  98. for (const child of fs.readdirSync(source)) {
  99. if (child === 'node_modules' || child === 'outputs' || child === '.git') continue;
  100. copyDirRecursive(path.join(source, child), path.join(destination, child));
  101. }
  102. return;
  103. }
  104. ensureDir(path.dirname(destination));
  105. fs.copyFileSync(source, destination);
  106. }
  107. function installSkill(target, force) {
  108. if (!fs.existsSync(SKILL_SOURCE)) {
  109. throw new Error(`Skill source missing: ${SKILL_SOURCE}`);
  110. }
  111. if (fs.existsSync(target)) {
  112. if (!canOverwriteTarget(target, force)) {
  113. throw new Error(`Refusing to overwrite custom target without --force: ${target}`);
  114. }
  115. fs.rmSync(target, { recursive: true, force: true });
  116. }
  117. ensureDir(target);
  118. copyDirRecursive(SKILL_SOURCE, target);
  119. }
  120. function checkSkill(target) {
  121. const required = ['SKILL.md'];
  122. const missing = required.filter(entry => !fs.existsSync(path.join(target, entry)));
  123. if (missing.length) {
  124. throw new Error(`Install target is missing required files: ${missing.join(', ')}`);
  125. }
  126. return { status: 'ok', skill: SKILL_NAME, target, required };
  127. }
  128. function runSmoke() {
  129. const result = spawnSync(process.execPath, ['scripts/smoke.js'], { cwd: SOURCE_ROOT, stdio: 'inherit', shell: false });
  130. if (result.status !== 0) throw new Error('smoke failed');
  131. }
  132. function printNextSteps(target) {
  133. const workspaceMode = isInside(WORKSPACE_SKILLS_ROOT, target);
  134. console.log('');
  135. console.log('Install complete.');
  136. console.log(`Skill installed at: ${target}`);
  137. console.log('');
  138. console.log(workspaceMode
  139. ? 'Project-level skill is ready. Restart the VSCode FmodeCode / FmodeCode session if it was open.'
  140. : 'User-level skill is ready for all FmodeCode / FmodeCode workspaces.');
  141. console.log('');
  142. console.log('API Key: set FMODE_API_KEY, or ~/.fmode/config.json -> api_key / fmodeApiToken.');
  143. console.log('');
  144. console.log('Try this prompt in FmodeCode / FmodeCode:');
  145. console.log(' 用 skill-image 生成一张"内容合规系统"的架构图,输出到当前目录。');
  146. }
  147. function generate(argv) {
  148. const result = spawnSync(process.execPath, [LIB, ...argv], { stdio: 'inherit' });
  149. process.exit(result.status ?? 0);
  150. }
  151. function main() {
  152. const args = parseArgs(process.argv.slice(2));
  153. if (args.command === 'generate') {
  154. if (!args.passthrough.length || args.passthrough[0] === '--help' || args.passthrough[0] === '-h') {
  155. printHelp();
  156. process.exit(0);
  157. }
  158. generate(args.passthrough);
  159. return;
  160. }
  161. if (args.help || args.command === 'help') { printHelp(); return; }
  162. if (args.command === 'path') { console.log(args.target); return; }
  163. if (args.command === 'install') {
  164. installSkill(args.target, args.force);
  165. console.log(JSON.stringify(checkSkill(args.target), null, 2));
  166. if (args.smoke) runSmoke();
  167. printNextSteps(args.target);
  168. return;
  169. }
  170. if (args.command === 'check') { console.log(JSON.stringify(checkSkill(args.target), null, 2)); return; }
  171. if (args.command === 'smoke') { runSmoke(); return; }
  172. printHelp();
  173. process.exitCode = 1;
  174. }
  175. try { main(); }
  176. catch (error) { console.error(`skill-image failed: ${error.message}`); process.exit(1); }