189 lines
5.2 KiB
TypeScript
189 lines
5.2 KiB
TypeScript
#!/usr/bin/env npx tsx
|
|
|
|
/**
|
|
* Browser launcher using CloakBrowser with persistent profile
|
|
*
|
|
* Usage:
|
|
* npx tsx browse.ts --url "https://example.com"
|
|
* npx tsx browse.ts --url "https://example.com" --screenshot --output page.png
|
|
* npx tsx browse.ts --url "https://example.com" --headless false --wait 5000
|
|
*/
|
|
|
|
import { launchPersistentContext } from 'cloakbrowser';
|
|
import { homedir } from 'os';
|
|
import { join } from 'path';
|
|
import { existsSync, mkdirSync } from 'fs';
|
|
import parseArgs from 'minimist';
|
|
import type { Page, BrowserContext } from 'playwright-core';
|
|
|
|
interface BrowseOptions {
|
|
url: string;
|
|
headless?: boolean;
|
|
screenshot?: boolean;
|
|
output?: string;
|
|
wait?: number;
|
|
timeout?: number;
|
|
interactive?: boolean;
|
|
}
|
|
|
|
interface BrowseResult {
|
|
title: string;
|
|
url: string;
|
|
screenshotPath?: string;
|
|
}
|
|
|
|
function sleep(ms: number): Promise<void> {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
const getProfilePath = (): string => {
|
|
const customPath = process.env.CLOAKBROWSER_PROFILE_PATH;
|
|
if (customPath) return customPath;
|
|
|
|
const profileDir = join(homedir(), '.cloakbrowser-profile');
|
|
if (!existsSync(profileDir)) {
|
|
mkdirSync(profileDir, { recursive: true });
|
|
}
|
|
return profileDir;
|
|
};
|
|
|
|
export async function launchBrowser(options: {
|
|
headless?: boolean;
|
|
}): Promise<BrowserContext> {
|
|
const profilePath = getProfilePath();
|
|
const envHeadless = process.env.CLOAKBROWSER_HEADLESS;
|
|
const headless = options.headless ?? (envHeadless ? envHeadless === 'true' : true);
|
|
|
|
console.log(`Using profile: ${profilePath}`);
|
|
console.log(`Headless mode: ${headless}`);
|
|
|
|
const context = await launchPersistentContext({
|
|
userDataDir: profilePath,
|
|
headless,
|
|
humanize: true,
|
|
});
|
|
|
|
return context;
|
|
}
|
|
|
|
export async function browse(options: BrowseOptions): Promise<BrowseResult> {
|
|
const browser = await launchBrowser({ headless: options.headless });
|
|
const page = browser.pages()[0] || await browser.newPage();
|
|
|
|
try {
|
|
console.log(`Navigating to: ${options.url}`);
|
|
await page.goto(options.url, {
|
|
timeout: options.timeout ?? 60000,
|
|
waitUntil: 'domcontentloaded',
|
|
});
|
|
|
|
if (options.wait) {
|
|
console.log(`Waiting ${options.wait}ms...`);
|
|
await sleep(options.wait);
|
|
}
|
|
|
|
const result: BrowseResult = {
|
|
title: await page.title(),
|
|
url: page.url(),
|
|
};
|
|
|
|
console.log(`Page title: ${result.title}`);
|
|
console.log(`Final URL: ${result.url}`);
|
|
|
|
if (options.screenshot) {
|
|
const outputPath = options.output ?? 'screenshot.png';
|
|
await page.screenshot({ path: outputPath, fullPage: true });
|
|
result.screenshotPath = outputPath;
|
|
console.log(`Screenshot saved: ${outputPath}`);
|
|
}
|
|
|
|
if (options.interactive) {
|
|
console.log('\nInteractive mode - browser will stay open.');
|
|
console.log('Press Ctrl+C to close.');
|
|
await new Promise(() => {});
|
|
}
|
|
|
|
return result;
|
|
} finally {
|
|
if (!options.interactive) {
|
|
await browser.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function getPage(options?: {
|
|
headless?: boolean;
|
|
}): Promise<{ page: Page; browser: BrowserContext }> {
|
|
const browser = await launchBrowser({ headless: options?.headless });
|
|
const page = browser.pages()[0] || await browser.newPage();
|
|
return { page, browser };
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2), {
|
|
string: ['url', 'output'],
|
|
boolean: ['screenshot', 'headless', 'interactive', 'help'],
|
|
default: {
|
|
headless: true,
|
|
screenshot: false,
|
|
interactive: false,
|
|
},
|
|
alias: {
|
|
u: 'url',
|
|
o: 'output',
|
|
s: 'screenshot',
|
|
h: 'help',
|
|
i: 'interactive',
|
|
},
|
|
});
|
|
|
|
if (args.help || !args.url) {
|
|
console.log(`
|
|
Web Browser with CloakBrowser
|
|
|
|
Usage:
|
|
npx tsx browse.ts --url <url> [options]
|
|
|
|
Options:
|
|
-u, --url <url> URL to navigate to (required)
|
|
-s, --screenshot Take a screenshot of the page
|
|
-o, --output <path> Output path for screenshot (default: screenshot.png)
|
|
--headless <bool> Run in headless mode (default: true)
|
|
--wait <ms> Wait time after page load in milliseconds
|
|
--timeout <ms> Navigation timeout (default: 60000)
|
|
-i, --interactive Keep browser open for manual interaction
|
|
-h, --help Show this help message
|
|
|
|
Examples:
|
|
npx tsx browse.ts --url "https://example.com"
|
|
npx tsx browse.ts --url "https://example.com" --screenshot --output page.png
|
|
npx tsx browse.ts --url "https://example.com" --headless false --interactive
|
|
|
|
Environment Variables:
|
|
CLOAKBROWSER_PROFILE_PATH Custom profile directory (default: ~/.cloakbrowser-profile/)
|
|
CLOAKBROWSER_HEADLESS Default headless mode (true/false)
|
|
`);
|
|
process.exit(args.help ? 0 : 1);
|
|
}
|
|
|
|
try {
|
|
await browse({
|
|
url: args.url,
|
|
headless: args.headless,
|
|
screenshot: args.screenshot,
|
|
output: args.output,
|
|
wait: args.wait ? parseInt(args.wait, 10) : undefined,
|
|
timeout: args.timeout ? parseInt(args.timeout, 10) : undefined,
|
|
interactive: args.interactive,
|
|
});
|
|
} catch (error) {
|
|
console.error('Error:', error instanceof Error ? error.message : error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
const isMainModule = process.argv[1]?.includes('browse.ts');
|
|
if (isMainModule) {
|
|
main();
|
|
}
|