userdirs: single resolver for the per-user config and cache dirs · Entire

userdirs: single resolver for the per-user config and cache dirs

e5bbf58→main·

toothbrush·1mo ago·16 files·+152 added/-145 removed

Per review discussion on #1411: config-dir resolution was implemented four times (contexts, discovery, versioncheck, tokenstore), and "contexts" was the wrong home for a concept shared by version checks, discovery caches, and token storage.

internal/entireclient/userdirs is now the only place that resolves ~/.config/entire (Config: $ENTIRE_CONFIG_DIR, test fallback, home) and ~/.cache/entire (Cache: $XDG_CACHE_HOME/entire, test fallback, home). contexts.DefaultConfigDir and discovery.DefaultCacheDir are gone; all callers use userdirs directly.

Behavior fix folded in: the file token store's default path (ENTIRE_TOKEN_STORE=file without ENTIRE_TOKEN_STORE_PATH) previously hard-coded ~/.config/entire/tokens.json, ignoring ENTIRE_CONFIG_DIR; it now derives from userdirs.Config().

tokenstore's unexported store interface joins the ireturn allow list: the local lint --fix pass (nolintlint) strips the inline directives that CI's non-fix run requires, so the inline form flaps; the config entry satisfies both.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

Sessions

279531381178View transcript

Changes

16


122 unmodified lines

123
124
125
126
127
128
129

122 unmodified lines

- github.com/entireio/cli/cmd/entire/cli/review.SynthesisProvider
        - github.com/entireio/cli/cmd/entire/cli/checkpoint.CommittedReader
        - github.com/entireio/cli/cmd/entire/cli/strategy.Strategy
        - github.com/entireio/cli/internal/entireclient/tokenstore.store
        - github.com/go-git/go-git/v6/x/plugin.Signer
        - github.com/go-git/go-git/v6/plumbing/storer.ReferenceIter
        - github.com/go-git/go-git/v6/plumbing.EncodedObject

M.golangci.yaml+1

178 unmodified lines

179
180
181
182
183
184
185
186
187
188
182
183
184
185
186
187
188
189
190
191
192
193
194
195

178 unmodified lines

cluster_cores.json, api_discovery.json), or OS keychain. The developer may be using `entire` for real while tests run.

- **In-process safety net**: `contexts.DefaultConfigDir()`, `discovery.DefaultCacheDir()`, versioncheck's config path, and the `tokenstore` default backend all detect `go test` (via `internal/testdirs`) and fall back to a throwaway per-process temp directory when their env override is unset. The fallback is shared across tests in one process — for per-test isolation still set `t.Setenv("ENTIRE_CONFIG_DIR", t.TempDir())` and `tokenstore.UseFileBackendForTesting(...)`.
- **Single resolver**: `internal/entireclient/userdirs` is the only place that resolves the per-user config dir (`userdirs.Config()`: `$ENTIRE_CONFIG_DIR` else `~/.config/entire`) and cache dir (`userdirs.Cache()`: `$XDG_CACHE_HOME/entire` else `~/.cache/entire`). Never derive these paths anywhere else.
- **In-process safety net**: `userdirs` and the `tokenstore` default backend detect `go test` (via `internal/testdirs`) and fall back to a throwaway per-process temp directory when their env override is unset. The fallback is shared across tests in one process — for per-test isolation still set `t.Setenv("ENTIRE_CONFIG_DIR", t.TempDir())` and `tokenstore.UseFileBackendForTesting(...)`.
- **Spawned binaries are NOT covered**: `testing.Testing()` is false in a subprocess. The integration and e2e TestMains set `ENTIRE_CONFIG_DIR`, `XDG_CACHE_HOME`, `ENTIRE_TOKEN_STORE=file`, `ENTIRE_TOKEN_STORE_PATH`, and

MCLAUDE.md+11/-7


5 unmodified lines

6
7
8
9
10
11
12
2 unmodified lines

15
16
17
17
18
19
20
21
16 unmodified lines

38
39
40
40
41
42
43
44
16 unmodified lines

61
62
63
63
64
65
66
67
25 unmodified lines

93
94
95
95
96
97
98
99
11 unmodified lines

111
112
113
113
114
115
116
117

5 unmodified lines

"github.com/entireio/auth-go/tokens"
    "github.com/entireio/cli/internal/entireclient/contexts"
    "github.com/entireio/cli/internal/entireclient/tokenstore"
    "github.com/entireio/cli/internal/entireclient/userdirs"

// CurrentContextToken returns the login JWT for the active context in
2 unmodified lines

// credential resolution; callers fall back to the legacy keyring entry so
// pre-contexts logins keep working until migrated.
func CurrentContextToken() (string, bool) {
    f, err := contexts.Load(contexts.DefaultConfigDir())
    f, err := contexts.Load(userdirs.Config())
    if err != nil {
        return "", false
    }
16 unmodified lines

// is exactly the one we capture the keychain slot from (separate Load +
    // Modify would race a concurrent `auth use`).
    var svc, handle string
    if err := contexts.Modify(contexts.DefaultConfigDir(), func(f *contexts.File) (bool, error) {
    if err := contexts.Modify(userdirs.Config(), func(f *contexts.File) (bool, error) {
        current := f.Find(f.CurrentContext)
        if current == nil {
            return false, nil
        }
16 unmodified lines

// the active one, so removing the current context this way also logs it out.
func RemoveContext(name string) error {
    var svc, handle string
    if err := contexts.Modify(contexts.DefaultConfigDir(), func(f *contexts.File) (bool, error) {
    if err := contexts.Modify(userdirs.Config(), func(f *contexts.File) (bool, error) {
        c := f.Find(name)
        if c == nil {
            return false, nil
        }
25 unmodified lines

// SetCurrentContext makes name the active context. Returns an error when
// no context with that name exists (a stale current pointer is a foot-gun).
func SetCurrentContext(name string) error {
    if err := contexts.Modify(contexts.DefaultConfigDir(), func(f *contexts.File) (bool, error) {
    if err := contexts.Modify(userdirs.Config(), func(f *contexts.File) (bool, error) {
        if f.Find(name) == nil {
            return false, fmt.Errorf("no login context named %q (run `entire auth contexts` to list)", name)
        }
    }
    return nil
    }); err != nil {
        return err
    }
11 unmodified lines

// Contexts returns all stored login contexts and the current context name,
// for listing/switching. Order matches on-disk order.
func Contexts() ([]*contexts.Context, string, error) {
    f, err := contexts.Load(contexts.DefaultConfigDir())
f, err := contexts.Load(userdirs.Config())
    if err != nil {
        return nil, "", fmt.Errorf("load contexts: %w", err)
    }