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
- Variables
- func ApplyAppearance(c *Config, a Appearance) error
- func ApplyConfluence(c *Config, enabled *bool, spaces []string) error
- func AttachmentDir() (string, error)
- func AttachmentDirFor(profile string) (string, error)
- func DBPath() (string, error)
- func DBPathFor(profile string) (string, error)
- func Dir() (string, error)
- func DirFor(profile string) (string, error)
- func Env(suffix string) string
- func FormatTokenTime(t time.Time) string
- func NormalizeFeatures(set map[string]bool) map[string]bool
- func ParseTokenExpiresAt(raw string) (time.Time, error)
- func Path() (string, error)
- func Profile() string
- func Profiles() ([]string, error)
- func RequireExistingProfile() error
- func SetProfile(name string)
- func SettingPaths() []string
- func ValidateDefaultIssueType(s string) (string, error)
- func ValidateDefaultIssueTypeID(s string) (string, error)
- func ValidateDefaultProject(s string) (string, error)
- func ValidateGroupQuery(q string) error
- func ValidateIntervals(syncSec, reconcileSec int) error
- func ValidateTheme(s string) (string, error)
- type Appearance
- type Config
- func (c *Config) ApplyTokenExpiry(userRaw, verifiedAt string) error
- func (c *Config) ApplyTokenExpiryIfNeeded(userRaw, verifiedAt string, tokenReplaced bool) error
- func (c *Config) ApplyVerifiedIdentity(accountID, displayName, verifiedAt string)
- func (c *Config) ClearTokenExpiry()
- func (c *Config) Directory() string
- func (c *Config) EffectiveReconcileIntervalSec() int
- func (c *Config) EffectiveSyncIntervalSec() int
- func (c *Config) EffectiveTheme() string
- func (c *Config) FieldSpecs() []FieldSpec
- func (c *Config) HasCredential() bool
- func (c *Config) IsStandalone() bool
- func (c *Config) NormalizeLegacyFields() (changed bool, shape string)
- func (c *Config) NotifyEnabled() bool
- func (c *Config) Save() error
- func (c *Config) TokenExpiryAt(now time.Time) TokenExpiry
- func (c *Config) UpdateCheckEnabled() bool
- func (c *Config) WorkspaceKind() string
- type ConfluenceConfig
- type FieldSpec
- type GroupRule
- type LinearConfig
- type Member
- type Product
- type Setting
- type TokenExpiry
Constants ¶
const ( KindConnected = "connected" KindStandalone = "standalone" )
Workspace kinds. Empty Kind on disk is connected — existing configs keep working with no rewrite.
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.
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_" )
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.
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 ¶
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
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 ¶
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 ¶
AttachmentDirFor is where attachment bytes are cached for the named profile.
func DirFor ¶
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 ¶
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
FormatTokenTime writes the on-disk / JSON form (UTC, millisecond).
func NormalizeFeatures ¶ added in v0.15.2
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
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 Profile ¶
func Profile() string
Profile returns the active profile name ("" for 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
ValidateDefaultIssueType stores an optional display label. Resolution never reads this value.
func ValidateDefaultIssueTypeID ¶ added in v0.16.0
ValidateDefaultIssueTypeID accepts empty (unset) or a Jira issue type id.
func ValidateDefaultProject ¶ added in v0.16.0
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
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
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
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 LoadFor ¶
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
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
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
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
Directory is the profile directory this Config was loaded from (LoadFor). Empty on a Config that was never loaded.
func (*Config) EffectiveReconcileIntervalSec ¶
EffectiveReconcileIntervalSec returns the reconcile interval Watch should use.
func (*Config) EffectiveSyncIntervalSec ¶
EffectiveSyncIntervalSec returns the interval Watch should use.
func (*Config) EffectiveTheme ¶ added in v0.15.2
EffectiveTheme is the UI theme id. Empty on disk means "system".
func (*Config) FieldSpecs ¶
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 ¶
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
IsStandalone reports a workspace whose origin is the in-process issuetap snapshot, not a Jira site.
func (*Config) NormalizeLegacyFields ¶ added in v0.15.0
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 ¶
NotifyEnabled is true unless the user set notify: false. Absent means on.
func (*Config) Save ¶
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 ¶
UpdateCheckEnabled is true unless the user set updateCheck: false. Absent means on.
func (*Config) WorkspaceKind ¶ added in v0.16.0
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 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
SettingByPath looks up one catalog entry.
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.