How PortPilot is built
PortPilot is a desktop app because the job is on the computer: list ports, kill processes, catch a global shortcut, sit in the menu bar. A website cannot do those things.
Who may touch the machine
Electron is Chromium (what you see) plus Node (what talks to the OS). The UI does not run OS commands. A preload script is the only door.
:preload.ts
contextBridge.exposeInMainWorld('portpilot', {
listPorts: () => ipcRenderer.invoke('ports:list'),
kill: (pid: number) => ipcRenderer.invoke('ports:kill', pid),
})
:main.ts
ipcMain.handle('ports:kill', async (_event, pid: number) => {
process.kill(pid)
return { ok: true }
})
Kill is more careful in the app (signals, Windows, confirm). The split is the point: screens in Chromium, sharp tools in Node.
Stack
| Piece | Job | |---|---| | React + TypeScript | Screens and types | | Zustand | Port list and settings | | Tailwind | Light / dark, follows the system | | Fuse.js | Cmd + K | | Bun | Install and scripts | | electron-vite | Bundle main, preload, renderer | | electron-log | Crash files after the window is gone | | electron-updater | GitHub builds update in-app; Store builds go through Apple |
One TypeScript codebase ships Mac and Windows. The cost is a desktop runtime, not a 20kb page. For this product, one team, two OSes, real process control beat a thinner Mac-only binary.
Next: the timer behind the list, what we do not scan, and why there is no login.