diff --git a/templates/cli/internal/app/install.go.twig b/templates/cli/internal/app/install.go.twig index 3a67a53e63..f03069689d 100644 --- a/templates/cli/internal/app/install.go.twig +++ b/templates/cli/internal/app/install.go.twig @@ -7,6 +7,7 @@ import ( "runtime" "strings" + "github.com/{{ sdk.gitUserName }}/{{ sdk.gitRepoName | caseDash }}/internal/config" "github.com/{{ sdk.gitUserName }}/{{ sdk.gitRepoName | caseDash }}/internal/update" ) @@ -165,7 +166,7 @@ func UpdateChecker() *update.Checker { return nil } - home, err := os.UserHomeDir() + directory, err := config.GlobalDirectory(ExecutableName) if err != nil { return nil } @@ -174,7 +175,7 @@ func UpdateChecker() *update.Checker { RegistryURL: NPMRegistryURL, PrereleaseRegistryURL: NPMPrereleaseRegistryURL, // Beside the preferences, which is already the directory this CLI owns. - CachePath: filepath.Join(home, "."+ExecutableName, "update-check.json"), + CachePath: filepath.Join(directory, "update-check.json"), Current: Version, } } diff --git a/templates/cli/internal/cmd/completion_test.go b/templates/cli/internal/cmd/completion_test.go index 66524c499f..25819766ac 100644 --- a/templates/cli/internal/cmd/completion_test.go +++ b/templates/cli/internal/cmd/completion_test.go @@ -23,6 +23,7 @@ func setHome(t *testing.T) string { home := t.TempDir() t.Setenv("HOME", home) t.Setenv("USERPROFILE", home) + t.Setenv("XDG_CONFIG_HOME", "") return home } diff --git a/templates/cli/internal/cmd/fanout_test.go b/templates/cli/internal/cmd/fanout_test.go index 4d007a8d8d..413d38748c 100644 --- a/templates/cli/internal/cmd/fanout_test.go +++ b/templates/cli/internal/cmd/fanout_test.go @@ -118,6 +118,7 @@ func TestAllSubcommandsNeverPrompt(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) t.Setenv("USERPROFILE", home) + t.Setenv("XDG_CONFIG_HOME", "") inDirectory(t, t.TempDir()) root := NewRootCommand() diff --git a/templates/cli/internal/cmd/generate_test.go b/templates/cli/internal/cmd/generate_test.go index 6520d341dd..5ebe9b470b 100644 --- a/templates/cli/internal/cmd/generate_test.go +++ b/templates/cli/internal/cmd/generate_test.go @@ -44,6 +44,7 @@ func TestGenerateEndpointFallsBackToTheDefault(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) t.Setenv("USERPROFILE", home) + t.Setenv("XDG_CONFIG_HOME", "") if got := resolveGenerateEndpoint(""); got != config.DefaultEndpoint { t.Errorf("endpoint = %q, want %q", got, config.DefaultEndpoint) diff --git a/templates/cli/internal/config/config_test.go b/templates/cli/internal/config/config_test.go index f6bf0a4a01..8405e2e0e8 100644 --- a/templates/cli/internal/config/config_test.go +++ b/templates/cli/internal/config/config_test.go @@ -394,3 +394,93 @@ func TestMigrateLegacySessionIgnoresAPartialOrModernFile(t *testing.T) { }) } } + +func TestGlobalPathFollowsXDGConfigHome(t *testing.T) { + cases := []struct { + name string + legacy bool + missing bool + xdg func(home string) string + want func(home, xdg string) string + }{ + { + name: "existing home directory wins over XDG", + legacy: true, + xdg: func(home string) string { return filepath.Join(home, "xdg") }, + want: func(home, _ string) string { return filepath.Join(home, ".appwrite", "prefs.json") }, + }, + { + name: "absolute XDG_CONFIG_HOME", + xdg: func(home string) string { return filepath.Join(home, "xdg") }, + want: func(_, xdg string) string { return filepath.Join(xdg, "appwrite", "prefs.json") }, + }, + { + name: "missing XDG_CONFIG_HOME directory", + missing: true, + xdg: func(home string) string { return filepath.Join(home, "xdg") }, + want: func(_, xdg string) string { return filepath.Join(xdg, "appwrite", "prefs.json") }, + }, + { + name: "unset XDG_CONFIG_HOME", + xdg: func(string) string { return "" }, + want: func(home, _ string) string { return filepath.Join(home, ".appwrite", "prefs.json") }, + }, + { + name: "relative XDG_CONFIG_HOME", + xdg: func(string) string { return filepath.Join("relative", "config") }, + want: func(home, _ string) string { return filepath.Join(home, ".appwrite", "prefs.json") }, + }, + } + + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + xdg := test.xdg(home) + t.Setenv("XDG_CONFIG_HOME", xdg) + + if filepath.IsAbs(xdg) && !test.missing { + if err := os.MkdirAll(xdg, 0o700); err != nil { + t.Fatal(err) + } + } + if test.legacy { + if err := os.MkdirAll(filepath.Join(home, ".appwrite"), 0o700); err != nil { + t.Fatal(err) + } + } + + got, err := GlobalPath("appwrite") + if err != nil { + t.Fatal(err) + } + if want := test.want(home, xdg); got != want { + t.Errorf("GlobalPath = %q, want %q", got, want) + } + }) + } +} + +func TestGlobalWriteCreatesTheXDGDirectory(t *testing.T) { + home := t.TempDir() + xdg := filepath.Join(t.TempDir(), "config") + t.Setenv("HOME", home) + t.Setenv("USERPROFILE", home) + t.Setenv("XDG_CONFIG_HOME", xdg) + + path, err := GlobalPath("appwrite") + if err != nil { + t.Fatal(err) + } + if err := LoadGlobal(path).Write(); err != nil { + t.Fatal(err) + } + + if _, err := os.Stat(filepath.Join(xdg, "appwrite", "prefs.json")); err != nil { + t.Errorf("prefs.json was not written under XDG_CONFIG_HOME: %v", err) + } + if _, err := os.Stat(filepath.Join(home, ".appwrite")); !os.IsNotExist(err) { + t.Errorf("the home directory was created alongside XDG_CONFIG_HOME: %v", err) + } +} diff --git a/templates/cli/internal/config/global.go b/templates/cli/internal/config/global.go index a79c53aeaa..e1bc37b98a 100644 --- a/templates/cli/internal/config/global.go +++ b/templates/cli/internal/config/global.go @@ -52,7 +52,7 @@ var ignoredAttributes = map[string]bool{ PreferenceClientID: true, } -// Global is the user-level preferences file, `~/.appwrite/prefs.json`. +// Global is the user-level preferences file, `prefs.json` in GlobalDirectory. type Global struct { path string data *Object @@ -67,12 +67,30 @@ type Session struct { // GlobalPath returns the default preferences path for the current user. func GlobalPath(executableName string) (string, error) { + directory, err := GlobalDirectory(executableName) + if err != nil { + return "", err + } + + return filepath.Join(directory, "prefs.json"), nil +} + +func GlobalDirectory(executableName string) (string, error) { home, err := homeDir() if err != nil { return "", err } - return filepath.Join(home, "."+executableName, "prefs.json"), nil + legacy := filepath.Join(home, "."+executableName) + if info, err := os.Stat(legacy); err == nil && info.IsDir() { + return legacy, nil + } + + if xdg := os.Getenv("XDG_CONFIG_HOME"); filepath.IsAbs(xdg) { + return filepath.Join(xdg, executableName), nil + } + + return legacy, nil } // LoadGlobal reads the preferences file. A missing or unparseable file yields diff --git a/tests/e2e/languages/cli/main.go b/tests/e2e/languages/cli/main.go index e98ca79f54..d6d2687158 100644 --- a/tests/e2e/languages/cli/main.go +++ b/tests/e2e/languages/cli/main.go @@ -115,6 +115,7 @@ func (h *harness) command(args ...string) *exec.Cmd { command.Env = append(os.Environ(), "HOME="+h.home, "USERPROFILE="+h.home, + "XDG_CONFIG_HOME="+filepath.Join(h.home, ".config"), ) return command