deploy-to-openclaw.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. #!/usr/bin/env node
  2. /**
  3. * OpenClaw WeChat Skill Deploy v1.2.0 (Node.js cross-platform)
  4. *
  5. * Usage:
  6. * node deploy-to-openclaw.js # normal deploy
  7. * node deploy-to-openclaw.js --dry-run # preview only
  8. * node deploy-to-openclaw.js --skills-root=/custom/path
  9. * node deploy-to-openclaw.js --source-root=/path/to/source
  10. */
  11. 'use strict';
  12. const fs = require('fs');
  13. const path = require('path');
  14. const os = require('os');
  15. // ---- ANSI color helpers (fallback to plain text if not TTY) ----
  16. const useColor = process.stdout.isTTY;
  17. const c = (code, s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : s);
  18. const cyan = (s) => c('36', s);
  19. const green = (s) => c('32', s);
  20. const yellow = (s) => c('33', s);
  21. const red = (s) => c('31', s);
  22. // ---- Parse CLI args ----
  23. function parseArgs(argv) {
  24. const args = { dryRun: false, skillsRoot: null, sourceRoot: null };
  25. for (const a of argv) {
  26. if (a === '--dry-run' || a === '-DryRun') args.dryRun = true;
  27. else if (a.startsWith('--skills-root=')) args.skillsRoot = a.slice('--skills-root='.length);
  28. else if (a.startsWith('--source-root=')) args.sourceRoot = a.slice('--source-root='.length);
  29. }
  30. return args;
  31. }
  32. const opts = parseArgs(process.argv.slice(2));
  33. const sourceRoot = path.resolve(opts.sourceRoot || __dirname);
  34. const skillsRoot = path.resolve(
  35. opts.skillsRoot || path.join(os.homedir(), '.openclaw', 'skills')
  36. );
  37. const dryRun = opts.dryRun;
  38. const categories = ['wechat'];
  39. // ---- FS helpers ----
  40. function exists(p) {
  41. try { fs.accessSync(p); return true; } catch { return false; }
  42. }
  43. function mkdirp(p) {
  44. fs.mkdirSync(p, { recursive: true });
  45. }
  46. function rmrf(p) {
  47. fs.rmSync(p, { recursive: true, force: true });
  48. }
  49. function copyFile(src, dst) {
  50. fs.copyFileSync(src, dst);
  51. }
  52. function listDirs(p) {
  53. return fs.readdirSync(p, { withFileTypes: true })
  54. .filter(d => d.isDirectory())
  55. .map(d => ({ name: d.name, fullPath: path.join(p, d.name) }));
  56. }
  57. function listFiles(p) {
  58. return fs.readdirSync(p, { withFileTypes: true })
  59. .filter(d => d.isFile())
  60. .map(d => ({ name: d.name, fullPath: path.join(p, d.name) }));
  61. }
  62. // ---- Main ----
  63. console.log('=== OpenClaw WeChat Skill Deploy v1.2.0 ===');
  64. console.log('Source: ' + sourceRoot);
  65. console.log('Target: ' + skillsRoot);
  66. if (dryRun) console.log(yellow('[DRY RUN]'));
  67. if (!dryRun && !exists(skillsRoot)) mkdirp(skillsRoot);
  68. let deployed = 0, skipped = 0, errors = 0;
  69. for (const cat of categories) {
  70. const catPath = path.join(sourceRoot, cat);
  71. if (!exists(catPath)) continue;
  72. console.log(cyan('--- ' + cat + ' ---'));
  73. for (const skillDir of listDirs(catPath)) {
  74. const sm = path.join(skillDir.fullPath, 'SKILL.md');
  75. if (!exists(sm)) { skipped++; continue; }
  76. // api-config.json is optional reference material in v1.1.0+
  77. const ac = path.join(skillDir.fullPath, 'api-config.json');
  78. if (exists(ac)) {
  79. try { JSON.parse(fs.readFileSync(ac, 'utf8')); }
  80. catch (e) {
  81. console.log(red(' [ERR] ' + skillDir.name + ': invalid api-config.json'));
  82. errors++; continue;
  83. }
  84. }
  85. const dest = path.join(skillsRoot, skillDir.name);
  86. if (dryRun) {
  87. console.log(' [' + (exists(dest) ? 'UPDATE' : 'NEW') + '] ' + skillDir.name);
  88. } else {
  89. if (exists(dest)) rmrf(dest);
  90. mkdirp(dest);
  91. for (const f of listFiles(skillDir.fullPath)) {
  92. copyFile(f.fullPath, path.join(dest, f.name));
  93. }
  94. console.log(green(' [OK] ' + skillDir.name));
  95. }
  96. deployed++;
  97. }
  98. }
  99. console.log(`Result: ${deployed} deployed, ${skipped} skipped, ${errors} errors`);
  100. // ---- Deploy workflows ----
  101. const wfSource = path.join(sourceRoot, 'workflows');
  102. const wfTarget = path.join(path.dirname(skillsRoot), 'workflows');
  103. if (exists(wfSource)) {
  104. console.log(cyan('--- workflows ---'));
  105. if (!dryRun) {
  106. if (!exists(wfTarget)) mkdirp(wfTarget);
  107. for (const f of listFiles(wfSource)) {
  108. copyFile(f.fullPath, path.join(wfTarget, f.name));
  109. console.log(green(' [OK] ' + f.name));
  110. }
  111. } else {
  112. for (const f of listFiles(wfSource)) {
  113. console.log(' [WORKFLOW] ' + f.name);
  114. }
  115. }
  116. }
  117. // ---- Credentials template & default file ----
  118. const openclawDir = path.dirname(skillsRoot);
  119. if (!dryRun) {
  120. const tpl = path.join(sourceRoot, '__config', 'wechat-credentials.template.json');
  121. const dst = path.join(openclawDir, 'wechat-credentials.template.json');
  122. if (exists(tpl)) {
  123. copyFile(tpl, dst);
  124. console.log('Template: ' + dst);
  125. const credFile = path.join(openclawDir, 'wechat-credentials.json');
  126. if (!exists(credFile)) {
  127. copyFile(tpl, credFile);
  128. console.log(yellow('[!] Created ' + credFile + ' - please edit wechatApiBase to your server address'));
  129. }
  130. }
  131. }
  132. // ---- Auto-reply daemon + config ----
  133. const daemonSrc = path.join(sourceRoot, 'daemon', 'auto-reply-daemon.js');
  134. const daemonDst = path.join(openclawDir, 'auto-reply-daemon.js');
  135. const cfgTplSrc = path.join(sourceRoot, 'daemon', 'wechat-auto-reply-config.template.json');
  136. const cfgTplDst = path.join(openclawDir, 'wechat-auto-reply-config.template.json');
  137. const cfgDst = path.join(openclawDir, 'wechat-auto-reply-config.json');
  138. if (exists(daemonSrc)) {
  139. console.log(cyan('--- auto-reply daemon ---'));
  140. if (dryRun) {
  141. console.log(' [DAEMON] ' + daemonDst);
  142. console.log(' [CONFIG-TEMPLATE] ' + cfgTplDst);
  143. if (!exists(cfgDst)) console.log(' [CONFIG-DEFAULT] ' + cfgDst);
  144. } else {
  145. copyFile(daemonSrc, daemonDst);
  146. try { fs.chmodSync(daemonDst, 0o755); } catch {}
  147. console.log(green(' [OK] ' + daemonDst));
  148. if (exists(cfgTplSrc)) {
  149. copyFile(cfgTplSrc, cfgTplDst);
  150. console.log(green(' [OK] ' + cfgTplDst));
  151. if (!exists(cfgDst)) {
  152. copyFile(cfgTplSrc, cfgDst);
  153. console.log(yellow(' [!] Created default config: ' + cfgDst + ' (edit to customize reply rules)'));
  154. }
  155. }
  156. }
  157. }
  158. if (!dryRun) console.log(green('Done!'));
  159. process.exit(errors > 0 ? 1 : 0);