deploy-to-openclaw.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. #!/usr/bin/env node
  2. /**
  3. * OpenClaw WeChat Skill Deploy v1.0.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.0.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. const ac = path.join(skillDir.fullPath, 'api-config.json');
  76. if (!exists(sm) || !exists(ac)) { skipped++; continue; }
  77. try {
  78. JSON.parse(fs.readFileSync(ac, 'utf8'));
  79. } catch (e) {
  80. console.log(red(' [ERR] ' + skillDir.name + ': invalid api-config.json'));
  81. errors++; continue;
  82. }
  83. const dest = path.join(skillsRoot, skillDir.name);
  84. if (dryRun) {
  85. console.log(' [' + (exists(dest) ? 'UPDATE' : 'NEW') + '] ' + skillDir.name);
  86. } else {
  87. if (exists(dest)) rmrf(dest);
  88. mkdirp(dest);
  89. for (const f of listFiles(skillDir.fullPath)) {
  90. copyFile(f.fullPath, path.join(dest, f.name));
  91. }
  92. console.log(green(' [OK] ' + skillDir.name));
  93. }
  94. deployed++;
  95. }
  96. }
  97. console.log(`Result: ${deployed} deployed, ${skipped} skipped, ${errors} errors`);
  98. // ---- Deploy workflows ----
  99. const wfSource = path.join(sourceRoot, 'workflows');
  100. const wfTarget = path.join(path.dirname(skillsRoot), 'workflows');
  101. if (exists(wfSource)) {
  102. console.log(cyan('--- workflows ---'));
  103. if (!dryRun) {
  104. if (!exists(wfTarget)) mkdirp(wfTarget);
  105. for (const f of listFiles(wfSource)) {
  106. copyFile(f.fullPath, path.join(wfTarget, f.name));
  107. console.log(green(' [OK] ' + f.name));
  108. }
  109. } else {
  110. for (const f of listFiles(wfSource)) {
  111. console.log(' [WORKFLOW] ' + f.name);
  112. }
  113. }
  114. }
  115. // ---- Credentials template & default file ----
  116. if (!dryRun) {
  117. const tpl = path.join(sourceRoot, '__config', 'wechat-credentials.template.json');
  118. const dst = path.join(path.dirname(skillsRoot), 'wechat-credentials.template.json');
  119. if (exists(tpl)) {
  120. copyFile(tpl, dst);
  121. console.log('Template: ' + dst);
  122. const credFile = path.join(path.dirname(skillsRoot), 'wechat-credentials.json');
  123. if (!exists(credFile)) {
  124. copyFile(tpl, credFile);
  125. console.log(yellow('[!] Created ' + credFile + ' - please edit wechatApiBase to your server address'));
  126. }
  127. }
  128. console.log(green('Done!'));
  129. }
  130. process.exit(errors > 0 ? 1 : 0);