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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ Detailed command documentation lives in [docs/commands/README.md](./docs/command
| [`codex-auth export --cpa [<dir>]`](./docs/commands/export.md) | Export CLIProxyAPI token JSON |
| [`codex-auth clean`](./docs/commands/clean.md) | Delete managed backup and stale account files |

### Codex App Launching

| Command | Description |
|---------|-------------|
| [`codex-auth app [--app-id <id>] [--codex-cli-path <path>]`](./docs/commands/app.md) | Launch Codex App with detected defaults, CODEX_HOME, CODEX_CLI_PATH, and platform overrides |

### Configuration

| Command | Description |
Expand Down
1 change: 1 addition & 0 deletions docs/commands/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This directory documents command behavior by command. Use `codex-auth <command>
| `alias` | [docs/commands/alias.md](./alias.md) |
| `clean` | [docs/commands/clean.md](./clean.md) |
| `config` | [docs/commands/config.md](./config.md) |
| `app` | [docs/commands/app.md](./app.md) |

## Shared Behavior

Expand Down
82 changes: 82 additions & 0 deletions docs/commands/app.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# `codex-auth app`

## Usage

```shell
codex-auth app [--app-id <id>] [--codex-cli-path <path>] [--codex-home <path>] [--platform win|wsl|mac]
```

## Behavior

Launches the official Codex App with per-process environment overrides.

- `codex-auth app` launches the app. There is no `launch` subcommand.
- If the Codex App is already running, `app` prints that status and exits before
resolving or downloading the managed CLI.
- `--app-id <id>` selects the packaged app to launch. On Windows it accepts an
AppX/MSIX package name such as `OpenAI.Codex` or `Loongphy.Codext`, or a full
AUMID. On macOS it accepts a bundle identifier such as `com.openai.codex`.
- If `--app-id` is omitted, `CODEX_AUTH_APP_ID` is used when set; otherwise the
default is `OpenAI.Codex` on Windows and `com.openai.codex` on macOS.
- `--codex-cli-path <path>` is injected as `CODEX_CLI_PATH` for this launch. Explicit CLI paths must exist. If it is omitted, `app` fetches the latest [`Loongphy/codext`](https://github.com/Loongphy/codext) release metadata, compares it with the managed cached CLI version for the selected platform, downloads only when the cached version differs or is missing, and uses that file; it does not reuse an existing `CODEX_CLI_PATH` from the current shell.
- `--codex-home <path>` is injected as `CODEX_HOME` for `app` launches and selects the accounts cache used for managed CLI resolution.
- `--platform win|wsl|mac` selects the app runtime platform:
- `win` writes the Windows global setting so the app runs the agent natively.
- `wsl` writes the Windows global setting so the app runs the agent inside WSL.
- `mac` launches the macOS app directly and does not use the Windows WSL setting.
- `--std` resolves the packaged app executable, then starts it with stdout/stderr attached to the current terminal. Use it for debugging app logs; normal launches stay quiet and use the platform GUI launcher.

`app` prints its launch plan and managed CLI resolution to stderr before
starting the GUI launcher. Example output:

```text
Codex App is already running, launch skipped.
```

When the app is not already running, the output continues with launch planning:

```text
- Checking latest https://github.com/Loongphy/codext release...
Downloading Codext CLI for WSL (v0.3.0)
https://github.com/Loongphy/codext/releases/download/.../codext-linux-x64.tar.gz
OK Downloaded Codext CLI for WSL (v0.3.0)

- Environment Configuration ------------------------------------------------
Platform: WSL (auto-detected)
Codex Home: C:\Users\Alice\.codext (explicit)
App ID: Loongphy.Codext (explicit)
CLI Path: C:\Users\Alice\.codext\accounts\codext-cli\codex-linux-x64 (downloaded)
----------------------------------------------------------------------------
Launching Codex App...
```

See [Windows](../windows.md) for Windows console color and character rules.

If `--platform` is omitted, Windows reads `$CODEX_HOME/.codex-global-state.json`
and uses `wsl` when `runCodexInWindowsSubsystemForLinux` is `true`; otherwise it
uses `win`. macOS defaults to `mac`.

Default downloaded CLIs are cached directly under:

```text
$CODEX_HOME/accounts/codext-cli/codex-<platform>
$CODEX_HOME/accounts/codext-cli/codex-<platform>.version
```

The default download prepares only the selected platform's
[`Loongphy/codext`](https://github.com/Loongphy/codext) asset for the current
CPU architecture, such as `win32-x64`, `linux-x64`, `darwin-x64`, or
`darwin-arm64`.

Windows App launching is handled by the Windows `codex-auth.exe` build. Normal
launch resolves the package name or AUMID and opens `shell:AppsFolder\<AUMID>`.
The WSL build does not launch Windows App packages.

For Windows-native App launches, `--codex-cli-path` must point to something the Windows
App process can spawn. A WSL command name such as `codex-custom` is not a
Windows executable path.

For macOS App launches, the app is opened with its bundle identifier. The
packaged macOS app normally uses `Contents/Resources/codex` directly as its
bundled CLI; setting `--codex-cli-path` injects `CODEX_CLI_PATH` and takes
precedence over that bundled resource.
59 changes: 59 additions & 0 deletions docs/windows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Windows

## CLI output

Windows console hosts vary in how they handle UTF-8 text and ANSI escape
sequences. `codex-auth` keeps Windows CLI output conservative so PowerShell,
Windows Terminal, `cmd.exe`, and CI logs stay readable.

### Color

- Color is enabled only for TTY output.
- `NO_COLOR` disables color.
- On Windows, ANSI color is emitted only after
`ENABLE_VIRTUAL_TERMINAL_PROCESSING` is already enabled or can be enabled for
the target console handle.
- If virtual-terminal processing cannot be verified, output falls back to plain
text with no ANSI escape sequences.

### Characters

- Windows-facing status markers must be ASCII by default.
- Do not use Unicode status glyphs such as check marks, warning signs, bullets,
arrows, or box-drawing characters in Windows default output.
- Unicode may be used for non-Windows output when it is already part of an
established command style.

Recommended Windows status markers:

```text
Codex App is already running, launch skipped.
- Checking latest https://github.com/Loongphy/codext release...
Downloading Codext CLI for WSL (v0.3.0)
OK Downloaded Codext CLI for WSL (v0.3.0)
```

### App command examples

Already running:

```text
Codex App is already running, launch skipped.
```

Launch with a managed CLI download:

```text
- Checking latest https://github.com/Loongphy/codext release...
Downloading Codext CLI for WSL (v0.3.0)
https://github.com/Loongphy/codext/releases/download/.../codext-linux-x64.tar.gz
OK Downloaded Codext CLI for WSL (v0.3.0)

- Environment Configuration ------------------------------------------------
Platform: WSL (auto-detected)
Codex Home: C:\Users\Loong\.codext (explicit)
App ID: Loongphy.Codext (explicit)
CLI Path: C:\Users\Loong\.codext\accounts\codext-cli\codex-linux-x64 (downloaded)
----------------------------------------------------------------------------
Launching Codex App...
```
73 changes: 73 additions & 0 deletions src/cli/commands/app.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const std = @import("std");
const types = @import("../types.zig");
const common = @import("common.zig");

pub fn parse(allocator: std.mem.Allocator, args: []const [:0]const u8) !types.ParseResult {
if (args.len == 0) return parseOptions(allocator, .launch, args);
const first = std.mem.sliceTo(args[0], 0);
if (common.isHelpFlag(first)) return .{ .command = .{ .help = .app } };

return parseOptions(allocator, .launch, args);
}

fn parseOptions(
allocator: std.mem.Allocator,
action: types.AppAction,
args: []const [:0]const u8,
) !types.ParseResult {
var opts = types.AppOptions{ .action = action };
var i: usize = 0;
while (i < args.len) : (i += 1) {
const arg = std.mem.sliceTo(args[i], 0);
if (std.mem.eql(u8, arg, "--")) return common.usageErrorResult(allocator, .app, "`app` does not accept passthrough arguments.", .{});
if (common.isHelpFlag(arg)) return .{ .command = .{ .help = .app } };
if (std.mem.eql(u8, arg, "--app-id")) {
if (i + 1 >= args.len) return common.usageErrorResult(allocator, .app, "missing value for `--app-id`.", .{});
if (opts.app_id != null) return common.usageErrorResult(allocator, .app, "duplicate `--app-id` for `app`.", .{});
i += 1;
opts.app_id = std.mem.sliceTo(args[i], 0);
continue;
}
if (std.mem.eql(u8, arg, "--codex-cli-path")) {
if (i + 1 >= args.len) return common.usageErrorResult(allocator, .app, "missing value for `--codex-cli-path`.", .{});
if (opts.codex_cli_path != null) return common.usageErrorResult(allocator, .app, "duplicate `--codex-cli-path` for `app`.", .{});
i += 1;
opts.codex_cli_path = std.mem.sliceTo(args[i], 0);
continue;
}
if (std.mem.eql(u8, arg, "--codex-home")) {
if (i + 1 >= args.len) return common.usageErrorResult(allocator, .app, "missing value for `--codex-home`.", .{});
if (opts.codex_home != null) return common.usageErrorResult(allocator, .app, "duplicate `--codex-home` for `app`.", .{});
i += 1;
opts.codex_home = std.mem.sliceTo(args[i], 0);
continue;
}
if (std.mem.eql(u8, arg, "--platform")) {
if (i + 1 >= args.len) return common.usageErrorResult(allocator, .app, "missing value for `--platform`.", .{});
if (opts.platform != null) return common.usageErrorResult(allocator, .app, "duplicate `--platform` for `app`.", .{});
i += 1;
const value = std.mem.sliceTo(args[i], 0);
if (std.mem.eql(u8, value, "win")) {
opts.platform = .win;
} else if (std.mem.eql(u8, value, "wsl")) {
opts.platform = .wsl;
} else if (std.mem.eql(u8, value, "mac")) {
opts.platform = .mac;
} else {
return common.usageErrorResult(allocator, .app, "`--platform` must be `win`, `wsl`, or `mac`.", .{});
}
continue;
}
if (std.mem.eql(u8, arg, "--std")) {
if (opts.inherit_stdio) return common.usageErrorResult(allocator, .app, "duplicate `--std` for `app`.", .{});
opts.inherit_stdio = true;
continue;
}
if (std.mem.startsWith(u8, arg, "-")) {
return common.usageErrorResult(allocator, .app, "unknown flag `{s}` for `app`.", .{arg});
}
return common.usageErrorResult(allocator, .app, "unexpected argument `{s}` for `app`.", .{arg});
}

return .{ .command = .{ .app = opts } };
}
3 changes: 3 additions & 0 deletions src/cli/commands/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const std = @import("std");
const types = @import("../types.zig");
const common = @import("common.zig");

const app = @import("app.zig");
const alias = @import("alias.zig");
const clean = @import("clean.zig");
const config = @import("config.zig");
Expand Down Expand Up @@ -47,6 +48,7 @@ pub fn parseArgs(allocator: std.mem.Allocator, args: []const [:0]const u8) !type
if (std.mem.eql(u8, cmd, "alias")) return alias.parse(allocator, args[2..]);
if (std.mem.eql(u8, cmd, "clean")) return clean.parse(allocator, args[2..]);
if (std.mem.eql(u8, cmd, "config")) return config.parse(allocator, args[2..]);
if (std.mem.eql(u8, cmd, "app")) return app.parse(allocator, args[2..]);

return common.usageErrorResult(allocator, .top_level, "unknown command `{s}`.", .{cmd});
}
Expand Down Expand Up @@ -107,5 +109,6 @@ fn helpTopicForName(name: []const u8) ?types.HelpTopic {
if (std.mem.eql(u8, name, "alias")) return .alias;
if (std.mem.eql(u8, name, "clean")) return .clean;
if (std.mem.eql(u8, name, "config")) return .config;
if (std.mem.eql(u8, name, "app")) return .app;
return null;
}
25 changes: 23 additions & 2 deletions src/cli/help.zig
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ pub fn writeHelp(
try writeCommandDetail(out, use_color, "clean background");
try writeCommandSummary(out, use_color, "config", "Manage configuration");
try writeCommandDetail(out, use_color, "config live --interval <seconds>");
try writeCommandSummary(out, use_color, "app", "Launch Codex App with CLI overrides");

try out.writeAll("\n");
if (use_color) try out.writeAll(style.ansi.cyan);
Expand Down Expand Up @@ -131,6 +132,7 @@ fn commandNameForTopic(topic: HelpTopic) []const u8 {
.alias => "alias",
.clean => "clean",
.config => "config",
.app => "app",
};
}

Expand All @@ -146,19 +148,20 @@ fn commandDescriptionForTopic(topic: HelpTopic) []const u8 {
.alias => "Set or clear an account alias by alias, email, display number, or partial query.",
.clean => "Delete backup and stale files under accounts/.",
.config => "Manage live refresh configuration.",
.app => "Launch Codex App with CLI overrides.",
};
}

fn commandHelpHasExamples(topic: HelpTopic) bool {
return switch (topic) {
.import_auth, .export_auth, .switch_account, .remove_account, .alias, .config => true,
.import_auth, .export_auth, .switch_account, .remove_account, .alias, .config, .app => true,
else => false,
};
}

fn commandHelpHasOptions(topic: HelpTopic) bool {
return switch (topic) {
.list, .login, .import_auth, .export_auth, .switch_account, .remove_account, .alias, .config => true,
.list, .login, .import_auth, .export_auth, .switch_account, .remove_account, .alias, .config, .app => true,
else => false,
};
}
Expand Down Expand Up @@ -221,6 +224,9 @@ fn writeUsageLines(out: *std.Io.Writer, topic: HelpTopic) !void {
.config => {
try out.writeAll(" codex-auth config live --interval <seconds>\n");
},
.app => {
try out.writeAll(" codex-auth app [--app-id <id>] [--codex-cli-path <path>] [--codex-home <path>] [--platform win|wsl|mac]\n");
},
}
}

Expand All @@ -236,6 +242,7 @@ pub fn helpCommandForTopic(topic: HelpTopic) []const u8 {
.alias => "codex-auth alias --help",
.clean => "codex-auth clean --help",
.config => "codex-auth config --help",
.app => "codex-auth app --help",
};
}

Expand Down Expand Up @@ -291,6 +298,16 @@ fn writeOptionLines(out: *std.Io.Writer, topic: HelpTopic) !void {
try out.writeAll(" live --interval <seconds>\n");
try out.writeAll(" Set the live TUI refresh interval from 5 to 3600 seconds.\n");
},
.app => {
try out.writeAll(" --app-id <id> Windows package/AUMID or macOS bundle identifier.\n");
try out.writeAll(" --codex-cli-path <path>\n");
try out.writeAll(" Value injected or persisted as CODEX_CLI_PATH. Defaults to latest managed Loongphy/codext.\n");
try out.writeAll(" --codex-home <path>\n");
try out.writeAll(" Value injected as CODEX_HOME for this launch.\n");
try out.writeAll(" --platform win|wsl|mac\n");
try out.writeAll(" Preselect the app platform. Defaults to the current app setting on Windows and mac on macOS.\n");
try out.writeAll(" --std Resolve the app package executable, then attach stdout/stderr to this terminal.\n");
},
else => {},
}
}
Expand Down Expand Up @@ -362,6 +379,10 @@ fn writeExampleLines(out: *std.Io.Writer, topic: HelpTopic) !void {
.config => {
try out.writeAll(" codex-auth config live --interval 60\n");
},
.app => {
try out.writeAll(" codex-auth app\n");
try out.writeAll(" codex-auth app --platform win\n");
},
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/cli/style.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,22 @@ const std = @import("std");

pub const ansi = struct {
pub const reset = "\x1b[0m";
pub const bold = "\x1b[1m";
pub const dim = "\x1b[2m";
pub const red = "\x1b[31m";
pub const green = "\x1b[32m";
pub const cyan = "\x1b[36m";
};

pub const role = struct {
pub const key = ansi.bold;
pub const secondary = ansi.dim;
pub const status = ansi.cyan;
pub const success = ansi.green;
pub const warning = ansi.cyan;
pub const error_text = ansi.red;
};

pub const StyledWriter = struct {
out: *std.Io.Writer,
color_enabled: bool,
Expand Down
4 changes: 2 additions & 2 deletions src/cli/table_layout.zig
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub const LiveTable = struct {
prefix_width: usize,

pub fn writeHeader(self: *const LiveTable, writer: *style.StyledWriter) !void {
try writer.writeStyle(style.ansi.cyan);
try writer.writeStyle(style.role.status);
try writeRepeat(writer.out, ' ', self.prefix_width);
try self.writeCells(writer.out, &.{
.{ .text = self.columns[0].header },
Expand All @@ -45,7 +45,7 @@ pub const LiveTable = struct {
}

pub fn writeGroupRow(self: *const LiveTable, writer: *style.StyledWriter, account: []const u8) !void {
try writer.writeStyle(style.ansi.dim);
try writer.writeStyle(style.role.secondary);
try writeRepeat(writer.out, ' ', self.prefix_width);
try writeAccountTruncatedPadded(writer.out, account, self.columns[0].width);
try writer.reset();
Expand Down
Loading
Loading