Merge pull request #75 from ekoeppen/xdg-state-home
Use XDG_STATE_HOME on all platforms
8 files · +46 -19
M README.md +5 -4
| @@ -73,9 +73,10 @@ The easiest way is to just run `zot` and type `/login`. The TUI opens even witho | |||
| 73 | 73 | 3. `$ZOT_HOME/auth.json` (API key or OAuth token; mode 0600) | |
| 74 | 74 | ||
| 75 | 75 | `$ZOT_HOME` defaults to: | |
| 76 | - | - macOS: `~/Library/Application Support/zot` | |
| 77 | - | - Linux: `$XDG_STATE_HOME/zot` or `~/.local/state/zot` | |
| 78 | - | - Windows: `%LOCALAPPDATA%\zot` | |
| 76 | + | - All platforms: `$XDG_STATE_HOME/zot` when `XDG_STATE_HOME` is set | |
| 77 | + | - macOS fallback: `~/Library/Application Support/zot` | |
| 78 | + | - Linux fallback: `~/.local/state/zot` | |
| 79 | + | - Windows fallback: `%LOCALAPPDATA%\zot` | |
| 79 | 80 | ||
| 80 | 81 | ### `/login` flow | |
| 81 | 82 | ||
| @@ -392,7 +393,7 @@ No configuration is required — the candidate list is built dynamically from yo | |||
| 392 | 393 | ||
| 393 | 394 | ### Custom models | |
| 394 | 395 | ||
| 395 | - | Place a `models.json` in `$ZOT_HOME` (macOS: `~/Library/Application Support/zot/`, Linux: `~/.local/state/zot/`) to add models that aren't in the baked-in catalog or to override existing entries: | |
| 396 | + | Place a `models.json` in `$ZOT_HOME` (`$XDG_STATE_HOME/zot/` when set, otherwise the platform default above) to add models that aren't in the baked-in catalog or to override existing entries: | |
| 396 | 397 | ||
| 397 | 398 | ```json | |
| 398 | 399 | { | |
| @@ -73,9 +73,10 @@ The easiest way is to just run `zot` and type `/login`. The TUI opens even witho | |||||
| 73 | 3. `$ZOT_HOME/auth.json` (API key or OAuth token; mode 0600) | 73 | 3. `$ZOT_HOME/auth.json` (API key or OAuth token; mode 0600) | ||
| 74 | 74 | ||||
| 75 | `$ZOT_HOME` defaults to: | 75 | `$ZOT_HOME` defaults to: | ||
| 76 | - | - macOS: `~/Library/Application Support/zot` | 76 | + | - All platforms: `$XDG_STATE_HOME/zot` when `XDG_STATE_HOME` is set |
| 77 | - | - Linux: `$XDG_STATE_HOME/zot` or `~/.local/state/zot` | 77 | + | - macOS fallback: `~/Library/Application Support/zot` |
| 78 | - | - Windows: `%LOCALAPPDATA%\zot` | 78 | + | - Linux fallback: `~/.local/state/zot` |
| 79 | + | - Windows fallback: `%LOCALAPPDATA%\zot` | |||
| 79 | 80 | ||||
| 80 | ### `/login` flow | 81 | ### `/login` flow | ||
| 81 | 82 | ||||
| @@ -392,7 +393,7 @@ No configuration is required — the candidate list is built dynamically from yo | |||||
| 392 | 393 | ||||
| 393 | ### Custom models | 394 | ### Custom models | ||
| 394 | 395 | ||||
| 395 | - | Place a `models.json` in `$ZOT_HOME` (macOS: `~/Library/Application Support/zot/`, Linux: `~/.local/state/zot/`) to add models that aren't in the baked-in catalog or to override existing entries: | 396 | + | Place a `models.json` in `$ZOT_HOME` (`$XDG_STATE_HOME/zot/` when set, otherwise the platform default above) to add models that aren't in the baked-in catalog or to override existing entries: |
| 396 | 397 | ||||
| 397 | ```json | 398 | ```json | ||
| 398 | { | 399 | { | ||
M docs/extensions.md +3 -2
| @@ -97,8 +97,9 @@ zot scans two directories on startup, in this order: | |||
| 97 | 97 | 2. **Global**: `$ZOT_HOME/extensions/<name>/extension.json` | |
| 98 | 98 | ||
| 99 | 99 | A project-local extension with the same name wins over a global one. | |
| 100 | - | On macOS `$ZOT_HOME` defaults to `~/Library/Application Support/zot/`; | |
| 101 | - | on Linux it's `$XDG_STATE_HOME/zot` or `~/.local/state/zot`. | |
| 100 | + | When `XDG_STATE_HOME` is set on any platform, `$ZOT_HOME` defaults to | |
| 101 | + | `$XDG_STATE_HOME/zot`. Otherwise it defaults to `~/Library/Application Support/zot/` | |
| 102 | + | on macOS, `~/.local/state/zot` on Linux, or `%LOCALAPPDATA%\zot` on Windows. | |
| 102 | 103 | ||
| 103 | 104 | Because each extension owns its own directory, the recommended place | |
| 104 | 105 | for extension state is inside that directory itself (for example | |
| @@ -97,8 +97,9 @@ zot scans two directories on startup, in this order: | |||||
| 97 | 2. **Global**: `$ZOT_HOME/extensions/<name>/extension.json` | 97 | 2. **Global**: `$ZOT_HOME/extensions/<name>/extension.json` | ||
| 98 | 98 | ||||
| 99 | A project-local extension with the same name wins over a global one. | 99 | A project-local extension with the same name wins over a global one. | ||
| 100 | - | On macOS `$ZOT_HOME` defaults to `~/Library/Application Support/zot/`; | 100 | + | When `XDG_STATE_HOME` is set on any platform, `$ZOT_HOME` defaults to |
| 101 | - | on Linux it's `$XDG_STATE_HOME/zot` or `~/.local/state/zot`. | 101 | + | `$XDG_STATE_HOME/zot`. Otherwise it defaults to `~/Library/Application Support/zot/` |
| 102 | + | on macOS, `~/.local/state/zot` on Linux, or `%LOCALAPPDATA%\zot` on Windows. | |||
| 102 | 103 | ||||
| 103 | Because each extension owns its own directory, the recommended place | 104 | Because each extension owns its own directory, the recommended place | ||
| 104 | for extension state is inside that directory itself (for example | 105 | for extension state is inside that directory itself (for example | ||
M docs/skills.md +3 -2
| @@ -64,8 +64,9 @@ skill ecosystem works in zot unchanged. Drop your existing | |||
| 64 | 64 | `.claude/skills/` or `.agents/skills/` directories into a project and | |
| 65 | 65 | zot will pick them up. | |
| 66 | 66 | ||
| 67 | - | `$ZOT_HOME` defaults to `~/Library/Application Support/zot/` on macOS, | |
| 68 | - | `$XDG_STATE_HOME/zot` on Linux, `%LOCALAPPDATA%\zot` on Windows. | |
| 67 | + | When `XDG_STATE_HOME` is set on any platform, `$ZOT_HOME` defaults to | |
| 68 | + | `$XDG_STATE_HOME/zot`. Otherwise it defaults to `~/Library/Application Support/zot/` | |
| 69 | + | on macOS, `~/.local/state/zot` on Linux, or `%LOCALAPPDATA%\zot` on Windows. | |
| 69 | 70 | ||
| 70 | 71 | ## Inspecting installed skills | |
| 71 | 72 | ||
| @@ -64,8 +64,9 @@ skill ecosystem works in zot unchanged. Drop your existing | |||||
| 64 | `.claude/skills/` or `.agents/skills/` directories into a project and | 64 | `.claude/skills/` or `.agents/skills/` directories into a project and | ||
| 65 | zot will pick them up. | 65 | zot will pick them up. | ||
| 66 | 66 | ||||
| 67 | - | `$ZOT_HOME` defaults to `~/Library/Application Support/zot/` on macOS, | 67 | + | When `XDG_STATE_HOME` is set on any platform, `$ZOT_HOME` defaults to |
| 68 | - | `$XDG_STATE_HOME/zot` on Linux, `%LOCALAPPDATA%\zot` on Windows. | 68 | + | `$XDG_STATE_HOME/zot`. Otherwise it defaults to `~/Library/Application Support/zot/` |
| 69 | + | on macOS, `~/.local/state/zot` on Linux, or `%LOCALAPPDATA%\zot` on Windows. | |||
| 69 | 70 | ||||
| 70 | ## Inspecting installed skills | 71 | ## Inspecting installed skills | ||
| 71 | 72 | ||||
M examples/extensions/mcp-bridge/Makefile +3 -3
| @@ -4,10 +4,10 @@ BIN := $(notdir $(CURDIR)) | |||
| 4 | 4 | # Must match "name" in extension.json (slash command/logical extension name). | |
| 5 | 5 | EXT := mcp | |
| 6 | 6 | ZOT_HOME ?= $(shell \ | |
| 7 | - | if [ "$$(uname -s)" = "Darwin" ]; then \ | |
| 8 | - | printf '%s/Library/Application Support/zot' "$$HOME"; \ | |
| 9 | - | elif [ -n "$$XDG_STATE_HOME" ]; then \ | |
| 7 | + | if [ -n "$$XDG_STATE_HOME" ]; then \ | |
| 10 | 8 | printf '%s/zot' "$$XDG_STATE_HOME"; \ | |
| 9 | + | elif [ "$$(uname -s)" = "Darwin" ]; then \ | |
| 10 | + | printf '%s/Library/Application Support/zot' "$$HOME"; \ | |
| 11 | 11 | else \ | |
| 12 | 12 | printf '%s/.local/state/zot' "$$HOME"; \ | |
| 13 | 13 | fi) | |
| @@ -4,10 +4,10 @@ BIN := $(notdir $(CURDIR)) | |||||
| 4 | # Must match "name" in extension.json (slash command/logical extension name). | 4 | # Must match "name" in extension.json (slash command/logical extension name). | ||
| 5 | EXT := mcp | 5 | EXT := mcp | ||
| 6 | ZOT_HOME ?= $(shell \ | 6 | ZOT_HOME ?= $(shell \ | ||
| 7 | - | if [ "$$(uname -s)" = "Darwin" ]; then \ | 7 | + | if [ -n "$$XDG_STATE_HOME" ]; then \ |
| 8 | - | printf '%s/Library/Application Support/zot' "$$HOME"; \ | |||
| 9 | - | elif [ -n "$$XDG_STATE_HOME" ]; then \ | |||
| 10 | printf '%s/zot' "$$XDG_STATE_HOME"; \ | 8 | printf '%s/zot' "$$XDG_STATE_HOME"; \ | ||
| 9 | + | elif [ "$$(uname -s)" = "Darwin" ]; then \ | |||
| 10 | + | printf '%s/Library/Application Support/zot' "$$HOME"; \ | |||
| 11 | else \ | 11 | else \ | ||
| 12 | printf '%s/.local/state/zot' "$$HOME"; \ | 12 | printf '%s/.local/state/zot' "$$HOME"; \ | ||
| 13 | fi) | 13 | fi) | ||
M examples/extensions/mcp-bridge/README.md +1 -1
| @@ -61,7 +61,7 @@ Config files are loaded from two locations (project overrides global per-server) | |||
| 61 | 61 | ||
| 62 | 62 | | Location | Scope | | |
| 63 | 63 | |---|---| | |
| 64 | - | | `$ZOT_HOME/mcp.json` | Global (macOS: `~/Library/Application Support/zot/mcp.json`) | | |
| 64 | + | | `$ZOT_HOME/mcp.json` | Global (`$XDG_STATE_HOME/zot/mcp.json` when `XDG_STATE_HOME` is set) | | |
| 65 | 65 | | `.zot/mcp.json` | Project-level (in your project root) | | |
| 66 | 66 | ||
| 67 | 67 | ### Config Format | |
| @@ -61,7 +61,7 @@ Config files are loaded from two locations (project overrides global per-server) | |||||
| 61 | 61 | ||||
| 62 | | Location | Scope | | 62 | | Location | Scope | | ||
| 63 | |---|---| | 63 | |---|---| | ||
| 64 | - | | `$ZOT_HOME/mcp.json` | Global (macOS: `~/Library/Application Support/zot/mcp.json`) | | 64 | + | | `$ZOT_HOME/mcp.json` | Global (`$XDG_STATE_HOME/zot/mcp.json` when `XDG_STATE_HOME` is set) | |
| 65 | | `.zot/mcp.json` | Project-level (in your project root) | | 65 | | `.zot/mcp.json` | Project-level (in your project root) | | ||
| 66 | 66 | ||||
| 67 | ### Config Format | 67 | ### Config Format | ||
M examples/extensions/mcp-bridge/config.go +4 -4
| @@ -3,7 +3,7 @@ | |||
| 3 | 3 | // Reads standard MCP config files (same format as Claude Desktop, Cursor, etc.) | |
| 4 | 4 | // from two locations: | |
| 5 | 5 | // | |
| 6 | - | // 1. Global: $ZOT_HOME/mcp.json (macOS: ~/Library/Application Support/zot/mcp.json) | |
| 6 | + | // 1. Global: $ZOT_HOME/mcp.json | |
| 7 | 7 | // 2. Project: .zot/mcp.json (in the current working directory) | |
| 8 | 8 | // | |
| 9 | 9 | // Project config overrides global config per-server (shallow merge). | |
| @@ -47,6 +47,9 @@ func zotHome() string { | |||
| 47 | 47 | if h := os.Getenv("ZOT_HOME"); h != "" { | |
| 48 | 48 | return h | |
| 49 | 49 | } | |
| 50 | + | if xdg := os.Getenv("XDG_STATE_HOME"); xdg != "" { | |
| 51 | + | return filepath.Join(xdg, "zot") | |
| 52 | + | } | |
| 50 | 53 | switch runtime.GOOS { | |
| 51 | 54 | case "darwin": | |
| 52 | 55 | home, _ := os.UserHomeDir() | |
| @@ -54,9 +57,6 @@ func zotHome() string { | |||
| 54 | 57 | case "windows": | |
| 55 | 58 | return filepath.Join(os.Getenv("APPDATA"), "zot") | |
| 56 | 59 | default: // linux, freebsd, etc. | |
| 57 | - | if xdg := os.Getenv("XDG_STATE_HOME"); xdg != "" { | |
| 58 | - | return filepath.Join(xdg, "zot") | |
| 59 | - | } | |
| 60 | 60 | home, _ := os.UserHomeDir() | |
| 61 | 61 | return filepath.Join(home, ".local", "state", "zot") | |
| 62 | 62 | } | |
| @@ -3,7 +3,7 @@ | |||||
| 3 | // Reads standard MCP config files (same format as Claude Desktop, Cursor, etc.) | 3 | // Reads standard MCP config files (same format as Claude Desktop, Cursor, etc.) | ||
| 4 | // from two locations: | 4 | // from two locations: | ||
| 5 | // | 5 | // | ||
| 6 | - | // 1. Global: $ZOT_HOME/mcp.json (macOS: ~/Library/Application Support/zot/mcp.json) | 6 | + | // 1. Global: $ZOT_HOME/mcp.json |
| 7 | // 2. Project: .zot/mcp.json (in the current working directory) | 7 | // 2. Project: .zot/mcp.json (in the current working directory) | ||
| 8 | // | 8 | // | ||
| 9 | // Project config overrides global config per-server (shallow merge). | 9 | // Project config overrides global config per-server (shallow merge). | ||
| @@ -47,6 +47,9 @@ func zotHome() string { | |||||
| 47 | if h := os.Getenv("ZOT_HOME"); h != "" { | 47 | if h := os.Getenv("ZOT_HOME"); h != "" { | ||
| 48 | return h | 48 | return h | ||
| 49 | } | 49 | } | ||
| 50 | + | if xdg := os.Getenv("XDG_STATE_HOME"); xdg != "" { | |||
| 51 | + | return filepath.Join(xdg, "zot") | |||
| 52 | + | } | |||
| 50 | switch runtime.GOOS { | 53 | switch runtime.GOOS { | ||
| 51 | case "darwin": | 54 | case "darwin": | ||
| 52 | home, _ := os.UserHomeDir() | 55 | home, _ := os.UserHomeDir() | ||
| @@ -54,9 +57,6 @@ func zotHome() string { | |||||
| 54 | case "windows": | 57 | case "windows": | ||
| 55 | return filepath.Join(os.Getenv("APPDATA"), "zot") | 58 | return filepath.Join(os.Getenv("APPDATA"), "zot") | ||
| 56 | default: // linux, freebsd, etc. | 59 | default: // linux, freebsd, etc. | ||
| 57 | - | if xdg := os.Getenv("XDG_STATE_HOME"); xdg != "" { | |||
| 58 | - | return filepath.Join(xdg, "zot") | |||
| 59 | - | } | |||
| 60 | home, _ := os.UserHomeDir() | 60 | home, _ := os.UserHomeDir() | ||
| 61 | return filepath.Join(home, ".local", "state", "zot") | 61 | return filepath.Join(home, ".local", "state", "zot") | ||
| 62 | } | 62 | } | ||
M packages/agent/config.go +3 -3
| @@ -108,6 +108,9 @@ func ZotHome() string { | |||
| 108 | 108 | if v := os.Getenv("ZOT_HOME"); v != "" { | |
| 109 | 109 | return v | |
| 110 | 110 | } | |
| 111 | + | if v := os.Getenv("XDG_STATE_HOME"); v != "" { | |
| 112 | + | return filepath.Join(v, "zot") | |
| 113 | + | } | |
| 111 | 114 | switch runtime.GOOS { | |
| 112 | 115 | case "darwin": | |
| 113 | 116 | if home, err := os.UserHomeDir(); err == nil { | |
| @@ -118,9 +121,6 @@ func ZotHome() string { | |||
| 118 | 121 | return filepath.Join(v, "zot") | |
| 119 | 122 | } | |
| 120 | 123 | } | |
| 121 | - | if v := os.Getenv("XDG_STATE_HOME"); v != "" { | |
| 122 | - | return filepath.Join(v, "zot") | |
| 123 | - | } | |
| 124 | 124 | if home, err := os.UserHomeDir(); err == nil { | |
| 125 | 125 | return filepath.Join(home, ".local", "state", "zot") | |
| 126 | 126 | } | |
| @@ -108,6 +108,9 @@ func ZotHome() string { | |||||
| 108 | if v := os.Getenv("ZOT_HOME"); v != "" { | 108 | if v := os.Getenv("ZOT_HOME"); v != "" { | ||
| 109 | return v | 109 | return v | ||
| 110 | } | 110 | } | ||
| 111 | + | if v := os.Getenv("XDG_STATE_HOME"); v != "" { | |||
| 112 | + | return filepath.Join(v, "zot") | |||
| 113 | + | } | |||
| 111 | switch runtime.GOOS { | 114 | switch runtime.GOOS { | ||
| 112 | case "darwin": | 115 | case "darwin": | ||
| 113 | if home, err := os.UserHomeDir(); err == nil { | 116 | if home, err := os.UserHomeDir(); err == nil { | ||
| @@ -118,9 +121,6 @@ func ZotHome() string { | |||||
| 118 | return filepath.Join(v, "zot") | 121 | return filepath.Join(v, "zot") | ||
| 119 | } | 122 | } | ||
| 120 | } | 123 | } | ||
| 121 | - | if v := os.Getenv("XDG_STATE_HOME"); v != "" { | |||
| 122 | - | return filepath.Join(v, "zot") | |||
| 123 | - | } | |||
| 124 | if home, err := os.UserHomeDir(); err == nil { | 124 | if home, err := os.UserHomeDir(); err == nil { | ||
| 125 | return filepath.Join(home, ".local", "state", "zot") | 125 | return filepath.Join(home, ".local", "state", "zot") | ||
| 126 | } | 126 | } | ||
A packages/agent/config_home_test.go +24 -0
| @@ -0,0 +1,24 @@ | |||
| 1 | + | package agent | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "path/filepath" | |
| 5 | + | "testing" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | func TestZotHomePrefersExplicitHomeOverXDGStateHome(t *testing.T) { | |
| 9 | + | t.Setenv("ZOT_HOME", filepath.Join("explicit", "zot")) | |
| 10 | + | t.Setenv("XDG_STATE_HOME", filepath.Join("xdg", "state")) | |
| 11 | + | ||
| 12 | + | if got, want := ZotHome(), filepath.Join("explicit", "zot"); got != want { | |
| 13 | + | t.Fatalf("ZotHome() = %q, want %q", got, want) | |
| 14 | + | } | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | func TestZotHomeUsesXDGStateHome(t *testing.T) { | |
| 18 | + | t.Setenv("ZOT_HOME", "") | |
| 19 | + | t.Setenv("XDG_STATE_HOME", filepath.Join("xdg", "state")) | |
| 20 | + | ||
| 21 | + | if got, want := ZotHome(), filepath.Join("xdg", "state", "zot"); got != want { | |
| 22 | + | t.Fatalf("ZotHome() = %q, want %q", got, want) | |
| 23 | + | } | |
| 24 | + | } | |
| @@ -0,0 +1,24 @@ | |||||
| 1 | + | package agent | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "path/filepath" | |||
| 5 | + | "testing" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | func TestZotHomePrefersExplicitHomeOverXDGStateHome(t *testing.T) { | |||
| 9 | + | t.Setenv("ZOT_HOME", filepath.Join("explicit", "zot")) | |||
| 10 | + | t.Setenv("XDG_STATE_HOME", filepath.Join("xdg", "state")) | |||
| 11 | + | ||||
| 12 | + | if got, want := ZotHome(), filepath.Join("explicit", "zot"); got != want { | |||
| 13 | + | t.Fatalf("ZotHome() = %q, want %q", got, want) | |||
| 14 | + | } | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | func TestZotHomeUsesXDGStateHome(t *testing.T) { | |||
| 18 | + | t.Setenv("ZOT_HOME", "") | |||
| 19 | + | t.Setenv("XDG_STATE_HOME", filepath.Join("xdg", "state")) | |||
| 20 | + | ||||
| 21 | + | if got, want := ZotHome(), filepath.Join("xdg", "state", "zot"); got != want { | |||
| 22 | + | t.Fatalf("ZotHome() = %q, want %q", got, want) | |||
| 23 | + | } | |||
| 24 | + | } | |||