chrome-cmd
Version:
Control Chrome from the command line - List tabs, execute JavaScript, and more
334 lines (333 loc) • 10.8 kB
JavaScript
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);
});