Files
bible-app/tools/publish-update.cjs
T
christianandClaude Opus 5 0db5e9b07d Bible App 1.4.34 — full source
First commit on the local Gitea server, made after moving the tree off the
Windows machine. Build outputs, node_modules and the AI models stay out
(see .gitignore); everything the installer build needs is tracked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 13:05:20 -04:00

159 lines
6.5 KiB
JavaScript

/*
* 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); });