diff --git a/browse/src/cookie-import-browser.ts b/browse/src/cookie-import-browser.ts index 29d9db3..a0f35fc 100644 --- a/browse/src/cookie-import-browser.ts +++ b/browse/src/cookie-import-browser.ts @@ -52,6 +52,11 @@ export interface DomainEntry { count: number; } +export interface ProfileInfo { + id: string; // directory name: 'Default', 'Profile 1', etc. + name: string; // display name from Preferences, falls back to id +} + export interface ImportResult { cookies: PlaywrightCookie[]; count: number; @@ -111,6 +116,58 @@ export function findInstalledBrowsers(): BrowserInfo[] { }); } +/** + * List available profiles for a browser (subdirs with a Cookies file). + * Returns ProfileInfo[] sorted Default-first, then Profile N numerically. + * Display names are read from Preferences JSON; fall back to dir name. + */ +export function listProfiles(browserName: string): ProfileInfo[] { + const browser = resolveBrowser(browserName); + const appSupport = path.join(os.homedir(), 'Library', 'Application Support'); + const dataDir = path.join(appSupport, browser.dataDir); + const profiles: ProfileInfo[] = []; + + try { + const entries = fs.readdirSync(dataDir, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const id = entry.name; + if (id !== 'Default' && !id.match(/^Profile \d+$/)) continue; + const cookiePath = path.join(dataDir, id, 'Cookies'); + if (!fs.existsSync(cookiePath)) continue; + const name = readProfileName(path.join(dataDir, id)) || id; + profiles.push({ id, name }); + } + } catch { + return [{ id: 'Default', name: 'Default' }]; + } + + profiles.sort((a, b) => { + if (a.id === 'Default') return -1; + if (b.id === 'Default') return 1; + const nA = parseInt(a.id.replace('Profile ', ''), 10); + const nB = parseInt(b.id.replace('Profile ', ''), 10); + return nA - nB; + }); + + return profiles.length > 0 ? profiles : [{ id: 'Default', name: 'Default' }]; +} + +function readProfileName(profileDir: string): string | null { + try { + const prefsPath = path.join(profileDir, 'Preferences'); + const raw = fs.readFileSync(prefsPath, 'utf-8'); + const prefs = JSON.parse(raw); + // Prefer signed-in account email (most distinctive for multi-account setups) + const email = prefs?.account_info?.[0]?.email; + if (email) return email; + // Fall back to profile display name + return prefs?.profile?.name ?? null; + } catch { + return null; + } +} + /** * List unique cookie domains + counts from a browser's DB. No decryption. */ diff --git a/browse/src/cookie-picker-routes.ts b/browse/src/cookie-picker-routes.ts index 6a4a431..9cbce5c 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, listDomains, listProfiles, importCookies, CookieImportError, type PlaywrightCookie, type ProfileInfo } from './cookie-import-browser'; import { getCookiePickerHTML } from './cookie-picker-ui'; // ─── State ────────────────────────────────────────────────────── @@ -90,13 +90,24 @@ export async function handleCookiePickerRoute( }, { port }); } + // 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: ProfileInfo[] = listProfiles(browserName); + return jsonResponse({ profiles }, { port }); + } + // GET /cookie-picker/domains?browser= — 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: importProfile } = 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, importProfile || '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 010c2dd..ff0fd64 100644 --- a/browse/src/cookie-picker-ui.ts +++ b/browse/src/cookie-picker-ui.ts @@ -101,6 +101,32 @@ export function getCookiePickerHTML(serverPort: number): string { background: #4ade80; } + /* ─── Profile Pills ──────────────────── */ + .profile-pills { + display: flex; + gap: 6px; + padding: 0 20px 12px; + flex-wrap: wrap; + } + .profile-pill { + padding: 4px 10px; + border-radius: 5px; + border: 1px solid #2a2a2a; + background: #141414; + color: #888; + font-size: 11px; + font-family: 'SF Mono', 'Fira Code', monospace; + cursor: pointer; + transition: all 0.15s; + } + .profile-pill:hover { border-color: #444; color: #ccc; } + .profile-pill.active { + border-color: #60a5fa; + background: #0a1828; + color: #60a5fa; + } + .profile-pills:empty { display: none; } + /* ─── Search ──────────────────────────── */ .search-wrap { padding: 0 20px 12px; @@ -268,6 +294,7 @@ export function getCookiePickerHTML(serverPort: number): string {
Source Browser
+
@@ -291,11 +318,13 @@ 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 $profilePills = document.getElementById('profile-pills'); const $search = document.getElementById('search'); const $sourceDomains = document.getElementById('source-domains'); const $importedDomains = document.getElementById('imported-domains'); @@ -364,6 +393,7 @@ export function getCookiePickerHTML(serverPort: number): string { browsers.forEach(b => { const pill = document.createElement('button'); pill.className = 'pill'; + pill.dataset.browser = b.name; pill.innerHTML = '' + escHtml(b.name); pill.onclick = () => selectBrowser(b.name); $pills.appendChild(pill); @@ -380,22 +410,67 @@ export function getCookiePickerHTML(serverPort: number): string { // ─── Select Browser ──────────────────── async function selectBrowser(name) { activeBrowser = name; + activeProfile = 'Default'; - // Update pills + // Update browser pills $pills.querySelectorAll('.pill').forEach(p => { - p.classList.toggle('active', p.textContent === name); + p.classList.toggle('active', p.dataset.browser === name); }); + $profilePills.innerHTML = ''; + $sourceDomains.innerHTML = '
Loading profiles...
'; + $sourceFooter.textContent = ''; + $search.value = ''; + + try { + const profileData = await api('/profiles?browser=' + encodeURIComponent(name)); + const profiles = profileData.profiles || ['Default']; + renderProfilePills(name, profiles); + await loadDomains(name, (profiles[0] && (profiles[0].id || profiles[0])) || 'Default'); + } catch (err) { + showBanner(err.message, 'error', err.action === 'retry' ? () => selectBrowser(name) : null); + $sourceDomains.innerHTML = '
Failed to load profiles
'; + } + } + + // ─── Render Profile Pills ────────────── + function renderProfilePills(browser, profiles) { + if (profiles.length <= 1) { + $profilePills.innerHTML = ''; + return; + } + $profilePills.innerHTML = ''; + profiles.forEach(p => { + const id = p.id || p; + const label = p.name || p; + const btn = document.createElement('button'); + btn.className = 'profile-pill' + (id === activeProfile ? ' active' : ''); + btn.textContent = label; + btn.dataset.profileId = id; + btn.onclick = () => { + activeProfile = id; + $profilePills.querySelectorAll('.profile-pill').forEach(el => { + el.classList.toggle('active', el.dataset.profileId === id); + }); + loadDomains(browser, id); + }; + $profilePills.appendChild(btn); + }); + } + + // ─── Load Domains for Profile ────────── + async function loadDomains(browser, profile) { + activeProfile = profile; $sourceDomains.innerHTML = '
Loading domains...
'; $sourceFooter.textContent = ''; $search.value = ''; try { - const data = await api('/domains?browser=' + encodeURIComponent(name)); + const data = await api('/domains?browser=' + encodeURIComponent(browser) + '&profile=' + encodeURIComponent(profile)); allDomains = data.domains; renderSourceDomains(); } catch (err) { - showBanner(err.message, 'error', err.action === 'retry' ? () => selectBrowser(name) : null); + showBanner(err.message, 'error', err.action === 'retry' ? () => loadDomains(browser, profile) : null); $sourceDomains.innerHTML = '
Failed to load domains
'; } } @@ -453,7 +528,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) { diff --git a/browse/src/write-commands.ts b/browse/src/write-commands.ts index 2b38492..4238c90 100644 --- a/browse/src/write-commands.ts +++ b/browse/src/write-commands.ts @@ -315,7 +315,9 @@ export async function handleWriteCommand( // Direct import mode — no UI const domain = args[domainIdx + 1]; const browser = browserArg || 'comet'; - const result = await importCookies(browser, [domain]); + const profileIdx = args.indexOf('--profile'); + const directProfile = profileIdx !== -1 && profileIdx + 1 < args.length ? args[profileIdx + 1] : 'Default'; + const result = await importCookies(browser, [domain], directProfile); if (result.cookies.length > 0) { await page.context().addCookies(result.cookies); }