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
M.golangci.yaml+1
MCLAUDE.md+11/-7
cmd
entire/cli
auth
Mcontext_store.go+6/-5
Mcontexts.go+3/-2
Mcontrol_plane.go+2/-1
Mdata_api.go+2/-2
versioncheck
Mversioncheck.go+11/-39
Mversioncheck_test.go+1/-4
git-remote-entire
Mmain.go+5/-6
internal
entireclient
contexts
Mcontexts.go-21
Mcontexts_test.go-29
discovery
Mcache.go-17
tokenstore
Mtokenstore.go+6/-9
userdirs
Auserdirs.go+48
Auserdirs_test.go+52
remotehelper/replicas
Mreplicas.go+4/-3
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)
}