import { readFile } from 'node:fs/promises'; import { createHash } from 'node:crypto'; import { execFileSync } from 'node:child_process'; import path from 'node:path'; import { valid } from 'semver'; export async function releasePlan(entriesFile, tag) { const config = JSON.parse(await readFile('release.json', 'utf8')); const repository = config.repository; if (!tag || !/^[a-zA-Z0-9][a-zA-Z0-9._+-]*$/.test(tag)) throw new Error('A release tag is required'); const commit = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(); const entries = JSON.parse(await readFile(entriesFile, 'utf8')).filter(entry => !entry.fault); if (!entries.length) throw new Error('No packages to publish'); const plugins = new Map(); const files = new Map(); const digest = bytes => createHash('sha256').update(bytes).digest('hex'); for (const entry of entries) { const manifest = entry.manifest; if (!config.ids.includes(manifest.id) || !valid(manifest.version)) throw new Error('Unexpected plugin identity/version'); if (tag !== `v${manifest.version}`) throw new Error('Release tag must match curio.json'); if (!['any', 'aarch64-apple-darwin', 'x86_64-apple-darwin'].includes(entry.artifact.target)) throw new Error('Unsupported package target'); const bytes = await readFile(entry.file); if (digest(bytes) !== entry.artifact.sha256 || bytes.length !== entry.artifact.size) throw new Error('Package differs from build metadata'); const asset = `${manifest.id}-${manifest.version}-${entry.artifact.target}.curio`; if (files.has(asset)) throw new Error(`Duplicate release asset: ${asset}`); files.set(asset, bytes); const key = `${manifest.id}@${manifest.version}`; if (!plugins.has(key)) { let icon; if (manifest.icon) { const file = path.resolve(entry.directory, manifest.icon); if (!file.startsWith(path.resolve(entry.directory) + path.sep)) throw new Error('Icon escapes package directory'); const data = await readFile(file); const type = { '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.webp': 'image/webp' }[path.extname(file)]; if (type && data.length <= 64 * 1024) icon = `data:${type};base64,${data.toString('base64')}`; } plugins.set(key, { id: manifest.id, name: manifest.name, description: manifest.description ?? '', icon, version: manifest.version, engine: manifest.engine, artifacts: [] }); } plugins.get(key).artifacts.push({ target: entry.artifact.target, asset, sha256: entry.artifact.sha256, size: bytes.length }); } const descriptor = Buffer.from(JSON.stringify({ schemaVersion: 1, repository, tag, commit, plugins: [...plugins.values()] }, null, 2) + '\n'); files.set('trove-release.json', descriptor); return { files, descriptor: JSON.parse(descriptor) }; }