diff --git a/launcher/config/plugin-runtime-config.ts b/launcher/config/plugin-runtime-config.ts index 06f8b10e..64498a14 100644 --- a/launcher/config/plugin-runtime-config.ts +++ b/launcher/config/plugin-runtime-config.ts @@ -5,6 +5,10 @@ import { log } from '../log.js'; import type { LogLevel, PluginRuntimeConfig } from '../types.js'; import { DEFAULT_SOCKET_PATH } from '../types.js'; +function getPlatformPath(platform: NodeJS.Platform): typeof path.posix | typeof path.win32 { + return platform === 'win32' ? path.win32 : path.posix; +} + export function getPluginConfigCandidates(options?: { platform?: NodeJS.Platform; homeDir?: string; @@ -13,21 +17,24 @@ export function getPluginConfigCandidates(options?: { }): string[] { const platform = options?.platform ?? process.platform; const homeDir = options?.homeDir ?? os.homedir(); + const platformPath = getPlatformPath(platform); if (platform === 'win32') { const appDataDir = options?.appDataDir?.trim() || process.env.APPDATA?.trim() || - path.join(homeDir, 'AppData', 'Roaming'); - return [path.join(appDataDir, 'mpv', 'script-opts', 'subminer.conf')]; + platformPath.join(homeDir, 'AppData', 'Roaming'); + return [platformPath.join(appDataDir, 'mpv', 'script-opts', 'subminer.conf')]; } const xdgConfigHome = - options?.xdgConfigHome?.trim() || process.env.XDG_CONFIG_HOME || path.join(homeDir, '.config'); + options?.xdgConfigHome?.trim() || + process.env.XDG_CONFIG_HOME || + platformPath.join(homeDir, '.config'); return Array.from( new Set([ - path.join(xdgConfigHome, 'mpv', 'script-opts', 'subminer.conf'), - path.join(homeDir, '.config', 'mpv', 'script-opts', 'subminer.conf'), + platformPath.join(xdgConfigHome, 'mpv', 'script-opts', 'subminer.conf'), + platformPath.join(homeDir, '.config', 'mpv', 'script-opts', 'subminer.conf'), ]), ); } diff --git a/launcher/main.test.ts b/launcher/main.test.ts index 8d7c2c65..236ba409 100644 --- a/launcher/main.test.ts +++ b/launcher/main.test.ts @@ -81,14 +81,14 @@ test('config path uses XDG_CONFIG_HOME override', () => { test('config discovery ignores lowercase subminer candidate', () => { const homeDir = '/home/tester'; const xdgConfigHome = '/tmp/xdg-config'; - const expected = path.join(xdgConfigHome, 'SubMiner', 'config.jsonc'); - const foundPaths = new Set([path.join(xdgConfigHome, 'subminer', 'config.json')]); + const expected = path.posix.join(xdgConfigHome, 'SubMiner', 'config.jsonc'); + const foundPaths = new Set([path.posix.join(xdgConfigHome, 'subminer', 'config.json')]); const resolved = resolveConfigFilePath({ xdgConfigHome, homeDir, platform: 'linux', - existsSync: (candidate) => foundPaths.has(path.normalize(candidate)), + existsSync: (candidate) => foundPaths.has(path.posix.normalize(candidate)), }); assert.equal(resolved, expected); diff --git a/src/config/path-resolution.test.ts b/src/config/path-resolution.test.ts index 15bfb78b..6be09f92 100644 --- a/src/config/path-resolution.test.ts +++ b/src/config/path-resolution.test.ts @@ -11,7 +11,7 @@ function existsSyncFrom(paths: string[]): (candidate: string) => boolean { test('resolveConfigBaseDirs trims xdg value and deduplicates fallback dir', () => { const homeDir = '/home/tester'; const trimmedXdgConfigHome = '/home/tester/.config'; - const fallbackDir = path.join(homeDir, '.config'); + const fallbackDir = path.posix.join(homeDir, '.config'); const baseDirs = resolveConfigBaseDirs(` ${trimmedXdgConfigHome} `, homeDir, 'linux'); const expected = Array.from(new Set([trimmedXdgConfigHome, fallbackDir])); assert.deepEqual(baseDirs, expected); @@ -29,8 +29,8 @@ test('resolveConfigBaseDirs prefers APPDATA on windows and deduplicates fallback test('resolveConfigDir prefers xdg SubMiner config when present', () => { const homeDir = '/home/tester'; const xdgConfigHome = '/tmp/xdg-config'; - const configDir = path.join(xdgConfigHome, 'SubMiner'); - const existsSync = existsSyncFrom([path.join(configDir, 'config.jsonc')]); + const configDir = path.posix.join(xdgConfigHome, 'SubMiner'); + const existsSync = existsSyncFrom([path.posix.join(configDir, 'config.jsonc')]); const resolved = resolveConfigDir({ xdgConfigHome, @@ -54,12 +54,12 @@ test('resolveConfigDir ignores lowercase subminer candidate', () => { existsSync, }); - assert.equal(resolved, path.join('/tmp/missing-xdg', 'SubMiner')); + assert.equal(resolved, path.posix.join('/tmp/missing-xdg', 'SubMiner')); }); test('resolveConfigDir falls back to existing directory when file is missing', () => { const homeDir = '/home/tester'; - const configDir = path.join(homeDir, '.config', 'SubMiner'); + const configDir = path.posix.join(homeDir, '.config', 'SubMiner'); const existsSync = existsSyncFrom([configDir]); const resolved = resolveConfigDir({ @@ -76,8 +76,8 @@ test('resolveConfigFilePath prefers jsonc before json', () => { const homeDir = '/home/tester'; const xdgConfigHome = '/tmp/xdg-config'; const existsSync = existsSyncFrom([ - path.join(xdgConfigHome, 'SubMiner', 'config.jsonc'), - path.join(xdgConfigHome, 'SubMiner', 'config.json'), + path.posix.join(xdgConfigHome, 'SubMiner', 'config.jsonc'), + path.posix.join(xdgConfigHome, 'SubMiner', 'config.json'), ]); const resolved = resolveConfigFilePath({ @@ -87,7 +87,7 @@ test('resolveConfigFilePath prefers jsonc before json', () => { existsSync, }); - assert.equal(resolved, path.join(xdgConfigHome, 'SubMiner', 'config.jsonc')); + assert.equal(resolved, path.posix.join(xdgConfigHome, 'SubMiner', 'config.jsonc')); }); test('resolveConfigFilePath keeps legacy fallback output path', () => { @@ -102,14 +102,14 @@ test('resolveConfigFilePath keeps legacy fallback output path', () => { existsSync, }); - assert.equal(resolved, path.join(xdgConfigHome, 'SubMiner', 'config.jsonc')); + assert.equal(resolved, path.posix.join(xdgConfigHome, 'SubMiner', 'config.jsonc')); }); test('resolveConfigDir prefers APPDATA SubMiner config on windows when present', () => { const homeDir = 'C:\\Users\\tester'; const appDataDir = 'C:\\Users\\tester\\AppData\\Roaming'; - const configDir = path.join(appDataDir, 'SubMiner'); - const existsSync = existsSyncFrom([path.join(configDir, 'config.jsonc')]); + const configDir = path.win32.join(appDataDir, 'SubMiner'); + const existsSync = existsSyncFrom([path.win32.join(configDir, 'config.jsonc')]); const resolved = resolveConfigDir({ platform: 'win32', @@ -133,5 +133,5 @@ test('resolveConfigFilePath uses APPDATA fallback output path on windows', () => existsSync, }); - assert.equal(resolved, path.join(appDataDir, 'SubMiner', 'config.jsonc')); + assert.equal(resolved, path.win32.join(appDataDir, 'SubMiner', 'config.jsonc')); }); diff --git a/src/config/path-resolution.ts b/src/config/path-resolution.ts index 55dcba40..ad123c02 100644 --- a/src/config/path-resolution.ts +++ b/src/config/path-resolution.ts @@ -15,19 +15,24 @@ type ConfigPathOptions = { const DEFAULT_APP_NAMES = ['SubMiner'] as const; const DEFAULT_FILE_NAMES = ['config.jsonc', 'config.json'] as const; +function getPlatformPath(platform: NodeJS.Platform): typeof path.posix | typeof path.win32 { + return platform === 'win32' ? path.win32 : path.posix; +} + export function resolveConfigBaseDirs( xdgConfigHome: string | undefined, homeDir: string, platform: NodeJS.Platform = process.platform, appDataDir?: string, ): string[] { + const platformPath = getPlatformPath(platform); if (platform === 'win32') { - const roamingBaseDir = path.join(homeDir, 'AppData', 'Roaming'); + const roamingBaseDir = platformPath.join(homeDir, 'AppData', 'Roaming'); const primaryBaseDir = appDataDir?.trim() || roamingBaseDir; return Array.from(new Set([primaryBaseDir, roamingBaseDir])); } - const fallbackBaseDir = path.join(homeDir, '.config'); + const fallbackBaseDir = platformPath.join(homeDir, '.config'); const primaryBaseDir = xdgConfigHome?.trim() || fallbackBaseDir; return Array.from(new Set([primaryBaseDir, fallbackBaseDir])); } @@ -41,19 +46,21 @@ function getDefaultAppName(options: ConfigPathOptions): string { } export function resolveConfigDir(options: ConfigPathOptions): string { + const platform = options.platform ?? process.platform; + const platformPath = getPlatformPath(platform); const baseDirs = resolveConfigBaseDirs( options.xdgConfigHome, options.homeDir, - options.platform, + platform, options.appDataDir, ); const appNames = getAppNames(options); for (const baseDir of baseDirs) { for (const appName of appNames) { - const dir = path.join(baseDir, appName); + const dir = platformPath.join(baseDir, appName); for (const fileName of DEFAULT_FILE_NAMES) { - if (options.existsSync(path.join(dir, fileName))) { + if (options.existsSync(platformPath.join(dir, fileName))) { return dir; } } @@ -62,21 +69,23 @@ export function resolveConfigDir(options: ConfigPathOptions): string { for (const baseDir of baseDirs) { for (const appName of appNames) { - const dir = path.join(baseDir, appName); + const dir = platformPath.join(baseDir, appName); if (options.existsSync(dir)) { return dir; } } } - return path.join(baseDirs[0]!, getDefaultAppName(options)); + return platformPath.join(baseDirs[0]!, getDefaultAppName(options)); } export function resolveConfigFilePath(options: ConfigPathOptions): string { + const platform = options.platform ?? process.platform; + const platformPath = getPlatformPath(platform); const baseDirs = resolveConfigBaseDirs( options.xdgConfigHome, options.homeDir, - options.platform, + platform, options.appDataDir, ); const appNames = getAppNames(options); @@ -84,7 +93,7 @@ export function resolveConfigFilePath(options: ConfigPathOptions): string { for (const baseDir of baseDirs) { for (const appName of appNames) { for (const fileName of DEFAULT_FILE_NAMES) { - const candidate = path.join(baseDir, appName, fileName); + const candidate = platformPath.join(baseDir, appName, fileName); if (options.existsSync(candidate)) { return candidate; } @@ -92,5 +101,5 @@ export function resolveConfigFilePath(options: ConfigPathOptions): string { } } - return path.join(baseDirs[0]!, getDefaultAppName(options), DEFAULT_FILE_NAMES[0]!); + return platformPath.join(baseDirs[0]!, getDefaultAppName(options), DEFAULT_FILE_NAMES[0]!); } diff --git a/src/shared/setup-state.test.ts b/src/shared/setup-state.test.ts index ca5905fc..97627f81 100644 --- a/src/shared/setup-state.test.ts +++ b/src/shared/setup-state.test.ts @@ -30,10 +30,11 @@ test('getDefaultConfigDir prefers existing SubMiner config directory', () => { platform: 'linux', xdgConfigHome, homeDir, - existsSync: (candidate) => candidate === path.join(xdgConfigHome, 'SubMiner', 'config.jsonc'), + existsSync: (candidate) => + candidate === path.posix.join(xdgConfigHome, 'SubMiner', 'config.jsonc'), }); - assert.equal(dir, path.join(xdgConfigHome, 'SubMiner')); + assert.equal(dir, path.posix.join(xdgConfigHome, 'SubMiner')); }); test('ensureDefaultConfigBootstrap creates config dir and default jsonc only when missing', () => { @@ -138,27 +139,27 @@ test('resolveDefaultMpvInstallPaths resolves linux, macOS, and Windows defaults' const xdgConfigHome = path.join(path.sep, 'tmp', 'xdg'); assert.deepEqual(resolveDefaultMpvInstallPaths('linux', linuxHomeDir, xdgConfigHome), { supported: true, - mpvConfigDir: path.join(xdgConfigHome, 'mpv'), - scriptsDir: path.join(xdgConfigHome, 'mpv', 'scripts'), - scriptOptsDir: path.join(xdgConfigHome, 'mpv', 'script-opts'), - pluginEntrypointPath: path.join(xdgConfigHome, 'mpv', 'scripts', 'subminer', 'main.lua'), - pluginDir: path.join(xdgConfigHome, 'mpv', 'scripts', 'subminer'), - pluginConfigPath: path.join(xdgConfigHome, 'mpv', 'script-opts', 'subminer.conf'), + mpvConfigDir: path.posix.join(xdgConfigHome, 'mpv'), + scriptsDir: path.posix.join(xdgConfigHome, 'mpv', 'scripts'), + scriptOptsDir: path.posix.join(xdgConfigHome, 'mpv', 'script-opts'), + pluginEntrypointPath: path.posix.join(xdgConfigHome, 'mpv', 'scripts', 'subminer', 'main.lua'), + pluginDir: path.posix.join(xdgConfigHome, 'mpv', 'scripts', 'subminer'), + pluginConfigPath: path.posix.join(xdgConfigHome, 'mpv', 'script-opts', 'subminer.conf'), }); const macHomeDir = path.join(path.sep, 'Users', 'tester'); assert.deepEqual(resolveDefaultMpvInstallPaths('darwin', macHomeDir, undefined), { supported: true, - mpvConfigDir: path.join(macHomeDir, 'Library', 'Application Support', 'mpv'), - scriptsDir: path.join(macHomeDir, 'Library', 'Application Support', 'mpv', 'scripts'), - scriptOptsDir: path.join( + mpvConfigDir: path.posix.join(macHomeDir, 'Library', 'Application Support', 'mpv'), + scriptsDir: path.posix.join(macHomeDir, 'Library', 'Application Support', 'mpv', 'scripts'), + scriptOptsDir: path.posix.join( macHomeDir, 'Library', 'Application Support', 'mpv', 'script-opts', ), - pluginEntrypointPath: path.join( + pluginEntrypointPath: path.posix.join( macHomeDir, 'Library', 'Application Support', @@ -167,7 +168,7 @@ test('resolveDefaultMpvInstallPaths resolves linux, macOS, and Windows defaults' 'subminer', 'main.lua', ), - pluginDir: path.join( + pluginDir: path.posix.join( macHomeDir, 'Library', 'Application Support', @@ -175,7 +176,7 @@ test('resolveDefaultMpvInstallPaths resolves linux, macOS, and Windows defaults' 'scripts', 'subminer', ), - pluginConfigPath: path.join( + pluginConfigPath: path.posix.join( macHomeDir, 'Library', 'Application Support', @@ -187,10 +188,16 @@ test('resolveDefaultMpvInstallPaths resolves linux, macOS, and Windows defaults' assert.deepEqual(resolveDefaultMpvInstallPaths('win32', 'C:\\Users\\tester', undefined), { supported: true, - mpvConfigDir: path.join('C:\\Users\\tester', 'AppData', 'Roaming', 'mpv'), - scriptsDir: path.join('C:\\Users\\tester', 'AppData', 'Roaming', 'mpv', 'scripts'), - scriptOptsDir: path.join('C:\\Users\\tester', 'AppData', 'Roaming', 'mpv', 'script-opts'), - pluginEntrypointPath: path.join( + mpvConfigDir: path.win32.join('C:\\Users\\tester', 'AppData', 'Roaming', 'mpv'), + scriptsDir: path.win32.join('C:\\Users\\tester', 'AppData', 'Roaming', 'mpv', 'scripts'), + scriptOptsDir: path.win32.join( + 'C:\\Users\\tester', + 'AppData', + 'Roaming', + 'mpv', + 'script-opts', + ), + pluginEntrypointPath: path.win32.join( 'C:\\Users\\tester', 'AppData', 'Roaming', @@ -199,8 +206,15 @@ test('resolveDefaultMpvInstallPaths resolves linux, macOS, and Windows defaults' 'subminer', 'main.lua', ), - pluginDir: path.join('C:\\Users\\tester', 'AppData', 'Roaming', 'mpv', 'scripts', 'subminer'), - pluginConfigPath: path.join( + pluginDir: path.win32.join( + 'C:\\Users\\tester', + 'AppData', + 'Roaming', + 'mpv', + 'scripts', + 'subminer', + ), + pluginConfigPath: path.win32.join( 'C:\\Users\\tester', 'AppData', 'Roaming', diff --git a/src/shared/setup-state.ts b/src/shared/setup-state.ts index 0a1625b4..c82cc994 100644 --- a/src/shared/setup-state.ts +++ b/src/shared/setup-state.ts @@ -40,6 +40,10 @@ export interface MpvInstallPaths { pluginConfigPath: string; } +function getPlatformPath(platform: NodeJS.Platform): typeof path.posix | typeof path.win32 { + return platform === 'win32' ? path.win32 : path.posix; +} + function asObject(value: unknown): Record | null { return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record) @@ -223,20 +227,21 @@ export function resolveDefaultMpvInstallPaths( homeDir: string, xdgConfigHome?: string, ): MpvInstallPaths { + const platformPath = getPlatformPath(platform); const mpvConfigDir = platform === 'darwin' - ? path.join(homeDir, 'Library', 'Application Support', 'mpv') + ? platformPath.join(homeDir, 'Library', 'Application Support', 'mpv') : platform === 'linux' - ? path.join(xdgConfigHome?.trim() || path.join(homeDir, '.config'), 'mpv') - : path.join(homeDir, 'AppData', 'Roaming', 'mpv'); + ? platformPath.join(xdgConfigHome?.trim() || platformPath.join(homeDir, '.config'), 'mpv') + : platformPath.join(homeDir, 'AppData', 'Roaming', 'mpv'); return { supported: platform === 'linux' || platform === 'darwin' || platform === 'win32', mpvConfigDir, - scriptsDir: path.join(mpvConfigDir, 'scripts'), - scriptOptsDir: path.join(mpvConfigDir, 'script-opts'), - pluginEntrypointPath: path.join(mpvConfigDir, 'scripts', 'subminer', 'main.lua'), - pluginDir: path.join(mpvConfigDir, 'scripts', 'subminer'), - pluginConfigPath: path.join(mpvConfigDir, 'script-opts', 'subminer.conf'), + scriptsDir: platformPath.join(mpvConfigDir, 'scripts'), + scriptOptsDir: platformPath.join(mpvConfigDir, 'script-opts'), + pluginEntrypointPath: platformPath.join(mpvConfigDir, 'scripts', 'subminer', 'main.lua'), + pluginDir: platformPath.join(mpvConfigDir, 'scripts', 'subminer'), + pluginConfigPath: platformPath.join(mpvConfigDir, 'script-opts', 'subminer.conf'), }; }