feat(config): add XDG platform path helpers

Add ConfigDir, DataDir, CacheDir, DefaultDBPath, DefaultConfigPath to
pkg/config — following XDG Base Directory spec on Linux, and the stdlib
os.UserConfigDir / os.UserCacheDir platform conventions on macOS/Windows.

All four directory helpers call MkdirAll(0700) so callers never need to
pre-create directories. DefaultDBPath returns <DataDir>/picoclaw.db and
DefaultConfigPath returns <ConfigDir>/config.json.

Wire into cmd/picoclaw:
- getConfigPath() now delegates to config.DefaultConfigPath() with a
  legacy fallback (~/.picoclaw/config.json) for systems where XDG fails
- setupSecureBus() resolves secrets.json via config.ConfigDir() with the
  same fallback, removing the hard-coded os.UserHomeDir + join pattern

Extracted and extended from Budgetsmith internal/infrastructure/config/paths.go.
This commit is contained in:
ZanzyTHEbar 2026-02-19 12:44:09 +00:00
parent 0db51c3b63
commit f4ec7103fe
2 changed files with 86 additions and 4 deletions

View file

@ -1533,6 +1533,10 @@ func daemonStatus() {
}
func getConfigPath() string {
if p, err := config.DefaultConfigPath(); err == nil {
return p
}
// Fallback: legacy ~/.picoclaw/config.json for systems where XDG resolution fails.
home, _ := os.UserHomeDir()
return filepath.Join(home, ".picoclaw", "config.json")
}
@ -1568,13 +1572,18 @@ func loadConfig() (*config.Config, error) {
// returns without attaching the bus — the agent continues in direct-execution mode.
// The returned closer must be called on shutdown when the bus is non-nil.
func setupSecureBus(agentLoop *agent.AgentLoop) (closer func()) {
home, err := os.UserHomeDir()
cfgDir, err := config.ConfigDir()
if err != nil {
logger.WarnC("itr", "SecureBus: cannot determine home dir — running without ITR")
// Fallback to ~/.picoclaw if XDG resolution fails.
home, herr := os.UserHomeDir()
if herr != nil {
logger.WarnC("itr", "SecureBus: cannot determine config dir — running without ITR")
return func() {}
}
cfgDir = filepath.Join(home, ".picoclaw")
}
secretsPath := filepath.Join(home, ".picoclaw", "secrets.json")
secretsPath := filepath.Join(cfgDir, "secrets.json")
// Use NoopKeyring by default; EnvKeyring when PICOCLAW_MASTER_KEY is set.
var keyring security.KeyringProvider

View file

@ -602,3 +602,76 @@ func expandHome(path string) string {
}
return path
}
// ─── XDG / platform path helpers ─────────────────────────────────────────────
const appName = "picoclaw"
// ConfigDir returns the platform-appropriate user configuration directory for
// picoclaw, following XDG Base Directory spec on Linux
// (~/.config/picoclaw), Library/Application Support on macOS, and
// %AppData%\picoclaw on Windows. The directory is created if it does not exist.
func ConfigDir() (string, error) {
base, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("resolve user config dir: %w", err)
}
dir := filepath.Join(base, appName)
if err := os.MkdirAll(dir, 0o700); err != nil {
return "", fmt.Errorf("create config dir %q: %w", dir, err)
}
return dir, nil
}
// DataDir returns the platform-appropriate user data directory for picoclaw.
// On Linux this is ~/.local/share/picoclaw (XDG_DATA_HOME); on macOS and
// Windows it falls back to the same base as ConfigDir. The directory is
// created if it does not exist.
func DataDir() (string, error) {
// XDG_DATA_HOME is Linux-standard; os.UserHomeDir gives us the root we need.
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home dir: %w", err)
}
dir := filepath.Join(home, ".local", "share", appName)
if err := os.MkdirAll(dir, 0o700); err != nil {
return "", fmt.Errorf("create data dir %q: %w", dir, err)
}
return dir, nil
}
// CacheDir returns the platform-appropriate user cache directory for picoclaw
// (XDG_CACHE_HOME on Linux → ~/.cache/picoclaw). The directory is created if
// it does not exist.
func CacheDir() (string, error) {
base, err := os.UserCacheDir()
if err != nil {
return "", fmt.Errorf("resolve user cache dir: %w", err)
}
dir := filepath.Join(base, appName)
if err := os.MkdirAll(dir, 0o700); err != nil {
return "", fmt.Errorf("create cache dir %q: %w", dir, err)
}
return dir, nil
}
// DefaultDBPath returns the canonical SQLite database path inside DataDir.
// Callers that want to override this should check for a CLI flag or the
// PICOCLAW_DB_PATH environment variable before falling back to this value.
func DefaultDBPath() (string, error) {
dataDir, err := DataDir()
if err != nil {
return "", err
}
return filepath.Join(dataDir, appName+".db"), nil
}
// DefaultConfigPath returns the path to the primary JSON config file inside
// ConfigDir (picoclaw/config.json).
func DefaultConfigPath() (string, error) {
cfgDir, err := ConfigDir()
if err != nil {
return "", err
}
return filepath.Join(cfgDir, "config.json"), nil
}