Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ zig-out/
.zig-cache/
dist/
*.tgz
node_modules/

# Local macOS packaging outputs
.DS_Store
build/*.png
build/*.icns
build/icon.iconset/

# IDE/Editor
.idea/
Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,43 @@ npx @loongphy/codex-auth list

npm packages currently support Linux x64, macOS x64, macOS arm64, Windows x64, and Windows arm64.

## GUI

This repo now includes a local browser GUI for account management. From the project root:

```shell
npm run gui
```

The server opens a local dashboard and will use a built repo binary when available, build with Zig when `zig` is installed, or fall back to an existing `codex-auth` on your `PATH`. From there you can:

- inspect accounts and the active session
- switch or remove accounts
- import auth JSON files
- refresh usage data
- change auto-switch and API settings

The GUI still uses the existing CLI logic underneath, so the browser stays in sync with the same `~/.codex` data the terminal commands use.

## Electron App

There is also a local Electron desktop wrapper for the dashboard:

```shell
npm install
npm run desktop
```

That opens the same account dashboard in a native desktop window instead of your browser. Internally, the Electron app starts the local GUI server for you and loads it inside the app window.

The desktop app exposes the main account-management flows directly in GUI form, including:

- login and device-auth login
- standard auth import, CPA import, and registry rebuild (`import --purge`)
- account switching and removal
- auto-switch and API configuration
- backup cleanup

> [!NOTE]
> If you only installed `@loongphy/codex-auth` with npm, you do not need any legacy cleanup steps.
> Older Bash/PowerShell GitHub-release installs could leave a standalone `codex-auth` binary outside npm's install path.
Expand Down
16 changes: 16 additions & 0 deletions build/dmg-background.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions build/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
175 changes: 175 additions & 0 deletions gui/electron/main.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
const { app, BrowserWindow, Menu, dialog, shell } = require("electron");
const path = require("node:path");
const { spawn } = require("node:child_process");

let mainWindow = null;
let serverProcess = null;
let appUrl = null;
let isQuitting = false;

const isSmokeTest = process.argv.includes("--smoke-test");

function createWindow() {
mainWindow = new BrowserWindow({
width: 1380,
height: 920,
minWidth: 1080,
minHeight: 760,
title: "Codex Auth Control Room",
backgroundColor: "#f4efe6",
autoHideMenuBar: true,
show: false,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: false
}
});

mainWindow.once("ready-to-show", () => {
mainWindow.show();
});

mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: "deny" };
});

mainWindow.webContents.on("will-navigate", (event, url) => {
if (appUrl && url !== appUrl) {
event.preventDefault();
shell.openExternal(url);
}
});

mainWindow.on("closed", () => {
mainWindow = null;
});
}

function waitForServerUrl(child) {
return new Promise((resolve, reject) => {
let stdout = "";
let stderr = "";
let settled = false;
const timeout = setTimeout(() => {
settleReject(new Error(`Timed out waiting for the local GUI server.\n${stderr.trim() || stdout.trim()}`.trim()));
}, 15000);

const settleResolve = (value) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
resolve(value);
};

const settleReject = (error) => {
if (settled) return;
settled = true;
clearTimeout(timeout);
reject(error);
};

child.stdout.on("data", (chunk) => {
const text = chunk.toString();
stdout += text;
const match = stdout.match(/Codex Auth GUI running at (http:\/\/[^\s]+)/);
if (match) {
settleResolve(match[1]);
}
});

child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});

child.once("error", (error) => {
settleReject(error);
});

child.once("exit", (code) => {
if (settled) return;
const detail = stderr.trim() || stdout.trim() || `Server exited with code ${code ?? "unknown"}.`;
settleReject(new Error(detail));
});
});
}

async function startServer() {
if (serverProcess && appUrl) {
return appUrl;
}

const serverPath = path.join(__dirname, "..", "server.mjs");
const appRoot = path.join(__dirname, "..", "..");
const workingRoot = path.basename(appRoot) === "app.asar" ? path.dirname(appRoot) : appRoot;
serverProcess = spawn(process.execPath, [serverPath], {
cwd: workingRoot,
env: {
...process.env,
ELECTRON_RUN_AS_NODE: "1",
PORT: "0"
},
stdio: ["ignore", "pipe", "pipe"]
});

serverProcess.stderr.on("data", (chunk) => {
process.stderr.write(chunk);
});

appUrl = await waitForServerUrl(serverProcess);
return appUrl;
}

function stopServer() {
if (!serverProcess) return;
const child = serverProcess;
serverProcess = null;
appUrl = null;

if (child.killed) return;
child.kill("SIGTERM");
setTimeout(() => {
if (!child.killed) {
child.kill("SIGKILL");
}
}, 1500).unref();
}

async function boot() {
Menu.setApplicationMenu(null);
createWindow();

try {
const url = await startServer();
await mainWindow.loadURL(url);
if (isSmokeTest) {
process.stdout.write(`Electron smoke test loaded ${url}\n`);
app.quit();
}
} catch (error) {
dialog.showErrorBox("Failed to Launch Codex Auth", error.message);
app.quit();
}
}

app.whenReady().then(boot);

app.on("activate", async () => {
if (BrowserWindow.getAllWindows().length === 0) {
await boot();
}
});

app.on("before-quit", () => {
isQuitting = true;
stopServer();
});

app.on("window-all-closed", () => {
if (process.platform !== "darwin" || isSmokeTest) {
app.quit();
}
});

process.on("exit", stopServer);
Loading