Skip to content
Merged
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
1 change: 1 addition & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ pub fn build(b: *std.Build) void {
"tests/tui_session_test.zig",
"tests/tui_table_test.zig",
"tests/workflows_core_test.zig",
"tests/workflows_daemon_test.zig",
"tests/workflows_live_test.zig",
};

Expand Down
26 changes: 26 additions & 0 deletions docs/commands/switch.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ codex-auth switch [--api|--skip-api]
codex-auth switch --live [--api|--skip-api]
codex-auth switch <query>
codex-auth switch <query> --json
codex-auth switch [<query>|-] --restart-daemon
```

## Previous Switch
Expand Down Expand Up @@ -65,3 +66,28 @@ When switching succeeds:
5. The success message uses the same identity label as singleton rows, for example `Switched to me(test@example.com)`.

The previous-account pointer is internal CLI state and is not included in JSON responses.

## Applying a Switch to a Running Daemon

Modern Codex clients can share a background app-server daemon that caches authentication. Updating `auth.json` alone may leave that daemon using the previous account, including when another terminal client connects.

One-shot switches detect a responsive daemon and print a restart hint on stderr. Restarting is opt-in:

```shell
codex-auth switch personal --restart-daemon
codex-auth switch - --restart-daemon
codex-auth switch personal --json --restart-daemon
```

`--restart-daemon` applies the selected credentials to the running daemon after account activation and registry persistence succeed. Codex daemons reload auth in place without interrupting attached sessions; upstream daemons without reload support are restarted instead, which can interrupt all attached sessions. It also covers stale daemon authentication and retries after a failed attempt. Reconnect, verify the account, and resume work as needed.

- The selected `CODEX_HOME` is passed to both lifecycle commands. The home’s managed executable is preferred, with supported legacy layouts and a PATH fallback.
- Responsiveness is checked with `codex app-server daemon version`. A missing control endpoint is treated as best-effort absence: no daemon is started. A stale endpoint or other probe error leaves the state unconfirmed; an explicit request then fails without invoking restart.
- The responsiveness check occurs immediately before restart. If the daemon disappears afterward, Codex’s supported restart command can start one; this race is accepted.
- The restart helper has a fixed 120-second timeout. Failure or timeout preserves the selected files, reports an unconfirmed daemon account state, and returns exit code 1. Only the helper process is terminated and reaped on timeout; daemon shutdown/startup may already have begun. No automatic retry or credential rollback occurs.
- Diagnostics and manual status/recovery commands are printed on stderr, with the selected home and executable. A successful version response proves responsiveness, not account identity.
- In JSON mode, stdout remains the existing switch-selection document. It can accompany exit code 1 when the subsequent requested restart fails.
- `--restart-daemon` is rejected with `--live`. Live switching, login, import, and remove retain their existing activation behavior and may also require manual daemon coordination.
- Older installations without a daemon endpoint remain a successful no-op. Platforms outside Linux, macOS, and Windows receive an honest unsupported-coordination hint for explicit requests.

No daemon-restart preference is persisted and the registry schema is unchanged.
12 changes: 12 additions & 0 deletions src/cli/commands/switch.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ pub fn parse(allocator: std.mem.Allocator, args: []const [:0]const u8) !types.Pa
var opts: types.SwitchOptions = .{};
for (args) |raw_arg| {
const arg = std.mem.sliceTo(raw_arg, 0);
if (std.mem.eql(u8, arg, "--restart-daemon")) {
if (opts.restart_daemon) {
freeTarget(allocator, opts.target);
return common.usageErrorResultWithJson(allocator, .switch_account, json_requested, "duplicate `--restart-daemon` for `switch`.", .{});
}
opts.restart_daemon = true;
continue;
}
if (std.mem.eql(u8, arg, "--live")) {
if (opts.live) {
freeTarget(allocator, opts.target);
Expand Down Expand Up @@ -68,6 +76,10 @@ pub fn parse(allocator: std.mem.Allocator, args: []const [:0]const u8) !types.Pa
else
.{ .query = try allocator.dupe(u8, arg) };
}
if (opts.live and opts.restart_daemon) {
freeTarget(allocator, opts.target);
return common.usageErrorResultWithJson(allocator, .switch_account, json_requested, "`--restart-daemon` cannot be combined with `--live`.", .{});
}
if (opts.live and opts.json) {
freeTarget(allocator, opts.target);
return common.usageErrorResultWithJson(allocator, .switch_account, true, "`--live` cannot be combined with `--json`.", .{});
Expand Down
6 changes: 6 additions & 0 deletions src/cli/help.zig
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub fn writeHelp(
try writeCommandDetail(out, use_color, "switch [--live] [--api|--skip-api]");
try writeCommandDetail(out, use_color, "switch <alias|email|display-number|query>");
try writeCommandDetail(out, use_color, "switch <query> --json");
try writeCommandDetail(out, use_color, "switch <query> --restart-daemon");
try writeCommandSummary(out, use_color, "remove", "Remove one or more accounts");
try writeCommandDetail(out, use_color, "remove [--live] [--api|--skip-api]");
try writeCommandDetail(out, use_color, "remove <alias|email|display-number|query>...");
Expand Down Expand Up @@ -214,6 +215,7 @@ fn writeUsageLines(out: *std.Io.Writer, topic: HelpTopic) !void {
try out.writeAll(" codex-auth switch [--live] [--api|--skip-api]\n");
try out.writeAll(" codex-auth switch <alias|email|display-number|query>\n");
try out.writeAll(" codex-auth switch <query> --json\n");
try out.writeAll(" codex-auth switch [<query>|-] --restart-daemon\n");
},
.remove_account => {
try out.writeAll(" codex-auth remove [--live] [--api|--skip-api]\n");
Expand Down Expand Up @@ -287,6 +289,9 @@ fn writeOptionLines(out: *std.Io.Writer, topic: HelpTopic) !void {
try out.writeAll(" --api Load usage and account data from APIs.\n");
try out.writeAll(" --skip-api Load usage and account data from local data only (may be inaccurate).\n");
try out.writeAll(" --json Emit one machine-readable JSON document for non-interactive switch.\n");
try out.writeAll(" --restart-daemon\n");
try out.writeAll(" Restart a responsive daemon after switching, including unchanged-file recovery.\n");
try out.writeAll(" Can interrupt attached sessions; cannot be combined with --live.\n");
try out.writeAll(" <alias|email|display-number|query>\n");
try out.writeAll(" Switch directly when the target resolves to one account.\n");
try out.writeAll(" - Switch to the previous active account.\n");
Expand Down Expand Up @@ -365,6 +370,7 @@ fn writeExampleLines(out: *std.Io.Writer, topic: HelpTopic) !void {
try out.writeAll(" codex-auth switch --api\n");
try out.writeAll(" codex-auth switch --skip-api\n");
try out.writeAll(" codex-auth switch personal\n");
try out.writeAll(" codex-auth switch personal --restart-daemon\n");
try out.writeAll(" codex-auth switch john@example.com\n");
try out.writeAll(" codex-auth switch 02\n");
try out.writeAll(" codex-auth switch work\n");
Expand Down
65 changes: 65 additions & 0 deletions src/cli/output.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const std = @import("std");
const builtin = @import("builtin");
const daemon = @import("../workflows/codex_daemon.zig");
const display_rows = @import("../tui/display.zig");
const registry = @import("../registry/root.zig");
const io_util = @import("../core/io_util.zig");
Expand All @@ -10,6 +12,69 @@ const io = @import("io.zig");

const UsageError = types.UsageError;

pub fn printCodexDaemonDiagnostic(home: []const u8, executable: []const u8, outcome: daemon.Outcome, requested: bool) !void {
var stderr: io_util.Stderr = undefined;
stderr.init();
try writeCodexDaemonDiagnosticTo(stderr.out(), home, executable, outcome, requested);
try stderr.out().flush();
}

pub fn writeCodexDaemonDiagnosticTo(out: *std.Io.Writer, home: []const u8, executable: []const u8, outcome: daemon.Outcome, requested: bool) !void {
switch (outcome) {
.not_running => {
if (requested) try out.writeAll("Account files switched successfully; no running Codex daemon was found, so no restart was attempted.\n");
return;
},
.restarted => {
try out.writeAll("Account files switched successfully; Codex daemon restart completed. Reconnect and verify the selected account.\n");
return;
},
.reloaded => {
try out.writeAll("Account files switched successfully; the running daemon reloaded the new credentials without a restart.\n");
return;
},
.deferred => {
try out.writeAll("Account files switched successfully; the daemon deferred the reload until the running turn finishes. Rerun `switch --restart-daemon` afterwards.\n");
return;
},
.running => try out.writeAll("A running Codex daemon may still use cached credentials. Use `switch --restart-daemon` to apply the selection or repair stale authentication. Restarting can interrupt attached sessions.\n"),
.unknown => try out.writeAll(if (requested)
"Account files switched successfully; daemon responsiveness could not be confirmed. The requested restart was not attempted.\n"
else
"Account files switched successfully; daemon responsiveness could not be confirmed. Its account state is unconfirmed.\n"),
.restart_failed => try out.writeAll("Account files switched successfully; requested daemon restart failed. The daemon may be stopping or starting; its account state is unconfirmed.\n"),
.timed_out => try out.writeAll("Account files switched successfully; daemon restart did not complete within 120 seconds. The daemon may still be stopping or starting; its account state is unconfirmed.\n"),
.unsupported => {
if (!requested) return;
try out.writeAll("Account files switched successfully; automatic daemon coordination is unsupported on this platform. Check which lifecycle commands your Codex installation supports.\n");
},
}
try out.writeAll("Check daemon status before retrying (a version response does not verify account identity):\n ");
try writeDaemonCommandTo(out, home, executable, "version");
try out.writeAll("\nIf a restart is needed:\n ");
try writeDaemonCommandTo(out, home, executable, "restart");
try out.writeAll("\n");
}

fn writeDaemonCommandTo(out: *std.Io.Writer, home: []const u8, executable: []const u8, command: []const u8) !void {
try out.writeAll(if (builtin.os.tag == .windows) "$env:CODEX_HOME = " else "CODEX_HOME=");
try writeShellArgumentTo(out, home);
try out.writeAll(if (builtin.os.tag == .windows) "; & " else " ");
try writeShellArgumentTo(out, executable);
try out.print(" app-server daemon {s}", .{command});
}

fn writeShellArgumentTo(out: *std.Io.Writer, arg: []const u8) !void {
try out.writeAll("'");
var parts = std.mem.splitScalar(u8, arg, '\'');
try out.writeAll(parts.first());
while (parts.next()) |part| {
try out.writeAll(if (builtin.os.tag == .windows) "''" else "'\\''");
try out.writeAll(part);
}
try out.writeAll("'");
}

fn importReportStyle(outcome: registry.ImportOutcome) []const u8 {
return switch (outcome) {
.imported, .updated => style.ansi.green,
Expand Down
1 change: 1 addition & 0 deletions src/cli/types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub const SwitchOptions = struct {
live: bool = false,
api_mode: ApiMode = .default,
json: bool = false,
restart_daemon: bool = false,
};
pub const RemoveOptions = struct {
selectors: [][]const u8,
Expand Down
Loading