modified: electron/main.js
This commit is contained in:
parent
dc9f7ea66e
commit
7e1e41bb02
1 changed files with 370 additions and 23 deletions
393
electron/main.js
393
electron/main.js
|
|
@ -1,35 +1,382 @@
|
||||||
import { app, globalShortcut } from "electron";
|
|
||||||
import {
|
import {
|
||||||
createMainWindow,
|
app,
|
||||||
createOverlayWindow,
|
BrowserWindow,
|
||||||
toggleMainVisibility,
|
ipcMain,
|
||||||
} from "./windows.js";
|
globalShortcut,
|
||||||
import { registerIpcHandlers } from "./ipc.js";
|
clipboard,
|
||||||
import { startClipboardSentinel, stopClipboardSentinel } from "./sentinel.js";
|
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(() => {
|
app.whenReady().then(() => {
|
||||||
registerIpcHandlers();
|
|
||||||
|
|
||||||
createMainWindow();
|
createMainWindow();
|
||||||
createOverlayWindow();
|
// createOverlayWindow(); // Disabled for debugging
|
||||||
|
|
||||||
startClipboardSentinel();
|
startClipboardSentinel();
|
||||||
|
|
||||||
|
// Global hotkey to toggle visibility
|
||||||
globalShortcut.register("Alt+Space", toggleMainVisibility);
|
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", () => {
|
app.on("window-all-closed", () => {
|
||||||
stopClipboardSentinel();
|
// Only quit on macOS - on Windows/Linux, keep the app running
|
||||||
if (process.platform !== "darwin") {
|
// This prevents the immediate close issue
|
||||||
app.quit();
|
if (process.platform === "darwin") app.quit();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("activate", () => {
|
|
||||||
createMainWindow();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on("will-quit", () => {
|
|
||||||
globalShortcut.unregisterAll();
|
|
||||||
stopClipboardSentinel();
|
|
||||||
});
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue