modified: electron/main.js

This commit is contained in:
MrPiglr 2026-01-02 04:07:51 -07:00
parent dc9f7ea66e
commit 7e1e41bb02

View file

@ -1,35 +1,382 @@
import { app, globalShortcut } from "electron";
import {
createMainWindow,
createOverlayWindow,
toggleMainVisibility,
} from "./windows.js";
import { registerIpcHandlers } from "./ipc.js";
import { startClipboardSentinel, stopClipboardSentinel } from "./sentinel.js";
app,
BrowserWindow,
ipcMain,
globalShortcut,
clipboard,
dialog, // Import dialog for file selection
shell, // Import shell for opening paths
} from "electron";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { promises as fs } from "node:fs"; // Import fs.promises for async file operations
import chokidar from "chokidar"; // Import chokidar for file watching
import { startWatcher, stopWatcher } from "../services/watcher.js";
import { scrubPII } from "../services/pii-scrub.js";
import { execa } from "execa"; // Import execa for running Git commands
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
let mainWindow = null;
let overlayWindow = null;
let pinned = false;
let sentinelInterval = null;
function getRendererUrl() {
return (
process.env.VITE_DEV_SERVER_URL ||
`file://${path.join(__dirname, "../dist/spa/index.html")}`
);
}
function createMainWindow() {
mainWindow = new BrowserWindow({
width: 1280,
height: 800,
frame: false,
titleBarStyle: "hidden",
backgroundColor: "#030712",
show: true,
webPreferences: {
preload: path.join(__dirname, "preload.cjs"),
contextIsolation: true,
nodeIntegration: false,
webSecurity: false,
sandbox: false,
},
});
const url = getRendererUrl();
console.log("[Main] Loading URL:", url);
// Clear cache to prevent stale assets
mainWindow.webContents.session.clearCache();
// Disable caching in development
mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
(details, callback) => {
details.requestHeaders["Cache-Control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
callback({ requestHeaders: details.requestHeaders });
}
);
// If loading from dev server, wait a moment for it to start
if (process.env.VITE_DEV_SERVER_URL) {
setTimeout(() => {
mainWindow.loadURL(url).catch(err => {
console.error('[Main] Failed to load:', err);
setTimeout(() => mainWindow.loadURL(url), 1000);
});
}, 1000);
} else {
mainWindow.loadURL(url);
}
mainWindow.webContents.openDevTools();
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
console.error('[Main] Failed to load:', errorCode, errorDescription);
setTimeout(() => mainWindow.loadURL(url), 1000);
});
mainWindow.webContents.on('crashed', () => {
console.error('[Main] Renderer crashed');
});
}
function createOverlayWindow() {
// Overlay window disabled for now - it was blocking clicks on main window
/*
overlayWindow = new BrowserWindow({
width: 420,
height: 640,
transparent: true,
frame: false,
alwaysOnTop: true,
resizable: true,
focusable: true,
backgroundColor: "#00000000",
webPreferences: {
preload: path.join(__dirname, "preload.js"),
contextIsolation: true,
nodeIntegration: false,
},
});
overlayWindow.setAlwaysOnTop(true, "floating");
const base = getRendererUrl();
// Assumes your SPA has a route for /overlay
overlayWindow.loadURL(base + "#/overlay");
*/
}
function toggleMainVisibility() {
if (!mainWindow) return;
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
mainWindow.show();
mainWindow.focus();
}
}
function startClipboardSentinel() {
if (sentinelInterval) return;
let last = clipboard.readText();
sentinelInterval = setInterval(() => {
const now = clipboard.readText();
if (now !== last) {
last = now;
const scrubbed = scrubPII(now);
if (scrubbed !== now) {
mainWindow?.webContents.send("sentinel:clipboard-alert", now);
}
}
}, 1500);
}
app.whenReady().then(() => {
registerIpcHandlers();
createMainWindow();
createOverlayWindow();
// createOverlayWindow(); // Disabled for debugging
startClipboardSentinel();
// Global hotkey to toggle visibility
globalShortcut.register("Alt+Space", toggleMainVisibility);
ipcMain.handle("watcher:start", async (_e, dir) => {
await startWatcher(dir);
return true;
});
ipcMain.handle("watcher:stop", async () => {
await stopWatcher();
return true;
});
ipcMain.handle("window:toggle-pin", () => {
pinned = !pinned;
mainWindow?.setAlwaysOnTop(pinned, "floating");
return pinned;
});
ipcMain.handle("window:close", () => {
mainWindow?.close();
});
ipcMain.handle("window:minimize", () => {
mainWindow?.minimize();
});
ipcMain.handle("window:maximize", () => {
if (!mainWindow) return false;
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
} else {
mainWindow.maximize();
}
return mainWindow.isMaximized();
});
// File system IPC handlers
ipcMain.handle("fs:select-folder", async () => {
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
properties: ["openDirectory"],
});
if (canceled) return null;
return filePaths[0];
});
ipcMain.handle("fs:select-files", async () => {
const { canceled, filePaths } = await dialog.showOpenDialog(mainWindow, {
properties: ["openFile", "multiSelections"],
});
if (canceled) return null;
return filePaths;
});
ipcMain.handle("fs:read-project-metadata", async (_e, folderPath) => {
try {
const packageJsonPath = path.join(folderPath, "package.json");
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf-8"));
return {
name: packageJson.name,
version: packageJson.version,
description: packageJson.description,
// Add more metadata as needed
};
} catch (error) {
console.error("Failed to read project metadata:", error);
return null;
}
});
let activeWatchers = {};
ipcMain.handle("fs:watch-folder", async (_e, folderPath) => {
if (activeWatchers[folderPath]) {
console.log(`Already watching ${folderPath}`);
return true;
}
const watcher = chokidar.watch(folderPath, {
ignored: /(^|\/)\../, // ignore dotfiles
persistent: true,
ignoreInitial: true,
});
watcher.on("add", (filePath) => {
mainWindow?.webContents.send("fs:file-change", {
type: "add",
filePath,
folderPath,
});
});
watcher.on("change", (filePath) => {
mainWindow?.webContents.send("fs:file-change", {
type: "change",
filePath,
folderPath,
});
});
watcher.on("unlink", (filePath) => {
mainWindow?.webContents.send("fs:file-change", {
type: "unlink",
filePath,
folderPath,
});
});
watcher.on("addDir", (dirPath) => {
mainWindow?.webContents.send("fs:folder-change", {
type: "add",
dirPath,
folderPath,
});
});
watcher.on("unlinkDir", (dirPath) => {
mainWindow?.webContents.send("fs:folder-change", {
type: "unlink",
dirPath,
folderPath,
});
});
activeWatchers[folderPath] = watcher;
console.log(`Started watching ${folderPath}`);
return true;
});
ipcMain.handle("fs:unwatch-folder", async (_e, folderPath) => {
if (activeWatchers[folderPath]) {
await activeWatchers[folderPath].close();
delete activeWatchers[folderPath];
console.log(`Stopped watching ${folderPath}`);
}
return true;
});
// Git IPC handlers
ipcMain.handle("git:get-status", async (_e, folderPath) => {
try {
const { stdout } = await execa("git", ["status", "--porcelain"], { cwd: folderPath });
const changes = stdout.split("\n").filter(line => line.trim() !== "");
let status = "clean";
if (changes.length > 0) {
status = "modified";
}
const { stdout: branchOutput } = await execa("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: folderPath });
const branch = branchOutput.trim();
return { status, branch, changes: changes.length };
} catch (error) {
console.error(`Failed to get Git status for ${folderPath}:`, error);
return { status: "error", branch: "N/A", changes: 0, error: error.message };
}
});
ipcMain.handle("git:pull", async (_e, folderPath) => {
try {
const { stdout } = await execa("git", ["pull"], { cwd: folderPath });
return { success: true, output: stdout };
} catch (error) {
console.error(`Failed to pull Git changes for ${folderPath}:`, error);
return { success: false, error: error.message };
}
});
ipcMain.handle("git:push", async (_e, folderPath) => {
try {
const { stdout } = await execa("git", ["push"], { cwd: folderPath });
return { success: true, output: stdout };
} catch (error) {
console.error(`Failed to push Git changes for ${folderPath}:`, error);
return { success: false, error: error.message };
}
});
ipcMain.handle("shell:open-path", async (_e, itemPath) => {
try {
await shell.openPath(itemPath);
return { success: true };
} catch (error) {
console.error(`Failed to open path ${itemPath}:`, error);
return { success: false, error: error.message };
}
});
// Build runner - runs a project's build script in a safe, cwd-restricted manner
ipcMain.handle("build:run", async (_e, folderPath) => {
try {
// Ensure folder exists
await fs.stat(folderPath);
} catch (err) {
console.error("Build run failed - folder not found:", folderPath, err?.message || err);
return { success: false, error: "folder-not-found" };
}
try {
// Default to running the repo's npm build script. This keeps scope narrow and predictable.
const child = execa("npm", ["run", "build"], { cwd: folderPath });
// Stream stdout/stderr back to renderer for live logs
if (child.stdout) {
child.stdout.on("data", (chunk) => {
mainWindow?.webContents.send("build:log", {
folderPath,
type: "stdout",
text: String(chunk),
});
});
}
if (child.stderr) {
child.stderr.on("data", (chunk) => {
mainWindow?.webContents.send("build:log", {
folderPath,
type: "stderr",
text: String(chunk),
});
});
}
mainWindow?.webContents.send("build:log", { folderPath, type: "start", text: "Build started" });
const result = await child;
mainWindow?.webContents.send("build:log", { folderPath, type: "finish", text: result.stdout || "Build finished" });
return { success: true, output: result.stdout || "" };
} catch (error) {
console.error(`Build failed for ${folderPath}:`, error);
mainWindow?.webContents.send("build:log", { folderPath, type: "error", text: error?.message || String(error) });
return { success: false, error: error?.message || String(error) };
}
});
});
// Don't quit the app when all windows are closed in development
// This allows the app to stay running and prevents auto-close
app.on("window-all-closed", () => {
stopClipboardSentinel();
if (process.platform !== "darwin") {
app.quit();
}
// Only quit on macOS - on Windows/Linux, keep the app running
// This prevents the immediate close issue
if (process.platform === "darwin") app.quit();
});
app.on("activate", () => {
createMainWindow();
});
app.on("will-quit", () => {
globalShortcut.unregisterAll();
stopClipboardSentinel();
});