Add 260+ Claude Code skills from skills.sh

Complete collection of AI agent skills including:
- Frontend Development (Vue, React, Next.js, Three.js)
- Backend Development (NestJS, FastAPI, Node.js)
- Mobile Development (React Native, Expo)
- Testing (E2E, frontend, webapp)
- DevOps (GitHub Actions, CI/CD)
- Marketing (SEO, copywriting, analytics)
- Security (binary analysis, vulnerability scanning)
- And many more...

Synchronized from: https://skills.sh/

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
admin
2026-01-23 18:02:28 +00:00
Unverified
commit 07242683bf
3300 changed files with 1223105 additions and 0 deletions

View File

@@ -0,0 +1,211 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { fakeBrowser } from "wxt/testing";
import { CDPRouter } from "../services/CDPRouter";
import { TabManager } from "../services/TabManager";
import type { Logger } from "../utils/logger";
import type { ExtensionCommandMessage } from "../utils/types";
// Mock chrome.debugger since fakeBrowser doesn't include it
const mockDebuggerSendCommand = vi.fn();
vi.stubGlobal("chrome", {
...fakeBrowser,
debugger: {
sendCommand: mockDebuggerSendCommand,
attach: vi.fn(),
detach: vi.fn(),
onEvent: { addListener: vi.fn(), hasListener: vi.fn() },
onDetach: { addListener: vi.fn(), hasListener: vi.fn() },
getTargets: vi.fn().mockResolvedValue([]),
},
});
describe("CDPRouter", () => {
let cdpRouter: CDPRouter;
let tabManager: TabManager;
let mockLogger: Logger;
let mockSendMessage: ReturnType<typeof vi.fn>;
beforeEach(() => {
fakeBrowser.reset();
mockDebuggerSendCommand.mockReset();
mockLogger = {
log: vi.fn(),
debug: vi.fn(),
error: vi.fn(),
};
mockSendMessage = vi.fn();
tabManager = new TabManager({
logger: mockLogger,
sendMessage: mockSendMessage,
});
cdpRouter = new CDPRouter({
logger: mockLogger,
tabManager,
});
});
describe("handleCommand", () => {
it("should return early for non-forwardCDPCommand methods", async () => {
const msg = {
id: 1,
method: "someOtherMethod" as const,
params: { method: "Test.method" },
};
// @ts-expect-error - testing invalid method
const result = await cdpRouter.handleCommand(msg);
expect(result).toBeUndefined();
});
it("should throw error when no tab found for command", async () => {
const msg: ExtensionCommandMessage = {
id: 1,
method: "forwardCDPCommand",
params: {
method: "Page.navigate",
sessionId: "unknown-session",
},
};
await expect(cdpRouter.handleCommand(msg)).rejects.toThrow(
"No tab found for method Page.navigate"
);
});
it("should find tab by sessionId", async () => {
tabManager.set(123, {
sessionId: "session-1",
targetId: "target-1",
state: "connected",
});
mockDebuggerSendCommand.mockResolvedValue({ result: "ok" });
const msg: ExtensionCommandMessage = {
id: 1,
method: "forwardCDPCommand",
params: {
method: "Page.navigate",
sessionId: "session-1",
params: { url: "https://example.com" },
},
};
await cdpRouter.handleCommand(msg);
expect(mockDebuggerSendCommand).toHaveBeenCalledWith(
{ tabId: 123, sessionId: undefined },
"Page.navigate",
{ url: "https://example.com" }
);
});
it("should find tab via child session", async () => {
tabManager.set(123, {
sessionId: "parent-session",
targetId: "target-1",
state: "connected",
});
tabManager.trackChildSession("child-session", 123);
mockDebuggerSendCommand.mockResolvedValue({});
const msg: ExtensionCommandMessage = {
id: 1,
method: "forwardCDPCommand",
params: {
method: "Runtime.evaluate",
sessionId: "child-session",
},
};
await cdpRouter.handleCommand(msg);
expect(mockDebuggerSendCommand).toHaveBeenCalledWith(
{ tabId: 123, sessionId: "child-session" },
"Runtime.evaluate",
undefined
);
});
});
describe("handleDebuggerEvent", () => {
it("should forward CDP events to relay", () => {
tabManager.set(123, {
sessionId: "session-1",
targetId: "target-1",
state: "connected",
});
const sendMessage = vi.fn();
cdpRouter.handleDebuggerEvent(
{ tabId: 123 },
"Page.loadEventFired",
{ timestamp: 12345 },
sendMessage
);
expect(sendMessage).toHaveBeenCalledWith({
method: "forwardCDPEvent",
params: {
sessionId: "session-1",
method: "Page.loadEventFired",
params: { timestamp: 12345 },
},
});
});
it("should track child sessions on Target.attachedToTarget", () => {
tabManager.set(123, {
sessionId: "session-1",
targetId: "target-1",
state: "connected",
});
const sendMessage = vi.fn();
cdpRouter.handleDebuggerEvent(
{ tabId: 123 },
"Target.attachedToTarget",
{ sessionId: "new-child-session", targetInfo: {} },
sendMessage
);
expect(tabManager.getParentTabId("new-child-session")).toBe(123);
});
it("should untrack child sessions on Target.detachedFromTarget", () => {
tabManager.set(123, {
sessionId: "session-1",
targetId: "target-1",
state: "connected",
});
tabManager.trackChildSession("child-session", 123);
const sendMessage = vi.fn();
cdpRouter.handleDebuggerEvent(
{ tabId: 123 },
"Target.detachedFromTarget",
{ sessionId: "child-session" },
sendMessage
);
expect(tabManager.getParentTabId("child-session")).toBeUndefined();
});
it("should ignore events for unknown tabs", () => {
const sendMessage = vi.fn();
cdpRouter.handleDebuggerEvent({ tabId: 999 }, "Page.loadEventFired", {}, sendMessage);
expect(sendMessage).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,45 @@
import { describe, it, expect, beforeEach } from "vitest";
import { fakeBrowser } from "wxt/testing";
import { StateManager } from "../services/StateManager";
describe("StateManager", () => {
let stateManager: StateManager;
beforeEach(() => {
fakeBrowser.reset();
stateManager = new StateManager();
});
describe("getState", () => {
it("should return default inactive state when no stored state", async () => {
const state = await stateManager.getState();
expect(state).toEqual({ isActive: false });
});
it("should return stored state when available", async () => {
await fakeBrowser.storage.local.set({
devBrowserActiveState: { isActive: true },
});
const state = await stateManager.getState();
expect(state).toEqual({ isActive: true });
});
});
describe("setState", () => {
it("should persist state to storage", async () => {
await stateManager.setState({ isActive: true });
const stored = await fakeBrowser.storage.local.get("devBrowserActiveState");
expect(stored.devBrowserActiveState).toEqual({ isActive: true });
});
it("should update state from active to inactive", async () => {
await stateManager.setState({ isActive: true });
await stateManager.setState({ isActive: false });
const state = await stateManager.getState();
expect(state).toEqual({ isActive: false });
});
});
});

View File

@@ -0,0 +1,170 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { fakeBrowser } from "wxt/testing";
import { TabManager } from "../services/TabManager";
import type { Logger } from "../utils/logger";
describe("TabManager", () => {
let tabManager: TabManager;
let mockLogger: Logger;
let mockSendMessage: ReturnType<typeof vi.fn>;
beforeEach(() => {
fakeBrowser.reset();
mockLogger = {
log: vi.fn(),
debug: vi.fn(),
error: vi.fn(),
};
mockSendMessage = vi.fn();
tabManager = new TabManager({
logger: mockLogger,
sendMessage: mockSendMessage,
});
});
describe("getBySessionId", () => {
it("should return undefined when no tabs exist", () => {
const result = tabManager.getBySessionId("session-1");
expect(result).toBeUndefined();
});
it("should find tab by session ID", () => {
tabManager.set(123, {
sessionId: "session-1",
targetId: "target-1",
state: "connected",
});
const result = tabManager.getBySessionId("session-1");
expect(result).toEqual({
tabId: 123,
tab: {
sessionId: "session-1",
targetId: "target-1",
state: "connected",
},
});
});
});
describe("getByTargetId", () => {
it("should return undefined when no tabs exist", () => {
const result = tabManager.getByTargetId("target-1");
expect(result).toBeUndefined();
});
it("should find tab by target ID", () => {
tabManager.set(456, {
sessionId: "session-2",
targetId: "target-2",
state: "connected",
});
const result = tabManager.getByTargetId("target-2");
expect(result).toEqual({
tabId: 456,
tab: {
sessionId: "session-2",
targetId: "target-2",
state: "connected",
},
});
});
});
describe("child sessions", () => {
it("should track child sessions", () => {
tabManager.trackChildSession("child-session-1", 123);
expect(tabManager.getParentTabId("child-session-1")).toBe(123);
});
it("should untrack child sessions", () => {
tabManager.trackChildSession("child-session-1", 123);
tabManager.untrackChildSession("child-session-1");
expect(tabManager.getParentTabId("child-session-1")).toBeUndefined();
});
});
describe("set/get/has", () => {
it("should set and get tab info", () => {
tabManager.set(789, { state: "connecting" });
expect(tabManager.get(789)).toEqual({ state: "connecting" });
expect(tabManager.has(789)).toBe(true);
});
it("should return undefined for unknown tabs", () => {
expect(tabManager.get(999)).toBeUndefined();
expect(tabManager.has(999)).toBe(false);
});
});
describe("detach", () => {
it("should send detached event and remove tab", () => {
tabManager.set(123, {
sessionId: "session-1",
targetId: "target-1",
state: "connected",
});
tabManager.detach(123, false);
expect(mockSendMessage).toHaveBeenCalledWith({
method: "forwardCDPEvent",
params: {
method: "Target.detachedFromTarget",
params: { sessionId: "session-1", targetId: "target-1" },
},
});
expect(tabManager.has(123)).toBe(false);
});
it("should clean up child sessions when detaching", () => {
tabManager.set(123, {
sessionId: "session-1",
targetId: "target-1",
state: "connected",
});
tabManager.trackChildSession("child-1", 123);
tabManager.trackChildSession("child-2", 123);
tabManager.detach(123, false);
expect(tabManager.getParentTabId("child-1")).toBeUndefined();
expect(tabManager.getParentTabId("child-2")).toBeUndefined();
});
it("should do nothing for unknown tabs", () => {
tabManager.detach(999, false);
expect(mockSendMessage).not.toHaveBeenCalled();
});
});
describe("clear", () => {
it("should clear all tabs and child sessions", () => {
tabManager.set(1, { state: "connected" });
tabManager.set(2, { state: "connected" });
tabManager.trackChildSession("child-1", 1);
tabManager.clear();
expect(tabManager.has(1)).toBe(false);
expect(tabManager.has(2)).toBe(false);
expect(tabManager.getParentTabId("child-1")).toBeUndefined();
});
});
describe("getAllTabIds", () => {
it("should return all tab IDs", () => {
tabManager.set(1, { state: "connected" });
tabManager.set(2, { state: "connecting" });
tabManager.set(3, { state: "error" });
const ids = tabManager.getAllTabIds();
expect(ids).toEqual([1, 2, 3]);
});
});
});

View File

@@ -0,0 +1,119 @@
import { describe, it, expect, beforeEach, vi } from "vitest";
import { createLogger } from "../utils/logger";
describe("createLogger", () => {
let mockSendMessage: ReturnType<typeof vi.fn>;
beforeEach(() => {
mockSendMessage = vi.fn();
vi.spyOn(console, "log").mockImplementation(() => {});
vi.spyOn(console, "debug").mockImplementation(() => {});
vi.spyOn(console, "error").mockImplementation(() => {});
});
describe("log", () => {
it("should log to console and send message", () => {
const logger = createLogger(mockSendMessage);
logger.log("test message", 123);
expect(console.log).toHaveBeenCalledWith("[dev-browser]", "test message", 123);
expect(mockSendMessage).toHaveBeenCalledWith({
method: "log",
params: {
level: "log",
args: ["test message", "123"],
},
});
});
});
describe("debug", () => {
it("should debug to console and send message", () => {
const logger = createLogger(mockSendMessage);
logger.debug("debug info");
expect(console.debug).toHaveBeenCalledWith("[dev-browser]", "debug info");
expect(mockSendMessage).toHaveBeenCalledWith({
method: "log",
params: {
level: "debug",
args: ["debug info"],
},
});
});
});
describe("error", () => {
it("should error to console and send message", () => {
const logger = createLogger(mockSendMessage);
logger.error("error occurred");
expect(console.error).toHaveBeenCalledWith("[dev-browser]", "error occurred");
expect(mockSendMessage).toHaveBeenCalledWith({
method: "log",
params: {
level: "error",
args: ["error occurred"],
},
});
});
});
describe("argument formatting", () => {
it("should format undefined as string", () => {
const logger = createLogger(mockSendMessage);
logger.log(undefined);
expect(mockSendMessage).toHaveBeenCalledWith({
method: "log",
params: {
level: "log",
args: ["undefined"],
},
});
});
it("should format null as string", () => {
const logger = createLogger(mockSendMessage);
logger.log(null);
expect(mockSendMessage).toHaveBeenCalledWith({
method: "log",
params: {
level: "log",
args: ["null"],
},
});
});
it("should JSON stringify objects", () => {
const logger = createLogger(mockSendMessage);
logger.log({ key: "value" });
expect(mockSendMessage).toHaveBeenCalledWith({
method: "log",
params: {
level: "log",
args: ['{"key":"value"}'],
},
});
});
it("should handle circular objects gracefully", () => {
const logger = createLogger(mockSendMessage);
const circular: Record<string, unknown> = { a: 1 };
circular.self = circular;
logger.log(circular);
// Should fall back to String() when JSON.stringify fails
expect(mockSendMessage).toHaveBeenCalledWith({
method: "log",
params: {
level: "log",
args: ["[object Object]"],
},
});
});
});
});

View File

@@ -0,0 +1,174 @@
/**
* dev-browser Chrome Extension Background Script
*
* This extension connects to the dev-browser relay server and allows
* Playwright automation of the user's existing browser tabs.
*/
import { createLogger } from "../utils/logger";
import { TabManager } from "../services/TabManager";
import { ConnectionManager } from "../services/ConnectionManager";
import { CDPRouter } from "../services/CDPRouter";
import { StateManager } from "../services/StateManager";
import type { PopupMessage, StateResponse } from "../utils/types";
export default defineBackground(() => {
// Create connection manager first (needed for sendMessage)
let connectionManager: ConnectionManager;
// Create logger with sendMessage function
const logger = createLogger((msg) => connectionManager?.send(msg));
// Create state manager for persistence
const stateManager = new StateManager();
// Create tab manager
const tabManager = new TabManager({
logger,
sendMessage: (msg) => connectionManager.send(msg),
});
// Create CDP router
const cdpRouter = new CDPRouter({
logger,
tabManager,
});
// Create connection manager
connectionManager = new ConnectionManager({
logger,
onMessage: (msg) => cdpRouter.handleCommand(msg),
onDisconnect: () => tabManager.detachAll(),
});
// Keep-alive alarm name for Chrome Alarms API
const KEEPALIVE_ALARM = "keepAlive";
// Update badge to show active/inactive state
function updateBadge(isActive: boolean): void {
chrome.action.setBadgeText({ text: isActive ? "ON" : "" });
chrome.action.setBadgeBackgroundColor({ color: "#4CAF50" });
}
// Handle state changes
async function handleStateChange(isActive: boolean): Promise<void> {
await stateManager.setState({ isActive });
if (isActive) {
chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: 0.5 });
connectionManager.startMaintaining();
} else {
chrome.alarms.clear(KEEPALIVE_ALARM);
connectionManager.disconnect();
}
updateBadge(isActive);
}
// Handle debugger events
function onDebuggerEvent(
source: chrome.debugger.DebuggerSession,
method: string,
params: unknown
): void {
cdpRouter.handleDebuggerEvent(source, method, params, (msg) => connectionManager.send(msg));
}
function onDebuggerDetach(
source: chrome.debugger.Debuggee,
reason: `${chrome.debugger.DetachReason}`
): void {
const tabId = source.tabId;
if (!tabId) return;
logger.debug(`Debugger detached for tab ${tabId}: ${reason}`);
tabManager.handleDebuggerDetach(tabId);
}
// Handle messages from popup
chrome.runtime.onMessage.addListener(
(
message: PopupMessage,
_sender: chrome.runtime.MessageSender,
sendResponse: (response: StateResponse) => void
) => {
if (message.type === "getState") {
(async () => {
const state = await stateManager.getState();
const isConnected = await connectionManager.checkConnection();
sendResponse({
isActive: state.isActive,
isConnected,
});
})();
return true; // Async response
}
if (message.type === "setState") {
(async () => {
await handleStateChange(message.isActive);
const state = await stateManager.getState();
const isConnected = await connectionManager.checkConnection();
sendResponse({
isActive: state.isActive,
isConnected,
});
})();
return true; // Async response
}
return false;
}
);
// Set up event listeners
chrome.tabs.onRemoved.addListener((tabId) => {
if (tabManager.has(tabId)) {
logger.debug("Tab closed:", tabId);
tabManager.detach(tabId, false);
}
});
// Register debugger event listeners
chrome.debugger.onEvent.addListener(onDebuggerEvent);
chrome.debugger.onDetach.addListener(onDebuggerDetach);
// Reset any stale debugger connections on startup
chrome.debugger.getTargets().then((targets) => {
const attached = targets.filter((t) => t.tabId && t.attached);
if (attached.length > 0) {
logger.log(`Detaching ${attached.length} stale debugger connections`);
for (const target of attached) {
chrome.debugger.detach({ tabId: target.tabId }).catch(() => {});
}
}
});
logger.log("Extension initialized");
// Initialize from stored state
stateManager.getState().then((state) => {
updateBadge(state.isActive);
if (state.isActive) {
// Create keep-alive alarm only when extension is active
chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: 0.5 });
connectionManager.startMaintaining();
}
});
// Set up Chrome Alarms keep-alive listener
// This ensures the connection is maintained even after service worker unloads
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === KEEPALIVE_ALARM) {
const state = await stateManager.getState();
if (state.isActive) {
const isConnected = connectionManager.isConnected();
if (!isConnected) {
logger.debug("Keep-alive: Connection lost, restarting...");
connectionManager.startMaintaining();
}
}
}
});
});

View File

@@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dev Browser</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div class="popup">
<h1>Dev Browser</h1>
<div class="toggle-row">
<label class="toggle">
<input type="checkbox" id="active-toggle" />
<span class="slider"></span>
</label>
<span id="status-text">Inactive</span>
</div>
<p id="connection-status" class="connection-status"></p>
</div>
<script type="module" src="./main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,52 @@
import type { GetStateMessage, SetStateMessage, StateResponse } from "../../utils/types";
const toggle = document.getElementById("active-toggle") as HTMLInputElement;
const statusText = document.getElementById("status-text") as HTMLSpanElement;
const connectionStatus = document.getElementById("connection-status") as HTMLParagraphElement;
function updateUI(state: StateResponse): void {
toggle.checked = state.isActive;
statusText.textContent = state.isActive ? "Active" : "Inactive";
if (state.isActive) {
connectionStatus.textContent = state.isConnected ? "Connected to relay" : "Connecting...";
connectionStatus.className = state.isConnected
? "connection-status connected"
: "connection-status connecting";
} else {
connectionStatus.textContent = "";
connectionStatus.className = "connection-status";
}
}
function refreshState(): void {
chrome.runtime.sendMessage<GetStateMessage, StateResponse>({ type: "getState" }, (response) => {
if (response) {
updateUI(response);
}
});
}
// Load initial state
refreshState();
// Poll for state updates while popup is open
const pollInterval = setInterval(refreshState, 1000);
// Clean up on popup close
window.addEventListener("unload", () => {
clearInterval(pollInterval);
});
// Handle toggle changes
toggle.addEventListener("change", () => {
const isActive = toggle.checked;
chrome.runtime.sendMessage<SetStateMessage, StateResponse>(
{ type: "setState", isActive },
(response) => {
if (response) {
updateUI(response);
}
}
);
});

View File

@@ -0,0 +1,96 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 14px;
background: #fff;
}
.popup {
width: 200px;
padding: 16px;
}
h1 {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
color: #333;
}
.toggle-row {
display: flex;
align-items: center;
gap: 12px;
}
#status-text {
font-weight: 500;
color: #555;
}
/* Toggle switch */
.toggle {
position: relative;
display: inline-block;
width: 44px;
height: 24px;
}
.toggle input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: 0.2s;
border-radius: 24px;
}
.slider::before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.2s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #4caf50;
}
input:checked + .slider::before {
transform: translateX(20px);
}
/* Connection status */
.connection-status {
margin-top: 12px;
font-size: 12px;
color: #888;
min-height: 16px;
}
.connection-status.connected {
color: #4caf50;
}
.connection-status.connecting {
color: #ff9800;
}

5902
dev-browser/extension/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
{
"name": "dev-browser-extension",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "wxt",
"dev:firefox": "wxt --browser firefox",
"build": "wxt build",
"build:firefox": "wxt build --browser firefox",
"zip": "wxt zip",
"zip:firefox": "wxt zip --browser firefox",
"test": "vitest",
"test:run": "vitest run"
},
"devDependencies": {
"@types/chrome": "^0.1.32",
"typescript": "^5.0.0",
"vitest": "^3.0.0",
"wxt": "^0.20.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,152 @@
/**
* Generate simple placeholder icons for the extension
* Usage: node scripts/generate-icons.mjs
*/
import { writeFileSync, mkdirSync } from "fs";
import { join, dirname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
// Minimal PNG generator (creates simple colored squares)
function createPng(size, r, g, b) {
// PNG header
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
// IHDR chunk
const ihdrData = Buffer.alloc(13);
ihdrData.writeUInt32BE(size, 0); // width
ihdrData.writeUInt32BE(size, 4); // height
ihdrData.writeUInt8(8, 8); // bit depth
ihdrData.writeUInt8(2, 9); // color type (RGB)
ihdrData.writeUInt8(0, 10); // compression
ihdrData.writeUInt8(0, 11); // filter
ihdrData.writeUInt8(0, 12); // interlace
const ihdr = createChunk("IHDR", ihdrData);
// IDAT chunk (image data)
const rawData = [];
for (let y = 0; y < size; y++) {
rawData.push(0); // filter byte
for (let x = 0; x < size; x++) {
// Create a circle
const cx = size / 2;
const cy = size / 2;
const radius = size / 2 - 1;
const dist = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2);
if (dist <= radius) {
// Inside circle - use the color
rawData.push(r, g, b);
} else {
// Outside circle - transparent (white for simplicity)
rawData.push(255, 255, 255);
}
}
}
// Use zlib-less compression (store method)
const compressed = deflateStore(Buffer.from(rawData));
const idat = createChunk("IDAT", compressed);
// IEND chunk
const iend = createChunk("IEND", Buffer.alloc(0));
return Buffer.concat([signature, ihdr, idat, iend]);
}
function createChunk(type, data) {
const length = Buffer.alloc(4);
length.writeUInt32BE(data.length);
const typeBuffer = Buffer.from(type);
const crc = crc32(Buffer.concat([typeBuffer, data]));
const crcBuffer = Buffer.alloc(4);
crcBuffer.writeUInt32BE(crc >>> 0);
return Buffer.concat([length, typeBuffer, data, crcBuffer]);
}
// Simple deflate store (no compression)
function deflateStore(data) {
const blocks = [];
let offset = 0;
while (offset < data.length) {
const remaining = data.length - offset;
const blockSize = Math.min(65535, remaining);
const isLast = offset + blockSize >= data.length;
const header = Buffer.alloc(5);
header.writeUInt8(isLast ? 1 : 0, 0);
header.writeUInt16LE(blockSize, 1);
header.writeUInt16LE(blockSize ^ 0xffff, 3);
blocks.push(header);
blocks.push(data.subarray(offset, offset + blockSize));
offset += blockSize;
}
// Zlib header
const zlibHeader = Buffer.from([0x78, 0x01]);
// Adler32 checksum
const adler = adler32(data);
const adlerBuffer = Buffer.alloc(4);
adlerBuffer.writeUInt32BE(adler);
return Buffer.concat([zlibHeader, ...blocks, adlerBuffer]);
}
function adler32(data) {
let a = 1;
let b = 0;
for (let i = 0; i < data.length; i++) {
a = (a + data[i]) % 65521;
b = (b + a) % 65521;
}
return ((b << 16) | a) >>> 0; // Ensure unsigned
}
// CRC32 lookup table
const crcTable = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let j = 0; j < 8; j++) {
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
}
crcTable[i] = c;
}
function crc32(data) {
let crc = 0xffffffff;
for (let i = 0; i < data.length; i++) {
crc = crcTable[(crc ^ data[i]) & 0xff] ^ (crc >>> 8);
}
return crc ^ 0xffffffff;
}
// Generate icons
const sizes = [16, 32, 48, 128];
const colors = {
black: [26, 26, 26],
gray: [156, 163, 175],
green: [34, 197, 94],
};
const iconsDir = join(__dirname, "..", "public", "icons");
mkdirSync(iconsDir, { recursive: true });
for (const [name, [r, g, b]] of Object.entries(colors)) {
for (const size of sizes) {
const png = createPng(size, r, g, b);
const filename = join(iconsDir, `icon-${name}-${size}.png`);
writeFileSync(filename, png);
console.log(`Created ${filename}`);
}
}
console.log("Done!");

View File

@@ -0,0 +1,211 @@
/**
* CDPRouter - Routes CDP commands to the correct tab.
*/
import type { Logger } from "../utils/logger";
import type { TabManager } from "./TabManager";
import type { ExtensionCommandMessage, TabInfo } from "../utils/types";
export interface CDPRouterDeps {
logger: Logger;
tabManager: TabManager;
}
export class CDPRouter {
private logger: Logger;
private tabManager: TabManager;
private devBrowserGroupId: number | null = null;
constructor(deps: CDPRouterDeps) {
this.logger = deps.logger;
this.tabManager = deps.tabManager;
}
/**
* Gets or creates the "Dev Browser" tab group, returning its ID.
*/
private async getOrCreateDevBrowserGroup(tabId: number): Promise<number> {
// If we have a cached group ID, verify it still exists
if (this.devBrowserGroupId !== null) {
try {
await chrome.tabGroups.get(this.devBrowserGroupId);
// Group exists, add tab to it
await chrome.tabs.group({ tabIds: [tabId], groupId: this.devBrowserGroupId });
return this.devBrowserGroupId;
} catch {
// Group no longer exists, reset cache
this.devBrowserGroupId = null;
}
}
// Create a new group with this tab
const groupId = await chrome.tabs.group({ tabIds: [tabId] });
await chrome.tabGroups.update(groupId, {
title: "Dev Browser",
color: "blue",
});
this.devBrowserGroupId = groupId;
return groupId;
}
/**
* Handle an incoming CDP command from the relay.
*/
async handleCommand(msg: ExtensionCommandMessage): Promise<unknown> {
if (msg.method !== "forwardCDPCommand") return;
let targetTabId: number | undefined;
let targetTab: TabInfo | undefined;
// Find target tab by sessionId
if (msg.params.sessionId) {
const found = this.tabManager.getBySessionId(msg.params.sessionId);
if (found) {
targetTabId = found.tabId;
targetTab = found.tab;
}
}
// Check child sessions (iframes, workers)
if (!targetTab && msg.params.sessionId) {
const parentTabId = this.tabManager.getParentTabId(msg.params.sessionId);
if (parentTabId) {
targetTabId = parentTabId;
targetTab = this.tabManager.get(parentTabId);
this.logger.debug(
"Found parent tab for child session:",
msg.params.sessionId,
"tabId:",
parentTabId
);
}
}
// Find by targetId in params
if (
!targetTab &&
msg.params.params &&
typeof msg.params.params === "object" &&
"targetId" in msg.params.params
) {
const found = this.tabManager.getByTargetId(msg.params.params.targetId as string);
if (found) {
targetTabId = found.tabId;
targetTab = found.tab;
}
}
const debuggee = targetTabId ? { tabId: targetTabId } : undefined;
// Handle special commands
switch (msg.params.method) {
case "Runtime.enable": {
if (!debuggee) {
throw new Error(
`No debuggee found for Runtime.enable (sessionId: ${msg.params.sessionId})`
);
}
// Disable and re-enable to reset state
try {
await chrome.debugger.sendCommand(debuggee, "Runtime.disable");
await new Promise((resolve) => setTimeout(resolve, 200));
} catch {
// Ignore errors
}
return await chrome.debugger.sendCommand(debuggee, "Runtime.enable", msg.params.params);
}
case "Target.createTarget": {
const url = (msg.params.params?.url as string) || "about:blank";
this.logger.debug("Creating new tab with URL:", url);
const tab = await chrome.tabs.create({ url, active: false });
if (!tab.id) throw new Error("Failed to create tab");
// Add tab to "Dev Browser" group
await this.getOrCreateDevBrowserGroup(tab.id);
await new Promise((resolve) => setTimeout(resolve, 100));
const targetInfo = await this.tabManager.attach(tab.id);
return { targetId: targetInfo.targetId };
}
case "Target.closeTarget": {
if (!targetTabId) {
this.logger.log(`Target not found: ${msg.params.params?.targetId}`);
return { success: false };
}
await chrome.tabs.remove(targetTabId);
return { success: true };
}
case "Target.activateTarget": {
if (!targetTabId) {
this.logger.log(`Target not found for activation: ${msg.params.params?.targetId}`);
return {};
}
await chrome.tabs.update(targetTabId, { active: true });
return {};
}
}
if (!debuggee || !targetTab) {
throw new Error(
`No tab found for method ${msg.params.method} sessionId: ${msg.params.sessionId}`
);
}
this.logger.debug("CDP command:", msg.params.method, "for tab:", targetTabId);
const debuggerSession: chrome.debugger.DebuggerSession = {
...debuggee,
sessionId: msg.params.sessionId !== targetTab.sessionId ? msg.params.sessionId : undefined,
};
return await chrome.debugger.sendCommand(debuggerSession, msg.params.method, msg.params.params);
}
/**
* Handle debugger events from Chrome.
*/
handleDebuggerEvent(
source: chrome.debugger.DebuggerSession,
method: string,
params: unknown,
sendMessage: (msg: unknown) => void
): void {
const tab = source.tabId ? this.tabManager.get(source.tabId) : undefined;
if (!tab) return;
this.logger.debug("Forwarding CDP event:", method, "from tab:", source.tabId);
// Track child sessions
if (
method === "Target.attachedToTarget" &&
params &&
typeof params === "object" &&
"sessionId" in params
) {
const sessionId = (params as { sessionId: string }).sessionId;
this.tabManager.trackChildSession(sessionId, source.tabId!);
}
if (
method === "Target.detachedFromTarget" &&
params &&
typeof params === "object" &&
"sessionId" in params
) {
const sessionId = (params as { sessionId: string }).sessionId;
this.tabManager.untrackChildSession(sessionId);
}
sendMessage({
method: "forwardCDPEvent",
params: {
sessionId: source.sessionId || tab.sessionId,
method,
params,
},
});
}
}

View File

@@ -0,0 +1,214 @@
/**
* ConnectionManager - Manages WebSocket connection to relay server.
*/
import type { Logger } from "../utils/logger";
import type { ExtensionCommandMessage, ExtensionResponseMessage } from "../utils/types";
const RELAY_URL = "ws://localhost:9222/extension";
const RECONNECT_INTERVAL = 3000;
export interface ConnectionManagerDeps {
logger: Logger;
onMessage: (message: ExtensionCommandMessage) => Promise<unknown>;
onDisconnect: () => void;
}
export class ConnectionManager {
private ws: WebSocket | null = null;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private shouldMaintain = false;
private logger: Logger;
private onMessage: (message: ExtensionCommandMessage) => Promise<unknown>;
private onDisconnect: () => void;
constructor(deps: ConnectionManagerDeps) {
this.logger = deps.logger;
this.onMessage = deps.onMessage;
this.onDisconnect = deps.onDisconnect;
}
/**
* Check if WebSocket is open (may be stale if server crashed).
*/
isConnected(): boolean {
return this.ws?.readyState === WebSocket.OPEN;
}
/**
* Validate connection by checking if server is reachable.
* More reliable than isConnected() as it detects server crashes.
*/
async checkConnection(): Promise<boolean> {
if (!this.isConnected()) {
return false;
}
// Verify server is actually reachable
try {
const response = await fetch("http://localhost:9222", {
method: "HEAD",
signal: AbortSignal.timeout(1000),
});
return response.ok;
} catch {
// Server unreachable - close stale socket
if (this.ws) {
this.ws.close();
this.ws = null;
this.onDisconnect();
}
return false;
}
}
/**
* Send a message to the relay server.
*/
send(message: unknown): void {
if (this.ws?.readyState === WebSocket.OPEN) {
try {
this.ws.send(JSON.stringify(message));
} catch (error) {
console.debug("Error sending message:", error);
}
}
}
/**
* Start maintaining connection (auto-reconnect).
*/
startMaintaining(): void {
this.shouldMaintain = true;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
this.tryConnect().catch(() => {});
this.reconnectTimer = setTimeout(() => this.startMaintaining(), RECONNECT_INTERVAL);
}
/**
* Stop connection maintenance.
*/
stopMaintaining(): void {
this.shouldMaintain = false;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
/**
* Disconnect from relay and stop maintaining connection.
*/
disconnect(): void {
this.stopMaintaining();
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.onDisconnect();
}
/**
* Ensure connection is established, waiting if needed.
*/
async ensureConnected(): Promise<void> {
if (this.isConnected()) return;
await this.tryConnect();
if (!this.isConnected()) {
await new Promise((resolve) => setTimeout(resolve, 1000));
await this.tryConnect();
}
if (!this.isConnected()) {
throw new Error("Could not connect to relay server");
}
}
/**
* Try to connect to relay server once.
*/
private async tryConnect(): Promise<void> {
if (this.isConnected()) return;
// Check if server is available
try {
await fetch("http://localhost:9222", { method: "HEAD" });
} catch {
return;
}
this.logger.debug("Connecting to relay server...");
const socket = new WebSocket(RELAY_URL);
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("Connection timeout"));
}, 5000);
socket.onopen = () => {
clearTimeout(timeout);
resolve();
};
socket.onerror = () => {
clearTimeout(timeout);
reject(new Error("WebSocket connection failed"));
};
socket.onclose = (event) => {
clearTimeout(timeout);
reject(new Error(`WebSocket closed: ${event.reason || event.code}`));
};
});
this.ws = socket;
this.setupSocketHandlers(socket);
this.logger.log("Connected to relay server");
}
/**
* Set up WebSocket event handlers.
*/
private setupSocketHandlers(socket: WebSocket): void {
socket.onmessage = async (event: MessageEvent) => {
let message: ExtensionCommandMessage;
try {
message = JSON.parse(event.data);
} catch (error) {
this.logger.debug("Error parsing message:", error);
this.send({
error: { code: -32700, message: "Parse error" },
});
return;
}
const response: ExtensionResponseMessage = { id: message.id };
try {
response.result = await this.onMessage(message);
} catch (error) {
this.logger.debug("Error handling command:", error);
response.error = (error as Error).message;
}
this.send(response);
};
socket.onclose = (event: CloseEvent) => {
this.logger.debug("Connection closed:", event.code, event.reason);
this.ws = null;
this.onDisconnect();
if (this.shouldMaintain) {
this.startMaintaining();
}
};
socket.onerror = (event: Event) => {
this.logger.debug("WebSocket error:", event);
};
}
}

View File

@@ -0,0 +1,28 @@
/**
* StateManager - Manages extension active/inactive state with persistence.
*/
const STORAGE_KEY = "devBrowserActiveState";
export interface ExtensionState {
isActive: boolean;
}
export class StateManager {
/**
* Get the current extension state.
* Defaults to inactive if no state is stored.
*/
async getState(): Promise<ExtensionState> {
const result = await chrome.storage.local.get(STORAGE_KEY);
const state = result[STORAGE_KEY] as ExtensionState | undefined;
return state ?? { isActive: false };
}
/**
* Set the extension state.
*/
async setState(state: ExtensionState): Promise<void> {
await chrome.storage.local.set({ [STORAGE_KEY]: state });
}
}

View File

@@ -0,0 +1,218 @@
/**
* TabManager - Manages tab state and debugger attachment.
*/
import type { TabInfo, TargetInfo } from "../utils/types";
import type { Logger } from "../utils/logger";
export type SendMessageFn = (message: unknown) => void;
export interface TabManagerDeps {
logger: Logger;
sendMessage: SendMessageFn;
}
export class TabManager {
private tabs = new Map<number, TabInfo>();
private childSessions = new Map<string, number>(); // sessionId -> parentTabId
private nextSessionId = 1;
private logger: Logger;
private sendMessage: SendMessageFn;
constructor(deps: TabManagerDeps) {
this.logger = deps.logger;
this.sendMessage = deps.sendMessage;
}
/**
* Get tab info by session ID.
*/
getBySessionId(sessionId: string): { tabId: number; tab: TabInfo } | undefined {
for (const [tabId, tab] of this.tabs) {
if (tab.sessionId === sessionId) {
return { tabId, tab };
}
}
return undefined;
}
/**
* Get tab info by target ID.
*/
getByTargetId(targetId: string): { tabId: number; tab: TabInfo } | undefined {
for (const [tabId, tab] of this.tabs) {
if (tab.targetId === targetId) {
return { tabId, tab };
}
}
return undefined;
}
/**
* Get parent tab ID for a child session (iframe, worker).
*/
getParentTabId(sessionId: string): number | undefined {
return this.childSessions.get(sessionId);
}
/**
* Get tab info by tab ID.
*/
get(tabId: number): TabInfo | undefined {
return this.tabs.get(tabId);
}
/**
* Check if a tab is tracked.
*/
has(tabId: number): boolean {
return this.tabs.has(tabId);
}
/**
* Set tab info (used for intermediate states like "connecting").
*/
set(tabId: number, info: TabInfo): void {
this.tabs.set(tabId, info);
}
/**
* Track a child session (iframe, worker).
*/
trackChildSession(sessionId: string, parentTabId: number): void {
this.logger.debug("Child target attached:", sessionId, "for tab:", parentTabId);
this.childSessions.set(sessionId, parentTabId);
}
/**
* Untrack a child session.
*/
untrackChildSession(sessionId: string): void {
this.logger.debug("Child target detached:", sessionId);
this.childSessions.delete(sessionId);
}
/**
* Attach debugger to a tab and register it.
*/
async attach(tabId: number): Promise<TargetInfo> {
const debuggee = { tabId };
this.logger.debug("Attaching debugger to tab:", tabId);
await chrome.debugger.attach(debuggee, "1.3");
const result = (await chrome.debugger.sendCommand(debuggee, "Target.getTargetInfo")) as {
targetInfo: TargetInfo;
};
const targetInfo = result.targetInfo;
const sessionId = `pw-tab-${this.nextSessionId++}`;
this.tabs.set(tabId, {
sessionId,
targetId: targetInfo.targetId,
state: "connected",
});
// Notify relay of new target
this.sendMessage({
method: "forwardCDPEvent",
params: {
method: "Target.attachedToTarget",
params: {
sessionId,
targetInfo: { ...targetInfo, attached: true },
waitingForDebugger: false,
},
},
});
this.logger.log("Tab attached:", tabId, "sessionId:", sessionId, "url:", targetInfo.url);
return targetInfo;
}
/**
* Detach a tab and clean up.
*/
detach(tabId: number, shouldDetachDebugger: boolean): void {
const tab = this.tabs.get(tabId);
if (!tab) return;
this.logger.debug("Detaching tab:", tabId);
this.sendMessage({
method: "forwardCDPEvent",
params: {
method: "Target.detachedFromTarget",
params: { sessionId: tab.sessionId, targetId: tab.targetId },
},
});
this.tabs.delete(tabId);
// Clean up child sessions
for (const [childSessionId, parentTabId] of this.childSessions) {
if (parentTabId === tabId) {
this.childSessions.delete(childSessionId);
}
}
if (shouldDetachDebugger) {
chrome.debugger.detach({ tabId }).catch((err) => {
this.logger.debug("Error detaching debugger:", err);
});
}
}
/**
* Handle debugger detach event from Chrome.
*/
handleDebuggerDetach(tabId: number): void {
if (!this.tabs.has(tabId)) return;
const tab = this.tabs.get(tabId);
if (tab) {
this.sendMessage({
method: "forwardCDPEvent",
params: {
method: "Target.detachedFromTarget",
params: { sessionId: tab.sessionId, targetId: tab.targetId },
},
});
}
// Clean up child sessions
for (const [childSessionId, parentTabId] of this.childSessions) {
if (parentTabId === tabId) {
this.childSessions.delete(childSessionId);
}
}
this.tabs.delete(tabId);
}
/**
* Clear all tabs and child sessions.
*/
clear(): void {
this.tabs.clear();
this.childSessions.clear();
}
/**
* Detach all tabs (used on disconnect).
*/
detachAll(): void {
for (const tabId of this.tabs.keys()) {
chrome.debugger.detach({ tabId }).catch(() => {});
}
this.clear();
}
/**
* Get all tab IDs.
*/
getAllTabIds(): number[] {
return Array.from(this.tabs.keys());
}
}

View File

@@ -0,0 +1,3 @@
{
"extends": "./.wxt/tsconfig.json"
}

View File

@@ -0,0 +1,63 @@
/**
* Logger utility for the dev-browser extension.
* Logs to console and optionally sends to relay server.
*/
export type LogLevel = "log" | "debug" | "error";
export interface LogMessage {
method: "log";
params: {
level: LogLevel;
args: string[];
};
}
export type SendMessageFn = (message: unknown) => void;
/**
* Creates a logger instance that logs to console and sends to relay.
*/
export function createLogger(sendMessage: SendMessageFn) {
function formatArgs(args: unknown[]): string[] {
return args.map((arg) => {
if (arg === undefined) return "undefined";
if (arg === null) return "null";
if (typeof arg === "object") {
try {
return JSON.stringify(arg);
} catch {
return String(arg);
}
}
return String(arg);
});
}
function sendLog(level: LogLevel, args: unknown[]): void {
sendMessage({
method: "log",
params: {
level,
args: formatArgs(args),
},
});
}
return {
log: (...args: unknown[]) => {
console.log("[dev-browser]", ...args);
sendLog("log", args);
},
debug: (...args: unknown[]) => {
console.debug("[dev-browser]", ...args);
sendLog("debug", args);
},
error: (...args: unknown[]) => {
console.error("[dev-browser]", ...args);
sendLog("error", args);
},
};
}
export type Logger = ReturnType<typeof createLogger>;

View File

@@ -0,0 +1,94 @@
/**
* Types for extension-relay communication
*/
export type ConnectionState =
| "disconnected"
| "connecting"
| "connected"
| "reconnecting"
| "error";
export type TabState = "connecting" | "connected" | "error";
export interface TabInfo {
sessionId?: string;
targetId?: string;
state: TabState;
errorText?: string;
}
export interface ExtensionState {
tabs: Map<number, TabInfo>;
connectionState: ConnectionState;
currentTabId?: number;
errorText?: string;
}
// Messages from relay to extension
export interface ExtensionCommandMessage {
id: number;
method: "forwardCDPCommand";
params: {
method: string;
params?: Record<string, unknown>;
sessionId?: string;
};
}
// Messages from extension to relay (responses)
export interface ExtensionResponseMessage {
id: number;
result?: unknown;
error?: string;
}
// Messages from extension to relay (events)
export interface ExtensionEventMessage {
method: "forwardCDPEvent";
params: {
method: string;
params?: Record<string, unknown>;
sessionId?: string;
};
}
// Log message from extension to relay
export interface ExtensionLogMessage {
method: "log";
params: {
level: string;
args: string[];
};
}
export type ExtensionMessage =
| ExtensionResponseMessage
| ExtensionEventMessage
| ExtensionLogMessage;
// Chrome debugger target info
export interface TargetInfo {
targetId: string;
type: string;
title: string;
url: string;
attached?: boolean;
}
// Popup <-> Background messaging
export interface GetStateMessage {
type: "getState";
}
export interface SetStateMessage {
type: "setState";
isActive: boolean;
}
export interface StateResponse {
isActive: boolean;
isConnected: boolean;
}
export type PopupMessage = GetStateMessage | SetStateMessage;

View File

@@ -0,0 +1,10 @@
import { defineConfig } from "vitest/config";
import { WxtVitest } from "wxt/testing";
export default defineConfig({
plugins: [WxtVitest()],
test: {
mockReset: true,
restoreMocks: true,
},
});

View File

@@ -0,0 +1,16 @@
import { defineConfig } from "wxt";
export default defineConfig({
manifest: {
name: "dev-browser",
description: "Connect your browser to dev-browser for Playwright automation",
permissions: ["debugger", "tabGroups", "storage", "alarms"],
host_permissions: ["<all_urls>"],
icons: {
16: "icons/icon-16.png",
32: "icons/icon-32.png",
48: "icons/icon-48.png",
128: "icons/icon-128.png",
},
},
});