#!/usr/bin/env node /** * dist-desktop/updates → latest.json (Tauri updater v2) * * Usage: * node scripts/generate-desktop-latest-json.mjs \ * --version 0.1.0 \ * --dir dist-desktop/updates \ * --base-url https://stock.exdev.co.kr/desktop/updates \ * --notes "Release notes" */ import fs from 'node:fs'; import path from 'node:path'; function arg(name, fallback) { const i = process.argv.indexOf(name); return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback; } const version = arg('--version', '0.1.0'); const dir = path.resolve(arg('--dir', 'dist-desktop/updates')); const rawBaseUrl = arg('--base-url', 'https://stock.exdev.co.kr/desktop/updates'); const baseUrl = rawBaseUrl .replace(/\/$/, '') .replace(/^http:\/\//i, 'https://') .replace(/^https:\/\/exdev\.co\.kr/i, 'https://stock.exdev.co.kr'); const notes = arg('--notes', `GoldenChart Desktop ${version}`); if (!fs.existsSync(dir)) { console.error('Directory not found:', dir); process.exit(1); } const files = fs.readdirSync(dir); function versionFromName(name) { const m = name.match(/(\d+\.\d+\.\d+)/); return m ? m[1] : '0.0.0'; } function cmpSemver(a, b) { const pa = a.split('.').map(Number); const pb = b.split('.').map(Number); for (let i = 0; i < 3; i++) { const d = (pa[i] || 0) - (pb[i] || 0); if (d !== 0) return d; } return 0; } /** @type {Record} */ const platforms = {}; /** Windows updater bundle 우선순위 (낮을수록 preferred) — setup.exe 수동 설치용은 제외 */ function windowsBundlePriority(name) { const lower = name.toLowerCase(); if (lower.endsWith('.nsis.zip')) return 0; if (lower.endsWith('.msi.zip')) return 1; if (lower.endsWith('-setup.exe') || (lower.includes('setup') && lower.endsWith('.exe'))) return 9; return 5; } function addPlatform(platformKey, bundleName) { const bundlePath = path.join(dir, bundleName); if (!fs.existsSync(bundlePath)) return; const sigPath = `${bundlePath}.sig`; let signature = ''; if (fs.existsSync(sigPath)) { signature = fs.readFileSync(sigPath, 'utf8').trim(); } else { console.warn(`[warn] missing signature: ${sigPath}`); return; } const bundleVersion = versionFromName(bundleName); const effectiveVersion = bundleVersion === '0.0.0' ? version : bundleVersion; const existing = platforms[platformKey]; if (existing) { const cmp = cmpSemver(effectiveVersion, existing.bundleVersion); if (cmp < 0) return; if (cmp === 0 && platformKey === 'windows-x86_64') { const prevPri = windowsBundlePriority(path.basename(existing.url.split('/').pop() ?? '')); const nextPri = windowsBundlePriority(bundleName); if (nextPri > prevPri) return; } else if (cmp === 0 && platformKey !== 'windows-x86_64') { return; } } platforms[platformKey] = { url: `${baseUrl}/${encodeURIComponent(bundleName)}`.replace( /^https:\/\/exdev\.co\.kr/i, 'https://stock.exdev.co.kr', ), signature, bundleVersion: effectiveVersion, }; } for (const f of files) { if (f.endsWith('.sig')) continue; if (f.endsWith('.dmg')) continue; // 설치용 dmg — updater manifest 대상 아님 const lower = f.toLowerCase(); if (lower.includes('aarch64') && (lower.endsWith('.tar.gz') || lower.endsWith('.app.tar.gz'))) { addPlatform('darwin-aarch64', f); } else if ((lower.includes('x64') || lower.includes('x86_64')) && lower.endsWith('.tar.gz')) { addPlatform('darwin-x86_64', f); } else if (lower.includes('aarch64') && lower.endsWith('.app.tar.gz')) { addPlatform('darwin-aarch64', f); } else if (lower.endsWith('.nsis.zip') || (lower.includes('setup') && lower.endsWith('.zip'))) { addPlatform('windows-x86_64', f); } else if (lower.endsWith('-setup.exe') || (lower.includes('setup') && lower.endsWith('.exe'))) { addPlatform('windows-x86_64', f); } else if (lower.endsWith('.app.tar.gz')) { addPlatform('darwin-aarch64', f); } } const bundleMaxVersion = Object.values(platforms).reduce( (max, p) => (cmpSemver(p.bundleVersion, max) > 0 ? p.bundleVersion : max), '0.0.0', ); // manifest 버전 = 빌드 버전(--version). 번들 파일명에 버전 없으면 CLI 버전 사용. // dmg 파일명으로 올리지 않음 — tar.gz 내용과 불일치하면 업데이트 무한 루프. let manifestVersion = version; if (bundleMaxVersion !== '0.0.0' && cmpSemver(bundleMaxVersion, version) < 0) { console.warn(`[warn] bundle version ${bundleMaxVersion} < build ${version} — using build version`); } const manifest = { version: manifestVersion, notes, pub_date: new Date().toISOString(), platforms: Object.fromEntries( Object.entries(platforms).map(([k, v]) => [k, { url: v.url, signature: v.signature }]), ), }; const outPath = path.join(dir, 'latest.json'); const body = `${JSON.stringify(manifest, null, 2)}\n`; if (body.length < 8) { console.error('[error] refusing to write empty latest.json'); process.exit(3); } const tmpPath = `${outPath}.tmp`; fs.writeFileSync(tmpPath, body, 'utf8'); fs.renameSync(tmpPath, outPath); console.log('Wrote', outPath); console.log('Platforms:', Object.keys(platforms).join(', ') || '(none — sign updater bundles first)'); if (Object.keys(platforms).length === 0) { process.exit(2); }