import { access, readFile } from "node:fs/promises"; import { constants } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { homedir } from "node:os"; import type { SpotifyConfig } from "./types.js"; export interface SpotifyPaths { configDir: string; configPath: string; tokenPath: string; } export interface ResolveConfigOptions { env?: NodeJS.ProcessEnv; startDir?: string; homeDir?: string; } const skillDir = resolve(dirname(fileURLToPath(import.meta.url)), ".."); function pathExists(path: string): Promise { return access(path, constants.F_OK).then(() => true, () => false); } function expandHome(path: string, homeDir: string): string { if (path === "~") { return homeDir; } if (path.startsWith("~/")) { return join(homeDir, path.slice(2)); } return path; } async function findUpwardCredentialDir(startDir: string): Promise { let current = resolve(startDir); while (true) { const candidate = join(current, ".clawdbot", "credentials", "spotify"); if (await pathExists(candidate)) { return candidate; } const parent = dirname(current); if (parent === current) { return undefined; } current = parent; } } export async function resolveSpotifyPaths(options: ResolveConfigOptions = {}): Promise { const env = options.env ?? process.env; const homeDir = options.homeDir ?? homedir(); const candidates: Array> = []; if (env.SPOTIFY_CONFIG_DIR) { candidates.push(Promise.resolve(resolve(expandHome(env.SPOTIFY_CONFIG_DIR, homeDir)))); } candidates.push(Promise.resolve(join(homeDir, ".openclaw", "workspace", ".clawdbot", "credentials", "spotify"))); candidates.push(findUpwardCredentialDir(options.startDir ?? skillDir)); candidates.push(Promise.resolve(join(homeDir, ".clawdbot", "credentials", "spotify"))); for (const candidatePromise of candidates) { const candidate = await candidatePromise; if (candidate && await pathExists(candidate)) { return { configDir: candidate, configPath: join(candidate, "config.json"), tokenPath: join(candidate, "token.json") }; } } const fallback = env.SPOTIFY_CONFIG_DIR ? resolve(expandHome(env.SPOTIFY_CONFIG_DIR, homeDir)) : join(homeDir, ".openclaw", "workspace", ".clawdbot", "credentials", "spotify"); return { configDir: fallback, configPath: join(fallback, "config.json"), tokenPath: join(fallback, "token.json") }; } export async function loadSpotifyConfig(options: ResolveConfigOptions = {}): Promise { const paths = await resolveSpotifyPaths(options); let raw: string; try { raw = await readFile(paths.configPath, "utf8"); } catch { throw new Error(`Spotify config not found. Create ${paths.configPath} with clientId and redirectUri.`); } let parsed: unknown; try { parsed = JSON.parse(raw); } catch { throw new Error("Spotify config is not valid JSON."); } if ( !parsed || typeof parsed !== "object" || typeof (parsed as SpotifyConfig).clientId !== "string" || typeof (parsed as SpotifyConfig).redirectUri !== "string" || !(parsed as SpotifyConfig).clientId.trim() || !(parsed as SpotifyConfig).redirectUri.trim() ) { throw new Error("Spotify config must include non-empty clientId and redirectUri."); } return { clientId: (parsed as SpotifyConfig).clientId, redirectUri: (parsed as SpotifyConfig).redirectUri }; }