#!/usr/bin/env node /** * 多平台构建脚本 * Multi-platform build script for Claude AI Installer * * 用法 / Usage: * node scripts/build.js [options] * * 选项 / Options: * --platform, -p 目标平台: win, mac, linux, all (默认: 当前平台) * --arch, -a 架构: x64, arm64, all (默认: 当前架构) * --publish, -P 发布模式: always, onTag, never (默认: never) * --skip-build 跳过前端构建,仅打包 * --skip-typecheck 跳过 TypeScript 类型检查 * --help, -h 显示帮助信息 */ const { execSync } = require('child_process'); const path = require('path'); const fs = require('fs'); // 颜色输出 const colors = { reset: '\x1b[0m', bright: '\x1b[1m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m', }; const log = { info: (msg) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`), success: (msg) => console.log(`${colors.green}✔${colors.reset} ${msg}`), warn: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`), error: (msg) => console.log(`${colors.red}✖${colors.reset} ${msg}`), step: (msg) => console.log(`\n${colors.cyan}${colors.bright}▶ ${msg}${colors.reset}`), }; // 解析命令行参数 function parseArgs() { const args = process.argv.slice(2); const options = { platform: process.platform === 'win32' ? 'win' : process.platform === 'darwin' ? 'mac' : 'linux', arch: process.arch, publish: 'never', skipBuild: false, skipTypecheck: false, help: false, }; for (let i = 0; i < args.length; i++) { const arg = args[i]; switch (arg) { case '--platform': case '-p': options.platform = args[++i]; break; case '--arch': case '-a': options.arch = args[++i]; break; case '--publish': case '-P': options.publish = args[++i]; break; case '--skip-build': case '-s': options.skipBuild = true; break; case '--skip-typecheck': options.skipTypecheck = true; break; case '--help': case '-h': options.help = true; break; } } return options; } // 显示帮助信息 function showHelp() { console.log(` ${colors.bright}Claude AI Installer 构建脚本${colors.reset} ${colors.cyan}用法:${colors.reset} node scripts/build.js [options] ${colors.cyan}选项:${colors.reset} --platform, -p 目标平台 win - Windows (NSIS 安装包) mac - macOS (DMG) linux - Linux (AppImage) all - 所有平台 默认: 当前平台 --arch, -a 目标架构 x64 - 64位 Intel/AMD arm64 - ARM64 (Apple Silicon, etc.) all - 所有架构 默认: 当前架构 --publish, -P 发布模式 always - 总是发布 onTag - 仅在 Git tag 时发布 never - 不发布 (默认) --skip-build, -s 跳过前端构建,仅执行打包 --skip-typecheck 跳过 TypeScript 类型检查 --help, -h 显示此帮助信息 ${colors.cyan}示例:${colors.reset} node scripts/build.js # 构建当前平台 node scripts/build.js -p win # 构建 Windows 版本 node scripts/build.js -p mac -a arm64 # 构建 macOS ARM64 版本 node scripts/build.js -p all # 构建所有平台 node scripts/build.js -p win -P always # 构建并发布 Windows 版本 `); } // 执行命令 function exec(command, options = {}) { log.info(`执行: ${command}`); try { execSync(command, { stdio: 'inherit', cwd: path.resolve(__dirname, '..'), ...options, }); return true; } catch (error) { log.error(`命令执行失败: ${command}`); return false; } } // 获取 electron-builder 平台参数 function getPlatformArgs(platform, arch) { const platformMap = { win: '--win', mac: '--mac', linux: '--linux', }; const archMap = { x64: '--x64', arm64: '--arm64', all: '--x64 --arm64', }; let args = []; if (platform === 'all') { args.push('--win', '--mac', '--linux'); } else { args.push(platformMap[platform] || platformMap.win); } if (arch && arch !== 'all') { args.push(archMap[arch] || ''); } else if (arch === 'all') { args.push('--x64', '--arm64'); } return args.join(' '); } // 清理旧的构建文件 function cleanBuild() { log.step('清理旧的构建文件...'); const dirsToClean = ['dist', 'dist-electron']; const projectRoot = path.resolve(__dirname, '..'); for (const dir of dirsToClean) { const fullPath = path.join(projectRoot, dir); if (fs.existsSync(fullPath)) { fs.rmSync(fullPath, { recursive: true, force: true }); log.info(`已删除: ${dir}`); } } // 清空 release 目录内容,但保留目录本身 const releaseDir = path.join(projectRoot, 'release'); if (fs.existsSync(releaseDir)) { const files = fs.readdirSync(releaseDir); for (const file of files) { const filePath = path.join(releaseDir, file); fs.rmSync(filePath, { recursive: true, force: true }); } log.info('已清空: release'); } log.success('清理完成'); } // 类型检查 function typeCheck() { log.step('执行类型检查...'); if (!exec('npx vue-tsc --noEmit')) { throw new Error('类型检查失败'); } log.success('类型检查通过'); } // 构建前端 function buildFrontend() { log.step('构建前端资源...'); if (!exec('npx vite build')) { throw new Error('前端构建失败'); } log.success('前端构建完成'); } // 打包 Electron 应用 function packageApp(options) { log.step(`打包应用 (平台: ${options.platform}, 架构: ${options.arch})...`); const platformArgs = getPlatformArgs(options.platform, options.arch); const publishArg = options.publish !== 'never' ? `--publish ${options.publish}` : ''; const command = `npx electron-builder ${platformArgs} ${publishArg}`.trim(); if (!exec(command)) { throw new Error('应用打包失败'); } log.success('应用打包完成'); } // 显示构建结果 function showResults() { log.step('构建结果:'); const releaseDir = path.resolve(__dirname, '..', 'release'); if (!fs.existsSync(releaseDir)) { log.warn('release 目录不存在'); return; } const files = fs.readdirSync(releaseDir); const packages = files.filter(f => { const ext = path.extname(f).toLowerCase(); return ['.exe', '.dmg', '.appimage', '.deb', '.rpm', '.zip'].includes(ext); }); if (packages.length === 0) { log.warn('未找到打包文件'); return; } console.log(''); for (const pkg of packages) { const filePath = path.join(releaseDir, pkg); const stats = fs.statSync(filePath); const sizeMB = (stats.size / (1024 * 1024)).toFixed(2); console.log(` ${colors.green}•${colors.reset} ${pkg} (${sizeMB} MB)`); } console.log(''); log.info(`输出目录: ${releaseDir}`); } // ==================== Portable 版本构建 ==================== const LAUNCHER_DIR = path.resolve(__dirname, '..', 'launcher'); const RELEASE_DIR = path.resolve(__dirname, '..', 'release'); /** * 查找 portable exe 文件 */ function findPortableExe() { const files = fs.readdirSync(RELEASE_DIR); return files.find(f => f.includes('portable') && f.endsWith('.exe')); } /** * 使用 pkg 打包启动器 */ function buildLauncher() { log.step('构建启动器...'); // 检查是否安装了 pkg try { execSync('npx pkg --version', { stdio: 'ignore' }); } catch { log.info('安装 pkg...'); execSync('npm install -g pkg', { stdio: 'inherit' }); } const launcherScript = path.join(LAUNCHER_DIR, 'launcher.js'); const outputPath = path.join(RELEASE_DIR, 'launcher.exe'); // 使用 pkg 打包 execSync(`npx pkg "${launcherScript}" --target node18-win-x64 --output "${outputPath}"`, { stdio: 'inherit', cwd: path.resolve(__dirname, '..'), }); log.success(`启动器构建完成: ${outputPath}`); return outputPath; } /** * 创建 Portable 目录结构 */ function createPortableStructure(portableExe, launcherExe) { const packageJson = require(path.resolve(__dirname, '..', 'package.json')); const version = packageJson.version; const portableDirName = `Claude-AI-Installer-${version}-portable`; const portableDir = path.join(RELEASE_DIR, portableDirName); const appDir = path.join(portableDir, 'app'); const updateDir = path.join(portableDir, 'update'); log.step('创建 Portable 目录结构...'); // 清理旧目录 if (fs.existsSync(portableDir)) { fs.rmSync(portableDir, { recursive: true }); } // 创建目录 fs.mkdirSync(appDir, { recursive: true }); fs.mkdirSync(updateDir, { recursive: true }); // 复制启动器 const launcherDest = path.join(portableDir, 'Claude-AI-Installer.exe'); fs.copyFileSync(launcherExe, launcherDest); log.info(`复制启动器: ${launcherDest}`); // 复制主程序到 app 目录 const portableExePath = path.join(RELEASE_DIR, portableExe); const appExeName = `Claude-AI-Installer-${version}.exe`; const appExeDest = path.join(appDir, appExeName); fs.copyFileSync(portableExePath, appExeDest); log.info(`复制主程序: ${appExeDest}`); // 创建 .gitkeep 文件保持 update 目录 fs.writeFileSync(path.join(updateDir, '.gitkeep'), ''); // 创建 README const readme = `Claude AI Installer - Portable 版本 使用方法: 1. 双击 Claude-AI-Installer.exe 启动程序 2. 程序会自动检查更新 3. 更新下载后,重启程序即可完成更新 目录说明: - Claude-AI-Installer.exe: 启动器 - app/: 主程序目录 - update/: 更新文件临时目录 版本: ${version} `; fs.writeFileSync(path.join(portableDir, 'README.txt'), readme, 'utf8'); log.success(`Portable 目录创建完成: ${portableDir}`); // 创建 zip 包 createPortableZip(portableDir, portableDirName); return portableDir; } /** * 创建 ZIP 压缩包 */ function createPortableZip(sourceDir, name) { const zipPath = path.join(RELEASE_DIR, `${name}.zip`); log.step('创建 ZIP 压缩包...'); // 使用 PowerShell 创建 zip const psCommand = `Compress-Archive -Path "${sourceDir}\\*" -DestinationPath "${zipPath}" -Force`; execSync(`powershell -Command "${psCommand}"`, { stdio: 'inherit' }); log.success(`ZIP 压缩包创建完成: ${zipPath}`); } /** * 构建 Portable 版本(带启动器) */ function buildPortableVersion() { log.step('构建 Portable 版本(带启动器)...'); // 查找 portable exe const portableExe = findPortableExe(); if (!portableExe) { throw new Error('未找到 portable exe 文件'); } log.info(`找到 Portable 文件: ${portableExe}`); // 构建启动器 const launcherExe = buildLauncher(); // 创建 Portable 目录结构 createPortableStructure(portableExe, launcherExe); log.success('Portable 版本构建完成'); } // 主函数 async function main() { const options = parseArgs(); if (options.help) { showHelp(); process.exit(0); } console.log(` ${colors.bright}╔════════════════════════════════════════════╗ ║ Claude AI Installer 构建脚本 ║ ╚════════════════════════════════════════════╝${colors.reset} `); log.info(`目标平台: ${options.platform}`); log.info(`目标架构: ${options.arch}`); log.info(`发布模式: ${options.publish}`); log.info(`跳过构建: ${options.skipBuild}`); log.info(`跳过类型检查: ${options.skipTypecheck}`); const startTime = Date.now(); try { // 清理旧的构建文件 cleanBuild(); if (!options.skipBuild) { // 类型检查 if (!options.skipTypecheck) { typeCheck(); } else { log.warn('跳过类型检查'); } // 构建前端 buildFrontend(); } else { log.warn('跳过前端构建'); } packageApp(options); // Windows 平台自动构建带启动器的 Portable 版本 if (options.platform === 'win' || options.platform === 'all') { buildPortableVersion(); } showResults(); const duration = ((Date.now() - startTime) / 1000).toFixed(1); log.success(`构建完成! 耗时: ${duration}s`); } catch (error) { log.error(error.message); process.exit(1); } } main();