From 91d4cd866c776455e5160898878f32431e066279 Mon Sep 17 00:00:00 2001 From: bhutano Date: Sun, 29 Mar 2026 22:57:25 +0200 Subject: [PATCH 1/2] fix(spotify): fix token refresh, null guards, env parse, missing credentials guidance, postinstall template --- scripts/postinstall.js | 7 +++---- src/clis/spotify/spotify.ts | 42 ++++++++++++++++++++++--------------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/scripts/postinstall.js b/scripts/postinstall.js index 175ecf88..f790baf5 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -203,13 +203,12 @@ function main() { if (!existsSync(spotifyEnvFile)) { writeFileSync(spotifyEnvFile, `# Spotify credentials — get them at https://developer.spotify.com/dashboard\n` + - `# Add http://127.0.0.1:8888/callback as a Redirect URI in your Spotify app\n` + - `SPOTIFY_CLIENT_ID=\n` + - `SPOTIFY_CLIENT_SECRET=\n`, + `SPOTIFY_CLIENT_ID=your_spotify_client_id_here\n` + + `SPOTIFY_CLIENT_SECRET=your_spotify_client_secret_here\n`, 'utf8' ); console.log(`✓ Spotify credentials template created at ${spotifyEnvFile}`); - console.log(` Fill in your Client ID and Secret, then run: opencli spotify auth`); + console.log(` Edit the file and add your Client ID and Secret, then run: opencli spotify auth`); } // ── Browser Bridge setup hint ─────────────────────────────────────── diff --git a/src/clis/spotify/spotify.ts b/src/clis/spotify/spotify.ts index 528d0541..49354765 100644 --- a/src/clis/spotify/spotify.ts +++ b/src/clis/spotify/spotify.ts @@ -5,13 +5,6 @@ import { createServer } from 'http'; import { homedir } from 'os'; import { join } from 'path'; import { exec } from 'child_process'; -import { - assertSpotifyCredentialsConfigured, - getFirstSpotifyTrack, - mapSpotifyTrackResults, - parseDotEnv, - resolveSpotifyCredentials, -} from './utils.js'; // ── Credentials ─────────────────────────────────────────────────────────────── // Set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET as environment variables, @@ -23,13 +16,18 @@ const ENV_FILE = join(homedir(), '.opencli', 'spotify.env'); function loadEnv(): Record { if (!existsSync(ENV_FILE)) return {}; - return parseDotEnv(readFileSync(ENV_FILE, 'utf-8')); + return Object.fromEntries( + readFileSync(ENV_FILE, 'utf-8') + .split('\n') + .map(l => l.trim()) + .filter(l => l && !l.startsWith('#') && l.includes('=')) + .map(l => { const i = l.indexOf('='); return [l.slice(0, i).trim(), l.slice(i + 1).trim()] as [string, string]; }) + ); } const env = loadEnv(); -const credentials = resolveSpotifyCredentials(env); -const CLIENT_ID = credentials.clientId; -const CLIENT_SECRET = credentials.clientSecret; +const CLIENT_ID = env.SPOTIFY_CLIENT_ID || process.env.SPOTIFY_CLIENT_ID || ''; +const CLIENT_SECRET = env.SPOTIFY_CLIENT_SECRET || process.env.SPOTIFY_CLIENT_SECRET || ''; const REDIRECT_URI = 'http://127.0.0.1:8888/callback'; const SCOPES = [ 'user-read-playback-state', @@ -109,9 +107,9 @@ async function api(method: string, path: string, body?: unknown): Promise { async function findTrackUri(query: string): Promise<{ uri: string; name: string; artist: string }> { const data = await api('GET', `/search?q=${encodeURIComponent(query)}&type=track&limit=1`); - const track = getFirstSpotifyTrack(data); + const track = data?.tracks?.items?.[0]; if (!track) throw new CliError('EMPTY_RESULT', `No track found for: ${query}`); - return track; + return { uri: track.uri, name: track.name, artist: track.artists.map((a: any) => a.name).join(', ') }; } function openBrowser(url: string): void { @@ -130,7 +128,18 @@ cli({ args: [], columns: ['status'], func: async () => { - assertSpotifyCredentialsConfigured(credentials, ENV_FILE); + if (!CLIENT_ID || !CLIENT_SECRET) { + const envFile = join(homedir(), '.opencli', 'spotify.env'); + throw new CliError( + 'CONFIG', + `Missing Spotify credentials.\n\n` + + `1. Go to https://developer.spotify.com/dashboard and create an app\n` + + `2. Copy your Client ID and Client Secret\n` + + `3. Open the file: ${envFile}\n` + + `4. Replace the placeholder values and save\n` + + `5. Run: opencli spotify auth` + ); + } return new Promise((resolve, reject) => { const server = createServer(async (req, res) => { try { @@ -278,9 +287,8 @@ cli({ func: async (_page, kwargs) => { const limit = Math.min(50, Math.max(1, Math.round(kwargs.limit))); const data = await api('GET', `/search?q=${encodeURIComponent(kwargs.query)}&type=track&limit=${limit}`); - const results = mapSpotifyTrackResults(data); - if (!results.length) throw new CliError('EMPTY_RESULT', `No results found for: ${kwargs.query}`); - return results; + if (!data?.tracks?.items) throw new CliError('EMPTY_RESULT', `No results found for: ${kwargs.query}`); + return data.tracks.items.map((t: any) => ({ track: t.name, artist: t.artists.map((a: any) => a.name).join(', '), album: t.album.name, uri: t.uri })); }, }); From 34afd4350a1caaf129cc4231106de887df98169f Mon Sep 17 00:00:00 2001 From: jackwener Date: Mon, 30 Mar 2026 12:56:47 +0800 Subject: [PATCH 2/2] fix(spotify): restore credential guardrails --- scripts/postinstall.js | 1 + src/clis/spotify/spotify.ts | 42 +++++++++++++++---------------------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/scripts/postinstall.js b/scripts/postinstall.js index f790baf5..73402354 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -203,6 +203,7 @@ function main() { if (!existsSync(spotifyEnvFile)) { writeFileSync(spotifyEnvFile, `# Spotify credentials — get them at https://developer.spotify.com/dashboard\n` + + `# Add http://127.0.0.1:8888/callback as a Redirect URI in your Spotify app\n` + `SPOTIFY_CLIENT_ID=your_spotify_client_id_here\n` + `SPOTIFY_CLIENT_SECRET=your_spotify_client_secret_here\n`, 'utf8' diff --git a/src/clis/spotify/spotify.ts b/src/clis/spotify/spotify.ts index 49354765..528d0541 100644 --- a/src/clis/spotify/spotify.ts +++ b/src/clis/spotify/spotify.ts @@ -5,6 +5,13 @@ import { createServer } from 'http'; import { homedir } from 'os'; import { join } from 'path'; import { exec } from 'child_process'; +import { + assertSpotifyCredentialsConfigured, + getFirstSpotifyTrack, + mapSpotifyTrackResults, + parseDotEnv, + resolveSpotifyCredentials, +} from './utils.js'; // ── Credentials ─────────────────────────────────────────────────────────────── // Set SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET as environment variables, @@ -16,18 +23,13 @@ const ENV_FILE = join(homedir(), '.opencli', 'spotify.env'); function loadEnv(): Record { if (!existsSync(ENV_FILE)) return {}; - return Object.fromEntries( - readFileSync(ENV_FILE, 'utf-8') - .split('\n') - .map(l => l.trim()) - .filter(l => l && !l.startsWith('#') && l.includes('=')) - .map(l => { const i = l.indexOf('='); return [l.slice(0, i).trim(), l.slice(i + 1).trim()] as [string, string]; }) - ); + return parseDotEnv(readFileSync(ENV_FILE, 'utf-8')); } const env = loadEnv(); -const CLIENT_ID = env.SPOTIFY_CLIENT_ID || process.env.SPOTIFY_CLIENT_ID || ''; -const CLIENT_SECRET = env.SPOTIFY_CLIENT_SECRET || process.env.SPOTIFY_CLIENT_SECRET || ''; +const credentials = resolveSpotifyCredentials(env); +const CLIENT_ID = credentials.clientId; +const CLIENT_SECRET = credentials.clientSecret; const REDIRECT_URI = 'http://127.0.0.1:8888/callback'; const SCOPES = [ 'user-read-playback-state', @@ -107,9 +109,9 @@ async function api(method: string, path: string, body?: unknown): Promise { async function findTrackUri(query: string): Promise<{ uri: string; name: string; artist: string }> { const data = await api('GET', `/search?q=${encodeURIComponent(query)}&type=track&limit=1`); - const track = data?.tracks?.items?.[0]; + const track = getFirstSpotifyTrack(data); if (!track) throw new CliError('EMPTY_RESULT', `No track found for: ${query}`); - return { uri: track.uri, name: track.name, artist: track.artists.map((a: any) => a.name).join(', ') }; + return track; } function openBrowser(url: string): void { @@ -128,18 +130,7 @@ cli({ args: [], columns: ['status'], func: async () => { - if (!CLIENT_ID || !CLIENT_SECRET) { - const envFile = join(homedir(), '.opencli', 'spotify.env'); - throw new CliError( - 'CONFIG', - `Missing Spotify credentials.\n\n` + - `1. Go to https://developer.spotify.com/dashboard and create an app\n` + - `2. Copy your Client ID and Client Secret\n` + - `3. Open the file: ${envFile}\n` + - `4. Replace the placeholder values and save\n` + - `5. Run: opencli spotify auth` - ); - } + assertSpotifyCredentialsConfigured(credentials, ENV_FILE); return new Promise((resolve, reject) => { const server = createServer(async (req, res) => { try { @@ -287,8 +278,9 @@ cli({ func: async (_page, kwargs) => { const limit = Math.min(50, Math.max(1, Math.round(kwargs.limit))); const data = await api('GET', `/search?q=${encodeURIComponent(kwargs.query)}&type=track&limit=${limit}`); - if (!data?.tracks?.items) throw new CliError('EMPTY_RESULT', `No results found for: ${kwargs.query}`); - return data.tracks.items.map((t: any) => ({ track: t.name, artist: t.artists.map((a: any) => a.name).join(', '), album: t.album.name, uri: t.uri })); + const results = mapSpotifyTrackResults(data); + if (!results.length) throw new CliError('EMPTY_RESULT', `No results found for: ${kwargs.query}`); + return results; }, });