Documentation
¶
Overview ¶
Package config is the layered configuration store for jira-cli: a koanf v2 loader over defaults, a TOML file, and a JIRA_* environment overlay (last wins), plus the profile model and the credential backends profiles point at.
Profiles are metadata only — name, base URL, email, and which secret backend holds the token. Credentials never live here: they are kept in the OS keyring, 1Password, or a JIRA_TOKEN_* environment variable, and must never reach the TOML file, a log, or an envelope. The store types (KeyringStore, OnePasswordStore, EnvCredentialStore) read and write those secrets; the loader only records where each profile's secret is found.
Decode is strict: an unknown key is rejected rather than dropped, so a rename without a migration path fails loudly instead of silently discarding a setting on the next Save. Load never writes; LoadOrInit bootstraps a missing file and deliberately skips the env overlay, so a transient env value is never persisted. Every write goes through an atomic, symlink-following temp-file+rename.
Index ¶
- Constants
- Variables
- func AutoTheme(out *os.File) *clibtheme.Theme
- func CredentialIdentitiesDiffer(a, b SecretRef) bool
- func DefaultPath() string
- func DefaultQueriesPath() string
- func DefaultTheme() *clibtheme.Theme
- func GatewayBaseURL(cloudID string) string
- func IsAutoTheme(name string) bool
- func KeyChoices(key string) []string
- func KeyringAvailable() bool
- func LoadQueries(dir string) (map[string]Query, error)
- func NormalizeBaseURL(raw string) string
- func OnePasswordSupported() bool
- func ReadSecret(raw string) (string, error)
- func RedactSecret(secret string) string
- func ResolveCredential(ctx context.Context, store CredentialStore, ref SecretRef) (string, error)
- func RevokeProfileCredential(ctx context.Context, store CredentialStore, ref SecretRef) (removed bool, note string, err error)
- func SanitizeCredentialError(err error) string
- func Save(path string, cfg *Config) error
- func StoreCredentialTransactionally(ctx context.Context, store CredentialStore, ref SecretRef, secret string, ...) error
- func StoredCredential(ctx context.Context, store CredentialStore, ref SecretRef) (string, error)
- func ThemeForName(name string) *clibtheme.Theme
- func ValidateBaseURL(raw string) error
- func ValidateCloudID(id string) error
- func ValidateProfileName(name string) error
- func ValidateThemeName(name string) error
- type AuthStatus
- type AuthType
- type CleanupFailure
- type CleanupNote
- type Config
- type CredentialError
- type CredentialMigration
- type CredentialStore
- type EnvCredentialStore
- type ErrorCode
- type ErrorContext
- type ErrorType
- type FileCredentialStore
- type KeyDesc
- type KeyringStore
- type MemoryCredentialStore
- type MigrationReport
- type OnePasswordStore
- type Option
- type Profile
- type ProfileIncompleteError
- type ProfileNotDefinedError
- type Query
- type SecretBackend
- type SecretRef
- type TUI
- type TUISection
- type Theme
- type UpstreamProvider
Constants ¶
const ( // ErrorTypeAuth marks a credential or backend failure: the credential is // missing, the backend is unreachable, or a provider rejected the request. ErrorTypeAuth = errtax.TypeAuth // ErrorTypeValidation marks a caller-input failure: a conflicting flag // combination, an empty credential, or an unsafe profile name. ErrorTypeValidation = errtax.TypeValidation )
const ( // ErrorCodeCredentialSourceConflict: more than one credential input // source was supplied for one operation. ErrorCodeCredentialSourceConflict = errtax.CodeCredentialSourceConflict // ErrorCodeCredentialMissing: no credential is stored or configured for // the profile. ErrorCodeCredentialMissing = errtax.CodeCredentialMissing // ErrorCodeCredentialEmpty: a credential was supplied but is empty. ErrorCodeCredentialEmpty = errtax.CodeCredentialEmpty // be reached or failed to service the request. ErrorCodeCredentialBackendUnavailable = errtax.CodeCredentialBackendUnavailable // ErrorCodeCredentialMigrationFailed: a backend-switch migration could // not complete; no metadata was changed. ErrorCodeCredentialMigrationFailed = errtax.CodeCredentialMigrationFailed // ErrorCodeCredentialCleanupFailed: a migration succeeded but the old // secret could not be removed from the source backend. ErrorCodeCredentialCleanupFailed = errtax.CodeCredentialCleanupFailed // ErrorCodeCredentialNamespaceCollision: a profile name cannot be encoded // into a credential namespace that round-trips uniquely. ErrorCodeCredentialNamespaceCollision = errtax.CodeCredentialNamespaceCollision // ErrorCodeOnePasswordItemAmbiguous: more than one 1Password item matched // the configured title. ErrorCodeOnePasswordItemAmbiguous = errtax.CodeOnePasswordItemAmbiguous // reached or rejected the request. ErrorCodeOnePasswordUnavailable = errtax.CodeOnePasswordUnavailable // ErrorCodeCredentialRejected: Jira rejected the email/token pair at // login verification. ErrorCodeCredentialRejected = errtax.CodeCredentialRejected // verify the credential at login. ErrorCodeCredentialVerifyUnavailable = errtax.CodeCredentialVerifyUnavailable // ErrorCodeOnePasswordUnsupportedBuild: the binary was built without // CGO, so 1Password support is compiled out. ErrorCodeOnePasswordUnsupportedBuild = errtax.CodeOnePasswordUnsupportedBuild // all (no Secret Service, unsupported platform). ErrorCodeKeyringUnavailable = errtax.CodeKeyringUnavailable // ErrorCodeEnvCredentialUnset: a profile on the env backend has no // JIRA_TOKEN_* variable set. ErrorCodeEnvCredentialUnset = errtax.CodeEnvCredentialUnset // ErrorCodeEnvBackendReadOnly: a write or delete was attempted against // the env backend, which only ever reads its JIRA_TOKEN_* variable. ErrorCodeEnvBackendReadOnly = errtax.CodeEnvBackendReadOnly )
const ( // DefaultRefreshIntervalSeconds is the per-profile polling default (watch // commands and the like). The dashboard's cadence is the separate // DefaultTUIRefreshSeconds below. DefaultRefreshIntervalSeconds = 30 // DefaultTUIRefreshSeconds is the dashboard's auto-refresh cadence; the // TUI refetches every section this often unless the user is mid-action. DefaultTUIRefreshSeconds = 60 // DefaultTimeoutSeconds is the per-request HTTP timeout applied to a // profile that does not set its own. DefaultTimeoutSeconds = 30 // DefaultWorkdaySeconds is the working day used to convert worklog // durations (8h), so "1d" logged means one workday, not 24 hours. DefaultWorkdaySeconds = 28800 )
const AtlassianGatewayBaseURL = "https://api.atlassian.com"
AtlassianGatewayBaseURL is the Atlassian API gateway root. Scoped (granular) API tokens are rejected by the site host and only work when REST calls are addressed through this gateway with the site's cloudId in the path. Classic API tokens hit the site host directly and never touch the gateway.
const EnvThemeName = "JIRA_THEME"
EnvThemeName overrides the default theme process-wide. It matches the prefix set via clibtheme.SetEnvPrefix("JIRA") at help-renderer setup, so the same variable drives help, plain output, and the TUI.
const TestCredentialStoreDirEnv = "JIRA_TEST_CREDENTIAL_STORE_DIR"
TestCredentialStoreDirEnv enables a subprocess-visible credential store for tests. Production code never sets it; when present, command construction can use FileCredentialStore instead of probing the developer's OS keyring.
const ThemeNameAuto = "auto"
ThemeNameAuto selects clib's light or dark theme by detecting the terminal background, so hash-based entity colors stay readable on either surface. It is opt-in, never the default: background detection misfires silently under tmux, SSH, and container exec — all common developer environments — and a wrong guess is actively worse than the fixed palette, so the default stays dark until detection has a release cycle of field reports. A user whose terminal is misdetected pins "light" or "dark" instead.
Variables ¶
var ErrCredentialEmpty = errors.New("credential is empty")
ErrCredentialEmpty reports that an explicitly supplied credential was empty after delimiter trimming. An empty credential is rejected rather than stored, so a blank line never silently becomes a saved secret. ReadSecret returns a CredentialError wrapping this sentinel, so callers can recover either the typed error or this value.
var ErrCredentialNotFound = errors.New("credential not found")
ErrCredentialNotFound is the sentinel every backend returns for a missing credential. Callers test it with errors.Is; logout and transactional prior reads treat it as benign so an absent credential is not an error.
var ErrProfileNotDefined = errors.New("profile is not defined")
ErrProfileNotDefined is returned when a requested profile, or the configured default profile, names no profile defined in the config. Callers test for it with errors.Is.
var ThemeNameValues = append([]string{ThemeNameAuto}, clibtheme.Names()...)
ThemeNameValues advertises the selectable theme names: the opt-in "auto" plus clib's built-in presets, sourced from clibtheme.Names so the list always matches the dependency. Names resolve straight through clib — there is no jira-cli-specific aliasing.
Functions ¶
func AutoTheme ¶ added in v0.4.0
AutoTheme resolves the opt-in "auto" theme: clib's light or dark theme chosen by the terminal background of out (the stream styled output goes to). The JIRA_THEME override still wins, matching every other resolution path. When there is nothing to detect — out is not a terminal, e.g. --color=always piped into a pager — it falls back to dark: dark matches the convention of every major ANSI CLI (git, ripgrep, bat) and the dark-terminal skew of users who deliberately force color through a pipe.
func CredentialIdentitiesDiffer ¶
CredentialIdentitiesDiffer reports whether two SecretRefs address different credential storage. It compares the secret backend, the keyring key (site host + profile) and, for the 1Password backend, the account, vault, and item. When an auth login re-points a profile — a new site, a backend switch, a different 1Password account, vault, or item — the re-derived identity differs and the credential under the old identity must be revoked.
func DefaultPath ¶
func DefaultPath() string
DefaultPath is the OS-native config file location (~/.config/jira-cli/config.toml on Unix, %AppData%\jira-cli\config.toml on Windows), resolved through the same config root as DefaultQueriesPath.
func DefaultQueriesPath ¶ added in v0.6.2
func DefaultQueriesPath() string
DefaultQueriesPath returns the default saved-queries directory, resolved under the same OS-native config root as DefaultPath so the two stay consistent across platforms (~/.config on Unix, %AppData% on Windows).
func DefaultTheme ¶ added in v0.4.0
DefaultTheme resolves the process default theme: the JIRA_THEME override when set and valid, otherwise the dark built-in. This restores clib v0.4.15's Default(), which the v0.5 upgrade removed, keeping the documented JIRA_THEME override working for plain output, help, and the TUI.
func GatewayBaseURL ¶ added in v0.6.2
GatewayBaseURL builds the REST base URL a scoped-token profile must target: https://api.atlassian.com/ex/jira/<cloudID>/ . The trailing slash is load bearing — the Jira client resolves its relative paths (rest/api/3/...) against this base, and a missing slash would drop the /ex/jira/<cloudID> segment.
func IsAutoTheme ¶ added in v0.4.0
IsAutoTheme reports whether name selects the background-detecting theme.
func KeyChoices ¶
KeyChoices returns the enum choices for a fully-qualified key, or nil if the key is freeform or unknown.
func KeyringAvailable ¶ added in v0.10.7
func KeyringAvailable() bool
KeyringAvailable reports whether the OS keyring can service requests at all. It probes with a read of a never-written entry: a clean not-found means the keyring answered (available); any other failure — no Secret Service on the D-Bus (WSL, headless Linux), an unsupported platform — means it cannot store anything. Interactive login uses it to offer only backends that can actually work, and store paths use it to fail before a token is prompted for or verified.
When the test file-store override is active (TestCredentialStoreDirEnv), keyring-backed profiles never touch the real keyring, so the backend counts as available.
func LoadQueries ¶
LoadQueries reads every .jql file in dir into a map keyed by file basename (sans extension). Each file is JQL with optional YAML (---) or TOML (+++) frontmatter. A tilde-prefixed dir is expanded. A malformed file's path is named in the error; a missing directory returns the os.ReadDir error.
func NormalizeBaseURL ¶
NormalizeBaseURL expands shorthand Jira URLs into their canonical form.
- "" → "" (callers handle the empty case explicitly)
- "company" → "https://company.atlassian.net"
- "company.atlassian.net" → "https://company.atlassian.net"
- "company.example.com" → "https://company.example.com" (Server/DC)
- "http://localhost:8080" → unchanged (loopback)
- "https://x.example/" → "https://x.example" (trailing slash dropped)
The function is intentionally permissive — it only adds the scheme and the `.atlassian.net` suffix when both are missing. Callers must still run ValidateBaseURL on the result.
func OnePasswordSupported ¶ added in v0.10.7
func OnePasswordSupported() bool
OnePasswordSupported reports whether this binary was built with the 1Password SDK compiled in. This CGO (or Windows) build has it; the no-CGO variant in onepassword_nocgo.go reports false so callers can fail early and keep the backend out of interactive choices.
func ReadSecret ¶
ReadSecret is the canonical reader for a credential supplied to the CLI (stdin, an environment variable, or a 1Password CLI field). It strips only the trailing CLI record delimiter — a single trailing CR and/or LF — and preserves every other byte, including interior and leading whitespace, so a token whose own bytes are meaningful is never corrupted.
An input that is empty, or empty once the delimiter is removed, is rejected with a CredentialError carrying ErrorCodeCredentialEmpty: an explicitly blank credential is an error, not a stored value.
func RedactSecret ¶
RedactSecret returns a display-safe preview of a credential: the empty string for none, "****" for a value too short to partially reveal, otherwise "****" plus the last four bytes. It is the only sanctioned way to surface any part of a token — never print the raw value.
func ResolveCredential ¶
ResolveCredential returns the effective credential for a profile: a JIRA_TOKEN_* environment override when set, otherwise the value stored in the backend. The env override is keyed by SecretRef.EnvTokenKey, which encodes the profile name bijectively so an override for one profile cannot bleed into a sibling whose name differs only by a hyphen/underscore swap.
Migration must NOT use this function: it would copy an env override rather than the configured secret. Use StoredCredential for a store-only read.
func RevokeProfileCredential ¶
func RevokeProfileCredential(ctx context.Context, store CredentialStore, ref SecretRef) (removed bool, note string, err error)
RevokeProfileCredential removes the credential for a profile from its backend. The keyring entry is keyed by site + profile and is jira-cli's own, so it is deleted outright. For the 1Password backend the store removes only jira-cli's managed credential field and always keeps the item itself — see OnePasswordStore.Delete.
It reports whether a credential was removed and, for the 1Password backend, an informational note that the item was kept. An absent credential is not an error: revocation is idempotent.
func SanitizeCredentialError ¶
SanitizeCredentialError returns a human-safe, secret-free description of a credential failure. It branches on typed CredentialError values rather than matching error message substrings, so a backend rewording its errors cannot change the classification or leak raw text.
func Save ¶
Save atomically writes cfg to path using a temp-file + rename idiom, so concurrent or interrupted writes never leave a half-truncated config file. The temp file is created in the same directory as path so that os.Rename is guaranteed to be atomic on the same filesystem.
func StoreCredentialTransactionally ¶
func StoreCredentialTransactionally(ctx context.Context, store CredentialStore, ref SecretRef, secret string, saveConfig func() error) error
StoreCredentialTransactionally writes a credential to a backend around a metadata save, so a save failure never leaves an orphaned secret. It mirrors the migration transaction: the destination is snapshotted, the new credential is written (staged), then saveConfig runs. On a save failure the write is rolled back — a pre-existing value is restored, or a freshly written one is removed — and the save error is returned. A rollback that itself fails is surfaced alongside the save error, never swallowed.
func StoredCredential ¶
StoredCredential reads the credential straight from the backend, ignoring any JIRA_TOKEN_* environment override. Migration uses this so a backend switch copies the configured secret, never a transient env value.
func ThemeForName ¶ added in v0.4.0
ThemeForName resolves an explicit theme name (e.g. config theme.name), falling back to DefaultTheme when the name is empty or unrecognized.
func ValidateBaseURL ¶
ValidateBaseURL accepts an empty URL (an unconfigured profile), any https URL, and http only for loopback hosts (localhost/127.0.0.1/::1, for tests). It rejects a plaintext http URL to a real host, since a token would travel unencrypted. Run NormalizeBaseURL first to expand shorthand.
func ValidateCloudID ¶ added in v0.6.2
ValidateCloudID checks that a cloud_id is a plausible, URL-path-safe Atlassian cloudId (a UUID in practice). It is deliberately lenient about the exact shape — Atlassian has never promised the UUID format — but rejects blanks and anything that could break the gateway path (spaces, slashes, control bytes).
func ValidateProfileName ¶ added in v0.3.0
ValidateProfileName reports whether a profile name can be encoded into a credential key that round-trips uniquely. It is the input-time guard for the same namespace safety CredentialIdentity enforces at store time, so a bad name (uppercase, whitespace, a slash, anything outside lowercase letters, digits, hyphen, and underscore) is rejected when it is typed or passed — not late, after a token has already been sent to Jira. The returned error is a validation-class CredentialError.
func ValidateThemeName ¶
ValidateThemeName reports whether name is a theme clib accepts (or "auto"). It is the write-time gate for `config theme --name`; config load deliberately does not call it, so a theme name that clib later renames degrades to the dark fallback at render rather than blocking every command.
Types ¶
type AuthStatus ¶
type AuthStatus struct {
Profile string `json:"profile"`
Valid bool `json:"valid"`
Source string `json:"source"`
Redacted string `json:"redacted"`
Error string `json:"error,omitempty"`
}
AuthStatus is the resolved credential state for a profile: whether a usable credential was found, which source supplied it (the backend, or "env" for a JIRA_TOKEN_* override), a redacted last-four preview, and any sanitized error. It is the shape `auth status` serializes and never carries the secret.
func CredentialStatus ¶
func CredentialStatus(ctx context.Context, store CredentialStore, ref SecretRef) AuthStatus
CredentialStatus resolves a profile's credential and reports it as an AuthStatus: valid with a redacted preview when found, or a sanitized, secret-free error when not. Source is reported as "env" when a JIRA_TOKEN_* override supplied the value. It resolves through ResolveCredential, so it reflects the effective runtime credential, env override included.
type AuthType ¶
type AuthType string
AuthType is a profile's authentication mechanism. Only AuthTypeToken is supported; an unsupported value is rejected at load, never stored as a fake-authenticated profile.
const AuthTypeToken AuthType = "token"
AuthTypeToken authenticates with an Atlassian API token over HTTP Basic. It is the only supported AuthType.
type CleanupFailure ¶
CleanupFailure records a source secret that could not be deleted after the destination write and config save both succeeded. The migration is durable; the old secret is simply still present and must be removed by hand.
type CleanupNote ¶
CleanupNote records source storage that a successful migration deliberately left in place because jira-cli does not own it — a credential read via the shared legacy keyring key, or a 1Password item the profile named itself. The migration succeeded; the old credential simply still exists and the user may remove it by hand. A note is informational, not a failure.
type Config ¶
type Config struct {
DefaultProfile string `koanf:"default_profile" toml:"default_profile"`
Profiles []Profile `koanf:"profiles" toml:"profiles"`
Theme Theme `koanf:"theme" toml:"theme"`
TUI TUI `koanf:"tui" toml:"tui"`
QueriesPath string `koanf:"queries_path" toml:"queries_path"`
Aliases map[string]string `koanf:"aliases" toml:"aliases"`
// Editor is the global default external-editor command for rich-text
// edits. Per-profile Editor overrides this when set.
// Resolution chain: $JIRA_EDITOR → profile.editor → config.editor →
// $EDITOR → $VISUAL → "vi".
Editor string `koanf:"editor" toml:"editor"`
}
Config is the fully decoded configuration: the selected default profile, the defined profiles, and the global theme, TUI, alias, and editor settings. It is the in-memory shape both the koanf loader and the TOML file map onto.
func Defaults ¶
func Defaults() Config
Defaults is the canonical zero-config Config: one keyring-backed "default" profile and the built-in TUI layout. It is the single source of truth for defaults, feeding both the koanf default layer and the file seeded by LoadOrInit, so the two can never drift.
func Load ¶
Load reads configuration for read-only and resolution paths without ever creating a file. When the resolved path exists it is parsed; when an explicit --config path is missing it returns an error so a path typo cannot be masked; when the default path is missing it returns validated defaults without touching disk.
Commands that intend to persist config must use LoadOrInit, which bootstraps a default file when none exists.
func LoadOrInit ¶
LoadOrInit loads configuration, creating a default config file when none exists. Use this only for explicit init and config-write commands.
LoadOrInit returns the persisted, file-backed config: it deliberately does NOT apply JIRA_* env overlays. Read-modify-write commands Save the result, and persisting a transient env overlay into TOML would corrupt the user's stored config. Read-only callers that want the effective runtime view (file plus env) must use Load instead.
func (Config) Get ¶
Get returns the string form of a configuration value addressed by its user-facing dot-notation key. Profile-scoped keys take the form `profiles.<name>.<field>`. Returns ok=false for unknown keys or missing profiles.
func (Config) Profile ¶
Profile resolves a profile by name, falling back to the default profile when name is empty. A name that matches no defined profile yields a synthetic Profile carrying only that name.
This fabricating behavior is convenient for read-only display paths but unsafe for credential-admin or destructive commands. Those callers must use ResolveProfile, which refuses an unknown name instead of inventing one.
func (Config) ResolveProfile ¶
ResolveProfile resolves the profile a command should act on. An empty name resolves the configured default profile; a non-empty name must match a defined profile exactly. An unknown name, or an empty name with no configured default, returns a ProfileNotDefinedError wrapping ErrProfileNotDefined. It never fabricates a synthetic profile, so a typoed --profile cannot silently target the wrong credential namespace or cache.
func (*Config) Set ¶
Set assigns a configuration value addressed by its user-facing dot-notation key. Profile-scoped keys take the form `profiles.<name>.<field>`. Returns an error describing why the assignment was rejected (unknown key, missing profile, invalid enum, unparsable int/bool).
func (*Config) Validate ¶
Validate normalizes and checks a decoded Config, mutating it in place: it fills blank per-profile fields with their defaults, rejects duplicate or unnamed profiles, unsupported auth types and secret backends, malformed base URLs and cloud IDs, and a default profile that names nothing. theme.name is deliberately not validated here — it is cosmetic and must not make config load (and thus every command) fail on an upstream rename.
type CredentialError ¶
type CredentialError struct {
// Type is the broad classification (auth or validation).
Type ErrorType
// ErrCode is the stable normalized code, the primary branch target.
ErrCode ErrorCode
// Message is a display message safe for humans and agents. It never
// contains a secret.
Message string
// HintMsg is a short next-action hint. It never contains a secret and
// never suggests a mutating command unless the failed command was itself
// mutating auth state.
HintMsg string
// IsRetryable reports whether retrying the same operation could succeed.
IsRetryable bool
// Context carries optional, fixed-shape context (flag, config key, etc.).
Context ErrorContext
// Upstream carries optional provider metadata when the backend exposes a
// structured code or status. Nil when none is available.
Upstream *UpstreamProvider
// Wrapped is the underlying error, preserved so provider-specific callers
// can still use errors.As against it.
Wrapped error
}
CredentialError is the concrete typed error for credential and auth failures in the config package. It exposes enough metadata for a later output layer to render a structured error contract without importing internal/cli into internal/config. Build it directly or via the classifier; recover it with errors.As and branch on Code, never on the message text.
func ClassifyCredentialError ¶
func ClassifyCredentialError(err error, backend string) *CredentialError
ClassifyCredentialError maps an error returned by a credential backend onto a typed CredentialError. It branches on typed errors — never on message substrings — so a backend changing its wording cannot reclassify a failure. A nil error returns nil.
The display message and hint are fixed strings: the upstream error is kept only as the wrapped cause, never copied into the message, so an upstream error that embeds a secret cannot leak through the classified error.
func KeyringUnavailableError ¶ added in v0.10.7
func KeyringUnavailableError(wrapped error) *CredentialError
KeyringUnavailableError classifies a non-not-found keyring failure as a typed keyring-unavailable error. On WSL and headless Linux the raw failure is a D-Bus message ("The name org.freedesktop.secrets was not provided by any .service files") that reads as noise and — worse — classifies as a generic validation failure; typing it here gives it an auth class and a hint that names the actual way out (the env backend, or installing a Secret Service). The raw cause is kept as the wrapped error, never copied into the message, so backend wording cannot leak into envelopes.
func OnePasswordUnsupportedBuildError ¶ added in v0.10.7
func OnePasswordUnsupportedBuildError() *CredentialError
OnePasswordUnsupportedBuildError reports that this binary was built without CGO, so the 1Password SDK is compiled out. It lives here — outside the CGO build tags — so availability checks (login, migrate) can construct the same typed error the no-CGO store methods return, without linking the SDK.
func (*CredentialError) Code ¶
func (e *CredentialError) Code() ErrorCode
Code returns the stable normalized failure code.
func (*CredentialError) Error ¶
func (e *CredentialError) Error() string
Error implements the error interface. It renders the type, the display message, and the hint so the next action survives when the error is shown as plain text; the wrapped cause is appended when present.
func (*CredentialError) FlagName ¶ added in v0.9.0
func (e *CredentialError) FlagName() string
FlagName names the command-line flag the failure is scoped to, when flag-scoped. The method is FlagName (not Flag) for symmetry with CLIInputError, whose Flag is a field.
func (*CredentialError) Retryable ¶
func (e *CredentialError) Retryable() bool
Retryable reports whether retrying the operation could succeed.
func (*CredentialError) Unwrap ¶
func (e *CredentialError) Unwrap() error
Unwrap exposes the wrapped upstream error to errors.Is / errors.As.
type CredentialMigration ¶
type CredentialMigration struct {
Profile string
ProfileIndex int
Source CredentialStore
Destination CredentialStore
SourceRef SecretRef
DestRef SecretRef
}
CredentialMigration describes moving one profile's credential from a source backend to a destination backend. The refs are the fully namespaced identities produced by CredentialIdentity. ProfileIndex records the profile's position in the caller's profile slice so the migration and the profile it belongs to are a single ordered source of truth — no parallel index map has to be kept in lockstep.
type CredentialStore ¶
type CredentialStore interface {
Get(context.Context, SecretRef) (string, error)
Put(context.Context, SecretRef, string) error
Delete(context.Context, SecretRef) error
}
CredentialStore is the backend-agnostic secret store: Get, Put, and Delete a credential addressed by a SecretRef. KeyringStore, OnePasswordStore, EnvCredentialStore, FileCredentialStore, and MemoryCredentialStore implement it. A missing credential is reported with ErrCredentialNotFound.
func FileCredentialStoreFromEnv ¶ added in v0.2.0
func FileCredentialStoreFromEnv() (CredentialStore, bool)
FileCredentialStoreFromEnv returns a test credential store when TestCredentialStoreDirEnv is set to a non-blank directory.
type EnvCredentialStore ¶ added in v0.10.7
type EnvCredentialStore struct{}
EnvCredentialStore is the credential store for the env secret backend: the profile's credential is the JIRA_TOKEN_* environment variable, read at run time, and nothing is ever written to disk or a keyring. It exists for environments with no usable OS keyring (WSL, headless Linux, containers) and for secret injectors like `op run` that provide the token per process.
The same variable is already the cross-backend override checked first by ResolveCredential; this store makes it a profile's sole, declared source so `auth status` reports it honestly instead of "credential not found" against a backend that was never going to hold anything.
func (EnvCredentialStore) Delete ¶ added in v0.10.7
func (EnvCredentialStore) Delete(_ context.Context, ref SecretRef) error
Delete removes nothing: the variable belongs to the launching shell or secret injector. An unset variable is normalized to ErrCredentialNotFound so logout of a not-set profile stays idempotent; a set variable is an error telling the user where the credential actually lives.
func (EnvCredentialStore) Get ¶ added in v0.10.7
Get reads the credential from the profile's JIRA_TOKEN_* variable. The value is returned verbatim — the same semantics as the ResolveCredential override path — so a token whose bytes matter is never altered. An unset or empty variable is a typed env-credential-unset error naming the exact variable, wrapping ErrCredentialNotFound so callers that treat a missing credential as benign (logout, transactional prior reads) keep working.
func (EnvCredentialStore) Put ¶ added in v0.10.7
Put always fails: the env backend cannot write to the parent process's environment. Login on the env backend never calls Put — it saves metadata only — so reaching this means a code path tried to store into env (a migration destination); the error says to manage the variable instead.
type ErrorCode ¶
ErrorCode is the stable, normalized failure code for a credential or auth error, aliased onto the shared taxonomy code so CredentialError satisfies errtax.Coded without conversion.
type ErrorContext ¶
type ErrorContext struct {
// Flag is the CLI flag involved, when the failure is flag-related.
Flag string
// ConfigKey is the config key involved, when the failure is config-related.
ConfigKey string
// Backend is the credential backend involved (keyring, 1password).
Backend string
// CredentialSource is the credential input source involved
// (stdin, env, prompt, json).
CredentialSource string
}
ErrorContext carries optional, fixed-shape context for a credential error. Every field is optional; an empty field is simply omitted by the consumer. Explicit fields are used rather than a map so the shape is checked at compile time and no unknown keys can leak.
type ErrorType ¶
ErrorType aliases the shared taxonomy type so credential failures and the envelope mapper speak one enum.
type FileCredentialStore ¶ added in v0.2.0
type FileCredentialStore struct {
// contains filtered or unexported fields
}
FileCredentialStore stores credentials in one directory using hashed file names derived from the same SecretRef identity as the keyring backend.
func NewFileCredentialStore ¶ added in v0.2.0
func NewFileCredentialStore(dir string) FileCredentialStore
NewFileCredentialStore returns a file-backed credential store rooted at dir.
func (FileCredentialStore) Delete ¶ added in v0.2.0
func (s FileCredentialStore) Delete(_ context.Context, ref SecretRef) error
Delete removes a credential from the file store.
type KeyDesc ¶
type KeyDesc struct {
Name string
Description string
// Choices lists the valid values when the key has a closed enum;
// nil means freeform string/int.
Choices []string
}
KeyDesc describes a single configuration key visible to `jira config get/set`.
type KeyringStore ¶
type KeyringStore struct{}
KeyringStore stores credentials in the OS keyring under a readable "<site-host>/<profile>" entry name. A credential belongs to a site and a profile; the entry name is that identity verbatim, with no digest.
func (KeyringStore) Delete ¶
func (KeyringStore) Delete(_ context.Context, ref SecretRef) error
Delete removes the credential's "<host>/<profile>" keyring entry — the entry jira-cli itself created. A missing entry is normalized to ErrCredentialNotFound rather than surfaced as a backend failure, so logout is idempotent.
func (KeyringStore) Get ¶
Get reads the credential for ref from its "<host>/<profile>" keyring entry. A missing entry is reported as a typed credential-missing error (wrapping ErrCredentialNotFound) that names the profile and points at `auth login`. There is no legacy fallback: a credential stored by a pre-namespacing release under a bare profile name is not auto-resolved — the user logs in once after upgrading.
func (KeyringStore) ListStoredRefs ¶ added in v0.11.0
func (KeyringStore) ListStoredRefs() []SecretRef
ListStoredRefs enumerates the {host, profile} identities this CLI has recorded storing in the OS keyring, as SecretRefs with the keyring backend set. The listing reflects the persisted index: writes from releases before the index existed are absent, and the caller (the orphan purge this enables) should treat it as "known stored", not "everything ever stored".
func (KeyringStore) Put ¶
Put writes the credential under the "<host>/<profile>" keyring entry and records the identity in the keyring index so it stays enumerable. The index write is best-effort: the credential is already stored, and an unindexed entry is exactly as discoverable as every pre-index entry.
type MemoryCredentialStore ¶
type MemoryCredentialStore struct {
// contains filtered or unexported fields
}
MemoryCredentialStore is an in-memory CredentialStore for tests. It holds secrets in a map keyed by the full SecretRef identity, so two profiles that differ only by site never collide.
func NewMemoryCredentialStore ¶
func NewMemoryCredentialStore() *MemoryCredentialStore
NewMemoryCredentialStore returns an empty in-memory credential store.
func (*MemoryCredentialStore) Delete ¶
func (s *MemoryCredentialStore) Delete(_ context.Context, ref SecretRef) error
Delete removes ref's secret; deleting an absent entry is a no-op.
type MigrationReport ¶
type MigrationReport struct {
CleanupFailures []CleanupFailure
CleanupNotes []CleanupNote
}
MigrationReport is the outcome of a MigrateCredentials batch. Cleanup failures and cleanup notes are reported, never hidden: the migration succeeded but a stale secret may remain in the source backend.
func MigrateCredentials ¶
func MigrateCredentials(ctx context.Context, migrations []CredentialMigration, saveConfig func() error) (MigrationReport, error)
MigrateCredentials moves a batch of credentials between backends transactionally around a single metadata save:
- Every destination secret is written first (staging). Before each destination write the destination is snapshotted so rollback can restore a pre-existing value. A write failure aborts the batch; any destination secrets already staged are rolled back and the source secrets are left untouched.
- saveConfig persists the new backend metadata. A save failure rolls back every staged destination secret and returns the save error; no source secret is deleted.
- Only after a durable save is each source credential cleaned up — but only storage jira-cli owns: a keyring entry, or a 1Password item jira-cli named by default. A 1Password item the profile named itself is left in place and recorded as a cleanup note. A delete failure does not fail the migration — it is recorded as a cleanup failure.
Rollback errors are not swallowed: if undoing a staged write fails, the returned error names the affected profiles, so the caller is never told a failed migration was cleanly rolled back when a destination write actually could not be undone.
The staged-write-before-save ordering means a crash between any two steps never leaves a profile whose config points at a backend with no secret.
func (MigrationReport) HasCleanupFailures ¶
func (r MigrationReport) HasCleanupFailures() bool
HasCleanupFailures reports whether any source secret could not be deleted.
type OnePasswordStore ¶
type OnePasswordStore struct {
// Client, when set, is used directly — tests inject an in-memory fake here.
Client onePasswordClient
// NewClient, when set, builds a client per request; it defaults to the
// real SDK client when neither Client nor NewClient is supplied.
NewClient onePasswordClientFactory
}
OnePasswordStore is the credential store for the 1password backend. It is SDK-only: it talks to 1Password through the official Go SDK, never the `op` CLI binary. Authentication comes from a service-account token in the environment or a desktop-app account pinned on the profile.
func (OnePasswordStore) Delete ¶
func (s OnePasswordStore) Delete(ctx context.Context, ref SecretRef) error
Delete revokes the credential from 1Password. It NEVER destroys a 1Password item — not even one jira-cli itself created. A 1Password item is a user-owned object, and an item's tags are user-editable metadata that can never prove who created it; so the only safe revocation is to remove jira-cli's own managed credential field — identified by its stable jira-cli-owned ID — and leave the item, and every other field and all its content, in place. An item jira-cli created solely to hold a credential simply ends up without that field; that is fine and expected.
A missing item normalizes to ErrCredentialNotFound so revocation is idempotent.
func (OnePasswordStore) Get ¶
Get resolves the credential field of the profile's 1Password item. It first confirms the item exists structurally — the SDK exposes no typed not-found error, so the existence check (the same ListItems path Put and Delete use) is the deterministic way to tell a missing item from a real backend failure. A missing item or vault normalizes to ErrCredentialNotFound.
func (OnePasswordStore) Put ¶
Put writes the credential to 1Password as a true upsert: an existing item is edited in place, otherwise the item is created. The in-place edit means a staged Put genuinely overwrites a prior value, so the migration rollback contract — "restore the prior value" — holds.
The credential always goes into the field with the stable jira-cli-owned ID; a user's own field merely titled "credential" with a different ID is left untouched. Editing a pre-existing item only writes that one field, so Put never disturbs anything else on an item jira-cli did not create.
type Profile ¶
type Profile struct {
Name string `koanf:"name" toml:"name"`
BaseURL string `koanf:"base_url" toml:"base_url"`
AuthType AuthType `koanf:"auth_type" toml:"auth_type"`
Email string `koanf:"email" toml:"email"`
DefaultProject string `koanf:"default_project" toml:"default_project"`
DefaultIssueType string `koanf:"default_issue_type" toml:"default_issue_type"`
// DefaultBoard is the profile-scoped default board NAME. When set and
// no `--board`/`--board-id` flag is supplied, the consuming command
// resolves it against the boards cache and applies the resulting
// scope. NOT validated at set time; the cache may not exist yet.
// Use-time validation lives in boardscope.FromFlags. An explicit
// `--board ""` suppresses any configured default.
DefaultBoard string `koanf:"default_board" toml:"default_board"`
RefreshInterval int `koanf:"refresh_interval" toml:"refresh_interval"`
TimeoutSeconds int `koanf:"timeout" toml:"timeout"`
WorkdaySeconds int `koanf:"workday_seconds" toml:"workday_seconds"`
SecretBackend SecretBackend `koanf:"secret_backend" toml:"secret_backend"`
// CloudID, when set, marks this profile as using a scoped (granular)
// Atlassian API token. The auth mechanism is unchanged — HTTP Basic with
// the account email and token — but REST calls must route through the
// Atlassian gateway (https://api.atlassian.com/ex/jira/<cloud_id>/...)
// because the site host rejects scoped tokens. BaseURL is still stored as
// the site URL for cloudId discovery, display, and browser links; the
// effective request base URL is derived by ClientBaseURL. Empty means a
// classic API token addressed directly at the site.
CloudID string `koanf:"cloud_id" toml:"cloud_id"`
OnePasswordAccount string `koanf:"onepassword_account" toml:"onepassword_account"`
Vault string `koanf:"vault" toml:"vault"`
Item string `koanf:"item" toml:"item"`
// TeamAccountIDs lists the account IDs of teammates whose issues count
// as "my team" in TUI filtering. Optional.
TeamAccountIDs []string `koanf:"team_account_ids" toml:"team_account_ids"`
// AccountID is the user's own Jira Cloud accountId. Used by `--assignee me`
// (CLI) and the "A" key (TUI) so assignments target the canonical user
// identifier. Optional; falls back to email when blank.
AccountID string `koanf:"account_id" toml:"account_id"`
// ReadOnly blocks every mutation command for this profile and returns a
// validation error (exit 3). Useful when handing credentials to an AI
// agent. The env var JIRA_READ_ONLY (truthy: 1/true/yes/on) layers on
// top — once set globally, every profile becomes read-only regardless
// of its own setting.
ReadOnly bool `koanf:"read_only" toml:"read_only"`
// Editor is the per-profile external-editor command override.
// When set, takes precedence over the global Config.Editor.
// Resolution chain: $JIRA_EDITOR → profile.editor → config.editor →
// $EDITOR → $VISUAL → "vi".
Editor string `koanf:"editor" toml:"editor"`
}
Profile is one Jira site's connection metadata: its base URL, account email, auth type, and which secret backend holds the token. It carries no credential — the secret lives in the backend named by SecretBackend. A scoped (granular) token profile also stores a CloudID for gateway routing.
func (Profile) ClientBaseURL ¶ added in v0.6.2
ClientBaseURL is the REST base URL the Jira client must target for this profile. A classic API-token profile addresses the site directly; a scoped (granular) token profile carries a CloudID and must route through the Atlassian gateway, the only base URL that accepts scoped tokens. This is the single chokepoint that turns a cloud_id into gateway routing — every client constructor and credential-verification path funnels its base URL through here, so classic and scoped profiles diverge in exactly one place.
type ProfileIncompleteError ¶ added in v0.7.1
type ProfileIncompleteError struct {
Name string
}
ProfileIncompleteError is returned when a requested profile exists but cannot serve live commands because it has no base URL (and no cloud_id to derive one). Callers recover the name with errors.As.
func (ProfileIncompleteError) Code ¶ added in v0.9.0
func (ProfileIncompleteError) Code() errtax.Code
Code classifies the failure under profile_incomplete: the profile cannot serve live commands until it has a base URL.
func (ProfileIncompleteError) Error ¶ added in v0.7.1
func (e ProfileIncompleteError) Error() string
type ProfileNotDefinedError ¶
type ProfileNotDefinedError struct {
Name string
}
ProfileNotDefinedError carries the name of the profile that could not be resolved. It wraps ErrProfileNotDefined, so callers can use either errors.Is(err, ErrProfileNotDefined) or errors.As to recover the name.
func (ProfileNotDefinedError) Code ¶ added in v0.9.0
func (ProfileNotDefinedError) Code() errtax.Code
Code classifies the failure under profile_not_defined: a typoed or unprovisioned --profile fails closed instead of degrading into fabricated empty results.
func (ProfileNotDefinedError) Error ¶
func (e ProfileNotDefinedError) Error() string
func (ProfileNotDefinedError) Unwrap ¶
func (ProfileNotDefinedError) Unwrap() error
type Query ¶
Query is a saved JQL search loaded from a .jql file: the JQL body plus optional frontmatter metadata (display name, description, default project).
type SecretBackend ¶
type SecretBackend string
SecretBackend names where a profile's credential is stored. It is metadata only: the profile records which backend to consult, and the matching store type (KeyringStore, OnePasswordStore, EnvCredentialStore) holds the secret.
const ( // SecretBackendKeyring stores the credential in the OS keyring under a // "<host>/<profile>" entry. It is the default. SecretBackendKeyring SecretBackend = "keyring" // SecretBackendOnePassword stores the credential in a 1Password item. It // is CGO-gated and absent from no-CGO release archives. SecretBackendOnePassword SecretBackend = "1password" // SecretBackendEnv reads the credential from the profile's JIRA_TOKEN_* // environment variable at run time and stores nothing. It exists for // environments with no usable OS keyring (WSL, headless Linux, containers) // and for secret injectors like `op run` — the same variable every backend // honors as an override becomes the profile's sole credential source. SecretBackendEnv SecretBackend = "env" )
type SecretRef ¶
type SecretRef struct {
Profile string
Backend SecretBackend
Account string
Vault string
Item string
// ItemIsDefault reports whether Item is the jira-cli-derived default name
// rather than a name the profile pinned itself. It records 1Password item
// ownership: jira-cli owns and may delete an item it named by default, but
// must never delete an item the user named.
ItemIsDefault bool
// Host is the site host of the profile's Jira base URL (e.g.
// "company.atlassian.net"). It scopes the credential to the site, so the
// same profile name pointing at two sites does not share an entry.
Host string
}
SecretRef identifies a credential in a backend. A credential belongs to a Jira site and a profile: Host is the site host (the host of the profile's base URL) and Profile is the profile name. Two profiles that share a name but point at different Jira sites address different credentials, because the site host is part of the identity. Account/Vault/Item are the backend-specific 1Password coordinates.
Construct a SecretRef with CredentialIdentity so the identity is derived consistently and unsafe profile names are rejected.
func CredentialIdentity ¶
CredentialIdentity derives the stable credential identity for a profile. The returned SecretRef carries the site host (derived from the profile's base URL) and profile name — the credential belongs to that site + profile pair — plus the 1Password coordinates (account/vault/item). For the 1Password backend the item title defaults to a site-scoped name ("jira-cli-<host>-<profile>") when the profile does not pin one, so two sites that share a profile name and a vault do not collide on one item.
A profile name that cannot be encoded into a safe keyring/env key is rejected with a CredentialError carrying ErrorCodeCredentialNamespaceCollision.
func (SecretRef) EnvTokenKey ¶
EnvTokenKey is the environment variable that overrides this profile's credential. The profile name is encoded bijectively: a literal underscore in the name is doubled and a hyphen maps to a single underscore, so "work-staging" (JIRA_TOKEN_WORK_STAGING) and "work_staging" (JIRA_TOKEN_WORK__STAGING) map to distinct keys and cannot bleed into each other. Letters are uppercased; namespace-safe profile names are lower-case only, so the uppercase transform is injective.
func (SecretRef) KeyringName ¶
KeyringName is the keyring entry name for this credential: the readable "<host>/<profile>" pair. There is no digest — the key is the credential's site + profile identity verbatim, so it is legible in any keyring browser.
type TUI ¶
type TUI struct {
RefreshInterval int `koanf:"refresh_interval" toml:"refresh_interval"`
DefaultTab string `koanf:"default_tab" toml:"default_tab"`
Tabs []string `koanf:"tabs" toml:"tabs"`
// Preview docks the issue sidebar: right, left, bottom, hidden, or auto
// (right on wide terminals, bottom on narrow). Applies on hot-reload too.
Preview string `koanf:"preview" toml:"preview"`
// Sections are user-defined dashboard tabs: each entry is a
// saved JQL query that becomes its own tab with its own result count.
Sections []TUISection `koanf:"sections" toml:"sections"`
// PreviewSize is the preview pane's share of the split as a percent
// (20–80; values outside clamp, 0/absent keeps the 50% default).
PreviewSize int `koanf:"preview_size" toml:"preview_size"`
// Keys rebinds TUI actions: action name → key list (the first key shows
// in help). Unknown action names surface as a footer error rather than
// failing the load. Applies on hot-reload; removing an entry restores
// the default binding.
Keys map[string][]string `koanf:"keys" toml:"keys"`
// Lenses replace the issues tab's built-in quick-filters (Mine/Sprint/
// Updated/Reported) with custom titled JQL queries. Absent or empty
// keeps the built-ins.
Lenses []TUISection `koanf:"lenses" toml:"lenses"`
// DefaultLens names the lens (by title, case-insensitive) the issues tab
// lands on. Absent or unmatched falls back to the first lens.
DefaultLens string `koanf:"default_lens" toml:"default_lens"`
// Icons picks the glyph set: "nerd" (Nerd Font codepoints), "unicode"
// (portable, the default look), or "auto" (Nerd only when the NERD_FONT
// environment convention says the font is installed). Applies on
// hot-reload.
Icons string `koanf:"icons" toml:"icons"`
}
TUI holds the alpha dashboard's settings — refresh cadence, tabs, preview layout, key rebindings, and lenses. These affect only `jira tui`.
type TUISection ¶ added in v0.6.0
type TUISection struct {
Title string `koanf:"title" toml:"title"`
JQL string `koanf:"jql" toml:"jql"`
}
TUISection is one configured dashboard tab: a title and the JQL it runs.
type Theme ¶
Theme selects the color palette: Name is a clib preset (or "auto"), Path is an optional custom theme file. Load never validates Name — an unrecognized name degrades to the dark fallback at render rather than blocking commands.
type UpstreamProvider ¶
type UpstreamProvider struct {
// Provider names the backend, e.g. "onepassword-sdk".
Provider string
// UpstreamCode is a bounded, redacted provider code when one can be parsed
// safely. Empty when the provider exposes none.
UpstreamCode string
// UpstreamStatus is a provider status or process exit code. Zero when the
// provider exposes none.
UpstreamStatus int
}
UpstreamProvider carries provider-specific failure metadata for a backend that exposes structured values. It is populated only when the provider reports a code or status; it is never derived by parsing a human message. Provider codes are kept separate from CredentialError.ErrCode so the normalized code stays the primary branch target.