Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Fetch any URL via Chrome CDP and convert the rendered page to clean markdown with YouTube transcript support.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/lib/browser/profile.ts
1import fs from "node:fs";2import os from "node:os";3import path from "node:path";4import process from "node:process";5import { spawnSync } from "node:child_process";67export interface ResolveSharedChromeProfileDirOptions {8envNames?: string[];9appDataDirName?: string;10profileDirName?: string;11}1213export interface FindExistingChromeDebugPortOptions {14profileDir: string;15timeoutMs?: number;16}1718interface ChromeVersionResponse {19webSocketDebuggerUrl?: string;20}2122const CHROME_LOCK_FILE_NAMES = ["SingletonLock", "SingletonSocket", "SingletonCookie", "chrome.pid"] as const;2324function resolveDataBaseDir(): string {25if (process.platform === "darwin") {26return path.join(os.homedir(), "Library", "Application Support");27}28if (process.platform === "win32") {29return process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");30}31return process.env.XDG_DATA_HOME ?? path.join(os.homedir(), ".local", "share");32}3334export function resolveSharedChromeProfileDir(35options: ResolveSharedChromeProfileDirOptions = {},36): string {37for (const envName of options.envNames ?? []) {38const override = process.env[envName]?.trim();39if (override) {40return path.resolve(override);41}42}4344const appDataDirName = options.appDataDirName ?? "baoyu-skills";45const profileDirName = options.profileDirName ?? "chrome-profile";46return path.join(resolveDataBaseDir(), appDataDirName, profileDirName);47}4849export function resolveChromeProfileDir(profileDir?: string): string {50if (profileDir?.trim()) {51return path.resolve(profileDir.trim());52}5354return resolveSharedChromeProfileDir({55envNames: ["BAOYU_CHROME_PROFILE_DIR"],56appDataDirName: "baoyu-skills",57profileDirName: "chrome-profile",58});59}6061export function ensureChromeProfileDir(profileDir: string): string {62fs.mkdirSync(profileDir, { recursive: true });63return profileDir;64}6566export function hasChromeLockArtifacts(entries: readonly string[]): boolean {67return CHROME_LOCK_FILE_NAMES.some((name) => entries.includes(name));68}6970export function shouldRetryChromeLaunchRecovery(options: {71hasLockArtifacts: boolean;72hasLiveOwner: boolean;73}): boolean {74return options.hasLockArtifacts && !options.hasLiveOwner;75}7677export function findChromeProcessUsingProfile(profileDir: string): boolean {78if (process.platform === "win32") {79return false;80}8182try {83const result = spawnSync("ps", ["aux"], {84encoding: "utf8",85timeout: 5_000,86});87if (result.status !== 0 || !result.stdout) {88return false;89}9091return result.stdout92.split("\n")93.some((line) => line.includes(`--user-data-dir=${profileDir}`));94} catch {95return false;96}97}9899export function cleanChromeLockArtifacts(profileDir: string): void {100for (const name of CHROME_LOCK_FILE_NAMES) {101try {102fs.unlinkSync(path.join(profileDir, name));103} catch {104// Ignore missing files and continue cleaning the remaining artifacts.105}106}107}108109export async function listChromeProfileEntries(profileDir: string): Promise<string[]> {110try {111return await fs.promises.readdir(profileDir);112} catch {113return [];114}115}116117async function fetchWithTimeout(url: string, timeoutMs = 3_000): Promise<Response> {118const controller = new AbortController();119const timer = setTimeout(() => controller.abort(), timeoutMs);120try {121return await fetch(url, {122redirect: "follow",123signal: controller.signal,124});125} finally {126clearTimeout(timer);127}128}129130async function fetchJson<T>(url: string, timeoutMs = 3_000): Promise<T> {131const response = await fetchWithTimeout(url, timeoutMs);132if (!response.ok) {133throw new Error(`Request failed: ${response.status} ${response.statusText}`);134}135return (await response.json()) as T;136}137138async function isDebugPortReady(port: number, timeoutMs = 3_000): Promise<boolean> {139try {140const version = await fetchJson<ChromeVersionResponse>(`http://127.0.0.1:${port}/json/version`, timeoutMs);141return Boolean(version.webSocketDebuggerUrl);142} catch {143return false;144}145}146147function parseDevToolsActivePort(filePath: string): { port: number; wsPath: string } | null {148try {149const content = fs.readFileSync(filePath, "utf8");150const lines = content.split(/\r?\n/);151const port = Number.parseInt(lines[0]?.trim() ?? "", 10);152const wsPath = lines[1]?.trim() ?? "";153if (port > 0 && wsPath) {154return { port, wsPath };155}156} catch {157// Ignore and fall back to process inspection.158}159160return null;161}162163export async function findExistingChromeDebugPort(164options: FindExistingChromeDebugPortOptions,165): Promise<number | null> {166const timeoutMs = options.timeoutMs ?? 3_000;167const activePort = parseDevToolsActivePort(path.join(options.profileDir, "DevToolsActivePort"));168if (activePort && await isDebugPortReady(activePort.port, timeoutMs)) {169return activePort.port;170}171172if (process.platform === "win32") {173return null;174}175176try {177const result = spawnSync("ps", ["aux"], {178encoding: "utf8",179timeout: 5_000,180});181if (result.status !== 0 || !result.stdout) {182return null;183}184185const lines = result.stdout186.split("\n")187.filter((line) => line.includes(options.profileDir) && line.includes("--remote-debugging-port="));188189for (const line of lines) {190const match = line.match(/--remote-debugging-port=(\d+)/);191const port = Number.parseInt(match?.[1] ?? "", 10);192if (port > 0 && await isDebugPortReady(port, timeoutMs)) {193return port;194}195}196} catch {197// Ignore and report no reusable debugger.198}199200return null;201}202