diff --git a/.changeset/quiet-states-rest.md b/.changeset/quiet-states-rest.md new file mode 100644 index 000000000..5a04b4089 --- /dev/null +++ b/.changeset/quiet-states-rest.md @@ -0,0 +1,5 @@ +--- +"hunkdiff": patch +--- + +Store Hunk state and globally installed extensions under the XDG state directory. diff --git a/src/core/run/paths.test.ts b/src/core/run/paths.test.ts index 0c24bc347..7b1786530 100644 --- a/src/core/run/paths.test.ts +++ b/src/core/run/paths.test.ts @@ -9,6 +9,7 @@ import { resolveCanonicalPath, resolveGlobalConfigPath, resolveAppStatePath, + resolveGlobalExtensionsDir, } from "./paths"; function createTempRoot(prefix: string) { @@ -17,10 +18,25 @@ function createTempRoot(prefix: string) { describe("paths", () => { test("resolves XDG config and state paths", () => { - const env = { XDG_CONFIG_HOME: join("/tmp", "xdg-home") } as NodeJS.ProcessEnv; + const env = { + XDG_CONFIG_HOME: join("/tmp", "xdg-config"), + XDG_STATE_HOME: join("/tmp", "xdg-state"), + } as NodeJS.ProcessEnv; + + expect(resolveGlobalConfigPath(env)).toBe(join("/tmp", "xdg-config", "hunk", "config.toml")); + expect(resolveAppStatePath(env)).toBe(join("/tmp", "xdg-state", "hunk", "state.json")); + expect(resolveGlobalExtensionsDir(env)).toBe(join("/tmp", "xdg-state", "hunk", "extensions")); + }); + + test("falls back to the platform-independent XDG state location", () => { + const env = { HOME: join("/tmp", "home") } as NodeJS.ProcessEnv; - expect(resolveGlobalConfigPath(env)).toBe(join("/tmp", "xdg-home", "hunk", "config.toml")); - expect(resolveAppStatePath(env)).toBe(join("/tmp", "xdg-home", "hunk", "state.json")); + expect(resolveAppStatePath(env)).toBe( + join("/tmp", "home", ".local", "state", "hunk", "state.json"), + ); + expect(resolveGlobalExtensionsDir(env)).toBe( + join("/tmp", "home", ".local", "state", "hunk", "extensions"), + ); }); test("falls back to HOME for config and state paths", () => { @@ -29,7 +45,9 @@ describe("paths", () => { expect(resolveGlobalConfigPath(env)).toBe( join("/tmp", "home", ".config", "hunk", "config.toml"), ); - expect(resolveAppStatePath(env)).toBe(join("/tmp", "home", ".config", "hunk", "state.json")); + expect(resolveAppStatePath(env)).toBe( + join("/tmp", "home", ".local", "state", "hunk", "state.json"), + ); }); test("falls back to USERPROFILE when HOME is unavailable", () => { @@ -39,7 +57,7 @@ describe("paths", () => { join("/tmp", "windows-profile", ".config", "hunk", "config.toml"), ); expect(resolveAppStatePath(env)).toBe( - join("/tmp", "windows-profile", ".config", "hunk", "state.json"), + join("/tmp", "windows-profile", ".local", "state", "hunk", "state.json"), ); }); diff --git a/src/core/run/paths.ts b/src/core/run/paths.ts index 7ded9f11c..bb17347d1 100644 --- a/src/core/run/paths.ts +++ b/src/core/run/paths.ts @@ -86,6 +86,16 @@ export function resolveUserConfigDir(env: NodeJS.ProcessEnv = process.env) { return undefined; } +/** Resolve the base state directory Hunk should use for user-scoped files. */ +export function resolveUserStateDir(env: NodeJS.ProcessEnv = process.env) { + if (env.XDG_STATE_HOME) { + return env.XDG_STATE_HOME; + } + + const home = env.HOME || env.USERPROFILE; + return home ? join(home, ".local", "state") : undefined; +} + /** Resolve the global Hunk config file path from the current environment. */ export function resolveGlobalConfigPath(env: NodeJS.ProcessEnv = process.env) { const configDir = resolveUserConfigDir(env); @@ -94,14 +104,14 @@ export function resolveGlobalConfigPath(env: NodeJS.ProcessEnv = process.env) { /** Resolve the persisted Hunk state file path from the current environment. */ export function resolveAppStatePath(env: NodeJS.ProcessEnv = process.env) { - const configDir = resolveUserConfigDir(env); - return configDir ? join(configDir, "hunk", "state.json") : undefined; + const stateDir = resolveUserStateDir(env); + return stateDir ? join(stateDir, "hunk", "state.json") : undefined; } /** Resolve the user-scoped directory Hunk scans for globally installed extensions. */ export function resolveGlobalExtensionsDir(env: NodeJS.ProcessEnv = process.env) { - const configDir = resolveUserConfigDir(env); - return configDir ? join(configDir, "hunk", "extensions") : undefined; + const stateDir = resolveUserStateDir(env); + return stateDir ? join(stateDir, "hunk", "extensions") : undefined; } /** diff --git a/src/extensions/discovery.test.ts b/src/extensions/discovery.test.ts index 479ef52c8..429fd7420 100644 --- a/src/extensions/discovery.test.ts +++ b/src/extensions/discovery.test.ts @@ -264,7 +264,7 @@ describe("extension discovery", () => { const candidates = discoverExtensions({ cwd: home, repoRoot: undefined, - env: { XDG_CONFIG_HOME: home } as NodeJS.ProcessEnv, + env: { XDG_STATE_HOME: home } as NodeJS.ProcessEnv, }); expect(candidates).toEqual([{ id: "themed", path: globalPath, origin: "global" }]); diff --git a/src/extensions/manage/install.test.ts b/src/extensions/manage/install.test.ts index e5135ec64..823fa5c2e 100644 --- a/src/extensions/manage/install.test.ts +++ b/src/extensions/manage/install.test.ts @@ -229,7 +229,7 @@ describe("hunk extension command runner", () => { stdout: (text: string) => out.push(text), stderr: (text: string) => err.push(text), ...(confirmAnswer !== undefined ? { confirm: async () => confirmAnswer } : {}), - env: { XDG_CONFIG_HOME: configDir } as NodeJS.ProcessEnv, + env: { XDG_STATE_HOME: configDir } as NodeJS.ProcessEnv, }, }; } diff --git a/src/extensions/startup.test.ts b/src/extensions/startup.test.ts index 1e12af04c..9d0d74ea3 100644 --- a/src/extensions/startup.test.ts +++ b/src/extensions/startup.test.ts @@ -44,7 +44,7 @@ describe("extension startup", () => { const result = await loadStartupExtensions({ extensions: createExtensionsConfig({ enabled: false }), cwd: home, - env: { XDG_CONFIG_HOME: home } as NodeJS.ProcessEnv, + env: { XDG_STATE_HOME: home } as NodeJS.ProcessEnv, }); const empty = createEmptyExtensionLoadResult(home); @@ -71,7 +71,7 @@ describe("extension startup", () => { extensionConfigs: { themed: { themeId: "midnight" } }, }), cwd: home, - env: { XDG_CONFIG_HOME: home } as NodeJS.ProcessEnv, + env: { XDG_STATE_HOME: home } as NodeJS.ProcessEnv, hostOverrides: { repoRoot: undefined }, }); @@ -100,7 +100,7 @@ export default function (hunk) { const provisional = await loadStartupExtensions({ extensions: createExtensionsConfig(), cwd: repo, - env: { XDG_CONFIG_HOME: configHome } as NodeJS.ProcessEnv, + env: { XDG_STATE_HOME: configHome } as NodeJS.ProcessEnv, deferEventBusBinding: true, }); const repoExtensions = join(repo, ".hunk", "extensions"); @@ -118,7 +118,7 @@ export default function (hunk) { const final = await loadStartupExtensions({ extensions: createExtensionsConfig(), cwd: repo, - env: { XDG_CONFIG_HOME: configHome } as NodeJS.ProcessEnv, + env: { XDG_STATE_HOME: configHome } as NodeJS.ProcessEnv, projectRoot: repo, previousLoad: provisional, hostOverrides: { resolveRepoTrustImpl: () => "trusted" }, @@ -145,12 +145,12 @@ export default function (hunk) { const provisional = await loadStartupExtensions({ extensions: createExtensionsConfig({ extensionConfigs: { configured: { value: 1 } } }), cwd: home, - env: { XDG_CONFIG_HOME: home } as NodeJS.ProcessEnv, + env: { XDG_STATE_HOME: home } as NodeJS.ProcessEnv, }); await loadStartupExtensions({ extensions: createExtensionsConfig({ extensionConfigs: { configured: { value: 2 } } }), cwd: home, - env: { XDG_CONFIG_HOME: home } as NodeJS.ProcessEnv, + env: { XDG_STATE_HOME: home } as NodeJS.ProcessEnv, previousLoad: provisional, });