///
/**
* Background script for Browser CLI Controller
* Manages native messaging connection and message routing
*
* @typedef {object} NativeMessage
* @property {boolean} [ready] - Whether the native messaging bridge is ready
* @property {string} [socket_path] - Path to the Unix socket
* @property {string} [command] - The command to execute
* @property {CommandParams} [params] - Command parameters
* @property {string} [id] - Unique message ID
* @property {string} [tabId] - Target tab ID for the command
*
* @typedef {object} Message
* @property {string} command - The command to execute
* @property {CommandParams} params - Command parameters
* @property {string} id - Unique message ID
* @property {string} [tabId] - Target tab ID for the command
*
* @typedef {object} CommandParams
* @property {string} [url] - URL for navigation or new tab
* @property {string} [tabId] - Tab ID for close-tab
* @property {string} [code] - JavaScript code to execute
* @property {string} [output_path] - Output path for screenshot
* @property {string} [filename] - Filename for download
*/
/** @type {browser.runtime.Port|undefined} Native messaging port */
let nativePort;
/** @type {Record} Message handlers from content scripts */
const messageHandlers = {};
/** @type {Map} Map of managed tabs with short IDs */
const managedTabs = new Map();
/** @type {string|undefined} Currently active managed tab ID */
let activeTabId;
/**
* Generate a short random ID for tabs
* @returns {string}
*/
function generateTabId() {
const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
for (let i = 0; i < 6; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* Connect to native messaging host
*/
function connectNativeHost() {
if (nativePort) return;
console.log("Connecting to native messaging host...");
try {
nativePort = browser.runtime.connectNative(
"io.thalheim.browser_cli.bridge",
);
nativePort.onMessage.addListener(async (msg) => {
const message = /** @type {NativeMessage} */ (msg);
console.log("Received from native host:", message);
if (message.ready && message.socket_path) {
console.log(
"Native messaging bridge ready, socket at:",
message.socket_path,
);
return;
}
// Handle responses to our requests (e.g., save-screenshot)
if (message.id && messageHandlers[message.id]) {
const handler = messageHandlers[message.id];
delete messageHandlers[message.id];
handler.resolve(message);
return;
}
if (message.command) {
await handleCommand(/** @type {Message} */ (message));
}
});
nativePort.onDisconnect.addListener(() => {
console.log("Native messaging disconnected");
if (browser.runtime.lastError) {
console.error("Native messaging error:", browser.runtime.lastError);
}
nativePort = undefined;
setTimeout(connectNativeHost, 5000);
});
} catch (error) {
console.error("Failed to connect to native messaging host:", error);
nativePort = undefined;
}
}
/**
* Send response back to native host
* @param {object} response
*/
function sendResponse(response) {
if (nativePort) {
try {
nativePort.postMessage(response);
} catch (error) {
console.error("Failed to send response:", error);
}
}
}
/**
* Get the browser tab ID to use for a command
* @param {string} [targetTabId]
* @returns {Promise}
*/
async function getTargetTab(targetTabId) {
if (targetTabId) {
const managedTab = managedTabs.get(targetTabId);
if (!managedTab) {
throw new Error(`Tab ${targetTabId} not found`);
}
return managedTab.tabId;
} else if (activeTabId) {
const managedTab = managedTabs.get(activeTabId);
if (!managedTab) {
throw new Error(`Active tab ${activeTabId} no longer exists`);
}
return managedTab.tabId;
} else {
throw new Error("No active tab. Create a tab first with newTab()");
}
}
/**
* Send command to content script
* @param {string} command
* @param {object} [params={}]
* @param {string} [targetTabId]
* @returns {Promise