diff --git a/package.json b/package.json index ecdb70b..309ebb1 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "stratosort-core", + "productName": "StratoSort Core", "version": "2.0.1", "description": "StratoSort Core - Local AI Document Organizer. Privacy-focused document organization using in-process AI (node-llama-cpp + Orama). Zero external dependencies.", "keywords": [ diff --git a/scripts/patch-electron-mac.js b/scripts/patch-electron-mac.js new file mode 100644 index 0000000..f0509d4 --- /dev/null +++ b/scripts/patch-electron-mac.js @@ -0,0 +1,158 @@ +#!/usr/bin/env node +/** + * Patch Electron.app Info.plist for macOS development + * + * During development, macOS reads the dock label and Activity Monitor process + * name from the Electron binary's Info.plist, which defaults to "Electron". + * This script patches CFBundleName and CFBundleDisplayName so the dock shows + * "StratoSort Core" instead. + * + * This is safe and idempotent — re-running produces the same result. + * Only affects node_modules (dev-only); production builds use electron-builder + * which sets these values correctly from electron-builder.json. + * + * Usage: + * node scripts/patch-electron-mac.js # Patch Info.plist + * node scripts/patch-electron-mac.js --check # Check current values (no changes) + */ + +const fs = require('fs'); +const path = require('path'); + +const APP_NAME = 'StratoSort Core'; +const BUNDLE_ID = 'com.stratosort.app'; + +/** + * Locate the Electron.app Info.plist inside node_modules. + * Returns null if not found (e.g. running on non-macOS or Electron not installed). + */ +function findInfoPlist() { + const candidates = [ + // Standard npm install location + path.join( + __dirname, + '..', + 'node_modules', + 'electron', + 'dist', + 'Electron.app', + 'Contents', + 'Info.plist' + ), + // Hoisted or alternative layouts + path.join(__dirname, '..', '..', 'electron', 'dist', 'Electron.app', 'Contents', 'Info.plist') + ]; + + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return candidate; + } + } + return null; +} + +/** + * Replace a plist string value. + * Matches: KEY\n\tOLD + * and replaces OLD with NEW. + */ +function replacePlistValue(content, key, newValue) { + // Match the key followed by a string value (handles various whitespace) + const regex = new RegExp(`(${escapeRegex(key)}\\s*)(.*?)()`, 'g'); + return content.replace(regex, `$1${newValue}$3`); +} + +function escapeRegex(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function readPlistValue(content, key) { + const regex = new RegExp(`${escapeRegex(key)}\\s*(.*?)`); + const match = content.match(regex); + return match ? match[1] : null; +} + +function main() { + if (process.platform !== 'darwin') { + console.log('[patch-electron-mac] Skipped — not macOS'); + return 0; + } + + const plistPath = findInfoPlist(); + if (!plistPath) { + console.log('[patch-electron-mac] Skipped — Electron.app not found'); + return 0; + } + + const content = fs.readFileSync(plistPath, 'utf8'); + + const currentName = readPlistValue(content, 'CFBundleName'); + const currentDisplayName = readPlistValue(content, 'CFBundleDisplayName'); + const currentBundleId = readPlistValue(content, 'CFBundleIdentifier'); + + // --check mode: report current values without modifying + if (process.argv.includes('--check')) { + console.log('[patch-electron-mac] Info.plist:', plistPath); + console.log(` CFBundleName: ${currentName}`); + console.log(` CFBundleDisplayName: ${currentDisplayName}`); + console.log(` CFBundleIdentifier: ${currentBundleId}`); + + const isPatched = currentName === APP_NAME; + console.log( + ` Status: ${isPatched ? '✅ Patched' : '⚠️ Not patched (shows "Electron" in dock)'}` + ); + return isPatched ? 0 : 1; + } + + // Check if already patched + if (currentName === APP_NAME && currentDisplayName === APP_NAME) { + console.log('[patch-electron-mac] Already patched — dock will show "StratoSort Core"'); + return 0; + } + + // Apply patches + let patched = content; + patched = replacePlistValue(patched, 'CFBundleName', APP_NAME); + + // CFBundleDisplayName may not exist in the original; add it if missing + if (currentDisplayName !== null) { + patched = replacePlistValue(patched, 'CFBundleDisplayName', APP_NAME); + } else { + // Insert after CFBundleName + patched = patched.replace( + /(CFBundleName<\/key>\s*.*?<\/string>)/, + `$1\n\tCFBundleDisplayName\n\t${APP_NAME}` + ); + } + + // Patch bundle identifier so macOS associates dock state correctly + if (currentBundleId && currentBundleId !== BUNDLE_ID) { + patched = replacePlistValue(patched, 'CFBundleIdentifier', BUNDLE_ID); + } + + fs.writeFileSync(plistPath, patched, 'utf8'); + + // Verify + const verify = fs.readFileSync(plistPath, 'utf8'); + const verifiedName = readPlistValue(verify, 'CFBundleName'); + if (verifiedName !== APP_NAME) { + console.error('[patch-electron-mac] ❌ Patch failed — CFBundleName is still:', verifiedName); + return 1; + } + + console.log('[patch-electron-mac] ✅ Patched Electron.app Info.plist'); + console.log(` CFBundleName: "${currentName}" → "${APP_NAME}"`); + console.log(` CFBundleDisplayName: "${currentDisplayName || '(missing)'}" → "${APP_NAME}"`); + if (currentBundleId && currentBundleId !== BUNDLE_ID) { + console.log(` CFBundleIdentifier: "${currentBundleId}" → "${BUNDLE_ID}"`); + } + console.log(' The macOS dock will now show "StratoSort Core" in development.'); + + return 0; +} + +if (require.main === module) { + process.exit(main()); +} + +module.exports = { main }; diff --git a/scripts/postinstall.js b/scripts/postinstall.js index ef29573..0786e5d 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -53,6 +53,17 @@ function main( } } + // On macOS, patch the Electron.app Info.plist so the dock label and Activity + // Monitor show "StratoSort Core" instead of "Electron" during development. + if (platform === 'darwin') { + try { + const { main: patchMac } = require('./patch-electron-mac'); + patchMac(); + } catch (e) { + log.warn(`[postinstall] macOS Electron patch skipped: ${e.message}`); + } + } + log.log('\n[StratoSort] Setup complete!'); log.log(' Run: npm run dev'); log.log(' The app will use local AI engine (node-llama-cpp + Orama).\n'); diff --git a/src/main/simple-main.js b/src/main/simple-main.js index fdd5543..311ed98 100644 --- a/src/main/simple-main.js +++ b/src/main/simple-main.js @@ -8,6 +8,14 @@ try { const { app, BrowserWindow, ipcMain, dialog, shell, crashReporter } = require('electron'); +// Set the application name as early as possible. +// On macOS this controls the dock label and application menu title. +// Electron defaults to package.json "productName" → "name", but the raw Electron +// binary in development reports itself as "Electron" to the OS. An explicit +// assignment ensures the correct name in all contexts (dock, Activity Monitor menu +// label, window title fallback) regardless of dev vs packaged mode. +app.name = 'StratoSort Core'; + const isDev = process.env.NODE_ENV === 'development'; // Logging utility @@ -543,6 +551,28 @@ if (gotTheLock || process.env.STRATOSORT_FORCE_LAUNCH === '1') { // Initialize services after app is ready app.whenReady().then(async () => { logger.info('[STARTUP] app.whenReady resolved'); + + // macOS: Set the dock icon to the StratoSort logo so it replaces the default + // Electron atom icon during development. In production (packaged) builds + // electron-builder bakes the icon into the .app bundle so this is a no-op. + if (process.platform === 'darwin' && app.dock) { + try { + const { nativeImage } = require('electron'); + const iconPath = path.join(__dirname, '..', 'assets', 'icons', 'png', '512x512.png'); + // Resolve from repo root in dev, or from resources in packaged build + const resolvedIcon = app.isPackaged + ? path.join(process.resourcesPath, 'assets', 'icons', 'png', '512x512.png') + : path.resolve(iconPath); + const fsSync = require('fs'); + if (fsSync.existsSync(resolvedIcon)) { + app.dock.setIcon(nativeImage.createFromPath(resolvedIcon)); + logger.info('[DOCK] Set macOS dock icon from', resolvedIcon); + } + } catch (dockErr) { + logger.debug('[DOCK] Could not set macOS dock icon:', dockErr?.message); + } + } + // FIX: Create a referenced interval to keep the event loop alive during startup // This prevents premature exit when async operations use unreferenced timeouts const startupKeepalive = trackInterval(() => {}, 1000);