const { execFileSync } = require('child_process'); const { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } = require('fs'); const { join } = require('path'); const MEOWA_RELEASE_CREDENTIAL_FILE_NAME = 'meowa-game-assets-credential.json'; exports.default = async function afterPack(context) { const platform = context.electronPlatformName; console.log(`[after-pack] Target: ${platform}/${context.arch}`); writeMeowaReleaseCredential(context); assertNoPersistedUserData(context.appOutDir); if (platform !== 'win32') { return; } patchWindowsExecutableResources(context); patchNsisExtractionMacro(); }; function writeMeowaReleaseCredential(context) { const apiKey = process.env.MEOWART_API_KEY?.trim(); if (!apiKey) { console.log('[after-pack] Meowa release credential not included; MEOWART_API_KEY is unset.'); return false; } const resourcesDir = context.packager.getResourcesDir(context.appOutDir); const target = join(resourcesDir, 'resources', MEOWA_RELEASE_CREDENTIAL_FILE_NAME); mkdirSync(join(resourcesDir, 'resources'), { recursive: true }); writeFileSync( target, `${JSON.stringify({ schemaVersion: 1, apiKey })}\n`, { encoding: 'utf8', mode: 0o600 }, ); console.log('[after-pack] Meowa release credential included in packaged Main resources.'); return true; } const PERSISTED_USER_DATA_FILES = new Set([ 'gateway-prelaunch-maintenance-cache.json', 'niancode-device-identity.json', 'niancode-providers.json', 'opencode-projects.json', 'settings.json', ]); function assertNoPersistedUserData(appOutDir) { const offenders = []; collectPersistedUserDataFiles(appOutDir, offenders); if (offenders.length > 0) { throw new Error( [ 'Persisted Makelore user data must not be packaged.', 'Remove these files from the package input:', ...offenders.map((file) => `- ${file}`), ].join('\n'), ); } } function collectPersistedUserDataFiles(dir, offenders) { if (!existsSync(dir)) return; for (const entry of readdirSync(dir, { withFileTypes: true })) { const path = join(dir, entry.name); if (entry.isDirectory()) { collectPersistedUserDataFiles(path, offenders); continue; } if (entry.isFile() && PERSISTED_USER_DATA_FILES.has(entry.name)) { offenders.push(path); } } } function patchWindowsExecutableResources(context) { const rcedit = findRcedit(); if (!rcedit) { console.log('[after-pack] rcedit-x64.exe not found; skipping executable resource patch.'); return; } const appInfo = context.packager.appInfo; const executable = join(context.appOutDir, `${appInfo.productFilename}.exe`); if (!existsSync(executable)) { console.log(`[after-pack] ${appInfo.productFilename}.exe not found; skipping executable resource patch.`); return; } const icon = join(context.packager.info.buildResourcesDir, 'icons', 'icon.ico'); const version = typeof appInfo.getVersionInWeirdWindowsForm === 'function' ? appInfo.getVersionInWeirdWindowsForm() : normalizeWindowsVersion(appInfo.version); const args = [ executable, '--set-version-string', 'FileDescription', appInfo.productName, '--set-version-string', 'ProductName', appInfo.productName, '--set-version-string', 'LegalCopyright', appInfo.copyright, '--set-file-version', version, '--set-product-version', version, '--set-version-string', 'InternalName', appInfo.productFilename, '--set-version-string', 'OriginalFilename', '', ]; if (existsSync(icon)) { args.push('--set-icon', icon); } execFileSync(rcedit, args, { stdio: 'inherit' }); console.log(`[after-pack] Patched ${appInfo.productFilename}.exe resources with ${appInfo.productName}.`); } function normalizeWindowsVersion(version) { const parts = String(version ?? '0.0.0') .split('.') .map((part) => Number.parseInt(part, 10)) .filter((part) => Number.isFinite(part)); while (parts.length < 4) parts.push(0); return parts.slice(0, 4).join('.'); } function findRcedit() { const explicitDir = process.env.ELECTRON_BUILDER_RCEDIT_PATH; if (explicitDir) { const explicit = join(explicitDir, 'rcedit-x64.exe'); if (existsSync(explicit)) return explicit; } const cacheRoots = [ process.env.ELECTRON_BUILDER_CACHE, process.env.LOCALAPPDATA ? join(process.env.LOCALAPPDATA, 'electron-builder', 'Cache') : null, join(__dirname, '..', '.cache', 'electron-builder', 'Cache'), ].filter(Boolean); for (const root of cacheRoots) { const winCodeSign = join(root, 'winCodeSign'); const found = findRceditInWinCodeSignCache(winCodeSign); if (found) return found; } return null; } function findRceditInWinCodeSignCache(winCodeSign) { if (!existsSync(winCodeSign)) return null; for (const entry of readdirSync(winCodeSign)) { const candidate = join(winCodeSign, entry, 'rcedit-x64.exe'); if (!existsSync(candidate)) continue; if (!statSync(candidate).isFile()) continue; return candidate; } return null; } function patchNsisExtractionMacro() { const extractNsh = join( __dirname, '..', 'node_modules', 'app-builder-lib', 'templates', 'nsis', 'include', 'extractAppPackage.nsh', ); if (!existsSync(extractNsh)) { console.log('[after-pack] NSIS extractAppPackage.nsh not found; skipping patch.'); return; } const original = readFileSync(extractNsh, 'utf8'); if (original.includes('Makelore-patched')) { console.log('[after-pack] NSIS extractAppPackage.nsh already patched.'); return; } const replacement = [ '!macro extractUsing7za file', ' ; Makelore-patched: extract directly to $INSTDIR.', ' Nsis7z::Extract "$PLUGINSDIR\\\\${file}" "$INSTDIR"', '!macroend', ].join('\n'); const patched = original.replace(/!macro extractUsing7za file[\s\S]*?!macroend/, replacement); if (patched === original) { console.log('[after-pack] NSIS extractUsing7za macro not found; skipping patch.'); return; } writeFileSync(extractNsh, patched, 'utf8'); console.log('[after-pack] Patched NSIS extractAppPackage.nsh for direct extraction.'); } exports.assertNoPersistedUserData = assertNoPersistedUserData; exports.writeMeowaReleaseCredential = writeMeowaReleaseCredential;