diff --git a/browse/src/cookie-import-browser.ts b/browse/src/cookie-import-browser.ts index 29d9db3e3..2dd1138f5 100644 --- a/browse/src/cookie-import-browser.ts +++ b/browse/src/cookie-import-browser.ts @@ -47,6 +47,11 @@ export interface BrowserInfo { aliases: string[]; } +export interface ProfileEntry { + folder: string; // e.g. "Default", "Profile 1" + name: string; // display name, e.g. "Nik (Nik Redl)" +} + export interface DomainEntry { domain: string; count: number; @@ -101,14 +106,77 @@ const keyCache = new Map(); // ─── Public API ───────────────────────────────────────────────── /** - * Find which browsers are installed (have a cookie DB on disk). + * Find which browsers are installed (have a cookie DB in any profile). */ export function findInstalledBrowsers(): BrowserInfo[] { const appSupport = path.join(os.homedir(), 'Library', 'Application Support'); return BROWSER_REGISTRY.filter(b => { - const dbPath = path.join(appSupport, b.dataDir, 'Default', 'Cookies'); - try { return fs.existsSync(dbPath); } catch { return false; } + const browserDir = path.join(appSupport, b.dataDir); + try { + if (!fs.existsSync(browserDir)) return false; + // Check Default profile first, then any Profile N directory + if (fs.existsSync(path.join(browserDir, 'Default', 'Cookies'))) return true; + return fs.readdirSync(browserDir).some(entry => + entry.startsWith('Profile ') && + fs.existsSync(path.join(browserDir, entry, 'Cookies')) + ); + } catch { return false; } + }); +} + +/** + * List profiles for a browser by reading Local State JSON. + * Returns folders that have a Cookies DB on disk. + */ +export function listProfiles(browserName: string): ProfileEntry[] { + const browser = resolveBrowser(browserName); + const appSupport = path.join(os.homedir(), 'Library', 'Application Support'); + const browserDir = path.join(appSupport, browser.dataDir); + const localStatePath = path.join(browserDir, 'Local State'); + + // Read display names from Local State + let profileNames: Record = {}; + try { + const raw = fs.readFileSync(localStatePath, 'utf-8'); + const data = JSON.parse(raw); + profileNames = data?.profile?.info_cache ?? {}; + } catch { + // No Local State — fall back to folder names only + } + + // Find profile folders that have a Cookies DB + const profiles: ProfileEntry[] = []; + try { + const entries = fs.readdirSync(browserDir); + for (const folder of entries) { + if (folder !== 'Default' && !folder.startsWith('Profile ')) continue; + const cookiePath = path.join(browserDir, folder, 'Cookies'); + if (!fs.existsSync(cookiePath)) continue; + + const info = profileNames[folder]; + let displayName = folder; + if (info) { + const parts = [info.name, info.gaia_name].filter(Boolean); + displayName = parts.length > 0 + ? (info.name && info.gaia_name && info.name !== info.gaia_name + ? `${info.name} (${info.gaia_name})` + : parts[0]!) + : folder; + } + profiles.push({ folder, name: displayName }); + } + } catch { + // If we can't read the directory, return empty + } + + // Sort: Default first, then by folder name + profiles.sort((a, b) => { + if (a.folder === 'Default') return -1; + if (b.folder === 'Default') return 1; + return a.folder.localeCompare(b.folder, undefined, { numeric: true }); }); + + return profiles; } /** diff --git a/browse/src/cookie-picker-routes.ts b/browse/src/cookie-picker-routes.ts index 6a4a43192..0e6972484 100644 --- a/browse/src/cookie-picker-routes.ts +++ b/browse/src/cookie-picker-routes.ts @@ -14,7 +14,7 @@ */ import type { BrowserManager } from './browser-manager'; -import { findInstalledBrowsers, listDomains, importCookies, CookieImportError, type PlaywrightCookie } from './cookie-import-browser'; +import { findInstalledBrowsers, listProfiles, listDomains, importCookies, CookieImportError, type PlaywrightCookie } from './cookie-import-browser'; import { getCookiePickerHTML } from './cookie-picker-ui'; // ─── State ────────────────────────────────────────────────────── @@ -90,13 +90,24 @@ export async function handleCookiePickerRoute( }, { port }); } - // GET /cookie-picker/domains?browser= — list domains + counts + // GET /cookie-picker/profiles?browser= — list profiles for a browser + if (pathname === '/cookie-picker/profiles' && req.method === 'GET') { + const browserName = url.searchParams.get('browser'); + if (!browserName) { + return errorResponse("Missing 'browser' parameter", 'missing_param', { port }); + } + const profiles = listProfiles(browserName); + return jsonResponse({ profiles }, { port }); + } + + // GET /cookie-picker/domains?browser=&profile= — list domains + counts if (pathname === '/cookie-picker/domains' && req.method === 'GET') { const browserName = url.searchParams.get('browser'); if (!browserName) { return errorResponse("Missing 'browser' parameter", 'missing_param', { port }); } - const result = listDomains(browserName); + const profile = url.searchParams.get('profile') || 'Default'; + const result = listDomains(browserName, profile); return jsonResponse({ browser: result.browser, domains: result.domains, @@ -112,14 +123,14 @@ export async function handleCookiePickerRoute( return errorResponse('Invalid JSON body', 'bad_request', { port }); } - const { browser, domains } = body; + const { browser, domains, profile } = body; if (!browser) return errorResponse("Missing 'browser' field", 'missing_param', { port }); if (!domains || !Array.isArray(domains) || domains.length === 0) { return errorResponse("Missing or empty 'domains' array", 'missing_param', { port }); } // Decrypt cookies from the browser DB - const result = await importCookies(browser, domains); + const result = await importCookies(browser, domains, profile || 'Default'); if (result.cookies.length === 0) { return jsonResponse({ diff --git a/browse/src/cookie-picker-ui.ts b/browse/src/cookie-picker-ui.ts index 010c2dd75..af4484bac 100644 --- a/browse/src/cookie-picker-ui.ts +++ b/browse/src/cookie-picker-ui.ts @@ -101,6 +101,31 @@ export function getCookiePickerHTML(serverPort: number): string { background: #4ade80; } + /* ─── Profile Select ────────────────────── */ + .profile-wrap { + padding: 0 20px 12px; + } + .profile-select { + width: 100%; + padding: 8px 12px; + border-radius: 8px; + border: 1px solid #333; + background: #141414; + color: #e0e0e0; + font-size: 13px; + outline: none; + transition: border-color 0.15s; + cursor: pointer; + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 5l3 3 3-3' stroke='%23666' fill='none' stroke-width='1.5'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + padding-right: 32px; + } + .profile-select:focus { border-color: #555; } + .profile-wrap.hidden { display: none; } + /* ─── Search ──────────────────────────── */ .search-wrap { padding: 0 20px 12px; @@ -268,6 +293,9 @@ export function getCookiePickerHTML(serverPort: number): string {
Source Browser
+
@@ -291,11 +319,14 @@ export function getCookiePickerHTML(serverPort: number): string { (function() { const BASE = '${baseUrl}'; let activeBrowser = null; + let activeProfile = 'Default'; let allDomains = []; let importedSet = {}; // domain → count let inflight = {}; // domain → true (prevents double-click) const $pills = document.getElementById('browser-pills'); + const $profileWrap = document.getElementById('profile-wrap'); + const $profileSelect = document.getElementById('profile-select'); const $search = document.getElementById('search'); const $sourceDomains = document.getElementById('source-domains'); const $importedDomains = document.getElementById('imported-domains'); @@ -380,22 +411,58 @@ export function getCookiePickerHTML(serverPort: number): string { // ─── Select Browser ──────────────────── async function selectBrowser(name) { activeBrowser = name; + activeProfile = 'Default'; // Update pills $pills.querySelectorAll('.pill').forEach(p => { p.classList.toggle('active', p.textContent === name); }); - $sourceDomains.innerHTML = '
Loading domains...
'; + $sourceDomains.innerHTML = '
Loading profiles...
'; $sourceFooter.textContent = ''; $search.value = ''; try { - const data = await api('/domains?browser=' + encodeURIComponent(name)); + // Fetch profiles for this browser + const profileData = await api('/profiles?browser=' + encodeURIComponent(name)); + const profiles = profileData.profiles || []; + + if (profiles.length > 1) { + // Show profile dropdown + $profileWrap.classList.remove('hidden'); + $profileSelect.innerHTML = ''; + for (const p of profiles) { + const opt = document.createElement('option'); + opt.value = p.folder; + opt.textContent = p.name + (p.name !== p.folder ? ' (' + p.folder + ')' : ''); + $profileSelect.appendChild(opt); + } + $profileSelect.value = 'Default'; + activeProfile = 'Default'; + } else { + $profileWrap.classList.add('hidden'); + activeProfile = profiles.length === 1 ? profiles[0].folder : 'Default'; + } + + await loadDomains(); + } catch (err) { + showBanner(err.message, 'error', err.action === 'retry' ? () => selectBrowser(name) : null); + $sourceDomains.innerHTML = '
Failed to load
'; + } + } + + // ─── Load Domains for Active Browser+Profile ── + async function loadDomains() { + $sourceDomains.innerHTML = '
Loading domains...
'; + $sourceFooter.textContent = ''; + + try { + const data = await api('/domains?browser=' + encodeURIComponent(activeBrowser) + + '&profile=' + encodeURIComponent(activeProfile)); allDomains = data.domains; renderSourceDomains(); } catch (err) { - showBanner(err.message, 'error', err.action === 'retry' ? () => selectBrowser(name) : null); + showBanner(err.message, 'error', err.action === 'retry' ? () => loadDomains() : null); $sourceDomains.innerHTML = '
Failed to load domains
'; } } @@ -453,7 +520,7 @@ export function getCookiePickerHTML(serverPort: number): string { const data = await api('/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ browser: activeBrowser, domains: [domain] }), + body: JSON.stringify({ browser: activeBrowser, domains: [domain], profile: activeProfile }), }); if (data.domainCounts) { @@ -529,6 +596,13 @@ export function getCookiePickerHTML(serverPort: number): string { } } + // ─── Profile Change ──────────────────── + $profileSelect.addEventListener('change', () => { + activeProfile = $profileSelect.value; + $search.value = ''; + loadDomains(); + }); + // ─── Search ──────────────────────────── $search.addEventListener('input', renderSourceDomains);