#!/usr/bin/env node /** * OpenClaw WeChat Skill Deploy v1.2.0 (Node.js cross-platform) * * Usage: * node deploy-to-openclaw.js # normal deploy * node deploy-to-openclaw.js --dry-run # preview only * node deploy-to-openclaw.js --skills-root=/custom/path * node deploy-to-openclaw.js --source-root=/path/to/source */ 'use strict'; const fs = require('fs'); const path = require('path'); const os = require('os'); // ---- ANSI color helpers (fallback to plain text if not TTY) ---- const useColor = process.stdout.isTTY; const c = (code, s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : s); const cyan = (s) => c('36', s); const green = (s) => c('32', s); const yellow = (s) => c('33', s); const red = (s) => c('31', s); // ---- Parse CLI args ---- function parseArgs(argv) { const args = { dryRun: false, skillsRoot: null, sourceRoot: null }; for (const a of argv) { if (a === '--dry-run' || a === '-DryRun') args.dryRun = true; else if (a.startsWith('--skills-root=')) args.skillsRoot = a.slice('--skills-root='.length); else if (a.startsWith('--source-root=')) args.sourceRoot = a.slice('--source-root='.length); } return args; } const opts = parseArgs(process.argv.slice(2)); const sourceRoot = path.resolve(opts.sourceRoot || __dirname); const skillsRoot = path.resolve( opts.skillsRoot || path.join(os.homedir(), '.openclaw', 'skills') ); const dryRun = opts.dryRun; const categories = ['wechat']; // ---- FS helpers ---- function exists(p) { try { fs.accessSync(p); return true; } catch { return false; } } function mkdirp(p) { fs.mkdirSync(p, { recursive: true }); } function rmrf(p) { fs.rmSync(p, { recursive: true, force: true }); } function copyFile(src, dst) { fs.copyFileSync(src, dst); } function listDirs(p) { return fs.readdirSync(p, { withFileTypes: true }) .filter(d => d.isDirectory()) .map(d => ({ name: d.name, fullPath: path.join(p, d.name) })); } function listFiles(p) { return fs.readdirSync(p, { withFileTypes: true }) .filter(d => d.isFile()) .map(d => ({ name: d.name, fullPath: path.join(p, d.name) })); } // ---- Main ---- console.log('=== OpenClaw WeChat Skill Deploy v1.2.0 ==='); console.log('Source: ' + sourceRoot); console.log('Target: ' + skillsRoot); if (dryRun) console.log(yellow('[DRY RUN]')); if (!dryRun && !exists(skillsRoot)) mkdirp(skillsRoot); let deployed = 0, skipped = 0, errors = 0; for (const cat of categories) { const catPath = path.join(sourceRoot, cat); if (!exists(catPath)) continue; console.log(cyan('--- ' + cat + ' ---')); for (const skillDir of listDirs(catPath)) { const sm = path.join(skillDir.fullPath, 'SKILL.md'); if (!exists(sm)) { skipped++; continue; } // api-config.json is optional reference material in v1.1.0+ const ac = path.join(skillDir.fullPath, 'api-config.json'); if (exists(ac)) { try { JSON.parse(fs.readFileSync(ac, 'utf8')); } catch (e) { console.log(red(' [ERR] ' + skillDir.name + ': invalid api-config.json')); errors++; continue; } } const dest = path.join(skillsRoot, skillDir.name); if (dryRun) { console.log(' [' + (exists(dest) ? 'UPDATE' : 'NEW') + '] ' + skillDir.name); } else { if (exists(dest)) rmrf(dest); mkdirp(dest); for (const f of listFiles(skillDir.fullPath)) { copyFile(f.fullPath, path.join(dest, f.name)); } console.log(green(' [OK] ' + skillDir.name)); } deployed++; } } console.log(`Result: ${deployed} deployed, ${skipped} skipped, ${errors} errors`); // ---- Deploy workflows ---- const wfSource = path.join(sourceRoot, 'workflows'); const wfTarget = path.join(path.dirname(skillsRoot), 'workflows'); if (exists(wfSource)) { console.log(cyan('--- workflows ---')); if (!dryRun) { if (!exists(wfTarget)) mkdirp(wfTarget); for (const f of listFiles(wfSource)) { copyFile(f.fullPath, path.join(wfTarget, f.name)); console.log(green(' [OK] ' + f.name)); } } else { for (const f of listFiles(wfSource)) { console.log(' [WORKFLOW] ' + f.name); } } } // ---- Credentials template & default file ---- const openclawDir = path.dirname(skillsRoot); if (!dryRun) { const tpl = path.join(sourceRoot, '__config', 'wechat-credentials.template.json'); const dst = path.join(openclawDir, 'wechat-credentials.template.json'); if (exists(tpl)) { copyFile(tpl, dst); console.log('Template: ' + dst); const credFile = path.join(openclawDir, 'wechat-credentials.json'); if (!exists(credFile)) { copyFile(tpl, credFile); console.log(yellow('[!] Created ' + credFile + ' - please edit wechatApiBase to your server address')); } } } // ---- Auto-reply daemon + config ---- const daemonSrc = path.join(sourceRoot, 'daemon', 'auto-reply-daemon.js'); const daemonDst = path.join(openclawDir, 'auto-reply-daemon.js'); const cfgTplSrc = path.join(sourceRoot, 'daemon', 'wechat-auto-reply-config.template.json'); const cfgTplDst = path.join(openclawDir, 'wechat-auto-reply-config.template.json'); const cfgDst = path.join(openclawDir, 'wechat-auto-reply-config.json'); if (exists(daemonSrc)) { console.log(cyan('--- auto-reply daemon ---')); if (dryRun) { console.log(' [DAEMON] ' + daemonDst); console.log(' [CONFIG-TEMPLATE] ' + cfgTplDst); if (!exists(cfgDst)) console.log(' [CONFIG-DEFAULT] ' + cfgDst); } else { copyFile(daemonSrc, daemonDst); try { fs.chmodSync(daemonDst, 0o755); } catch {} console.log(green(' [OK] ' + daemonDst)); if (exists(cfgTplSrc)) { copyFile(cfgTplSrc, cfgTplDst); console.log(green(' [OK] ' + cfgTplDst)); if (!exists(cfgDst)) { copyFile(cfgTplSrc, cfgDst); console.log(yellow(' [!] Created default config: ' + cfgDst + ' (edit to customize reply rules)')); } } } } if (!dryRun) console.log(green('Done!')); process.exit(errors > 0 ? 1 : 0);