config

package
v0.18.1 Latest Latest
Warning

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

Go to latest
Published: Aug 26, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Overview

Package config loads and saves ~/.gadak/config.json.

Credentials (email/token) share the file with the site settings, but they never reach the database, a log line, or a snapshot (constitution article 8). The file is written 0600.

Index

Constants

View Source
const (
	ActorSourceEnv    = "env"    // GADAK_ACTOR
	ActorSourceConfig = "config" // config.json actor
	ActorSourceAuto   = "auto"   // Claude Code detection
)

Actor sources, in ladder order. status reports which rung answered so an agent can see that its environment was (or was not) recognized.

View Source
const (
	KindConnected  = "connected"
	KindStandalone = "standalone"
)

Workspace kinds. Empty Kind on disk is connected — existing configs keep working with no rewrite.

View Source
const (
	DefaultSyncIntervalSec      = 60   // 1 minute
	DefaultReconcileIntervalSec = 3600 // 1 hour
	MinSyncIntervalSec          = 15   // seconds
	MinReconcileIntervalSec     = 300  // 5 minutes
)

Sync loop defaults and floors. Zero in the file means "use default". Floors reject values that would thrash Jira or busy-loop the local process.

View Source
const (
	SourceFlag    = "flag"
	SourceEnv     = "env"
	SourceStored  = "stored"
	SourceDefault = "default"
)

Workspace selection source. WorkspaceSource reports one of these; when the kind is SourceEnv, the env variable name is returned alongside.

View Source
const (
	// Name is the CLI binary, desktop app, and user-facing product name.
	Name = "gadak"
	// DirName is the directory under $HOME that holds the default profile.
	DirName = ".gadak"
	// DBFile is the SQLite filename inside a profile directory.
	DBFile = "gadak.db"
	// EnvPrefix is prepended to HOME, PROFILE, WORKSPACE, TOKEN, SITE, EMAIL, PROJECTS.
	EnvPrefix = "GADAK_"

	// Legacy names from the 2026-08 rename (scry → gadak). Still accepted so
	// an existing install keeps working until the user next launches gadak.
	LegacyName      = "scry"
	LegacyDirName   = ".scry"
	LegacyDBFile    = "scry.db"
	LegacyEnvPrefix = "SCRY_"
)
View Source
const (
	TokenExpiryOK       = "ok"
	TokenExpiryExpiring = "expiring"
	TokenExpiryExpired  = "expired"
	TokenExpiryUnknown  = "unknown"

	TokenExpirySourceUser    = "user"
	TokenExpirySourceAssumed = "assumed"

	// TokenDefaultLifetimeDays is Atlassian's default API-token lifetime
	// when the user skipped the date from the create dialog.
	TokenDefaultLifetimeDays = 365
	// TokenExpiryWarnDays is the first day the warning surfaces (inclusive).
	TokenExpiryWarnDays = 14
	// TokenExpiryUrgentDays is the first day the warning is urgent (inclusive).
	TokenExpiryUrgentDays = 3

	// TokenTimeFormat is ISOMilli so assumed dates line up with
	// tokenVerifiedAt and every other store timestamp.
	TokenTimeFormat = ISOMilli
)

Token expiry assessment. One owner: AssessTokenExpiry. Surfaces (status, sync_health, the freshness chip) render what this returns; they do not re-derive state.

View Source
const ISOMilli = "2006-01-02T15:04:05.000Z"

ISOMilli is the millisecond-precision UTC ISO-8601 layout every timestamp gadak writes — store columns, token expiry, usage flush — and that the `delta` cursor contract depends on. Milliseconds are not decoration: a whole-second cursor would drop a row written in the same second the cursor was taken.

Variables

View Source
var ErrNotConfigured = errors.New("not configured — run gadak init (Jira), gadak init --standalone (local), or gadak init --pairing-code (another machine's serve)")

ErrNotConfigured is the single owner of "this workspace has no origin yet" (GDK-454). CLI verbs that refuse because HasCredential is false print this — sync, writes, open, api, pairing list, status stderr, init's missing- values path. Paired workspaces are configured: HasCredential already counts remote-origin.json (GDK-442) and these verbs must not use this sentence for them. A Linear apiKey is a credential too: this sentence must not fire on a Linear-only workspace.

origin.errNeedCredential is a different error (Client/Wiki construction when a connected workspace lacks site/email/token) and is quoted by wikiPathStatus; it is not this sentence.

View Source
var FeatureNames = []string{"feed", "deploy", "qa", "teamGroups"}

FeatureNames is every optional-surface flag PUT / gadak config accept. Unknown keys in a features map are dropped.

Functions

func ApplyAppearance added in v0.15.2

func ApplyAppearance(c *Config, a Appearance) error

ApplyAppearance writes a validated theme onto c. "system" and empty store as the zero value so the default is not persisted.

func ApplyConfluence added in v0.15.2

func ApplyConfluence(c *Config, enabled *bool, spaces []string) error

ApplyConfluence is the PUT settings/ confluence rule, shared with `gadak config set confluence*`. enabled:false turns the source off; enabled:true creates/replaces the block; spaces alone requires it on.

func ApplyUIConfig added in v0.17.2

func ApplyUIConfig(c *Config, next *UIConfig) error

ApplyUIConfig validates next and installs it on c. Warnings are printed to stderr here (LoadFor's migration notices set the precedent: the write succeeded and the warning is part of its output, not an API result).

func ApplyUIConfigWithWarnings added in v0.17.3

func ApplyUIConfigWithWarnings(c *Config, next *UIConfig) ([]tokencheck.Violation, error)

ApplyUIConfigWithWarnings is ApplyUIConfig with the write-time warnings returned as well: under warn+save the warning IS the interesting part of the answer — why the saved look will render the way it does — and the settings PUT carries them on its response (uiWarnings) so a writer that never sees the CLI's stderr still reads them. The stderr print stays; server logs keep the same record a CLI session leaves.

func AttachmentDir

func AttachmentDir() (string, error)

AttachmentDir is where attachment bytes are cached, next to the mirror it belongs to (so a profile keeps its own, and deleting a profile takes its cache with it).

func AttachmentDirFor

func AttachmentDirFor(profile string) (string, error)

AttachmentDirFor is where attachment bytes are cached for the named profile.

func ClearStoredWorkspace added in v0.17.0

func ClearStoredWorkspace() error

ClearStoredWorkspace removes the stored default so Profile() falls through to the root. Missing file is success.

func ConfigVersionOfDir added in v0.17.2

func ConfigVersionOfDir(dir string) string

ConfigVersionOfDir is the disk identity of a profile's config.json, for a directory already in hand (webConfig gets the loaded Config's Directory()). Every writer (CLI `config set`, PUT settings/, LoadFor's legacy-field rewrite) goes through the same atomic temp+rename, so mtime+size changes exactly when the content does. The ui-focus poll carries it; the web refetches config.json when it moves. A monotonic counter was rejected because it needed a second owner (local_meta) and a CLI↔SQLite coupling on a path that already had a single owner: the file.

func DBPath

func DBPath() (string, error)

DBPath is the default SQLite path for the active profile.

func DBPathFor

func DBPathFor(profile string) (string, error)

DBPathFor is the SQLite path for the named profile.

func Dir

func Dir() (string, error)

Dir is GADAK_HOME or ~/.gadak, plus profiles/<name> when a profile is active.

func DirFor

func DirFor(profile string) (string, error)

DirFor is the config directory for a named profile. "" or "default" means the root (GADAK_HOME / ~/.gadak); any other name lives under profiles/<name>. Names that fail validProfileName return an error and no path.

func Env

func Env(suffix string) string

Env returns GADAK_<suffix>, then SCRY_<suffix> if the new name is unset or empty. An empty GADAK_* value is treated as unset so a blank export cannot hide a real SCRY_* fallback (decision 0007: read SCRY_* when GADAK_* is unset).

func FormatTokenTime added in v0.14.2

func FormatTokenTime(t time.Time) string

FormatTokenTime writes the on-disk / JSON form (UTC, millisecond).

func LooksLikeProjectKey added in v0.17.2

func LooksLikeProjectKey(s string) bool

LooksLikeProjectKey reports that shape without case-folding. "Fix" (the create --help example) is a summary word; "FIX" / "GDK" look like keys.

func NormalizeFeatures added in v0.15.2

func NormalizeFeatures(set map[string]bool) map[string]bool

NormalizeFeatures projects the optional-surface flags. An explicit key wins; missing keys stay off except feed, which defaults on.

func NormalizeProfile added in v0.17.0

func NormalizeProfile(name string) string

NormalizeProfile is the display name of a profile: the empty string and "default" name the same root profile (Profile() returns "" for it) and both serialize as "default". Single owner of that mapping (GDK-619) — serve's workspace list, settings' profile display, and sameProfile all call this instead of re-spelling the branch.

func NotConfiguredWith added in v0.17.0

func NotConfiguredWith(addendum string) error

NotConfiguredWith appends a verb-specific addendum to ErrNotConfigured.

func ParseActorShorthand added in v0.17.0

func ParseActorShorthand(v string) (slug, name string)

ResolveActor is the single owner of "who does this process write as" (GDK-586). Ladder, first match wins:

  1. env GADAK_ACTOR — "slug" or "slug|display name"
  2. the config.json actor block (the workspace default)
  3. Claude Code auto-detection: CLAUDECODE=1 derives "claude:<first 8 of CLAUDE_CODE_SESSION_ID>" (or bare "claude" with no session id), display name "Claude Code"

Nothing set means no actor: origins see the identity they always did (the in-process user / seed account) and no header is sent.

AI_AGENT is deliberately not a slug source: as measured it carries the harness version ("claude-code_2-1-239_agent"), so a slug built from it would mint a new agent identity per upgrade — the opposite of the stable-slug contract. ParseActorShorthand splits the one-line actor form shared by GADAK_ACTOR and `gadak config set actor`: "slug" or "slug|display name". An empty slug means unset.

func ParseTokenExpiresAt added in v0.14.2

func ParseTokenExpiresAt(raw string) (time.Time, error)

ParseTokenExpiresAt accepts a calendar date (YYYY-MM-DD, from an HTML date input or Atlassian's create dialog) or an RFC3339 timestamp. Date-only values are midnight UTC that day.

func Path

func Path() (string, error)

func Profile

func Profile() string

Profile is the single owner of workspace resolution: flag > env > stored default > root. "" is the root profile. "default" normalizes to "".

func Profiles

func Profiles() ([]string, error)

Profiles lists the configured profile names, excluding the default one.

func ReloadWorkspaceFromEnv added in v0.17.0

func ReloadWorkspaceFromEnv()

ReloadWorkspaceFromEnv forgets a SetProfile override and reads GADAK_WORKSPACE, then GADAK_PROFILE, then SCRY_PROFILE. Empty values are unset. When none of those are set, Profile() still applies the stored default (then the root). Each process main must call this before flags (cmd/gadak does); importing this package does not. Tests call it after t.Setenv.

func RequireExistingProfile added in v0.13.0

func RequireExistingProfile() error

RequireExistingProfile is the single owner of "may this named profile be used without creating it?". The default profile (empty / "default") is always allowed so first-run can mint ~/.gadak. A named profile whose directory does not exist is an error; names that do exist are listed so a typo is obvious.

func SetProfile

func SetProfile(name string)

SetProfile is called by the CLI's --workspace/--profile flag, which wins over the env var and the stored default. The source becomes SourceFlag even when name is empty.

func SetStoredWorkspace added in v0.17.0

func SetStoredWorkspace(name string) error

SetStoredWorkspace writes the home-root stored default used by Profile() when no flag or env selected a workspace. Empty and "default" clear it. A name whose directory does not exist is refused with the same error RequireExistingProfile prints — Profile() must not be pointed at a missing workspace that then silently becomes the root.

func SettingPaths added in v0.15.2

func SettingPaths() []string

SettingPaths returns catalog paths in catalog order.

func UIDataColors added in v0.17.2

func UIDataColors(u *UIConfig) map[string]map[string]string

UIDataColors passes the stored data inks through the same value gate and returns what may render. Keys were validated at write time; this is the stale-schema defense for values that reached disk anyway.

func UIDimensionVars added in v0.17.3

func UIDimensionVars(u *UIConfig) (vars map[string]string, warns []tokencheck.Violation)

UIDimensionVars expands the stored dimension overrides into the single palette-agnostic CSS variable map the web consumes: cssVar ("--spacing-row") → value ("44px"). It is the sibling of UITokenVars with the palette axis removed — dimensions do not vary by theme. Mirrors its filter exactly: only names the dim catalog knows, tiers that may render, and values that parse are injected; everything else degrades to a load-time advisory (GDK-769 axis 3) so a config written by a newer build never blocks a boot. The map is overrides-only: base values keep coming from app.css and the JS layout constants.

func UITokenVars added in v0.17.2

func UITokenVars(u *UIConfig) (vars map[string]map[string]string, warns []tokencheck.Violation)

UITokenVars expands the stored overrides into the final CSS variable maps the web consumes: palette → cssVar ("--color-accent") → hex. Only names the catalog knows and valid hex values are injected — anything else is reported as a load-time advisory so a config written by a newer build (renamed token) degrades to "override ignored" instead of an unbootable UI. Locked tiers RENDER (the write path warned and saved the value — filtering it here would turn that warning into a silent lie); the dimension sibling still filters its one locked token because a stored docked-min is genuinely dead (the runtime recomputes it).

func ValidateDataColors added in v0.17.2

func ValidateDataColors(dc map[string]map[string]string) error

ValidateDataColors enforces the family/key/value rules. The value rule is hex only — data inks are decorative (dots, chip text), not grounds, so no contrast floor applies; the token tiers carry the legibility contract.

func ValidateDefaultIssueType added in v0.16.0

func ValidateDefaultIssueType(s string) (string, error)

ValidateDefaultIssueType stores an optional display label. Resolution never reads this value.

func ValidateDefaultIssueTypeID added in v0.16.0

func ValidateDefaultIssueTypeID(s string) (string, error)

ValidateDefaultIssueTypeID accepts empty (unset) or a Jira issue type id.

func ValidateDefaultProject added in v0.16.0

func ValidateDefaultProject(s string) (string, error)

ValidateDefaultProject accepts empty (unset) or a project key with no whitespace. Membership in Projects is not checked here — that list can be empty ("every project") and can change after the default is set.

func ValidateGroupQuery added in v0.16.0

func ValidateGroupQuery(q string) error

ValidateGroupQuery accepts empty (disabled) or a single SELECT/WITH. Writes, PRAGMA, ATTACH, and multi-statement payloads are refused here so a bad save fails before the derived view tries to run it.

func ValidateIntervals added in v0.15.2

func ValidateIntervals(syncSec, reconcileSec int) error

ValidateIntervals is the PUT / gadak config rule for the two watch periods. 0 means "use the package default". A positive value below the floor is rejected so a typo cannot thrash Jira.

func ValidateLocale added in v0.17.0

func ValidateLocale(s string) (string, error)

ValidateLocale accepts "", en, ko, ja, de (GDK-597). Empty stores as the zero value so the default is not persisted.

func ValidateProjectKeys added in v0.17.2

func ValidateProjectKeys(keys []string) ([]string, error)

ValidateProjectKeys is the single choke for the `projects` setting (GDK-809). Empty means "every project this account can see". Entries are trimmed and upper-cased (same as gadak init). Mixed-case values are refused as display names. Load does not call this — a file that already contains an unknown key keeps working until the next Set.

func ValidateTheme added in v0.15.2

func ValidateTheme(s string) (string, error)

ValidateTheme accepts empty/"system" (stored as "") and any lowercase identifier. "system", "light", and "dark" are always valid.

func ValidateUIConfig added in v0.17.2

func ValidateUIConfig(u *UIConfig) (warns []tokencheck.Violation, err error)

ValidateUIConfig is the write gate for the whole `ui` block. It returns the carry-along warnings (unknown names, unknown palettes) and an error that refuses the save. A nil config is valid and clears nothing.

func WorkspaceSource added in v0.17.0

func WorkspaceSource() (kind, envName string)

WorkspaceSource names what Profile() used: SourceFlag, SourceEnv, SourceStored, or SourceDefault. envName is the variable that supplied the value when kind is SourceEnv, otherwise "".

Types

type ActorConfig added in v0.17.0

type ActorConfig struct {
	Slug string `json:"slug,omitempty"`
	Name string `json:"name,omitempty"`
}

ActorConfig is the workspace-default acting identity for writes to an issuetap origin (standalone or paired, GDK-586): the slug becomes the origin accountId verbatim and names an agent account there; Name is an optional display name. Nil (or empty slug) means unset. Per-machine identity — never team-exported.

func ValidateActor added in v0.17.0

func ValidateActor(slug, name string) (*ActorConfig, error)

ValidateActor is the `gadak config set actor` rule: empty slug clears the block; a non-empty slug must be a stable identity, not a display name — no whitespace, within issuetap's cap.

type Appearance added in v0.15.2

type Appearance struct {
	Theme string `json:"theme,omitempty"`
}

Appearance is the look block in config.json. Empty Theme means "system".

type Config

type Config struct {
	// Kind is the workspace kind. Empty (or any value other than
	// KindStandalone) is a connected workspace — Jira-site bound, the
	// default. Absent from existing configs; no migration.
	// Do not store the word "local" here: gadak is already local-first.
	Kind string `json:"kind,omitempty"`

	// Frozen stops every request to this workspace's origin — pulls and
	// writes alike (GDK-507). A scrubbed fixture with a live credential
	// decontaminates itself the moment something opens it (GDK-181), and the
	// same live credential could create real issues on the origin; both are
	// refused at the client mint (origin.ErrWorkspaceFrozen). Demo, recording
	// and screenshot workspaces set this. Mirror reads are unaffected.
	Frozen bool `json:"frozen,omitempty"`

	// The credential and what it connects to. Token is never copied out of this file.
	// A standalone workspace leaves these empty.
	Site     string   `json:"site,omitempty"` // https://your-site.atlassian.net
	Email    string   `json:"email,omitempty"`
	Token    string   `json:"token,omitempty"`
	Projects []string `json:"projects,omitempty"`

	// DevStatus turns on development-panel mirroring (GDK-496/497): sync
	// fetches each changed issue's dev-status (pull requests) from the origin
	// into dev_links. Off by default on a connected Cloud workspace — that
	// path calls Jira's internal /rest/dev-status API (Atlassian marks it
	// unstable) and adds per-issue requests. Standalone / issuetap always
	// fetches regardless of this flag (GDK-536): the panel is local.
	DevStatus bool `json:"devStatus,omitempty"`

	// Locale is the display-name language of a standalone workspace's
	// origin ("" | en | ko | ja | de; "" and "en" are both English,
	// GDK-597). It rides EmbeddedConfig into the embedded issuetap, so
	// status / issue-type / field-catalog names — and the agent alias
	// dictionary (GDK-593) — follow it; priority names stay English, like
	// a live Cloud site. A connected workspace ignores it: there the
	// language is the Atlassian account's, not ours.
	Locale string `json:"locale,omitempty"`

	// DefaultProject is the project key used when create omits --project /
	// project_key. Site-bound: never team-exported. Empty means unset.
	DefaultProject string `json:"defaultProject,omitempty"`
	// DefaultIssueTypeID is the Jira issue type id used when create omits
	// --type / issue_type. Stored as id, never a localized display name —
	// a Korean account's "Task" is "작업". Empty means unset. Display names
	// are not a fallback; see DefaultIssueType.
	DefaultIssueTypeID string `json:"defaultIssueTypeId,omitempty"`
	// DefaultIssueType is an optional display label for DefaultIssueTypeID.
	// Resolution never reads this field (names localize per account).
	DefaultIssueType string `json:"defaultIssueType,omitempty"`

	// Result of verifying the credential: when `PUT credential/` last confirmed it
	// against /myself, and who owns it. Unlike the token itself, both may be
	// returned in a response.
	TokenVerifiedAt string `json:"tokenVerifiedAt,omitempty"`
	TokenOwner      string `json:"tokenOwner,omitempty"`
	// TokenExpiresAt is when the stored API token stops working (RFC3339).
	// Set on connect / replace: the date typed from Atlassian's create
	// dialog, or verification time plus 365 days when that field was skipped.
	// There is no Atlassian API for this; the token string is opaque.
	// TokenExpirySource is "user" or "assumed" so a warning can hedge.
	TokenExpiresAt    string `json:"tokenExpiresAt,omitempty"`
	TokenExpirySource string `json:"tokenExpirySource,omitempty"`
	// AccountID is the Jira accountId returned by /myself. Used for feed
	// relevance (assignee/reporter/mention) and self-action filtering. Empty
	// when the credential was never verified against a live site.
	AccountID string `json:"account_id,omitempty"`

	// Sync field mapping (contracts/sync.md, "Field mapping").
	// Fields is the sole on-disk truth. FieldMap/EditableFields exist only as
	// unmarshal targets so LoadFor can migrate a pre-Fields config once.
	Fields []FieldSpec `json:"fields,omitempty"`
	// FieldsAppliedAt is when `gadak fields --apply` last succeeded (RFC3339,
	// millisecond UTC). Mapping lives in Fields; the timestamp lives here too
	// so a regenerated mirror does not lose "when discovery last ran".
	FieldsAppliedAt string `json:"fieldsAppliedAt,omitempty"`
	// FieldMap is a migration-only unmarshal target (alias → customfield id).
	// LoadFor converts it into Fields and clears it; new writes must not set it.
	FieldMap   map[string]string `json:"fieldMap,omitempty"`
	BodyFields []string          `json:"bodyFields,omitempty"` // ADF custom-field ids to fold into FTS
	// EditableFields is a migration-only unmarshal target (alias → field id).
	// LoadFor overlays it onto Fields (legacy wins per alias) and clears it.
	EditableFields map[string]string `json:"editableFields,omitempty"`

	// Optional surfaces carried over from the tool this was extracted from.
	Members    []Member    `json:"members,omitempty"`
	GroupRules []GroupRule `json:"groupRules,omitempty"`
	// GroupQuery is an optional read-only SELECT/WITH run when the derived
	// view is rebuilt (config or sync version change), never on a keystroke.
	// It must return two columns: issue key, group. An empty group string
	// leaves the issue unclassified; NULL (or a missing key) falls through
	// to groupRules and then the assignee's member group. Installation
	// logic belongs in this string, not in gadak source.
	GroupQuery     string             `json:"groupQuery,omitempty"`
	GroupLabels    map[string]string  `json:"groupLabels,omitempty"`
	GroupColors    map[string]string  `json:"groupColors,omitempty"`
	ProductByGroup map[string]Product `json:"productByGroup,omitempty"`
	Features       map[string]bool    `json:"features,omitempty"` // feed/deploy/qa/teamGroups
	QaDashboardURL string             `json:"qaDashboardUrl,omitempty"`

	StaleThresholdHours int `json:"staleThresholdHours,omitempty"` // 0 = the client default (72)
	// AttachmentCacheMB caps the on-disk attachment byte cache. 0 = package
	// default (512 MB); a negative value is treated as 0.
	AttachmentCacheMB int `json:"attachmentCacheMB,omitempty"`

	// Sync periods in seconds. 0 means use DefaultSyncIntervalSec /
	// DefaultReconcileIntervalSec. Watch re-reads config on each cycle when
	// opts.Reload is set (serve, desktop, and workspace all pass config.Load),
	// so a change applies on the next tick without restarting the process.
	SyncIntervalSec      int `json:"syncIntervalSec,omitempty"`
	ReconcileIntervalSec int `json:"reconcileIntervalSec,omitempty"`

	// Notify enables OS desktop notifications from the sync watch loop after
	// new personal-feed events. Default true when absent; set false to opt out.
	// Pointer so omitempty can distinguish "unset" from explicit false.
	Notify *bool `json:"notify,omitempty"`

	// UpdateCheck enables the once-per-day GitHub release lookup that surfaces
	// a newer version on sync/status/serve bootstrap. Default true when absent;
	// set false to opt out (restores the prior "outbound is only Jira" model).
	UpdateCheck *bool `json:"updateCheck,omitempty"`

	// Appearance is the look of the web/desktop UI. Nil (or empty Theme) means
	// "system" and is not written — the default is not persisted. A pointer so
	// encoding/json omitempty can drop the block; a zero struct would write {}.
	Appearance *Appearance `json:"appearance,omitempty"`

	// UI is the user color-override block (GDK-786/791): token overrides,
	// per-palette overlays, and per-data-key inks. Nil means nothing is
	// overridden and is not written. See uitokens.go for the write contract.
	UI *UIConfig `json:"ui,omitempty"`

	// Actor is the workspace-default acting identity for agent writes to an
	// issuetap origin (GDK-586): the X-Issuetap-Actor slug (the origin
	// accountId, verbatim) plus an optional display name. Nil or empty slug
	// means unset; GADAK_ACTOR wins over it and Claude Code is auto-detected
	// when neither is set (internal/config/actor.go ResolveActor). Nil blocks
	// are not written.
	Actor *ActorConfig `json:"actor,omitempty"`

	// Confluence, when non-nil, enables the wiki-page mirror (second source).
	// Spaces empty means every *global* space — not every space the account can
	// see, which is what this comment used to claim and what a warning written
	// from it went on to tell users. Cloud gives each person a personal space,
	// so an unfiltered listing is mostly noise; personal spaces are mirrored
	// only when named here. The rule itself lives in internal/sync/confluence.go.
	Confluence *ConfluenceConfig `json:"confluence,omitempty"`

	// Linear, when non-nil, enables the Linear issue mirror (third source,
	// read-only — GDK-263). Unlike Confluence it carries its own credential:
	// APIKey is a Linear personal API key and gets the same article-8
	// treatment as Token — never a log line, a snapshot, or a team export
	// (teamconfig classifies the whole block never-export).
	Linear *LinearConfig `json:"linear,omitempty"`
	// contains filtered or unexported fields
}

Config is the on-disk profile document (~/.gadak/config.json, or ~/.gadak/profiles/<name>/config.json). Credentials share this file with site settings but never reach the database, a log, or a snapshot; the file is written 0600.

func Load

func Load() (*Config, error)

Load returns an empty Config when the file does not exist; that is not an error.

func LoadFor

func LoadFor(profile string) (*Config, error)

LoadFor reads config.json for the named profile. Missing file returns an empty Config with dir set (not an error), matching Load's convention.

LoadFor is the single owner of the field-mapping rewrite rule: leftover fieldMap/editableFields are always normalized in memory so callers never see the legacy shape. A failed Save is a stderr warning, not a load error — a read-only home must still serve (the rewrite is a convenience, not a precondition). Callers (serve, desktop, MCP, status) must not re-implement this tolerate-the-write-failure rule.

func (*Config) ApplyTokenExpiry added in v0.14.2

func (c *Config) ApplyTokenExpiry(userRaw, verifiedAt string) error

ApplyTokenExpiry writes TokenExpiresAt and TokenExpirySource. A non-empty userRaw is source "user". An empty userRaw with a parseable verifiedAt assumes verifiedAt + 365 days. Empty userRaw and no verifiedAt leaves the fields untouched (offline init must not invent a date).

func (*Config) ApplyTokenExpiryIfNeeded added in v0.14.2

func (c *Config) ApplyTokenExpiryIfNeeded(userRaw, verifiedAt string, tokenReplaced bool) error

ApplyTokenExpiryIfNeeded is the init path: do not reset an existing date when the token was kept and the user did not supply a new one. Connect and replace-token always call ApplyTokenExpiry (they always store a token).

func (*Config) ApplyVerifiedIdentity added in v0.13.0

func (c *Config) ApplyVerifiedIdentity(accountID, displayName, verifiedAt string)

ApplyVerifiedIdentity stamps the three fields a successful Jira /myself call produces. CLI init uses this; the server onboarding path writes the same keys inline (internal/server/onboarding.go, write.go) — that package is outside this change's file boundary.

func (*Config) ClearTokenExpiry added in v0.14.2

func (c *Config) ClearTokenExpiry()

ClearTokenExpiry drops the stored date. Called when the credential is deleted.

func (*Config) CustomFieldsStatus added in v0.17.0

func (c *Config) CustomFieldsStatus() map[string]any

CustomFieldsStatus is the object `gadak status --json` and MCP gadak_status emit for field-mapping visibility (GDK-522). mapped is the effective spec count (FieldSpecs, so a leftover fieldMap still counts). applied_at is omitted when discovery has never been recorded. Does not scan the mirror.

func (*Config) Directory added in v0.16.0

func (c *Config) Directory() string

Directory is the profile directory this Config was loaded from (LoadFor). Empty on a Config that was never loaded.

func (*Config) EffectiveLocale added in v0.17.0

func (c *Config) EffectiveLocale() string

EffectiveLocale is the origin's display-name language (GDK-597). Empty on disk means English. Only a standalone workspace consumes it; see Locale.

func (*Config) EffectiveReconcileIntervalSec

func (c *Config) EffectiveReconcileIntervalSec() int

EffectiveReconcileIntervalSec returns the reconcile interval Watch should use.

func (*Config) EffectiveSyncIntervalSec

func (c *Config) EffectiveSyncIntervalSec() int

EffectiveSyncIntervalSec returns the interval Watch should use.

func (*Config) EffectiveTheme added in v0.15.2

func (c *Config) EffectiveTheme() string

EffectiveTheme is the UI theme id. Empty on disk means "system".

func (*Config) FieldSpecs

func (c *Config) FieldSpecs() []FieldSpec

FieldSpecs returns the effective field specs. After LoadFor, this is Fields. In-memory configs that never went through LoadFor (tests, settings PUT) may still carry only FieldMap; synthesize the same Label/Role defaults the migration writes so those callers keep working until they switch.

func (*Config) HasAtlassianCredential added in v0.17.0

func (c *Config) HasAtlassianCredential() bool

HasAtlassianCredential reports whether Jira-family origin writes are possible: standalone, site+email+token, or a pairing remote-origin.json. Linear's key is a different origin and is not counted here — callers that mean "can I talk to Jira / Confluence / issuetap" use this, not HasCredential.

func (*Config) HasCredential

func (c *Config) HasCredential() bool

HasCredential reports whether writes and the attachment proxy are possible. A standalone workspace has no site token; writes still go through the in-process origin, so it reports true. A connected workspace still requires site+email+token — that gate is not weakened. A Linear API key also counts: Linear writes are possible, so the workspace is configured. Jira-only callers that must not treat a Linear key as a site token use HasAtlassianCredential.

func (*Config) HasLinearCredential added in v0.17.0

func (c *Config) HasLinearCredential() bool

HasLinearCredential reports whether Linear origin writes are possible: a linear block with an API key. Presence of an empty block is not a credential — origin.Linear still refuses a missing key.

func (*Config) IsStandalone added in v0.16.0

func (c *Config) IsStandalone() bool

IsStandalone reports a workspace whose origin is the in-process issuetap snapshot, not a Jira site.

func (*Config) NormalizeLegacyFields added in v0.15.0

func (c *Config) NormalizeLegacyFields() (changed bool, shape string)

NormalizeLegacyFields converts leftover FieldMap/EditableFields into Fields using FieldSpecs() synthesis plus the EditableFields overlay (legacy wins per alias) and clears the legacy maps. No disk write — LoadFor persists. shape names the keys that were present, for the rewrite log line.

func (*Config) NotifyEnabled

func (c *Config) NotifyEnabled() bool

NotifyEnabled is true unless the user set notify: false. Absent means on.

func (*Config) ProfileName added in v0.17.0

func (c *Config) ProfileName() string

ProfileName is the profile this Config was loaded for (LoadFor); "" is the root profile. A Config that was never loaded also answers "" — callers that need the active profile as a fallback must check Directory() first, the way origin's profileDir does. This exists so a Config loaded for a mounted workspace carries its own identity: comparing a probe answer against the process-global Profile() routed a mounted workspace's origin calls to the primary profile's route (GDK-677).

func (*Config) Save

func (c *Config) Save() error

Save writes the file atomically with mode 0600. When c.dir is set (LoadFor), the write goes to that profile's config.json; otherwise the active Path(). The profile directory (and ~/.gadak / GADAK_HOME when writing the default profile) is created and owner-writable dirs are tightened to 0700 (fsperm.EnsurePrivateDir); chmod failures are logged only.

func (*Config) SyncFrozen added in v0.17.0

func (c *Config) SyncFrozen() bool

SyncFrozen is the only question the sync gate asks. Deliberately separate from HasCredential: a standalone workspace has a credential by definition (see HasCredential), and writes must keep working here.

func (*Config) TokenExpiryAt added in v0.14.2

func (c *Config) TokenExpiryAt(now time.Time) TokenExpiry

TokenExpiryAt is AssessTokenExpiry against this config and now.

func (*Config) UpdateCheckEnabled

func (c *Config) UpdateCheckEnabled() bool

UpdateCheckEnabled is true unless the user set updateCheck: false. Absent means on.

func (*Config) WorkspaceKind added in v0.16.0

func (c *Config) WorkspaceKind() string

WorkspaceKind is KindStandalone or KindConnected. Empty/unknown Kind is connected so an existing config.json is unchanged.

type ConfluenceConfig

type ConfluenceConfig struct {
	Spaces []string `json:"spaces,omitempty"`
}

ConfluenceConfig is the optional wiki-page source. Presence (non-nil) is the on switch; same site/email/token as Jira, REST base under /wiki.

type DimCatalogEntry added in v0.17.3

type DimCatalogEntry struct {
	Axis      string   `json:"axis"`
	Name      string   `json:"name"`
	CSSVar    string   `json:"cssVar"`
	Tier      string   `json:"tier"`
	Unit      string   `json:"unit"`
	Default   string   `json:"default"`
	Min       *float64 `json:"min"`
	Max       *float64 `json:"max"`
	Relations []string `json:"relations"`
}

DimCatalogEntry is one row of the read-only dimension-token discovery catalog — the sibling of tokencheck.CatalogToken on the color side. Min/Max read null where a relation owns the bound (row-excerpt, detail-max) or the token is locked, mirroring the embedded DimToken; Relations is always present, [] when the token stands alone, like the color catalog's rules field. Locked tokens (docked-min) stay in the same list, tier-marked — the color catalog's locked notation.

type FieldSpec

type FieldSpec struct {
	Alias string   `json:"alias"`          // stable key: ascii slug of the name, else cf_<id>
	Label string   `json:"label"`          // Jira display name, in the account's language
	IDs   []string `json:"ids"`            // all field ids sharing the name, most-filled first
	Role  string   `json:"role"`           // body | facet | user | plain
	Kind  string   `json:"kind,omitempty"` // editor: option | multi_option | user | version_array | ""
	Auto  bool     `json:"auto,omitempty"` // discovery-owned; regenerated on re-apply
}

FieldSpec is one logical custom field. Jira creates a separate field id per board template for the same concept, so one spec can carry several ids; the sync coalesces the first filled value (measured fact: 57 of 353 custom field names on one large site map to 2+ ids).

type GroupRule

type GroupRule struct {
	Group      string   `json:"group"`
	Projects   []string `json:"projects,omitempty"`
	Labels     []string `json:"labels,omitempty"`
	Components []string `json:"components,omitempty"`
}

GroupRule classifies an issue into a group. Rules are read top-down and the first match wins. Conditions AND together; the list inside one condition ORs. An empty condition is always true. For classification that does not fit these three lists, set GroupQuery instead of growing this struct.

type LinearConfig added in v0.16.1

type LinearConfig struct {
	APIKey  string   `json:"apiKey,omitempty"`
	TeamIDs []string `json:"teamIds,omitempty"`
}

LinearConfig is the optional Linear issue source. Presence (non-nil) is the on switch, matching ConfluenceConfig. TeamIDs are Linear team uuids (never display keys — the same localization/rename hazard as Jira names); empty means every team the key can see.

type Member

type Member struct {
	Email         string `json:"email"`
	Name          string `json:"name,omitempty"`
	DisplayName   string `json:"display_name,omitempty"`
	Group         string `json:"group,omitempty"`
	Department    string `json:"department,omitempty"`
	JobRole       string `json:"job_role,omitempty"`
	JiraAccountID string `json:"jira_account_id,omitempty"`
	AvatarURL     string `json:"avatar_url,omitempty"`
}

Member is one entry of the static member directory injected through settings. It merges into bootstrap's members[], which is what gives an avatar its ring, tooltip, and team preset.

type Product

type Product struct {
	Key   string `json:"key"`
	Label string `json:"label"`
}

Product is the product bucket a group maps to.

type ResolvedActor added in v0.17.0

type ResolvedActor struct {
	Slug   string `json:"slug"`
	Name   string `json:"name,omitempty"`
	Source string `json:"source"`
}

ResolvedActor is the ladder's answer: Slug is the X-Issuetap-Actor value, Name the optional X-Issuetap-Actor-Name, Source which rung produced it.

func ResolveActor added in v0.17.0

func ResolveActor(cfg *Config) (ResolvedActor, bool)

type Setting added in v0.15.2

type Setting struct {
	Path        string
	Root        string
	Description string
	Get         func(*Config) any
	Set         func(*Config, json.RawMessage) error
}

Setting is one dotted path `gadak config` may get or set. Root is the matching PUT /api/settings JSON key so a coverage test can keep the two surfaces on one schema.

func SettingByPath added in v0.15.2

func SettingByPath(path string) (Setting, bool)

SettingByPath looks up one catalog entry. Concrete ui.tokens.<axis>.<name> leaves are not enumerated in the catalog listing (that would add one row per token and bury the rest of the table); they resolve here from the four ui.tokens.<axis>.<name> templates.

func Settings added in v0.15.2

func Settings() []Setting

Settings is the editable-path catalog. Credentials are not on it.

type TokenExpiry added in v0.14.2

type TokenExpiry struct {
	State     string `json:"state"`
	DaysLeft  *int   `json:"days_left,omitempty"`
	ExpiresAt string `json:"expires_at,omitempty"`
	Source    string `json:"source,omitempty"`
	Urgent    bool   `json:"urgent,omitempty"`
	// Message is the one English warning line. Empty when there is nothing
	// to say (ok / unknown).
	Message string `json:"message,omitempty"`
}

TokenExpiry is the computed warning state for a stored API token. DaysLeft is nil when State is unknown.

func AssessTokenExpiry added in v0.14.2

func AssessTokenExpiry(now time.Time, expiresAt, source string) TokenExpiry

AssessTokenExpiry maps (now, stored expiry, source) onto a warning state. Missing or unparseable dates are unknown — there is nothing to warn from.

DaysLeft is remaining/elapsed time in whole 24h periods, truncated toward zero. now >= expiresAt is expired (including a 0-day remaining of exactly now). 15 days is still ok; 14 days is the first warning; 3 days is urgent.

func (TokenExpiry) WarningLine added in v0.14.2

func (e TokenExpiry) WarningLine() string

WarningLine is the English sentence status and sync_health surface. Empty when there is nothing to warn about.

type UIConfig added in v0.17.2

type UIConfig struct {
	Tokens *UITokens `json:"tokens,omitempty"`
	// TokensByTheme overlays Tokens for one palette. Keys are palette ids;
	// ids outside today's catalog are carried with a warning (theme ids are
	// an open set by design — ValidateTheme accepts any [a-z0-9-]+).
	TokensByTheme map[string]*UITokens `json:"tokensByTheme,omitempty"`
	// DataColors is family → key → hex. Families: label, type, status.
	DataColors map[string]map[string]string `json:"dataColors,omitempty"`
}

UIConfig is the `ui` block of config.json. Nil means "nothing overridden" and is not written.

func (*UIConfig) EffectiveTokenColors added in v0.17.2

func (u *UIConfig) EffectiveTokenColors(palette string) map[string]string

EffectiveTokenColors merges the palette-agnostic and palette-scoped overrides for one palette (theme wins). Nil maps are fine.

type UITokens added in v0.17.2

type UITokens struct {
	Colors  map[string]string `json:"colors,omitempty"`
	Spacing map[string]string `json:"spacing,omitempty"`
	Layout  map[string]string `json:"layout,omitempty"`
	Type    map[string]string `json:"type,omitempty"`
}

UITokens is one token override set. Colors are palette-scoped hex overrides; the dimension axes (spacing, layout, type) are palette-agnostic CSS lengths that ride the same wrapper but never the per-theme overlay. Values are bare token names ("accent", "row") or CSS variable names ("--color-accent", "--spacing-row") — tokencheck normalizes both.

Directories

Path Synopsis
Dimension-token validation (dim-token chunk 1) — the sibling of the color side of this package.
Dimension-token validation (dim-token chunk 1) — the sibling of the color side of this package.

Jump to

Keyboard shortcuts

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