config

package
v0.15.0 Latest Latest
Warning

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

Go to latest
Published: Aug 17, 2026 License: Apache-2.0 Imports: 10 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 (
	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.

View Source
const LocalDBFile = "local.db"

LocalDBFile is the personal-history SQLite filename (visits/searches). It sits next to the mirror and is never sent to Jira or into a snapshot.

Variables

This section is empty.

Functions

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 LocalDBPath added in v0.13.0

func LocalDBPath() (string, error)

LocalDBPath is local.db for the active profile.

func LocalDBPathFor added in v0.13.0

func LocalDBPathFor(profile string) (string, error)

LocalDBPathFor is local.db for the named profile (same directory as gadak.db).

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.

Types

type Config

type Config struct {
	// The credential and what it connects to. Token is never copied out of this file.
	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"`

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

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

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.

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.

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