Files
zero/internal/config/paths.go
smallnestandsmallnest 027a8f2768 fix(config): use ~/.config on macOS and enter setup when no provider (#371) (#372)
On macOS, DefaultUserConfigPath used Go's os.UserConfigDir which returns
~/Library/Application Support, while users (and Claude Code) place config
at ~/.config/zero/config.json. The user config was never read, leaving
activeProvider empty and the launch failing with `active provider "" not
found` on a fresh `go run cmd/zero setup`.

- Add config.UserConfigDir(): on darwin, resolve to ~/.config (honoring
  XDG_CONFIG_HOME), matching Linux and Claude Code. DefaultUserConfigPath,
  the TUI user-commands dir, and specialist manifests all route through it.
- Add config.ErrNoActiveProvider sentinel; the interactive TUI now drops
  into the setup wizard when resolve fails solely due to a missing/
  unresolvable active provider, instead of exiting fatally. Other resolve
  errors (malformed JSON, directory conflicts) remain fatal.

Note: `zero setup`/`zero auth` already MkdirAll the config dir before
writing, so the directory is auto-created on first write.

Co-authored-by: smallnest <smallnest@gmail>
2026-07-02 20:03:44 +08:00

76 lines
2.1 KiB
Go

package config
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
)
// DefaultResolveOptions builds config resolution inputs from the local process
// environment and workspace.
func DefaultResolveOptions(workspaceRoot string) (ResolveOptions, error) {
userConfigPath, err := DefaultUserConfigPath()
if err != nil {
return ResolveOptions{}, err
}
userConfigPath, err = existingConfigFile(userConfigPath)
if err != nil {
return ResolveOptions{}, err
}
projectConfigPath, err := existingConfigFile(filepath.Join(workspaceRoot, ".zero", "config.json"))
if err != nil {
return ResolveOptions{}, err
}
return ResolveOptions{
UserConfigPath: userConfigPath,
ProjectConfigPath: projectConfigPath,
ProviderCommand: strings.TrimSpace(os.Getenv("ZERO_PROVIDER_COMMAND")),
}, nil
}
func DefaultUserConfigPath() (string, error) {
userConfigDir, err := UserConfigDir()
if err != nil {
return "", fmt.Errorf("resolve user config directory: %w", err)
}
return filepath.Join(userConfigDir, "zero", "config.json"), nil
}
// UserConfigDir returns the base directory Zero stores per-user config under.
// It mirrors os.UserConfigDir everywhere except macOS: there Go defaults to
// ~/Library/Application Support, but Zero deliberately uses ~/.config (XDG-style,
// matching Linux and Claude Code) so a single config path works cross-platform.
// $XDG_CONFIG_HOME still wins on macOS when set.
func UserConfigDir() (string, error) {
if runtime.GOOS == "darwin" {
if xdg := strings.TrimSpace(os.Getenv("XDG_CONFIG_HOME")); xdg != "" {
return xdg, nil
}
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err)
}
return filepath.Join(home, ".config"), nil
}
return os.UserConfigDir()
}
func existingConfigFile(path string) (string, error) {
info, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return "", nil
}
return "", fmt.Errorf("inspect config %s: %w", path, err)
}
if info.IsDir() {
return "", fmt.Errorf("config path %s is a directory, want a file", path)
}
return path, nil
}