/* * publish-update.cjs — copy the freshly built installer to the church's update folder and write * the latest.json manifest the app reads. Run after a build: * * npm run release (build + publish) * npm run publish-update (publish the newest existing build) * npm run publish-update -- "D:\Builds" (publish to a specific folder) * npm run publish-update -- "D:\Builds" "Notes about this version" * * Destination order: CLI arg -> BIBLEAPP_UPDATE_DIR env -> the folder configured in the app * (Notes/update-config.json) -> dist-installer/publish */ const fs = require('fs'); const path = require('path'); const os = require('os'); const crypto = require('crypto'); // Fingerprint a file so a truncated or interrupted copy can never be mistaken for a good build. function sha256(file) { return new Promise((resolve, reject) => { const h = crypto.createHash('sha256'); const s = fs.createReadStream(file); s.on('data', c => h.update(c)); s.on('error', reject); s.on('end', () => resolve(h.digest('hex'))); }); } const ROOT = path.join(__dirname, '..'); const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf-8')); const version = pkg.version; function appNotesDir() { // Mirrors the app's user-content location so we can reuse the configured update folder. const base = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'); return path.join(base, 'Bible App', 'user-content', 'Notes'); } function configuredDest() { try { const p = path.join(appNotesDir(), 'update-config.json'); if (fs.existsSync(p)) { const cfg = JSON.parse(fs.readFileSync(p, 'utf-8')); const s = String(cfg.source || '').trim(); if (s && !/^https?:\/\//i.test(s)) return s; // http sources are uploaded by hand } } catch (_) {} return ''; } const argDest = process.argv[2] && !process.argv[2].startsWith('--') ? process.argv[2] : ''; const notes = process.argv[3] || ''; const dest = argDest || process.env.BIBLEAPP_UPDATE_DIR || configuredDest() || path.join(ROOT, 'dist-installer', 'publish'); const distDir = path.join(ROOT, 'dist-installer'); if (!fs.existsSync(distDir)) { console.error('No dist-installer folder — run the build first (npm run build:win).'); process.exit(1); } // Prefer the installer matching package.json's version; else the newest .exe in dist-installer. const exact = fs.readdirSync(distDir).find(f => f.endsWith('.exe') && f.includes(version)); let installer = exact; if (!installer) { const exes = fs.readdirSync(distDir) .filter(f => f.toLowerCase().endsWith('.exe')) .map(f => ({ f, t: fs.statSync(path.join(distDir, f)).mtimeMs })) .sort((a, b) => b.t - a.t); if (!exes.length) { console.error('No installer (.exe) found in dist-installer.'); process.exit(1); } installer = exes[0].f; console.warn(`! No installer named for v${version}; publishing newest: ${installer}`); } fs.mkdirSync(dest, { recursive: true }); const srcPath = path.join(distDir, installer); const destPath = path.join(dest, installer); const sizeMB = (fs.statSync(srcPath).size / 1048576).toFixed(0); console.log(`Publishing v${version}`); console.log(` installer : ${installer} (${sizeMB} MB)`); console.log(` to : ${dest}`); fs.copyFileSync(srcPath, destPath); /* * The AI model does NOT need to go in the update folder any more: a new machine gets it from the * one-file "Bible App Full Setup" bundle, and machines that already have it keep it (it lives in * ProgramData/user-data, which no installer touches). So updates stay ~300 MB instead of ~2.2 GB. * * We still refresh a model.gguf that is ALREADY published, so an existing Settings > Local AI * install path keeps working; pass --with-model to publish it for the first time. */ let modelPublished = false; try { const modelSrc = path.join(ROOT, 'vendor', 'llama', 'model.gguf'); const modelDest = path.join(dest, 'model.gguf'); const wanted = process.argv.includes('--with-model') || fs.existsSync(modelDest); if (!wanted) { console.log(' AI model : skipped (ships in the full one-file installer; use --with-model to publish it here)'); } else if (fs.existsSync(modelSrc)) { const srcSize = fs.statSync(modelSrc).size; const needCopy = !fs.existsSync(modelDest) || fs.statSync(modelDest).size !== srcSize; if (needCopy) { console.log(` AI model : copying model.gguf (${(srcSize / 1073741824).toFixed(2)} GB) — one time…`); fs.copyFileSync(modelSrc, modelDest); } else { console.log(' AI model : already published (unchanged)'); } modelPublished = true; } else { console.warn('! vendor/llama/model.gguf not found — publish it manually so machines can install the AI model.'); } } catch (e) { console.warn('! Could not publish the AI model:', e.message); } (async () => { const installerSize = fs.statSync(destPath).size; const installerHash = await sha256(destPath); let modelSize, modelHash; try { const md = path.join(dest, 'model.gguf'); if (modelPublished && fs.existsSync(md)) { modelSize = fs.statSync(md).size; console.log(' hashing model (integrity check)…'); modelHash = await sha256(md); } } catch (_) {} const manifest = { version, file: installer, size: installerSize, sha256: installerHash, notes: notes || `Bible App ${version}`, date: new Date().toISOString().slice(0, 10), model: modelPublished ? 'model.gguf' : undefined, modelSize, modelSha256: modelHash }; fs.writeFileSync(path.join(dest, 'latest.json'), JSON.stringify(manifest, null, 2) + '\n'); // Companion file so a person can verify a hand-copied installer: // Get-FileHash "Bible App Setup x.y.z.exe" -Algorithm SHA256 fs.writeFileSync(path.join(dest, installer + '.sha256'), installerHash + ' ' + installer + '\n'); console.log(` sha256 : ${installerHash}`); // Keep the folder tidy: retain the 3 newest installers alongside the current one. try { const olds = fs.readdirSync(dest) .filter(f => f.toLowerCase().endsWith('.exe') && f !== installer) .map(f => ({ f, t: fs.statSync(path.join(dest, f)).mtimeMs })) .sort((a, b) => b.t - a.t) .slice(3); olds.forEach(o => { try { fs.unlinkSync(path.join(dest, o.f)); console.log(` removed old build: ${o.f}`); } catch (_) {} }); } catch (_) {} console.log('\nDone. The app will offer this version from Settings > Updates.'); console.log(`Manifest: ${path.join(dest, 'latest.json')}`); })().catch(e => { console.error('Publish failed:', e && e.message || e); process.exit(1); });