UNPKG

chrome-cmd

Version:

Control Chrome from the command line - List tabs, execute JavaScript, and more

334 lines (333 loc) 10.8 kB
#!/usr/bin/env node import { appendFileSync } from "node:fs"; import { createServer } from "node:http"; import { stdin, stdout } from "node:process"; import { profileManager } from "../cli/core/managers/profile.js"; import { FILES_CONFIG } from "../shared/configs/files.config.js"; import { getExtensionPhysicalPath } from "../shared/utils/helpers/chrome-extension-path.js"; import { readJsonFile, writeJsonFile } from "../shared/utils/helpers/file-utils.js"; import { PathHelper } from "../shared/utils/helpers/path.helper.js"; import { BRIDGE_CONFIG } from "./bridge.config.js"; PathHelper.ensureDir(FILES_CONFIG.BRIDGE_LOG_FILE); function log(message) { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); appendFileSync(FILES_CONFIG.BRIDGE_LOG_FILE, `[${timestamp}] ${message} `); } const pendingRequests = /* @__PURE__ */ new Map(); let profileId = null; let profileName = null; let extensionId = null; let assignedPort = null; async function findAvailablePort() { for (let port = BRIDGE_CONFIG.PORT_START; port <= BRIDGE_CONFIG.PORT_END; port++) { try { await new Promise((resolve, reject) => { const testServer = createServer(); testServer.once("error", (err) => { testServer.close(); reject(err); }); testServer.once("listening", () => { testServer.close(); resolve(); }); testServer.listen(port, "localhost"); }); return port; } catch { } } throw new Error(`No available ports in range ${BRIDGE_CONFIG.PORT_START}-${BRIDGE_CONFIG.PORT_END}`); } const httpServer = createServer((req, res) => { if (req.method === "POST" && req.url === "/command") { let body = ""; req.on("data", (chunk) => { body += chunk.toString(); }); req.on("end", () => { try { const command = JSON.parse(body); const id = command.id || Date.now().toString(); log(`[HTTP] Received command: ${command.command}`); pendingRequests.set(id, res); sendToExtension({ ...command, id }); const timeoutMs = command.command === "capture_screenshot" ? 6e5 : 1e4; setTimeout(() => { if (pendingRequests.has(id)) { pendingRequests.delete(id); res.writeHead(504); res.end(JSON.stringify({ success: false, error: "Timeout" })); } }, timeoutMs); } catch (_error) { res.writeHead(400); res.end(JSON.stringify({ success: false, error: "Invalid JSON" })); } }); } else if (req.method === "GET" && req.url === "/ping") { res.writeHead(200); res.end(JSON.stringify({ status: "ok" })); } else { res.writeHead(404); res.end("Not found"); } }); function sendToExtension(message) { const json = JSON.stringify(message); const buffer = Buffer.from(json, "utf-8"); const lengthBuffer = Buffer.alloc(4); lengthBuffer.writeUInt32LE(buffer.length, 0); stdout.write(lengthBuffer); stdout.write(buffer); log(`[BridgeMsg] Sent to extension: ${message.command ?? "response"}`); } async function readFromExtension() { return new Promise((resolve) => { const lengthBuffer = Buffer.alloc(4); let lengthRead = 0; const readLength = () => { const chunk = stdin.read(4 - lengthRead); if (chunk) { chunk.copy(lengthBuffer, lengthRead); lengthRead += chunk.length; if (lengthRead === 4) { const messageLength = lengthBuffer.readUInt32LE(0); readContent(messageLength); } else { stdin.once("readable", readLength); } } else { stdin.once("readable", readLength); } }; const readContent = (length) => { const messageBuffer = Buffer.alloc(length); let messageRead = 0; const readChunk = () => { const chunk = stdin.read(length - messageRead); if (chunk) { chunk.copy(messageBuffer, messageRead); messageRead += chunk.length; if (messageRead === length) { try { const message = JSON.parse(messageBuffer.toString("utf-8")); resolve(message); } catch (error) { log(`[BridgeMsg] Parse error: ${error}`); resolve(null); } } else { stdin.once("readable", readChunk); } } else { stdin.once("readable", readChunk); } }; readChunk(); }; readLength(); }); } async function handleRegister(message) { log("[Bridge] Processing REGISTER command"); const { data, id } = message; extensionId = data?.extensionId ?? null; const installationId = data?.installationId; profileName = data?.profileName ?? "Unknown"; const extensionPath = extensionId ? getExtensionPhysicalPath(extensionId) : null; log(`[Bridge] Extension ID: ${extensionId}`); log(`[Bridge] Installation ID: ${installationId}`); log(`[Bridge] Profile Name: ${profileName}`); log(`[Bridge] Extension Path: ${extensionPath}`); if (!installationId) { log(`[Bridge] ERROR: No installationId provided`); sendToExtension({ id, success: false, error: "installationId is required" }); return; } try { const configPath = FILES_CONFIG.CONFIG_FILE; const config = readJsonFile(configPath, {}); let profile = config.profiles?.find((p) => p.id === installationId); if (!profile) { log(`[Bridge] No profile found for installationId ${installationId}`); log(`[Bridge] Creating new profile...`); if (!extensionId) { log(`[Bridge] ERROR: extensionId is required to create profile`); sendToExtension({ id, success: false, error: "extensionId is required" }); return; } profile = { id: installationId, profileName, extensionId, extensionPath: extensionPath || void 0, installedAt: (/* @__PURE__ */ new Date()).toISOString() }; if (!config.profiles) { config.profiles = []; } config.profiles.push(profile); const isFirstProfile = config.profiles.length === 1; if (isFirstProfile) { config.activeProfileId = installationId; log(`[Bridge] First profile - auto-activated`); } else { log(`[Bridge] Additional profile - not activated (use 'chrome-cmd extension select' to activate)`); } writeJsonFile(configPath, config); log(`[Bridge] Created profile with ID: ${installationId}`); log(`[Bridge] Profile Name: ${profileName}`); } else { log(`[Bridge] Found existing profile: ${profile.id}`); let needsUpdate = false; if (profile.profileName !== profileName) { log(`[Bridge] Updating profile name: "${profile.profileName}" \u2192 "${profileName}"`); profile.profileName = profileName; needsUpdate = true; } if (extensionPath && profile.extensionPath !== extensionPath) { log(`[Bridge] Updating extension path: "${profile.extensionPath}" \u2192 "${extensionPath}"`); profile.extensionPath = extensionPath; needsUpdate = true; } if (needsUpdate) { writeJsonFile(configPath, config); } } if (!profile) { log(`[Bridge] ERROR: Profile not found after registration`); sendToExtension({ id, success: false, error: "Profile not found" }); return; } profileId = profile.id; log(`[Bridge] Profile ID resolved: ${profileId}`); if (!profileId || !assignedPort || !extensionId || !profileName) { log(`[Bridge] ERROR: Missing required data for registration`); sendToExtension({ id, success: false, error: "Missing required data for registration" }); return; } profileManager.registerBridge({ profileId, port: assignedPort, pid: process.pid, extensionId, profileName }); log(`[Bridge] Registered in bridges.json`); sendToExtension({ id, success: true, result: { profileId, port: assignedPort } }); log(`[Bridge] Registration complete!`); } catch (error) { log(`[Bridge] ERROR during registration: ${error}`); sendToExtension({ id, success: false, error: `Registration failed: ${error}` }); } } function handleExtensionMessage(message) { log(`[BridgeMsg] Received from extension: ${JSON.stringify(message)}`); const typedMessage = message; if (typedMessage.command === "REGISTER" || typedMessage.command === "register") { handleRegister(typedMessage); return; } const { id } = typedMessage; const httpRes = pendingRequests.get(id); if (httpRes) { pendingRequests.delete(id); httpRes.writeHead(200, { "Content-Type": "application/json" }); httpRes.end(JSON.stringify(message)); log(`[HTTP] Sent response to CLI`); } } function startHttpServer(port) { return new Promise((resolve) => { httpServer.once("error", (error) => { log(`[HTTP] Server error on port ${port}: ${error}`); resolve(false); }); httpServer.listen(port, "localhost", () => { log(`[HTTP] Server running on http://localhost:${port}`); resolve(true); }); }); } async function main() { log("[Bridge] Starting..."); try { assignedPort = await findAvailablePort(); log(`[Bridge] Using port ${assignedPort}`); } catch (error) { log(`[Bridge] FATAL: ${error}`); process.exit(1); } const serverStarted = await startHttpServer(assignedPort); if (!serverStarted) { log("[Bridge] FATAL: Failed to start HTTP server"); process.exit(1); } log("[Bridge] HTTP server started successfully"); log("[Bridge] Waiting for REGISTER command from extension..."); while (true) { try { const message = await readFromExtension(); if (message) { handleExtensionMessage(message); } } catch (error) { log(`[Bridge] Error reading message: ${error}`); break; } } } process.on("exit", () => { if (profileId) { log(`[Bridge] Cleaning up, unregistering profile ${profileId}`); profileManager.unregisterBridge(profileId); } }); process.on("SIGTERM", () => { if (profileId) { profileManager.unregisterBridge(profileId); } process.exit(0); }); process.on("SIGINT", () => { if (profileId) { profileManager.unregisterBridge(profileId); } process.exit(0); }); process.on("uncaughtException", (error) => { log(`[Bridge] Uncaught exception: ${error}`); }); process.on("unhandledRejection", (error) => { log(`[Bridge] Unhandled rejection: ${error}`); }); main().catch((error) => { log(`[Bridge] Fatal error in main: ${error}`); process.exit(1); });