Skip to content

Commit 4155c52

Browse files
committed
feat(core): adapter ecosystem hardening
- Add minCoreVersion + AdapterRequirements to SiteAdapter interface - Add version-check.ts: satisfies(), readCoreVersion(), readAdapterVersion(), parseSemver() - Wire version check into loadAdapter (server.ts) and defineAdapter — clear error on mismatch - Add browserkit doctor CLI command — version table, compat check, patchright check, requirements hints - Update create-adapter scaffold: dynamic version injection, minCoreVersion in template, publishConfig, patchright dep - Add .github/workflows/test-adapters.yml: matrix CI testing all 5 adapters after core publishes Made-with: Cursor
1 parent 8ae495e commit 4155c52

9 files changed

Lines changed: 425 additions & 4 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Test Adapters Against New Core
2+
3+
# Runs automatically after a successful Release workflow (which publishes core to npm).
4+
# Also triggerable manually to check all adapters at any time.
5+
on:
6+
workflow_run:
7+
workflows: ["Release"]
8+
types: [completed]
9+
workflow_dispatch:
10+
11+
jobs:
12+
test-adapter:
13+
# Only run if the Release workflow succeeded (or if triggered manually)
14+
if: >
15+
github.event_name == 'workflow_dispatch' ||
16+
github.event.workflow_run.conclusion == 'success'
17+
name: Test ${{ matrix.adapter }}
18+
runs-on: ubuntu-latest
19+
strategy:
20+
fail-fast: false # test all adapters even if one fails
21+
matrix:
22+
adapter:
23+
- adapter-hackernews
24+
- adapter-google-discover
25+
- adapter-linkedin
26+
- adapter-reddit
27+
- adapter-booking
28+
29+
steps:
30+
- uses: actions/checkout@v4
31+
with:
32+
repository: browserkit-dev/${{ matrix.adapter }}
33+
34+
- uses: actions/setup-node@v4
35+
with:
36+
node-version: 20
37+
38+
- name: Install dependencies
39+
run: npm install # picks up the latest @browserkit-dev/core from npm
40+
41+
- name: Install Patchright Chromium
42+
run: npx patchright install chromium --with-deps
43+
44+
- name: Build
45+
run: npm run build
46+
47+
- name: Test
48+
run: npm test
49+
timeout-minutes: 10

packages/core/src/cli.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { SessionManager, getDefaultDataDir } from "./session-manager.js";
88
import { runLoginCommand } from "./human-handoff.js";import { createAdapter } from "./create-adapter.js";
99
import type { FrameworkConfig, DaemonStatus, AdapterStatus } from "./types.js";
1010
import { getLogger } from "./logger.js";
11+
import { readCoreVersion, satisfies, readAdapterVersion } from "./version-check.js";
1112

1213
const log = getLogger("cli");
1314

@@ -28,6 +29,8 @@ async function main(): Promise<void> {
2829
return cmdStatus(args);
2930
case "config":
3031
return cmdConfig(args);
32+
case "doctor":
33+
return cmdDoctor(args);
3134
case "create-adapter": {
3235
const name = args[0];
3336
if (!name) {
@@ -386,6 +389,7 @@ Commands:
386389
status Show daemon status
387390
config cursor Generate Cursor MCP settings JSON
388391
create-adapter <name> Scaffold a new standalone adapter package
392+
doctor Check version compatibility for all configured adapters
389393
390394
Options (start):
391395
--adapter <pkg> Add an adapter by npm package name (repeatable)
@@ -398,11 +402,149 @@ Examples:
398402
browserkit logout linkedin
399403
browserkit reload google-discover
400404
browserkit status
405+
browserkit doctor
401406
browserkit config cursor
402407
browserkit create-adapter my-site
403408
`);
404409
}
405410

411+
// ─── doctor ──────────────────────────────────────────────────────────────────
412+
413+
async function cmdDoctor(args: string[]): Promise<void> {
414+
const config = await resolveConfig(args);
415+
const coreVer = readCoreVersion();
416+
417+
const col = (s: string, w: number) => s.slice(0, w).padEnd(w);
418+
const issues: string[] = [];
419+
420+
// ── Environment summary ───────────────────────────────────────────────────
421+
console.log();
422+
console.log(` @browserkit-dev/core ${coreVer}`);
423+
const patchrightVer = getPatchrightVersion();
424+
console.log(` patchright ${patchrightVer ?? "(not found)"}`);
425+
console.log(` node ${process.versions.node}`);
426+
console.log();
427+
428+
// ── Adapter table ─────────────────────────────────────────────────────────
429+
const adapterKeys = Object.keys(config.adapters ?? {});
430+
if (adapterKeys.length === 0) {
431+
console.log(" No adapters configured.");
432+
console.log();
433+
return;
434+
}
435+
436+
console.log(
437+
` ${col("Adapter", 36)}${col("Version", 10)}${col("Core req", 12)}Status`
438+
);
439+
console.log(` ${"-".repeat(72)}`);
440+
441+
for (const key of adapterKeys) {
442+
const adapterConfig = config.adapters[key];
443+
if (!adapterConfig) continue;
444+
445+
// Try to load the adapter module to read its metadata
446+
let adapterVersion = readAdapterVersion(key) ?? "?";
447+
let minCoreVer: string | undefined;
448+
let reqs: Record<string, unknown> | undefined;
449+
let loadError: string | undefined;
450+
451+
try {
452+
const mod = await import(key).catch(() => null);
453+
const adapter = mod?.default ?? mod;
454+
if (adapter && typeof adapter === "object") {
455+
minCoreVer = typeof adapter.minCoreVersion === "string" ? adapter.minCoreVersion : undefined;
456+
reqs = adapter.requirements;
457+
}
458+
} catch (err) {
459+
loadError = err instanceof Error ? err.message.split("\n")[0] : String(err);
460+
}
461+
462+
// Short display name
463+
const displayName = key.startsWith("/") || key.startsWith(".")
464+
? path.basename(path.dirname(key))
465+
: key.replace("@browserkit-dev/", "");
466+
467+
let status: string;
468+
if (loadError) {
469+
status = `ERROR — ${loadError}`;
470+
issues.push(`${displayName}: failed to load — ${loadError}`);
471+
} else if (minCoreVer && !satisfies(coreVer, minCoreVer)) {
472+
status = `MISMATCH — needs core >= ${minCoreVer} (have ${coreVer})`;
473+
issues.push(`${displayName}: needs @browserkit-dev/core >= ${minCoreVer}`);
474+
} else {
475+
status = "OK";
476+
}
477+
478+
const coreReq = minCoreVer ? `>= ${minCoreVer}` : "(any)";
479+
console.log(` ${col(displayName, 36)}${col(adapterVersion, 10)}${col(coreReq, 12)}${status}`);
480+
481+
// Requirements vs config mismatch hints
482+
if (reqs && typeof reqs === "object") {
483+
const r = reqs as {
484+
chromeChannelRequired?: boolean;
485+
deviceEmulation?: string;
486+
useCloakBrowser?: boolean;
487+
headedLoginRequired?: boolean;
488+
};
489+
if (r.chromeChannelRequired && !adapterConfig.channel) {
490+
const msg = ` ${" ".repeat(36)} hint: adapter recommends channel:"chrome" in config`;
491+
console.log(msg);
492+
issues.push(`${displayName}: missing channel:"chrome" in config`);
493+
}
494+
if (r.deviceEmulation && !adapterConfig.deviceEmulation) {
495+
const msg = ` ${" ".repeat(36)} hint: adapter recommends deviceEmulation:"${r.deviceEmulation}"`;
496+
console.log(msg);
497+
issues.push(`${displayName}: missing deviceEmulation:"${r.deviceEmulation}" in config`);
498+
}
499+
if (r.useCloakBrowser && !adapterConfig.antiDetection?.useCloakBrowser) {
500+
const msg = ` ${" ".repeat(36)} hint: adapter recommends antiDetection.useCloakBrowser:true`;
501+
console.log(msg);
502+
issues.push(`${displayName}: missing antiDetection.useCloakBrowser:true in config`);
503+
}
504+
}
505+
}
506+
507+
// ── Patchright peer dep check ─────────────────────────────────────────────
508+
console.log();
509+
const patchrightOk = checkPatchrightPeer(patchrightVer);
510+
if (patchrightOk) {
511+
console.log(` patchright: OK (${patchrightVer} satisfies peer dep ^1.51.0)`);
512+
} else {
513+
const msg = `patchright ${patchrightVer ?? "(not found)"} — expected ^1.51.0`;
514+
console.log(` patchright: WARN — ${msg}`);
515+
issues.push(msg);
516+
}
517+
518+
// ── Summary ───────────────────────────────────────────────────────────────
519+
console.log();
520+
if (issues.length === 0) {
521+
console.log(" Everything looks good.");
522+
} else {
523+
console.log(` ${issues.length} issue${issues.length > 1 ? "s" : ""} found.`);
524+
}
525+
console.log();
526+
527+
process.exit(issues.length > 0 ? 1 : 0);
528+
}
529+
530+
function getPatchrightVersion(): string | null {
531+
try {
532+
const pkgPath = new URL("../node_modules/patchright/package.json", import.meta.url);
533+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")) as { version?: string };
534+
return pkg.version ?? null;
535+
} catch {
536+
return null;
537+
}
538+
}
539+
540+
function checkPatchrightPeer(version: string | null): boolean {
541+
if (!version) return false;
542+
// peer dep is ^1.51.0 — major must be 1, minor >= 51
543+
const parts = version.split(".").map(Number);
544+
if (parts.length < 2) return false;
545+
return parts[0] === 1 && (parts[1] ?? 0) >= 51;
546+
}
547+
406548
function readPackageVersion(): string {
407549
try {
408550
const pkgPath = new URL("../package.json", import.meta.url);

packages/core/src/create-adapter.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from "node:fs";
22
import path from "node:path";
3+
import { readCoreVersion } from "./version-check.js";
34

45
/**
56
* Generate a standalone adapter project in the current directory.
@@ -50,9 +51,10 @@ function writeFile(filePath: string, content: string): void {
5051
}
5152

5253
function packageJson(name: string): string {
54+
const coreVersion = readCoreVersion();
5355
return JSON.stringify(
5456
{
55-
name: `browserkit-adapter-${name}`,
57+
name: `@browserkit-dev/adapter-${name}`,
5658
version: "0.1.0",
5759
description: `${name} adapter for browserkit`,
5860
type: "module",
@@ -64,13 +66,14 @@ function packageJson(name: string): string {
6466
test: "vitest run",
6567
lint: "tsc --noEmit",
6668
},
69+
publishConfig: { access: "public" },
6770
peerDependencies: {
68-
"@browserkit-dev/core": ">=0.1.0",
71+
"@browserkit-dev/core": `>=${coreVersion}`,
6972
},
7073
devDependencies: {
71-
"@browserkit-dev/core": "^0.1.0",
74+
"@browserkit-dev/core": `^${coreVersion}`,
7275
"@types/node": "^22.0.0",
73-
playwright: "^1.51.0",
76+
patchright: "^1.51.0",
7477
tsx: "^4.0.0",
7578
typescript: "^5.7.0",
7679
vitest: "^3.0.0",
@@ -161,6 +164,7 @@ export const SELECTORS = {
161164
}
162165

163166
function indexTs(name: string): string {
167+
const coreVersion = readCoreVersion();
164168
const domain = `${name}.com`;
165169
const loginUrl = `https://www.${domain}/login`;
166170
return `import { defineAdapter } from "@browserkit-dev/core";
@@ -172,6 +176,7 @@ export default defineAdapter({
172176
site: "${name}",
173177
domain: "${domain}",
174178
loginUrl: "${loginUrl}",
179+
minCoreVersion: "${coreVersion}",
175180
rateLimit: { minDelayMs: 2000 },
176181
177182
async isLoggedIn(page: Page): Promise<boolean> {

packages/core/src/define-adapter.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { SiteAdapter } from "./types.js";
2+
import { satisfies, readCoreVersion, parseSemver } from "./version-check.js";
23

34
/**
45
* Type-safe helper for defining adapters.
@@ -9,6 +10,17 @@ import type { SiteAdapter } from "./types.js";
910
*/
1011
export function defineAdapter(adapter: SiteAdapter): SiteAdapter {
1112
validateAdapter(adapter);
13+
// Dev-time version check: catch mismatches immediately when the module loads
14+
if (adapter.minCoreVersion) {
15+
const coreVer = readCoreVersion();
16+
if (!satisfies(coreVer, adapter.minCoreVersion)) {
17+
throw new Error(
18+
`Adapter "${adapter.site}" requires @browserkit-dev/core >= ${adapter.minCoreVersion}, ` +
19+
`but the installed version is ${coreVer}.\n` +
20+
`Run: pnpm add @browserkit-dev/core@latest`
21+
);
22+
}
23+
}
1224
return adapter;
1325
}
1426

@@ -45,6 +57,13 @@ function validateAdapter(adapter: SiteAdapter): void {
4557
}
4658
}
4759

60+
// Validate minCoreVersion format if provided
61+
if (adapter.minCoreVersion !== undefined) {
62+
if (parseSemver(adapter.minCoreVersion) === null) {
63+
errors.push(`minCoreVersion: "${adapter.minCoreVersion}" is not a valid X.Y.Z version string`);
64+
}
65+
}
66+
4867
if (errors.length > 0) {
4968
throw new Error(
5069
`Invalid adapter definition:\n${errors.map((e) => ` - ${e}`).join("\n")}`

packages/core/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export type {
2828
AuthErrorType,
2929
LoginOptions,
3030
PossibleLoginResults,
31+
AdapterRequirements,
3132
} from "./types.js";
3233
export { LoginError } from "./types.js";
3334

@@ -86,6 +87,9 @@ export {
8687
// ─── Login flow (opt-in automated login) ─────────────────────────────────────
8788
export { withLoginFlow } from "./login-flow.js";
8889

90+
// ─── Version utilities ────────────────────────────────────────────────────────
91+
export { readCoreVersion, satisfies, parseSemver, readAdapterVersion } from "./version-check.js";
92+
8993
// ─── Observability ────────────────────────────────────────────────────────────
9094
export { withObservability } from "./observability.js";
9195
export type { TraceEntry, ObservabilityOptions } from "./observability.js";

packages/core/src/server.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { FrameworkConfig, DaemonStatus } from "./types.js";
33
import { SessionManager } from "./session-manager.js";
44
import { createAdapterServer, type AdapterServerHandle } from "./adapter-server.js";
55
import { getLogger } from "./logger.js";
6+
import { satisfies, readCoreVersion } from "./version-check.js";
67

78
const log = getLogger("server");
89

@@ -23,6 +24,17 @@ async function loadAdapter(packageName: string) {
2324
`Make sure it uses defineAdapter() and has a default export.`
2425
);
2526
}
27+
// Version compatibility check
28+
if (typeof adapter.minCoreVersion === "string") {
29+
const coreVer = readCoreVersion();
30+
if (!satisfies(coreVer, adapter.minCoreVersion)) {
31+
throw new Error(
32+
`Adapter "${adapter.site}" requires @browserkit-dev/core >= ${adapter.minCoreVersion}, ` +
33+
`but the running version is ${coreVer}.\n` +
34+
`Run: pnpm add @browserkit-dev/core@latest`
35+
);
36+
}
37+
}
2638
return adapter;
2739
} catch (err: unknown) {
2840
if (isModuleNotFoundError(err)) {

0 commit comments

Comments
 (0)