release.cjs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. #!/usr/bin/env node
  2. /**
  3. * 发布脚本
  4. * Release script for Claude AI Installer
  5. *
  6. * 功能:
  7. * - 自动更新版本号
  8. * - 构建所有平台的安装包
  9. * - 生成发布说明
  10. * - 可选: 创建 Git tag 并推送
  11. *
  12. * 用法 / Usage:
  13. * node scripts/release.js [version] [options]
  14. *
  15. * 示例:
  16. * node scripts/release.js patch # 0.0.1 -> 0.0.2
  17. * node scripts/release.js minor # 0.0.1 -> 0.1.0
  18. * node scripts/release.js major # 0.0.1 -> 1.0.0
  19. * node scripts/release.js 1.2.3 # 设置为指定版本
  20. * node scripts/release.js patch --tag # 更新版本并创建 Git tag
  21. */
  22. const { execSync } = require('child_process');
  23. const path = require('path');
  24. const fs = require('fs');
  25. // 颜色输出
  26. const colors = {
  27. reset: '\x1b[0m',
  28. bright: '\x1b[1m',
  29. red: '\x1b[31m',
  30. green: '\x1b[32m',
  31. yellow: '\x1b[33m',
  32. blue: '\x1b[34m',
  33. cyan: '\x1b[36m',
  34. magenta: '\x1b[35m',
  35. };
  36. const log = {
  37. info: (msg) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
  38. success: (msg) => console.log(`${colors.green}✔${colors.reset} ${msg}`),
  39. warn: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`),
  40. error: (msg) => console.log(`${colors.red}✖${colors.reset} ${msg}`),
  41. step: (msg) => console.log(`\n${colors.cyan}${colors.bright}▶ ${msg}${colors.reset}`),
  42. };
  43. const projectRoot = path.resolve(__dirname, '..');
  44. const packageJsonPath = path.join(projectRoot, 'package.json');
  45. // 解析命令行参数
  46. function parseArgs() {
  47. const args = process.argv.slice(2);
  48. // 每个平台默认只构建对应的版本,避免跨平台构建失败
  49. const platformMap = { win32: 'win', darwin: 'mac', linux: 'linux' };
  50. const currentPlatform = platformMap[process.platform] || 'linux';
  51. const defaultPlatforms = [currentPlatform];
  52. const options = {
  53. version: null,
  54. platforms: defaultPlatforms,
  55. dryRun: false,
  56. help: false,
  57. };
  58. for (let i = 0; i < args.length; i++) {
  59. const arg = args[i];
  60. switch (arg) {
  61. case '--platform':
  62. case '-p':
  63. options.platforms = args[++i].split(',');
  64. break;
  65. case '--dry-run':
  66. options.dryRun = true;
  67. break;
  68. case '--help':
  69. case '-h':
  70. options.help = true;
  71. break;
  72. default:
  73. if (!arg.startsWith('-') && !options.version) {
  74. options.version = arg;
  75. }
  76. }
  77. }
  78. return options;
  79. }
  80. // 显示帮助信息
  81. function showHelp() {
  82. console.log(`
  83. ${colors.bright}Claude AI Installer 发布脚本${colors.reset}
  84. ${colors.cyan}用法:${colors.reset}
  85. node scripts/release.cjs [version] [options]
  86. ${colors.cyan}版本参数:${colors.reset}
  87. patch 补丁版本 (0.0.1 -> 0.0.2)
  88. minor 次版本 (0.0.1 -> 0.1.0)
  89. major 主版本 (0.0.1 -> 1.0.0)
  90. x.y.z 指定版本号
  91. ${colors.cyan}选项:${colors.reset}
  92. --platform, -p 指定平台 (逗号分隔): win,mac,linux
  93. --dry-run 仅显示将要执行的操作,不实际执行
  94. --help, -h 显示此帮助信息
  95. ${colors.cyan}示例:${colors.reset}
  96. node scripts/release.cjs patch # 更新补丁版本并构建
  97. node scripts/release.cjs 1.0.0 # 设置版本为 1.0.0 并构建
  98. node scripts/release.cjs minor -p win,mac # 更新次版本,仅构建 Win 和 Mac
  99. node scripts/release.cjs --dry-run # 预览发布操作
  100. `);
  101. }
  102. // 读取 package.json
  103. function readPackageJson() {
  104. return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
  105. }
  106. // 写入 package.json
  107. function writePackageJson(pkg) {
  108. fs.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n');
  109. }
  110. // 计算新版本号
  111. function calculateNewVersion(currentVersion, versionArg) {
  112. if (!versionArg) {
  113. return currentVersion;
  114. }
  115. // 如果是具体版本号
  116. if (/^\d+\.\d+\.\d+/.test(versionArg)) {
  117. return versionArg;
  118. }
  119. const parts = currentVersion.split('.').map(Number);
  120. switch (versionArg) {
  121. case 'major':
  122. return `${parts[0] + 1}.0.0`;
  123. case 'minor':
  124. return `${parts[0]}.${parts[1] + 1}.0`;
  125. case 'patch':
  126. return `${parts[0]}.${parts[1]}.${parts[2] + 1}`;
  127. default:
  128. throw new Error(`无效的版本参数: ${versionArg}`);
  129. }
  130. }
  131. // 执行命令
  132. function exec(command, options = {}) {
  133. log.info(`执行: ${command}`);
  134. try {
  135. return execSync(command, {
  136. stdio: options.silent ? 'pipe' : 'inherit',
  137. cwd: projectRoot,
  138. encoding: 'utf-8',
  139. ...options,
  140. });
  141. } catch (error) {
  142. if (!options.ignoreError) {
  143. throw error;
  144. }
  145. return null;
  146. }
  147. }
  148. // 更新版本号
  149. function updateVersion(newVersion, dryRun) {
  150. log.step(`更新版本号到 ${newVersion}...`);
  151. if (dryRun) {
  152. log.info('[DRY RUN] 将更新 package.json 和 tauri.conf.json 版本号');
  153. return;
  154. }
  155. // 更新 package.json
  156. const pkg = readPackageJson();
  157. pkg.version = newVersion;
  158. writePackageJson(pkg);
  159. log.success(`package.json 版本号已更新: ${newVersion}`);
  160. // 更新 tauri.conf.json
  161. const tauriConfPath = path.join(projectRoot, 'src-tauri', 'tauri.conf.json');
  162. if (fs.existsSync(tauriConfPath)) {
  163. const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf-8'));
  164. tauriConf.version = newVersion;
  165. fs.writeFileSync(tauriConfPath, JSON.stringify(tauriConf, null, 2) + '\n');
  166. log.success(`tauri.conf.json 版本号已更新: ${newVersion}`);
  167. }
  168. // 更新 Cargo.toml
  169. const cargoTomlPath = path.join(projectRoot, 'src-tauri', 'Cargo.toml');
  170. if (fs.existsSync(cargoTomlPath)) {
  171. let cargoToml = fs.readFileSync(cargoTomlPath, 'utf-8');
  172. cargoToml = cargoToml.replace(/^version = ".*"$/m, `version = "${newVersion}"`);
  173. fs.writeFileSync(cargoTomlPath, cargoToml);
  174. log.success(`Cargo.toml 版本号已更新: ${newVersion}`);
  175. }
  176. }
  177. // 构建所有平台
  178. function buildAllPlatforms(platforms, dryRun) {
  179. log.step('构建应用...');
  180. for (const platform of platforms) {
  181. log.info(`构建 ${platform} 平台...`);
  182. if (dryRun) {
  183. log.info(`[DRY RUN] 将构建 ${platform} 平台`);
  184. continue;
  185. }
  186. // Tauri 构建(build.js 不支持 -p 参数,直接调用)
  187. exec(`node scripts/build.js`);
  188. }
  189. // 复制构建产物到 release 目录
  190. if (!dryRun) {
  191. copyArtifactsToRelease();
  192. }
  193. log.success('所有平台构建完成');
  194. }
  195. // 复制 Tauri 构建产物到 release 目录
  196. function copyArtifactsToRelease() {
  197. log.step('复制构建产物到 release 目录...');
  198. const releaseDir = path.join(projectRoot, 'release');
  199. const tauriDir = path.join(projectRoot, 'src-tauri');
  200. const bundleDir = path.join(tauriDir, 'target', 'release', 'bundle');
  201. // 清空 release 目录(保留目录本身)
  202. if (fs.existsSync(releaseDir)) {
  203. const files = fs.readdirSync(releaseDir);
  204. for (const file of files) {
  205. const filePath = path.join(releaseDir, file);
  206. const stat = fs.statSync(filePath);
  207. if (stat.isFile()) {
  208. fs.unlinkSync(filePath);
  209. } else if (stat.isDirectory()) {
  210. fs.rmSync(filePath, { recursive: true });
  211. }
  212. }
  213. log.success('已清空 release 目录');
  214. } else {
  215. fs.mkdirSync(releaseDir, { recursive: true });
  216. }
  217. // 复制 NSIS 安装程序
  218. const nsisDir = path.join(bundleDir, 'nsis');
  219. if (fs.existsSync(nsisDir)) {
  220. const files = fs.readdirSync(nsisDir).filter(f => f.endsWith('.exe'));
  221. for (const file of files) {
  222. const src = path.join(nsisDir, file);
  223. const dest = path.join(releaseDir, file);
  224. fs.copyFileSync(src, dest);
  225. log.success(`复制: ${file}`);
  226. }
  227. }
  228. // 复制 MSI 安装程序
  229. const msiDir = path.join(bundleDir, 'msi');
  230. if (fs.existsSync(msiDir)) {
  231. const files = fs.readdirSync(msiDir).filter(f => f.endsWith('.msi'));
  232. for (const file of files) {
  233. const src = path.join(msiDir, file);
  234. const dest = path.join(releaseDir, file);
  235. fs.copyFileSync(src, dest);
  236. log.success(`复制: ${file}`);
  237. }
  238. }
  239. // 复制更新签名文件(如果存在)
  240. if (fs.existsSync(nsisDir)) {
  241. const sigFiles = fs.readdirSync(nsisDir).filter(f => f.endsWith('.sig'));
  242. for (const file of sigFiles) {
  243. const src = path.join(nsisDir, file);
  244. const dest = path.join(releaseDir, file);
  245. fs.copyFileSync(src, dest);
  246. log.success(`复制: ${file}`);
  247. }
  248. }
  249. // 复制 Portable exe(从 bundle 目录)
  250. const pkg = readPackageJson();
  251. const version = pkg.version;
  252. const portableExeName = `Claude-AI-Installer-${version}-win-x64-portable.exe`;
  253. const portableExePath = path.join(bundleDir, portableExeName);
  254. if (fs.existsSync(portableExePath)) {
  255. const dest = path.join(releaseDir, portableExeName);
  256. fs.copyFileSync(portableExePath, dest);
  257. log.success(`复制: ${portableExeName}`);
  258. }
  259. }
  260. // 提交版本更新
  261. function commitVersionUpdate(version) {
  262. log.step(`提交版本更新 v${version}...`);
  263. // 添加所有版本相关文件
  264. exec('git add package.json', { ignoreError: true });
  265. exec('git add src-tauri/tauri.conf.json', { ignoreError: true });
  266. exec('git add src-tauri/Cargo.toml', { ignoreError: true });
  267. // 提交
  268. exec(`git commit -m "本地发布版本: ${version}"`, { ignoreError: true });
  269. log.success(`版本更新已提交: v${version}`);
  270. }
  271. // 生成发布说明
  272. function generateReleaseNotes(version) {
  273. log.step('生成发布说明...');
  274. const releaseDir = path.join(projectRoot, 'release');
  275. const notesPath = path.join(releaseDir, `RELEASE_NOTES_v${version}.md`);
  276. // 获取最近的 commits
  277. let commits = '';
  278. try {
  279. commits = execSync('git log --oneline -20', {
  280. cwd: projectRoot,
  281. encoding: 'utf-8',
  282. });
  283. } catch (e) {
  284. commits = '无法获取提交历史';
  285. }
  286. // 获取构建产物列表
  287. let artifacts = [];
  288. if (fs.existsSync(releaseDir)) {
  289. artifacts = fs.readdirSync(releaseDir).filter(f => {
  290. const ext = path.extname(f).toLowerCase();
  291. return ['.exe', '.dmg', '.appimage', '.deb', '.rpm', '.zip'].includes(ext);
  292. });
  293. }
  294. const notes = `# Claude AI Installer v${version}
  295. ## 发布日期
  296. ${new Date().toISOString().split('T')[0]}
  297. ## 下载
  298. ${artifacts.map(a => `- ${a}`).join('\n') || '暂无构建产物'}
  299. ## 平台支持
  300. | 平台 | 文件格式 | 架构 |
  301. |------|----------|------|
  302. | Windows | NSIS 安装包 (.exe) | x64 |
  303. | macOS | DMG 镜像 (.dmg) | x64, arm64 |
  304. | Linux | AppImage (.AppImage) | x64 |
  305. ## 更新内容
  306. 请查看提交历史了解详细更新内容。
  307. ## 最近提交
  308. \`\`\`
  309. ${commits}
  310. \`\`\`
  311. ## 安装说明
  312. ### Windows
  313. 1. 下载 \`.exe\` 安装包
  314. 2. 双击运行安装程序
  315. 3. 按照向导完成安装
  316. ### macOS
  317. 1. 下载 \`.dmg\` 文件
  318. 2. 双击打开 DMG 镜像
  319. 3. 将应用拖拽到 Applications 文件夹
  320. ### Linux
  321. 1. 下载 \`.AppImage\` 文件
  322. 2. 添加执行权限: \`chmod +x *.AppImage\`
  323. 3. 双击运行或在终端执行
  324. ---
  325. 🤖 Generated with Claude AI Installer Build System
  326. `;
  327. fs.writeFileSync(notesPath, notes);
  328. log.success(`发布说明已生成: ${notesPath}`);
  329. }
  330. // 显示发布摘要
  331. function showSummary(version, platforms) {
  332. const releaseDir = path.join(projectRoot, 'release');
  333. console.log(`
  334. ${colors.bright}╔════════════════════════════════════════════╗
  335. ║ 发布摘要 - v${version.padEnd(20)}║
  336. ╚════════════════════════════════════════════╝${colors.reset}
  337. `);
  338. if (fs.existsSync(releaseDir)) {
  339. const files = fs.readdirSync(releaseDir).filter(f => {
  340. const ext = path.extname(f).toLowerCase();
  341. return ['.exe', '.dmg', '.appimage', '.deb', '.rpm', '.zip'].includes(ext);
  342. });
  343. if (files.length > 0) {
  344. console.log(`${colors.cyan}构建产物:${colors.reset}`);
  345. for (const file of files) {
  346. const filePath = path.join(releaseDir, file);
  347. const stats = fs.statSync(filePath);
  348. const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
  349. console.log(` ${colors.green}•${colors.reset} ${file} (${sizeMB} MB)`);
  350. }
  351. }
  352. }
  353. console.log(`
  354. ${colors.cyan}输出目录:${colors.reset} ${releaseDir}
  355. ${colors.cyan}版本号:${colors.reset} v${version}
  356. ${colors.cyan}构建平台:${colors.reset} ${platforms.join(', ')}
  357. `);
  358. }
  359. // 主函数
  360. async function main() {
  361. const options = parseArgs();
  362. if (options.help) {
  363. showHelp();
  364. process.exit(0);
  365. }
  366. console.log(`
  367. ${colors.bright}${colors.magenta}╔════════════════════════════════════════════╗
  368. ║ Claude AI Installer 发布脚本 ║
  369. ╚════════════════════════════════════════════╝${colors.reset}
  370. `);
  371. if (options.dryRun) {
  372. log.warn('DRY RUN 模式 - 不会执行实际操作');
  373. }
  374. const startTime = Date.now();
  375. try {
  376. // 读取当前版本
  377. const pkg = readPackageJson();
  378. const currentVersion = pkg.version;
  379. log.info(`当前版本: ${currentVersion}`);
  380. // 计算新版本
  381. const newVersion = calculateNewVersion(currentVersion, options.version);
  382. log.info(`目标版本: ${newVersion}`);
  383. // 更新版本号
  384. if (options.version) {
  385. updateVersion(newVersion, options.dryRun);
  386. }
  387. // 构建所有平台
  388. buildAllPlatforms(options.platforms, options.dryRun);
  389. // 生成发布说明
  390. if (!options.dryRun) {
  391. generateReleaseNotes(newVersion);
  392. }
  393. // 提交版本更新
  394. if (options.version && !options.dryRun) {
  395. commitVersionUpdate(newVersion);
  396. }
  397. // 显示摘要
  398. if (!options.dryRun) {
  399. showSummary(newVersion, options.platforms);
  400. }
  401. const duration = ((Date.now() - startTime) / 1000).toFixed(1);
  402. log.success(`发布完成! 耗时: ${duration}s`);
  403. } catch (error) {
  404. log.error(error.message);
  405. process.exit(1);
  406. }
  407. }
  408. main();