config

package
v0.16.1 Latest Latest
Warning

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

Go to latest
Published: Aug 20, 2026 License: Apache-2.0 Imports: 12 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 (
	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 (
	// 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, 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 FeatureNames = []string{"feed", "push", "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 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 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 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 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 returns the active profile name ("" for the default one).

func Profiles

func Profiles() ([]string, error)

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

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 --profile flag, which wins over the env var.

func SettingPaths added in v0.15.2

func SettingPaths() []string

SettingPaths returns catalog paths in catalog order.

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 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.

Types

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"`

	// 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"`

	// 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"`
	// 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/push/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"`

	// 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) 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) 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) 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.

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) 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 tightened to 0700; chmod failures are logged only.

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 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 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.

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.

Jump to

Keyboard shortcuts

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