159 lines
6.5 KiB
JavaScript
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); });
|