|
|
@@ -0,0 +1,143 @@
|
|
|
+#!/usr/bin/env node
|
|
|
+/**
|
|
|
+ * OpenClaw WeChat Skill Deploy v1.0.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.0.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');
|
|
|
+ const ac = path.join(skillDir.fullPath, 'api-config.json');
|
|
|
+ if (!exists(sm) || !exists(ac)) { skipped++; continue; }
|
|
|
+ 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 ----
|
|
|
+if (!dryRun) {
|
|
|
+ const tpl = path.join(sourceRoot, '__config', 'wechat-credentials.template.json');
|
|
|
+ const dst = path.join(path.dirname(skillsRoot), 'wechat-credentials.template.json');
|
|
|
+ if (exists(tpl)) {
|
|
|
+ copyFile(tpl, dst);
|
|
|
+ console.log('Template: ' + dst);
|
|
|
+
|
|
|
+ const credFile = path.join(path.dirname(skillsRoot), 'wechat-credentials.json');
|
|
|
+ if (!exists(credFile)) {
|
|
|
+ copyFile(tpl, credFile);
|
|
|
+ console.log(yellow('[!] Created ' + credFile + ' - please edit wechatApiBase to your server address'));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ console.log(green('Done!'));
|
|
|
+}
|
|
|
+
|
|
|
+process.exit(errors > 0 ? 1 : 0);
|