Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions browse/src/cookie-import-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*/
Expand Down
19 changes: 15 additions & 4 deletions browse/src/cookie-picker-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ──────────────────────────────────────────────────────
Expand Down Expand Up @@ -90,13 +90,24 @@ export async function handleCookiePickerRoute(
}, { port });
}

// GET /cookie-picker/profiles?browser=<name> — 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=<name> — 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,
Expand All @@ -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({
Expand Down
85 changes: 80 additions & 5 deletions browse/src/cookie-picker-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -268,6 +294,7 @@ export function getCookiePickerHTML(serverPort: number): string {
<div class="panel panel-left">
<div class="panel-header">Source Browser</div>
<div id="browser-pills" class="browser-pills"></div>
<div id="profile-pills" class="profile-pills"></div>
<div class="search-wrap">
<input type="text" class="search-input" id="search" placeholder="Search domains..." />
</div>
Expand All @@ -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');
Expand Down Expand Up @@ -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 = '<span class="dot"></span>' + escHtml(b.name);
pill.onclick = () => selectBrowser(b.name);
$pills.appendChild(pill);
Expand All @@ -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 = '<div class="loading-row"><span class="spinner"></span> Loading profiles...</div>';
$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 = '<div class="imported-empty">Failed to load profiles</div>';
}
}

// ─── 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 = '<div class="loading-row"><span class="spinner"></span> Loading domains...</div>';
$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 = '<div class="imported-empty">Failed to load domains</div>';
}
}
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion browse/src/write-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down