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.