#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const ROOT = path.resolve(__dirname, '..'); const DIST = path.join(ROOT, 'release'); const COMMANDS = ['prepare', 'check', 'build', 'zip', 'clean']; function printHelp() { console.log('Usage: node scripts/release.js '); console.log('Commands:'); console.log(' prepare Install production deps'); console.log(' check Validate release readiness'); console.log(' build Copy release files to release/'); console.log(' zip Create release zip from release/'); console.log(' clean Remove release/ and dist/'); } function prepare() { console.log('Installing production dependencies...'); execSync('npm install --production', { cwd: ROOT, stdio: 'inherit' }); } function check() { console.log('Checking release readiness...'); const required = ['index.js', 'package.json']; const missing = required.filter(file => !fs.existsSync(path.join(ROOT, file))); if (missing.length) { console.error('Missing required files:', missing.join(', ')); process.exit(1); } const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')); if (!pkg.files || pkg.files.length === 0) { console.error('package.json missing "files" field'); process.exit(1); } console.log('Check passed.'); } function build() { if (!fs.existsSync(DIST)) { fs.mkdirSync(DIST, { recursive: true }); } const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')); const files = pkg.files || []; files.forEach(file => { const src = path.join(ROOT, file); const dest = path.join(DIST, file); if (!fs.existsSync(src)) { return; } const stat = fs.statSync(src); if (stat.isDirectory()) { if (fs.existsSync(dest)) { fs.rmSync(dest, { recursive: true }); } fs.mkdirSync(dest, { recursive: true }); fs.readdirSync(src).forEach(child => { const childSrc = path.join(src, child); const childDest = path.join(dest, child); if (fs.statSync(childSrc).isDirectory()) { if (fs.existsSync(childDest)) { fs.rmSync(childDest, { recursive: true }); } fs.mkdirSync(childDest, { recursive: true }); fs.cpSync(childSrc, childDest, { recursive: true }); } else { fs.copyFileSync(childSrc, childDest); } }); } else { fs.copyFileSync(src, dest); } }); const nodeModules = path.join(ROOT, 'node_modules'); const destModules = path.join(DIST, 'node_modules'); if (fs.existsSync(nodeModules)) { if (fs.existsSync(destModules)) { fs.rmSync(destModules, { recursive: true }); } fs.cpSync(nodeModules, destModules, { recursive: true }); } console.log('Release files prepared at:', DIST); } function zip() { if (!fs.existsSync(DIST)) { console.error('release/ not found. Run build first.'); process.exit(1); } const outDir = path.join(ROOT, 'dist'); if (!fs.existsSync(outDir)) { fs.mkdirSync(outDir, { recursive: true }); } const safeName = (ROOT.match(/[^\\/]+$/) || ['release'])[0]; const zipName = `${safeName}-release-${Date.now()}.zip`; const zipPath = path.join(outDir, zipName); if (process.platform === 'win32') { const psCmd = `Compress-Archive -Path 'release\*' -DestinationPath '${zipPath.replace(/'/g, "'")}' -Force`; execSync(`powershell -Command "${psCmd}"`, { cwd: ROOT, stdio: 'inherit' }); } else { const excludes = [ 'node_modules/.cache/*', 'scripts/*', '*.git*', 'release/*', 'dist/*', '.gitignore', '*.tgz' ]; const args = ['-r', `"${zipPath}"`, '.']; excludes.forEach(exclude => { args.push('-x', `"${exclude}"`); }); const command = `zip ${args.join(' ')}`; console.log('Running:', command); execSync(command, { cwd: ROOT, stdio: 'inherit' }); } const stats = fs.statSync(zipPath); const sizeMB = (stats.size / (1024 * 1024)).toFixed(2); console.log('Created zip:', zipPath); console.log('Zip size:', sizeMB, 'MB'); } function clean() { [DIST, path.join(ROOT, 'dist')].forEach(dir => { if (fs.existsSync(dir)) { fs.rmSync(dir, { recursive: true }); console.log('Removed:', dir); } }); } const command = process.argv[2]; if (!COMMANDS.includes(command)) { printHelp(); process.exit(1); } switch (command) { case 'prepare': prepare(); break; case 'check': check(); break; case 'build': build(); break; case 'zip': zip(); break; case 'clean': clean(); break; }