feat(M4): Reusable code abstractions and dead-code removal

This commit is contained in:
Stefano Fiorini
2026-05-03 21:45:49 -05:00
parent 86ad783f82
commit 7495020a9c
98 changed files with 1696 additions and 950 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
* npx tsx auth.ts --url "https://example.com" --type auto
*/
import { getPage, launchBrowser } from './browse.js';
import { getPage, launchBrowser } from './lib/browser.js';
import parseArgs from 'minimist';
import type { Page, BrowserContext } from 'playwright-core';
import { createInterface } from 'readline';
+6 -43
View File
@@ -10,12 +10,13 @@
* 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';
import type { BrowserContext } from 'playwright-core';
import { getProfilePath, launchBrowser, getPage } from './lib/browser.js';
// Re-export shared helpers so existing imports of browse.ts continue to work.
export { getProfilePath, launchBrowser, getPage };
interface BrowseOptions {
url: string;
@@ -37,36 +38,6 @@ 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();
@@ -112,14 +83,6 @@ export async function browse(options: BrowseOptions): Promise<BrowseResult> {
}
}
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'],
+1 -1
View File
@@ -3,7 +3,7 @@
import parseArgs from 'minimist';
import type { Page } from 'playwright-core';
import { launchBrowser } from './browse';
import { launchBrowser } from './lib/browser.js';
type Step =
| { action: 'goto'; url: string }
@@ -0,0 +1,76 @@
// ⚠️ GENERATED FILE do not edit directly. Edit the canonical source in skills/web-automation/shared/ and run `pnpm run sync:pi`.
/**
* Shared browser-launch and profile helpers for web-automation scripts.
*
* Centralises the three reusable primitives that every command entry point
* needs:
* - getProfilePath() — resolve the persistent CloakBrowser profile dir
* - launchBrowser() — launch a CloakBrowser persistent context
* - getPage() — get a ready Page + BrowserContext pair
*
* All command entry points (auth.ts, browse.ts, flow.ts, scan-local-app.ts)
* import from here instead of duplicating these bodies.
*/
import { launchPersistentContext } from 'cloakbrowser';
import { existsSync, mkdirSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import type { BrowserContext, Page } from 'playwright-core';
/**
* Return the path to the persistent CloakBrowser profile directory.
*
* Uses `CLOAKBROWSER_PROFILE_PATH` env var when set; otherwise defaults to
* `~/.cloakbrowser-profile/` and creates it if it does not exist.
*/
export function 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;
}
/**
* Launch a CloakBrowser persistent context with the shared profile.
*
* Headless mode is resolved in order:
* 1. `options.headless` (explicit caller preference)
* 2. `CLOAKBROWSER_HEADLESS` env var
* 3. `true` (safe default)
*/
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;
}
/**
* Return a ready `{ page, browser }` pair using the shared persistent profile.
*
* Re-uses the first existing page or opens a new one if the context is empty.
*/
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 };
}
@@ -3,7 +3,8 @@
import { mkdirSync, writeFileSync } from 'fs';
import { dirname, resolve } from 'path';
import { getPage } from './browse.js';
import type { Page } from 'playwright-core';
import { getPage } from './lib/browser.js';
type NavResult = {
requestedUrl: string;
@@ -40,30 +41,34 @@ function getRoutes(baseUrl: string): string[] {
return [baseUrl];
}
async function gotoWithStatus(page: any, url: string): Promise<NavResult> {
type GotoError = { error: unknown };
async function gotoWithStatus(page: Page, url: string): Promise<NavResult> {
const response = await page
.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 })
.catch((error: unknown) => ({ error }));
.catch((error: unknown): GotoError => ({ error }));
if (response?.error) {
if (response !== null && response !== undefined && 'error' in response) {
const gotoError = response as GotoError;
return {
requestedUrl: url,
url: page.url(),
status: null,
title: await page.title().catch(() => ''),
error: String(response.error),
error: String(gotoError.error),
};
}
const httpResponse = response as Awaited<ReturnType<Page['goto']>>;
return {
requestedUrl: url,
url: page.url(),
status: response ? response.status() : null,
status: httpResponse ? httpResponse.status() : null,
title: await page.title().catch(() => ''),
};
}
async function textOrNull(page: any, selector: string): Promise<string | null> {
async function textOrNull(page: Page, selector: string): Promise<string | null> {
const locator = page.locator(selector).first();
try {
if ((await locator.count()) === 0) return null;
@@ -74,7 +79,7 @@ async function textOrNull(page: any, selector: string): Promise<string | null> {
}
}
async function loginIfConfigured(page: any, baseUrl: string, lines: string[]) {
async function loginIfConfigured(page: Page, baseUrl: string, lines: string[]) {
const loginPath = env('SCAN_LOGIN_PATH');
const username = env('SCAN_USERNAME') ?? env('CLOAKBROWSER_USERNAME');
const password = env('SCAN_PASSWORD') ?? env('CLOAKBROWSER_PASSWORD');
@@ -110,7 +115,7 @@ async function loginIfConfigured(page: any, baseUrl: string, lines: string[]) {
lines.push('');
}
async function checkRoutes(page: any, baseUrl: string, lines: string[]) {
async function checkRoutes(page: Page, baseUrl: string, lines: string[]) {
const routes = getRoutes(baseUrl);
const routeChecks: RouteCheck[] = [];
@@ -11,6 +11,6 @@
"outDir": "./dist",
"rootDir": "."
},
"include": ["*.ts"],
"include": ["*.ts", "lib/**/*.ts"],
"exclude": ["node_modules", "dist"]
}