Files
stef-openclaw-skills/skills/spotify/src/config.ts
T

117 lines
3.5 KiB
TypeScript

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<boolean> {
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<string | undefined> {
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<SpotifyPaths> {
const env = options.env ?? process.env;
const homeDir = options.homeDir ?? homedir();
const candidates: Array<Promise<string | undefined>> = [];
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<SpotifyConfig> {
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
};
}