#!/usr/bin/env node /** * APS 桌面端离线升级清单生成器(packaging 内新增文件,不占用 scripts/ 写范围)。 * * 用法: * node packaging/build-upgrade-manifest.mjs \ * --installer release/aps-agent-desktop-0.2.0.exe \ * --version 0.2.0 \ * --out packaging/upgrade-manifest.json \ * [--product aps-agent-desktop] [--platform win32] [--arch x64] \ * [--releasedAt 2026-08-02T00:00:00Z] [--minSupportedVersion 0.1.0] * * # 校验已有清单与安装包是否一致(CI / 上传前自检) * node packaging/build-upgrade-manifest.mjs --verify packaging/upgrade-manifest.json * * 设计说明: * - 签名/SBOM 上链需要外部证书与信任根,本轮只生成「校验和 + 大小 + 版本」的完整性清单; * signature / provenance 字段预留为可选占位,由外部流程填充后发布。 * - 输出文件与 electron-builder 产物同目录使用时,fileName 建议与 artifactName 模板一致: * aps-agent-desktop-${version}.${ext} */ import { createHash } from 'node:crypto'; import { createReadStream } from 'node:fs'; import { readFile, stat, writeFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); function usage() { process.stderr.write( `usage: node ${path.basename(process.argv[1] || 'build-upgrade-manifest.mjs')} ` + `--installer --version --out [--product p] [--platform p] [--arch a] [--releasedAt ISO] [--minSupportedVersion v]\n` + ` node ${path.basename(process.argv[1] || 'build-upgrade-manifest.mjs')} --verify \n`, ); process.exit(2); } function parseArgs(argv) { const options = {}; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; const next = () => argv[++i]; if (arg === '--installer') options.installer = next(); else if (arg === '--version') options.version = next(); else if (arg === '--out') options.out = next(); else if (arg === '--product') options.product = next(); else if (arg === '--platform') options.platform = next(); else if (arg === '--arch') options.arch = next(); else if (arg === '--releasedAt') options.releasedAt = next(); else if (arg === '--minSupportedVersion') options.minSupportedVersion = next(); else if (arg === '--verify') options.verify = next(); else if (arg === '--help' || arg === '-h') usage(); else { process.stderr.write(`unknown argument: ${arg}\n`); usage(); } } return options; } async function computeSha256(filePath) { const hash = createHash('sha256'); await new Promise((resolve, reject) => { const stream = createReadStream(filePath); stream.on('data', (chunk) => hash.update(chunk)); stream.on('end', resolve); stream.on('error', reject); }); return hash.digest('hex'); } async function verifyManifest(manifestPath) { const manifest = JSON.parse(await readFile(manifestPath, 'utf8')); const required = ['schemaVersion', 'product', 'version', 'package']; for (const key of required) { if (!manifest[key]) throw new Error(`manifest missing required field: ${key}`); } const pkg = manifest.package; const installerPath = path.resolve(path.dirname(manifestPath), pkg.fileName); let info; try { info = await stat(installerPath); } catch (error) { throw new Error(`installer not found next to manifest: ${installerPath} (${error.message})`); } if (info.size !== pkg.size) { throw new Error(`size mismatch: manifest=${pkg.size} actual=${info.size}`); } const actual = await computeSha256(installerPath); if (actual !== pkg.sha256) { throw new Error(`sha256 mismatch: manifest=${pkg.sha256} actual=${actual}`); } return { manifest, installerPath }; } async function main() { const options = parseArgs(process.argv.slice(2)); if (options.verify) { const { manifest, installerPath } = await verifyManifest(options.verify); process.stdout.write( `OK ${manifest.version} ${installerPath} size=${manifest.package.size} sha256=${manifest.package.sha256}\n`, ); return; } if (!options.installer || !options.version || !options.out) usage(); const installerPath = path.resolve(options.installer); const info = await stat(installerPath); if (!info.isFile()) throw new Error(`not a file: ${installerPath}`); const sha256 = await computeSha256(installerPath); const manifest = { schemaVersion: 1, product: options.product || 'aps-agent-desktop', version: options.version, ...(options.platform ? { platform: options.platform } : {}), ...(options.arch ? { arch: options.arch } : {}), package: { fileName: path.basename(installerPath), sha256, size: info.size, }, releasedAt: options.releasedAt || new Date().toISOString(), ...(options.minSupportedVersion ? { minSupportedVersion: options.minSupportedVersion } : {}), }; const outPath = path.resolve(options.out); await writeFile(outPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8'); process.stdout.write( `wrote ${path.relative(process.cwd(), outPath)}: ${manifest.version} size=${info.size} sha256=${sha256}\n`, ); } main().catch((error) => { process.stderr.write(`ERROR: ${error.message}\n`); process.exit(1); });