#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const os = require('os'); const { spawnSync } = require('child_process'); const PROJECT_ROOT = path.resolve(__dirname, '..', '..'); const DIST_DIR = path.join(PROJECT_ROOT, 'dist'); const OPENCLAW_SKILLS_ROOT = path.join(PROJECT_ROOT, 'openclaw-skills'); const SUITE_DIR = path.join(OPENCLAW_SKILLS_ROOT, 'douyin-speaking-daily'); const SUITE_MANIFEST = path.join(SUITE_DIR, 'skill-package-manifest.json'); const PACKAGE_ZIP = path.join(DIST_DIR, 'douyin-speaking-daily.zip'); const DIST_MANIFEST = path.join(DIST_DIR, 'douyin-speaking-daily-suite-manifest.json'); const NODE_CMD = process.execPath || (process.platform === 'win32' ? 'node.exe' : 'node'); const SKILLS = [ 'voc-token-preflight', 'douyin-general-search', 'douyin-hashtag-search', 'douyin-video-detail', 'douyin-video-comments', 'douyin-comment-replies', 'douyin-user-search', 'douyin-user-profile', 'douyin-user-posts', 'douyin-mini-voc', 'douyin-speaking-profile-builder', 'douyin-speaking-profile-memory', 'douyin-speaking-keyword-monitor', 'douyin-speaking-account-monitor', 'douyin-speaking-daily-runner', 'douyin-speaking-daily-report', 'douyin-viral-script-analyzer', 'douyin-video-transcript' ]; const TOOL_FILES = [ 'openclaw-tool-runner.js', 'set-voc-token.js', 'voc-token-preflight.js', 'douyin-speaking-profile-memory.js', 'douyin-speaking-daily-runner.js', 'douyin-speaking-daily-report.js', 'douyin-speaking-daily-p1-smoke.js', 'douyin-viral-script-analyzer.js', 'douyin-video-transcriber.js' ]; const REQUIRED_FILES = [ 'openclaw-skills/douyin-speaking-daily/README.md', 'openclaw-skills/douyin-speaking-daily/douyin-speaking-daily-playbook.md', 'openclaw-skills/douyin-speaking-daily/openclaw-startup.md', 'openclaw-skills/douyin-speaking-daily/skill-package-manifest.json', 'openclaw-skills/douyin-speaking-daily/docs/specs/douyin-speaking-daily-p1-optimization-spec.md', 'openclaw-skills/douyin-speaking-daily/memory-templates/douyin-speaking-profile.json', 'scripts/tools/douyin-speaking-profile-memory.js', 'scripts/tools/douyin-speaking-daily-runner.js', 'scripts/tools/douyin-speaking-daily-report.js', 'scripts/tools/douyin-speaking-daily-p1-smoke.js', 'scripts/tools/douyin-viral-script-analyzer.js', 'scripts/tools/douyin-video-transcriber.js' ]; const SKILL_SOURCE_ROOTS = [ 'voc', 'social-media', 'douyin', 'xiaohongshu', 'review-analysis', 'competitor-analysis', 'synthesis', 'social-voc', 'workshop' ].map(item => path.join(OPENCLAW_SKILLS_ROOT, item)); function parseArgs(argv) { const opts = { validate: false, build: false, deploy: false, upload: false, dryRun: false, openclawDir: path.join(os.homedir(), '.openclaw'), help: false }; for (let i = 0; i < argv.length; i++) { const token = argv[i]; if (token === '--validate') opts.validate = true; else if (token === '--build') opts.build = true; else if (token === '--deploy') opts.deploy = true; else if (token === '--all') { opts.validate = true; opts.build = true; opts.deploy = true; } else if (token === '--upload') opts.upload = true; else if (token === '--dry-run') opts.dryRun = true; else if (token === '--openclaw-dir') opts.openclawDir = path.resolve(argv[++i] || opts.openclawDir); else if (token === '--help' || token === '-h') opts.help = true; } if (!opts.validate && !opts.build && !opts.deploy && !opts.help) { opts.validate = true; opts.build = true; } return opts; } function usage() { return [ 'Usage:', ' node scripts/deploy/douyin-speaking-daily-suite.js', ' node scripts/deploy/douyin-speaking-daily-suite.js --validate', ' node scripts/deploy/douyin-speaking-daily-suite.js --build', ' node scripts/deploy/douyin-speaking-daily-suite.js --build --upload', ' node scripts/deploy/douyin-speaking-daily-suite.js --deploy --dry-run', ' node scripts/deploy/douyin-speaking-daily-suite.js --all', '', 'Default mode is --validate --build without CDN upload.', '', 'Options:', ' --upload Upload zip to CDN through package-and-upload.js.', ' --deploy Install suite files into ~/.openclaw.', ' --openclaw-dir Override OpenClaw root for deploy.', ' --dry-run Preview deploy writes.', ' --help, -h Show help.' ].join('\n'); } function ensureDir(dir) { fs.mkdirSync(dir, { recursive: true }); } function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } function findSkillDir(skillName) { for (const root of SKILL_SOURCE_ROOTS) { const candidate = path.join(root, skillName); if (fs.existsSync(path.join(candidate, 'SKILL.md'))) return candidate; } return ''; } function copyDirRecursive(srcDir, destDir, opts) { let count = 0; if (!opts.dryRun) ensureDir(destDir); for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) { const src = path.join(srcDir, entry.name); const dest = path.join(destDir, entry.name); if (entry.isDirectory()) { count += copyDirRecursive(src, dest, opts); } else if (entry.isFile()) { if (opts.dryRun) { console.log(` [DRY] ${src} -> ${dest}`); } else { ensureDir(path.dirname(dest)); fs.copyFileSync(src, dest); } count++; } } return count; } function removeDirInside(targetDir, parentDir, opts) { const target = path.resolve(targetDir); const parent = path.resolve(parentDir); if (!target.startsWith(parent + path.sep)) { throw new Error(`Refusing to remove path outside target root: ${target}`); } if (opts.dryRun) { console.log(` [DRY] remove ${target}`); return; } fs.rmSync(target, { recursive: true, force: true }); } function runNodeCheck(filePath) { const source = fs.readFileSync(filePath, 'utf8').replace(/^#!.*\r?\n/, ''); try { // Syntax-only check without spawning a nested Node process. This keeps the // suite validator usable inside restricted runners that block child Node. new Function('require', 'module', 'exports', '__filename', '__dirname', source); } catch (error) { throw new Error(`syntax check failed for ${filePath}: ${error.message}`); } } function validateSuite() { console.log('[validate] douyin-speaking-daily suite'); const missing = []; for (const rel of REQUIRED_FILES) { const file = path.join(PROJECT_ROOT, rel); if (!fs.existsSync(file)) missing.push(rel); } for (const skill of SKILLS) { const dir = findSkillDir(skill); if (!dir) { missing.push(`skill:${skill}`); continue; } const apiConfig = path.join(dir, 'api-config.json'); if (fs.existsSync(apiConfig)) readJson(apiConfig); } for (const file of [ 'scripts/tools/douyin-speaking-profile-memory.js', 'scripts/tools/douyin-speaking-daily-runner.js', 'scripts/tools/douyin-speaking-daily-report.js', 'scripts/tools/douyin-speaking-daily-p1-smoke.js', 'scripts/tools/douyin-viral-script-analyzer.js', 'scripts/tools/douyin-video-transcriber.js' ]) { runNodeCheck(path.join(PROJECT_ROOT, file)); } readJson(SUITE_MANIFEST); if (missing.length) { throw new Error(`Missing suite files:\n${missing.map(item => ` - ${item}`).join('\n')}`); } console.log(` ok: ${SKILLS.length} skills, ${TOOL_FILES.length} tools, workspace bundle present`); } function buildSuite(opts) { console.log('[build] dist/douyin-speaking-daily.zip'); ensureDir(DIST_DIR); const args = [ path.join('scripts', 'deploy', 'package-and-upload.js'), '--only', 'douyin-speaking-daily', '--skip-workshop', '--skip-all' ]; if (!opts.upload) args.push('--skip-upload'); const child = spawnSync(NODE_CMD, args, { cwd: PROJECT_ROOT, encoding: 'utf8', maxBuffer: 1024 * 1024 * 100 }); if (child.stdout) process.stdout.write(child.stdout); if (child.stderr) process.stderr.write(child.stderr); if (child.status !== 0) { const details = child.error ? ` (${child.error.code || child.error.name}: ${child.error.message})` : ''; throw new Error(`package-and-upload failed with exit ${child.status}${details}`); } if (!fs.existsSync(PACKAGE_ZIP)) { throw new Error(`Expected package was not generated: ${PACKAGE_ZIP}`); } writeDistManifest({ upload: opts.upload }); } function deploySuite(opts) { const openclawDir = path.resolve(opts.openclawDir); const skillsTarget = path.join(openclawDir, 'skills'); const toolsTarget = path.join(openclawDir, 'tools'); const workspaceTarget = path.join(openclawDir, 'workspace'); const workspaceToolsTarget = path.join(workspaceTarget, 'scripts', 'tools'); console.log(`[deploy] ${openclawDir}${opts.dryRun ? ' (dry run)' : ''}`); if (!opts.dryRun && !fs.existsSync(openclawDir)) { throw new Error(`OpenClaw directory not found: ${openclawDir}`); } for (const skill of SKILLS) { const srcDir = findSkillDir(skill); if (!srcDir) throw new Error(`Cannot deploy missing skill: ${skill}`); const destDir = path.join(skillsTarget, skill); removeDirInside(destDir, skillsTarget, opts); copyDirRecursive(srcDir, destDir, opts); console.log(` [skill] ${skill}`); } for (const tool of TOOL_FILES) { const src = path.join(PROJECT_ROOT, 'scripts', 'tools', tool); if (!fs.existsSync(src)) throw new Error(`Missing tool: ${src}`); for (const targetRoot of [toolsTarget, workspaceToolsTarget]) { const dest = path.join(targetRoot, tool); if (opts.dryRun) { console.log(` [DRY] ${src} -> ${dest}`); } else { ensureDir(path.dirname(dest)); fs.copyFileSync(src, dest); } } console.log(` [tool] ${tool}`); } const bundleDest = path.join(workspaceTarget, 'douyin-speaking-daily'); removeDirInside(bundleDest, workspaceTarget, opts); copyDirRecursive(SUITE_DIR, bundleDest, opts); console.log(' [workspace] douyin-speaking-daily'); } function writeDistManifest(extra = {}) { const packageMeta = readJson(SUITE_MANIFEST); const zipStat = fs.existsSync(PACKAGE_ZIP) ? fs.statSync(PACKAGE_ZIP) : undefined; const manifest = { name: 'douyin-speaking-daily', version: packageMeta.version, generatedAt: new Date().toISOString(), packageZip: fs.existsSync(PACKAGE_ZIP) ? path.relative(PROJECT_ROOT, PACKAGE_ZIP).replace(/\\/g, '/') : '', packageZipBytes: zipStat ? zipStat.size : 0, skills: SKILLS, tools: TOOL_FILES, workspaceBundle: 'douyin-speaking-daily', installCommand: 'node install.js', localDeployCommand: 'node scripts/deploy/douyin-speaking-daily-suite.js --deploy', uploadCommand: 'node scripts/deploy/douyin-speaking-daily-suite.js --build --upload', ...extra }; fs.writeFileSync(DIST_MANIFEST, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8'); console.log(`[manifest] ${path.relative(PROJECT_ROOT, DIST_MANIFEST)}`); } function main() { const opts = parseArgs(process.argv.slice(2)); if (opts.help) { console.log(usage()); return; } try { if (opts.validate) validateSuite(); if (opts.build) buildSuite(opts); if (opts.deploy) deploySuite(opts); if (!opts.build) writeDistManifest({ upload: opts.upload }); console.log('[done] douyin-speaking-daily suite is ready'); } catch (error) { console.error(`[error] ${error.message}`); process.exit(1); } } main();