diff --git a/src/core/global-config.ts b/src/core/global-config.ts index 1f213c7cb..ad321ceb8 100644 --- a/src/core/global-config.ts +++ b/src/core/global-config.ts @@ -69,30 +69,36 @@ export interface GlobalDataDirOptions { homedir?: string; } +function joinGlobalDataPath(platform: NodeJS.Platform, ...segments: string[]): string { + return platform === 'win32' + ? path.win32.join(...segments) + : path.posix.join(...segments); +} + export function getGlobalDataDir(options: GlobalDataDirOptions = {}): string { const env = options.env ?? process.env; + const platform = options.platform ?? os.platform(); // XDG_DATA_HOME takes precedence on all platforms when explicitly set const xdgDataHome = env.XDG_DATA_HOME; if (xdgDataHome) { - return path.join(xdgDataHome, GLOBAL_DATA_DIR_NAME); + return joinGlobalDataPath(platform, xdgDataHome, GLOBAL_DATA_DIR_NAME); } - const platform = options.platform ?? os.platform(); const homedir = options.homedir ?? os.homedir(); if (platform === 'win32') { // Windows: use %LOCALAPPDATA% const localAppData = env.LOCALAPPDATA; if (localAppData) { - return path.win32.join(localAppData, GLOBAL_DATA_DIR_NAME); + return joinGlobalDataPath(platform, localAppData, GLOBAL_DATA_DIR_NAME); } // Fallback for Windows if LOCALAPPDATA is not set - return path.win32.join(homedir, 'AppData', 'Local', GLOBAL_DATA_DIR_NAME); + return joinGlobalDataPath(platform, homedir, 'AppData', 'Local', GLOBAL_DATA_DIR_NAME); } // Unix/macOS fallback: ~/.local/share - return path.join(homedir, '.local', 'share', GLOBAL_DATA_DIR_NAME); + return joinGlobalDataPath(platform, homedir, '.local', 'share', GLOBAL_DATA_DIR_NAME); } /** diff --git a/test/cli-e2e/basic.test.ts b/test/cli-e2e/basic.test.ts index 0d7e46de0..22657513d 100644 --- a/test/cli-e2e/basic.test.ts +++ b/test/cli-e2e/basic.test.ts @@ -134,7 +134,9 @@ describe('openspec CLI e2e basics', () => { const result = await runCLI(['init', '--tools', 'all'], { cwd: emptyProjectDir, env: { CODEX_HOME: codexHome }, + timeoutMs: 20000, }); + expect(result.timedOut).toBe(false); expect(result.exitCode).toBe(0); expect(result.stdout).toContain('OpenSpec Setup Complete'); @@ -143,7 +145,7 @@ describe('openspec CLI e2e basics', () => { const cursorSkillPath = path.join(emptyProjectDir, '.cursor/skills/openspec-explore/SKILL.md'); expect(await fileExists(claudeSkillPath)).toBe(true); expect(await fileExists(cursorSkillPath)).toBe(true); - }); + }, 25000); it('initializes with --tools list option', async () => { const projectDir = await prepareFixture('tmp-init'); diff --git a/test/core/global-config.test.ts b/test/core/global-config.test.ts index 71668c7ff..03310060e 100644 --- a/test/core/global-config.test.ts +++ b/test/core/global-config.test.ts @@ -6,6 +6,7 @@ import * as os from 'node:os'; import { getGlobalConfigDir, getGlobalConfigPath, + getGlobalDataDir, getGlobalConfig, saveGlobalConfig, GLOBAL_CONFIG_DIR_NAME, @@ -95,6 +96,44 @@ describe('global-config', () => { }); }); + describe('getGlobalDataDir', () => { + it('should use POSIX separators for Unix-like platform overrides', () => { + expect( + getGlobalDataDir({ + env: {}, + platform: 'linux', + homedir: '/home/tabish', + }) + ).toBe('/home/tabish/.local/share/openspec'); + + expect( + getGlobalDataDir({ + env: { XDG_DATA_HOME: '/var/data' }, + platform: 'darwin', + homedir: '/Users/tabish', + }) + ).toBe('/var/data/openspec'); + }); + + it('should use Windows separators for native Windows platform overrides', () => { + expect( + getGlobalDataDir({ + env: {}, + platform: 'win32', + homedir: 'C:\\Users\\Tabish', + }) + ).toBe('C:\\Users\\Tabish\\AppData\\Local\\openspec'); + + expect( + getGlobalDataDir({ + env: { LOCALAPPDATA: 'D:\\Users\\Tabish\\AppData\\Local' }, + platform: 'win32', + homedir: 'C:\\Users\\Tabish', + }) + ).toBe('D:\\Users\\Tabish\\AppData\\Local\\openspec'); + }); + }); + describe('getGlobalConfig', () => { it('should return defaults when config file does not exist', () => { process.env.XDG_CONFIG_HOME = tempDir;