cli

package
v0.0.0-...-e0b90a1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 6, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package cli defines the agentctl command-line surface: the command tree with every subcommand, flag, default and help string, the duration grammar shared by the interval and timeout flags, and the exit codes that the credential-swap outcomes of `claude use` report.

The package parses the command line into typed option structs and dispatches through a Handlers value injected by the caller, so the surface stays reviewable in one place while the command implementations arrive independently.

Help strings contain no backticks, and that is this surface's one deliberate divergence from the published help text: the flag library consumes a backquoted span in a usage string as the flag's placeholder name instead of printing it, so a term the help would quote in backticks is stated plain.

Index

Constants

View Source
const (
	// WatchFloor is the shortest polling interval `watch` accepts, so the
	// usage API is never polled more often than once every 60 seconds.
	WatchFloor = 60 * time.Second

	// WatchDefault is the polling interval `watch` uses when --interval is
	// not given.
	WatchDefault = 300 * time.Second

	// HTTPTimeoutDefault is the per-request HTTP timeout used when
	// --timeout is not given.
	HTTPTimeoutDefault = 10 * time.Second
)
View Source
const (
	// SwapExitRefusedA is refusal A: the lock held for the swap was
	// compromised — its mtime moved underneath the process, or the held
	// mtime could not be read — or acquisition failed for a reason other
	// than the store being unreachable, so nothing may be written.
	SwapExitRefusedA = 10

	// SwapExitRefusedC is refusal C: CLAUDE_CODE_OAUTH_TOKEN holds a
	// non-empty value in the process's own environment, which
	// short-circuits every credential store.
	SwapExitRefusedC = 11

	// SwapExitRefusedD is refusal D: the encoded credential line,
	// including its newline, does not fit the 4032-byte keychain stdin
	// bound.
	SwapExitRefusedD = 12

	// SwapExitRefusedE is refusal E: CLAUDE_SECURESTORAGE_CONFIG_DIR
	// holds a non-empty value, so this shell names a namespace rather
	// than the live store — and the pass was asked for the live one.
	// Reachable only through an undo of a live-target swap, whose target
	// comes from the audit log and can disagree with the environment.
	SwapExitRefusedE = 13

	// SwapExitRefusedF is refusal F: the outgoing credential cannot be
	// adopted safely, so the swap would lose it.
	SwapExitRefusedF = 14

	// SwapExitPrecondition means the inherited
	// CLAUDE_SECURESTORAGE_CONFIG_DIR names no store this registry owns.
	// Not a lettered refusal: it is decided before the swap begins, so
	// the JSON report carries reason "not_owned" and no refusal member.
	SwapExitPrecondition = 15

	// SwapExitBusy means another process holds the store's Claude Code
	// locks and they were not broken.
	SwapExitBusy = 16

	// SwapExitDiscarded means the item changed or became unreadable under
	// the hold, or the remaining hold budget was too short to write, so
	// the refreshed credential was thrown away rather than written over a
	// newer one.
	SwapExitDiscarded = 17

	// SwapExitUnknown means the write timed out and the verifying read
	// did not settle whether it landed. It means "re-run status", not
	// "failed".
	SwapExitUnknown = 18

	// SwapExitWriteFailed means the write ran and failed definitely, for
	// a reason other than a timeout, and the item is demonstrably
	// untouched. The JSON report carries outcome "failed" and no refusal
	// member: an ordinary write failure is not a security signal.
	SwapExitWriteFailed = 19

	// SwapExitCancelled means nobody agreed to the swap: the confirmation
	// was declined, or there was no terminal to ask at and --yes was not
	// given. --json does not imply --yes. The JSON report carries outcome
	// "cancelled" and no refusal member.
	SwapExitCancelled = 20

	// SwapExitNeedsRefresh means the incoming account's credential has
	// expired and its store has migrated into the keychain, so the swap
	// will not refresh it: the refresh could not be saved back, and a
	// spent refresh token would strand the account. The JSON report
	// carries outcome "needs_refresh" and no refusal member.
	SwapExitNeedsRefresh = 21

	// SwapExitAuditRefused means the append-only audit log cannot be
	// written, so a live-store swap is refused rather than performed
	// unrecorded. The JSON report carries outcome "refused" with reason
	// "audit_refused" and no refusal member.
	SwapExitAuditRefused = 22

	// SwapExitLiveUnreachable means the live store could not be resolved:
	// nothing is at the path the environment names, or the symbolic link
	// there dangles. The JSON report carries reason "live_unreachable"
	// and no refusal member.
	SwapExitLiveUnreachable = 23

	// SwapExitLiveItemAbsent means the live keychain item is absent, so
	// the live store has not migrated and its credential is still in a
	// plaintext file no swap may touch. Transient and self-healing; the
	// JSON report carries reason "live_item_absent" and no refusal
	// member.
	SwapExitLiveItemAbsent = 24

	// SwapExitLiveUndoItemChanged means an undo found the live item
	// holding a third account's credential — neither the one the undo
	// would put back nor the one the swap being undone installed. The
	// JSON report carries reason "live_undo_foreign_login" and no refusal
	// member.
	SwapExitLiveUndoItemChanged = 27

	// SwapExitIdentityUnavailable means nothing can say whose credential
	// the live item holds: the profile request did not answer, or the
	// item's access token has expired without an owned audit attribution.
	// Decided before any lock, prompt or write, so nothing is written.
	SwapExitIdentityUnavailable = 29

	// SwapExitRCNotDisconnected means Remote Control could not be
	// disconnected before a live swap: the preflight found an unsupported
	// platform, no TTY to attest at, or a session that stayed connected.
	// Nothing is written.
	SwapExitRCNotDisconnected = 30
)

Exit codes for the refusals and outcomes of a `claude use` credential swap. Each one gets its own message and its own exit code, so a script can act on one without parsing English.

The block starts at 10 because 0, 1 and 2 are taken: 0 is a clean run, 1 is a fatal failure, 2 is a run whose output holds at least one degraded row — and a command-line usage error also exits 2, so a swap code of 2 would be ambiguous between "the item changed under the hold" and "you misspelled a flag". Codes 25, 26 and 28 were assigned once, retired, and are never defined again: a script that learned one of them must not see it come back meaning something else.

Refusal B — a secure-storage backend is active or of unknown kind — has no code here on purpose: it is a warning line and exit 0, because no such backend exists in this build.

Variables

View Source
var SwapExitCodes = [...]struct {
	Name string
	Code int
}{
	{Name: "refused_a", Code: SwapExitRefusedA},
	{Name: "refused_c", Code: SwapExitRefusedC},
	{Name: "refused_d", Code: SwapExitRefusedD},
	{Name: "refused_e", Code: SwapExitRefusedE},
	{Name: "refused_f", Code: SwapExitRefusedF},
	{Name: "precondition", Code: SwapExitPrecondition},
	{Name: "busy", Code: SwapExitBusy},
	{Name: "discarded", Code: SwapExitDiscarded},
	{Name: "unknown", Code: SwapExitUnknown},
	{Name: "write_failed", Code: SwapExitWriteFailed},
	{Name: "cancelled", Code: SwapExitCancelled},
	{Name: "needs_refresh", Code: SwapExitNeedsRefresh},
	{Name: "audit_refused", Code: SwapExitAuditRefused},
	{Name: "live_unreachable", Code: SwapExitLiveUnreachable},
	{Name: "live_item_absent", Code: SwapExitLiveItemAbsent},
	{Name: "live_undo_item_changed", Code: SwapExitLiveUndoItemChanged},
	{Name: "identity_unavailable", Code: SwapExitIdentityUnavailable},
	{Name: "remote_control_not_disconnected", Code: SwapExitRCNotDisconnected},
}

SwapExitCodes pairs every swap exit name with its code, for the exhaustiveness and uniqueness checks; nothing in the shipped binary reads the table as a table.

Functions

func LogLevel

func LogLevel(value string) slog.Level

LogLevel maps an AGENTCTL_LOG value onto a slog level. The accepted values are exactly debug, info, warn and error; anything else, including an unset variable, falls back to warn so diagnostics stay quiet by default instead of failing the run over a logging knob.

func ParseDuration

func ParseDuration(input string) (time.Duration, error)

ParseDuration parses a duration written as `10s`, `5m`, `2h`, or a bare `300` meaning seconds, with outer whitespace trimmed.

The grammar is a single unsigned integer with at most one unit, so fractions (`1.5s`), negatives (`-5s`), sub-second units (`10ms`) and multi-unit forms (`2h30m`) are all rejected. All arithmetic is checked: a count that does not fit, either as a 64-bit unsigned second count or as a time.Duration, is an overflow error rather than a silently wrapped value.

func ParseWatchInterval

func ParseWatchInterval(input string) (time.Duration, error)

ParseWatchInterval parses an interval argument with ParseDuration and additionally enforces the WatchFloor minimum, naming the floor in the error so the caller learns the limit, not merely that the value was refused.

func SignalExitCode

func SignalExitCode(sig os.Signal) int

SignalExitCode returns the process exit status for a run ended by sig: 128 plus the signal's number, the shell convention for a signal death, so TERM exits 143, HUP 129 and INT 130. A value that carries no POSIX signal number maps to 1, the fatal exit, because dying to an unnameable signal is a run that produced nothing useful.

Types

type CLI

type CLI struct {
	// contains filtered or unexported fields
}

CLI is the assembled agentctl command tree.

func New

func New(h Handlers) *CLI

New assembles the command tree, dispatching each subcommand through h.

func (*CLI) Dispatched

func (c *CLI) Dispatched() bool

Dispatched reports whether a subcommand's dispatch began. An error from an execution that never dispatched is a command-line usage error, which exits 2; an error after dispatch follows the application's own exit mapping.

func (*CLI) Root

func (c *CLI) Root() *cobra.Command

Root exposes the underlying root command, so the caller can set the context, arguments and output streams before executing it.

type ClaudeAccountsForgetOptions

type ClaudeAccountsForgetOptions struct {
	Service string
}

ClaudeAccountsForgetOptions carries the parsed arguments of `claude accounts forget`.

type ClaudeAccountsListOptions

type ClaudeAccountsListOptions struct {
	All bool
}

ClaudeAccountsListOptions carries the parsed flags of `claude accounts list`.

type ClaudeAccountsRelocateOptions

type ClaudeAccountsRelocateOptions struct {
	ID  string
	Yes bool
}

ClaudeAccountsRelocateOptions carries the parsed arguments of `claude accounts relocate`.

type ClaudeAccountsRemoveOptions

type ClaudeAccountsRemoveOptions struct {
	ID           string
	DeleteSecret bool
	Yes          bool
}

ClaudeAccountsRemoveOptions carries the parsed arguments of `claude accounts remove`.

type ClaudeAccountsShowOptions

type ClaudeAccountsShowOptions struct {
	ID string
}

ClaudeAccountsShowOptions carries the parsed arguments of `claude accounts show`.

type ClaudeAccountsUnforgetOptions

type ClaudeAccountsUnforgetOptions struct {
	Service string
}

ClaudeAccountsUnforgetOptions carries the parsed arguments of `claude accounts unforget`.

type ClaudeDoctorOptions

type ClaudeDoctorOptions struct {
	RemoveStale string
	Yes         bool
}

ClaudeDoctorOptions carries the parsed flags of `claude doctor`.

type ClaudeEnvOptions

type ClaudeEnvOptions struct {
	ID              string
	ClaudeConfigDir string
	FreshContext    bool
	NoMCP           bool
	Shell           Shell
}

ClaudeEnvOptions carries the parsed arguments of `claude env`.

type ClaudeExecOptions

type ClaudeExecOptions struct {
	ID              string
	ClaudeConfigDir string
	FreshContext    bool
	NoMCP           bool
	Command         []string
}

ClaudeExecOptions carries the parsed arguments of `claude exec`.

type ClaudeImportOptions

type ClaudeImportOptions struct {
	From             ImportSource
	ClaudeConfigDirs []string
	DryRun           bool
}

ClaudeImportOptions carries the parsed flags of `claude import`.

type ClaudeLoginOptions

type ClaudeLoginOptions struct {
	Manual      bool
	Label       string
	NoDuplicate bool
}

ClaudeLoginOptions carries the parsed flags of `claude login`.

type ClaudeStatusOptions

type ClaudeStatusOptions struct {
	JSON       bool
	Raw        bool
	Refresh    bool
	NoCache    bool
	All        bool
	ByIdentity bool
	Accounts   []string
	Timeout    time.Duration
}

ClaudeStatusOptions carries the parsed flags of `claude status`.

type ClaudeUseOptions

type ClaudeUseOptions struct {
	ID                   string
	Live                 bool
	RestartRemoteControl bool
	NewOnly              bool
	Undo                 bool
	Forget               string
	ClaudeConfigDir      string
	FreshContext         bool
	NoMCP                bool
	Yes                  bool
	JSON                 bool
}

ClaudeUseOptions carries the parsed flags of `claude use`. Three shapes share this struct, distinguished by which fields are set: an isolated session for one account, an undo of the most recent live swap, and a forget of an earlier session directory.

type ClaudeWatchOptions

type ClaudeWatchOptions struct {
	Interval time.Duration
}

ClaudeWatchOptions carries the parsed flags of `claude watch`.

type CodexAccountsForgetOptions

type CodexAccountsForgetOptions struct {
	ID string
}

CodexAccountsForgetOptions carries the parsed arguments of `codex accounts forget`.

type CodexAccountsListOptions

type CodexAccountsListOptions struct {
	All bool
}

CodexAccountsListOptions carries the parsed flags of `codex accounts list`.

type CodexAccountsRefreshOptions

type CodexAccountsRefreshOptions struct {
	ID         string
	Resend     bool
	ResetFloor bool
	Yes        bool
}

CodexAccountsRefreshOptions carries the parsed arguments of `codex accounts refresh`.

type CodexAccountsRemoveOptions

type CodexAccountsRemoveOptions struct {
	ID           string
	DeleteSecret bool
	Yes          bool
}

CodexAccountsRemoveOptions carries the parsed arguments of `codex accounts remove`.

type CodexAccountsSetOptions

type CodexAccountsSetOptions struct {
	ID      string
	Refresh RefreshMode
}

CodexAccountsSetOptions carries the parsed arguments of `codex accounts set`.

type CodexAccountsShowOptions

type CodexAccountsShowOptions struct {
	ID string
}

CodexAccountsShowOptions carries the parsed arguments of `codex accounts show`.

type CodexAccountsUnforgetOptions

type CodexAccountsUnforgetOptions struct {
	ID string
}

CodexAccountsUnforgetOptions carries the parsed arguments of `codex accounts unforget`.

type CodexDoctorOptions

type CodexDoctorOptions struct {
	JSON bool
}

CodexDoctorOptions carries the parsed flags of `codex doctor`.

type CodexImportOptions

type CodexImportOptions struct {
	From      CodexImportSource
	CodexHome string
	DryRun    bool
}

CodexImportOptions carries the parsed flags of `codex import`.

type CodexImportSource

type CodexImportSource string

CodexImportSource names where `codex import` reads accounts from.

const CodexImportSourceCodexHome CodexImportSource = "codex-home"

CodexImportSourceCodexHome reads a Codex home directory holding a credential file.

type CodexLoginOptions

type CodexLoginOptions struct {
	Label     string
	NoRefresh bool
}

CodexLoginOptions carries the parsed flags of `codex login`.

type CodexStatusOptions

type CodexStatusOptions struct {
	JSON     bool
	Raw      bool
	Refresh  bool
	NoCache  bool
	All      bool
	Accounts []string
	Timeout  time.Duration
}

CodexStatusOptions carries the parsed flags of `codex status`.

type CodexWatchOptions

type CodexWatchOptions struct {
	Interval time.Duration
}

CodexWatchOptions carries the parsed flags of `codex watch`.

type DurationParseError

type DurationParseError struct {
	// Kind classifies the failure.
	Kind DurationParseKind

	// Input is the trimmed argument, for context.
	Input string

	// Unit is the unrecognised suffix when Kind is DurationUnknownUnit.
	Unit string
}

DurationParseError reports why a duration argument could not be parsed.

func (*DurationParseError) Error

func (e *DurationParseError) Error() string

Error renders the failure with the input it refused, so the message can stand alone on stderr.

type DurationParseKind

type DurationParseKind int

DurationParseKind classifies why a duration argument was rejected.

const (
	// DurationEmpty means the argument was empty or entirely whitespace.
	DurationEmpty DurationParseKind = iota

	// DurationNoDigits means the argument did not start with a digit.
	DurationNoDigits

	// DurationOverflow means the numeric part did not fit, or the unit
	// conversion did not fit.
	DurationOverflow

	// DurationUnknownUnit means the unit suffix is not one the parser
	// knows.
	DurationUnknownUnit
)

type Globals

type Globals struct {
	// ConfigDir is the configuration directory named by --config-dir, or
	// by AGENTCTL_CONFIG_DIR when the flag is not given. Empty when
	// neither is set, which lets the configuration package apply its own
	// resolution order instead of baking a default in here.
	ConfigDir string
}

Globals carries the options every subcommand shares.

type Handlers

type Handlers struct {
	ClaudeStatus           func(ctx context.Context, globals Globals, opts ClaudeStatusOptions) error
	ClaudeWatch            func(ctx context.Context, globals Globals, opts ClaudeWatchOptions) error
	ClaudeLogin            func(ctx context.Context, globals Globals, opts ClaudeLoginOptions) error
	ClaudeAccountsList     func(ctx context.Context, globals Globals, opts ClaudeAccountsListOptions) error
	ClaudeAccountsShow     func(ctx context.Context, globals Globals, opts ClaudeAccountsShowOptions) error
	ClaudeAccountsRemove   func(ctx context.Context, globals Globals, opts ClaudeAccountsRemoveOptions) error
	ClaudeAccountsRelocate func(ctx context.Context, globals Globals, opts ClaudeAccountsRelocateOptions) error
	ClaudeAccountsForget   func(ctx context.Context, globals Globals, opts ClaudeAccountsForgetOptions) error
	ClaudeAccountsUnforget func(ctx context.Context, globals Globals, opts ClaudeAccountsUnforgetOptions) error
	ClaudeImport           func(ctx context.Context, globals Globals, opts ClaudeImportOptions) error
	ClaudeDoctor           func(ctx context.Context, globals Globals, opts ClaudeDoctorOptions) error
	ClaudeUse              func(ctx context.Context, globals Globals, opts ClaudeUseOptions) error
	ClaudeExec             func(ctx context.Context, globals Globals, opts ClaudeExecOptions) error
	ClaudeEnv              func(ctx context.Context, globals Globals, opts ClaudeEnvOptions) error

	CodexStatus           func(ctx context.Context, globals Globals, opts CodexStatusOptions) error
	CodexWatch            func(ctx context.Context, globals Globals, opts CodexWatchOptions) error
	CodexLogin            func(ctx context.Context, globals Globals, opts CodexLoginOptions) error
	CodexAccountsList     func(ctx context.Context, globals Globals, opts CodexAccountsListOptions) error
	CodexAccountsShow     func(ctx context.Context, globals Globals, opts CodexAccountsShowOptions) error
	CodexAccountsRemove   func(ctx context.Context, globals Globals, opts CodexAccountsRemoveOptions) error
	CodexAccountsForget   func(ctx context.Context, globals Globals, opts CodexAccountsForgetOptions) error
	CodexAccountsUnforget func(ctx context.Context, globals Globals, opts CodexAccountsUnforgetOptions) error
	CodexAccountsSet      func(ctx context.Context, globals Globals, opts CodexAccountsSetOptions) error
	CodexAccountsRefresh  func(ctx context.Context, globals Globals, opts CodexAccountsRefreshOptions) error
	CodexImport           func(ctx context.Context, globals Globals, opts CodexImportOptions) error
	CodexDoctor           func(ctx context.Context, globals Globals, opts CodexDoctorOptions) error
}

Handlers holds one function per subcommand. The command tree parses the command line into the typed options struct and calls the matching field; a nil field makes the subcommand fail with a NotImplementedError.

type ImportSource

type ImportSource string

ImportSource names where `claude import` reads accounts from. One source, still spelled as a value rather than as a bare flag: a command line that already says which source it read does not change shape when a second one arrives.

const ImportSourceKeychain ImportSource = "keychain"

ImportSourceKeychain reads Claude Code credential services discovered in the login keychain.

type NotImplementedError

type NotImplementedError struct {
	// Command is the space-joined command path, such as "claude status".
	Command string
}

NotImplementedError reports a subcommand that parsed but has no handler wired in. A nil Handlers field yields this error on purpose: the command surface is reviewed as one piece while the implementations arrive independently, and an unwired command must fail loudly with exit status 1 rather than exit 0 having done nothing.

func (*NotImplementedError) Error

func (e *NotImplementedError) Error() string

Error names the command so the caller's "agentctl: " prefix completes the sentence.

type RefreshMode

type RefreshMode string

RefreshMode says whether agentctl refreshes an owned Codex account on its own. It is the command line's spelling of the registry's refresh policy, kept separate so the stored vocabulary and the flag's accepted values can change independently.

const (
	// RefreshModeAuto refreshes when the access token is expired or
	// rejected.
	RefreshModeAuto RefreshMode = "auto"

	// RefreshModeNever never sends a refresh token.
	RefreshModeNever RefreshMode = "never"
)

type Shell

type Shell string

Shell names which login shell `claude env` prints for.

const (
	// ShellZsh prints export, unset and alias lines.
	ShellZsh Shell = "zsh"

	// ShellBash is identical to ShellZsh: both read the same syntax.
	ShellBash Shell = "bash"

	// ShellFish prints set -gx and set -e lines and a function.
	ShellFish Shell = "fish"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL