{"id":27941181,"url":"https://github.com/robertheadley/playwright-userscript-manager","last_synced_at":"2026-04-16T01:32:59.205Z","repository":{"id":291447155,"uuid":"977648628","full_name":"robertheadley/playwright-userscript-manager","owner":"robertheadley","description":"Userscript manager using Playwright","archived":false,"fork":false,"pushed_at":"2025-05-04T17:25:04.000Z","size":38,"stargazers_count":2,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-25T10:38:10.268Z","etag":null,"topics":["developer-tools","playwright","roo-code","userscripts"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/robertheadley.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-05-04T17:18:40.000Z","updated_at":"2025-09-01T01:59:19.000Z","dependencies_parsed_at":"2025-05-04T18:39:24.589Z","dependency_job_id":null,"html_url":"https://github.com/robertheadley/playwright-userscript-manager","commit_stats":null,"previous_names":["robertheadley/playwright-userscript-manager"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/robertheadley/playwright-userscript-manager","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robertheadley%2Fplaywright-userscript-manager","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robertheadley%2Fplaywright-userscript-manager/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robertheadley%2Fplaywright-userscript-manager/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robertheadley%2Fplaywright-userscript-manager/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/robertheadley","download_url":"https://codeload.github.com/robertheadley/playwright-userscript-manager/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/robertheadley%2Fplaywright-userscript-manager/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31867710,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-15T15:24:51.572Z","status":"ssl_error","status_checked_at":"2026-04-15T15:24:39.138Z","response_time":63,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["developer-tools","playwright","roo-code","userscripts"],"created_at":"2025-05-07T10:38:19.892Z","updated_at":"2026-04-16T01:32:59.163Z","avatar_url":"https://github.com/robertheadley.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Playwright Userscript Manager\n\n## Project Overview\n\nThis project provides a Node.js-based runner that executes userscripts within a Playwright-controlled browser instance (Chromium or Firefox). It aims to replicate a significant portion of the Greasemonkey/Tampermonkey environment, allowing for testing and running userscripts in an automated fashion. Key capabilities include support for common GM\\_ functions, persistent storage, network request interception, execution of registered menu commands, and loading browser extensions.\n\n## Features\n\n*   Runs userscripts matching specified URLs in Playwright.\n*   Supports Chromium and Firefox browsers.\n*   Provides implementations for common Greasemonkey API functions (GM\\_*).\n*   Persistent storage for `GM_setValue`/`GM_getValue` using a JSON file (`gm_values.json` by default).\n*   Intercepts and logs network requests.\n*   Executes userscript-registered menu commands via CLI.\n*   Loads unpacked browser extensions.\n*   Configurable via command-line arguments.\n*   Supports basic userscript metadata (`@name`, `@match`, `@run-at`).\n*   Applies polyfills for enhanced compatibility.\n\n## Installation\n\nEnsure you have Node.js installed. Then, install the necessary dependencies:\n\n```bash\nnpm install playwright yargs tmp\n```\n\n*(Note: `tmp` might be needed depending on specific polyfill or userscript requirements, include if necessary based on `main.js` dependencies not shown)*\n\n## Dependencies\n\n*   **Playwright:** ([NPM](https://www.npmjs.com/package/playwright), [GitHub](https://github.com/microsoft/playwright)) - Browser automation library.\n*   **yargs:** ([NPM](https://www.npmjs.com/package/yargs), [GitHub](https://github.com/yargs/yargs)) - Command-line argument parser.\n*   **tmp:** ([NPM](https://www.npmjs.com/package/tmp), [GitHub](https://github.com/raszi/node-tmp)) - Temporary file and directory creation.\n## Usage\n\nRun the manager using Node.js, providing the target URL and the directory containing your userscripts.\n\n```bash\nnode main.js --url \u003ctarget-url\u003e --dir \u003cuserscripts-directory\u003e [options]\n```\n\n**Command-Line Options:**\n\n*   `--url`, `-u`: (Required) The URL to navigate to and run userscripts against.\n*   `--dir`, `-d`: (Required) The directory containing the userscripts to load. Defaults to `./userscripts`.\n*   `--polyfill`, `-p`: Path to a JavaScript polyfill file to inject before userscripts. Defaults to `./polyfill.js`.\n*   `--headless`, `-h`: Run the browser in headless mode (no UI). Defaults to `false`.\n*   `--timeout`, `-t`: Navigation timeout in milliseconds. Defaults to `30000`.\n*   `--run-menu-command`, `-m`: The name of a registered menu command to execute after the page loads.\n*   `--intercept-network`, `-i`: Enable network request interception and logging. Defaults to `false`.\n*   `--storage-path`, `-s`: Path to the JSON file for persistent GM\\_ storage. Defaults to `./gm_values.json`.\n*   `--extensions`, `-e`: Comma-separated list of paths to unpacked browser extensions to load.\n*   `--browser`, `-b`: Browser to use ('chromium' or 'firefox'). Defaults to 'chromium'.\n\n**Examples:**\n\n1.  Run userscripts from the default `userscripts/` directory on `example.com`:\n    ```bash\n    node main.js -u https://example.com\n    ```\n\n2.  Run userscripts from a specific directory (`my-scripts/`) on `google.com` in headless mode using Firefox, and load an extension:\n    ```bash\n    node main.js --url https://google.com --dir ./my-scripts --headless --browser firefox --extensions ./path/to/my-extension\n    ```\n\n3.  Run userscripts on `test.page`, intercept network requests, use a custom storage file, and execute the 'My Command' menu item:\n    ```bash\n    node main.js -u http://test.page -i -s ./data/storage.json -m \"My Command\"\n    ```\n\n## Userscript Development\n\n*   **Location:** Place your userscript files (ending in `.user.js`) inside the directory specified by the `--dir` option (`userscripts/` by default).\n*   **Metadata:** The runner recognizes the following metadata blocks:\n    *   `@name`: The name of the script (currently informational).\n    *   `@match`: URL match patterns. The script will run on pages whose URLs match these patterns. Uses simple glob-like matching (e.g., `*://*.example.com/*`). Multiple `@match` lines are allowed.\n    *   `@run-at`: Specifies when the script should run relative to the page load.\n        *   `document-start`: Injects as early as possible.\n        *   `document-end`: Injects after the DOM is loaded, but before resources like images.\n        *   `document-idle`: (Default) Injects after the `document-end` event and the page seems idle.\n*   **@match Patterns:** Define where your script should execute.\n    *   `*` matches any sequence of characters.\n    *   Example: `*://github.com/*` matches all GitHub pages (HTTP and HTTPS).\n    *   Example: `https://*.google.com/search*` matches Google search result pages.\n\n## Supported Greasemonkey API Functions\n\nThe following GM\\_ functions are bridged and available within userscripts:\n\n1.  **`GM_setValue(name, value)`**\n    *   **Purpose:** Persistently stores a `value` associated with a `name`. The value can be any JSON-serializable type (string, number, boolean, array, simple object). Storage is backed by the file specified via `--storage-path`.\n    *   **Signature:** `GM_setValue(name: string, value: any): Promise\u003cvoid\u003e`\n    *   **Examples:**\n        ```javascript\n        // Example 1: Store a user preference\n        GM_setValue('userTheme', 'dark').then(() =\u003e {\n          console.log('Theme preference saved.');\n        });\n\n        // Example 2: Store configuration settings\n        const settings = { fontSize: 12, showTooltips: true };\n        GM_setValue('pluginSettings', settings); // Can often omit .then() if not waiting\n        ```\n\n2.  **`GM_getValue(name, defaultValue)`**\n    *   **Purpose:** Retrieves a previously stored value associated with `name`. If the `name` is not found, `defaultValue` is returned.\n    *   **Signature:** `GM_getValue(name: string, defaultValue?: any): Promise\u003cany\u003e`\n    *   **Examples:**\n        ```javascript\n        // Example 1: Retrieve a theme, defaulting to 'light'\n        GM_getValue('userTheme', 'light').then(theme =\u003e {\n          document.body.classList.add(`theme-${theme}`);\n        });\n\n        // Example 2: Get settings, providing a default object\n        const defaultSettings = { fontSize: 10, showTooltips: false };\n        GM_getValue('pluginSettings', defaultSettings).then(settings =\u003e {\n          console.log('Current font size:', settings.fontSize);\n        });\n        ```\n\n3.  **`GM_deleteValue(name)`**\n    *   **Purpose:** Removes a previously stored value associated with `name` from persistent storage.\n    *   **Signature:** `GM_deleteValue(name: string): Promise\u003cvoid\u003e`\n    *   **Examples:**\n        ```javascript\n        // Example 1: Delete a specific setting\n        GM_deleteValue('userTheme').then(() =\u003e {\n          console.log('Theme preference deleted.');\n        });\n\n        // Example 2: Clear temporary data\n        GM_deleteValue('tempSessionData');\n        ```\n\n4.  **`GM_listValues()`**\n    *   **Purpose:** Retrieves an array of all names (keys) currently stored in the persistent storage.\n    *   **Signature:** `GM_listValues(): Promise\u003cstring[]\u003e`\n    *   **Examples:**\n        ```javascript\n        // Example 1: Log all stored keys\n        GM_listValues().then(keys =\u003e {\n          console.log('Stored keys:', keys);\n        });\n\n        // Example 2: Check if a specific key exists\n        GM_listValues().then(keys =\u003e {\n          if (keys.includes('pluginSettings')) {\n            console.log('Plugin settings exist.');\n          }\n        });\n        ```\n\n5.  **`GM_xmlhttpRequest(details)`**\n    *   **Purpose:** Performs an asynchronous HTTP request (XHR). This allows userscripts to fetch data from or send data to other servers, bypassing standard same-origin policy restrictions.\n    *   **Signature:** `GM_xmlhttpRequest(details: object): Promise\u003cobject\u003e` (The returned Promise resolves with a response object).\n    *   **Details Object Properties:** `method`, `url`, `headers`, `data`, `timeout`, `responseType`, `onload`, `onerror`, `ontimeout`, etc. (Refer to Greasemonkey documentation for full details). The implementation bridges common properties.\n    *   **Response Object Properties:** `status`, `statusText`, `responseText`, `responseHeaders`, `finalUrl`, etc.\n    *   **Examples:**\n        ```javascript\n        // Example 1: Fetch JSON data using GET\n        GM_xmlhttpRequest({\n          method: \"GET\",\n          url: \"https://api.example.com/data\",\n          responseType: \"json\", // Automatically parses JSON response\n          onload: function(response) {\n            if (response.status === 200) {\n              console.log(\"Received data:\", response.response); // Access parsed JSON\n            } else {\n              console.error(\"Request failed:\", response.statusText);\n            }\n          },\n          onerror: function(error) {\n            console.error(\"Network error:\", error);\n          }\n        });\n\n        // Example 2: Send data using POST\n        GM_xmlhttpRequest({\n          method: \"POST\",\n          url: \"https://api.example.com/submit\",\n          headers: { \"Content-Type\": \"application/json\" },\n          data: JSON.stringify({ name: \"Test User\", value: 123 }),\n          onload: function(response) {\n            console.log(\"Server response:\", response.responseText);\n          }\n        });\n        ```\n\n6.  **`GM_openInTab(url, options)`**\n    *   **Purpose:** Opens a new browser tab with the specified `url`.\n    *   **Signature:** `GM_openInTab(url: string, options?: object | boolean): Promise\u003cvoid\u003e`\n    *   **Options:** Can be a boolean (`true` for active, `false` for background) or an object `{ active?: boolean, insert?: boolean, setParent?: boolean }`. `active: true` makes the new tab focused.\n    *   **Examples:**\n        ```javascript\n        // Example 1: Open a link in a new active tab\n        GM_openInTab(\"https://www.google.com\", true);\n\n        // Example 2: Open a documentation link in a background tab\n        GM_openInTab(\"https://wiki.greasespot.net/GM_openInTab\", { active: false });\n        ```\n\n7.  **`GM_setClipboard(data, info)`**\n    *   **Purpose:** Copies the given `data` to the system clipboard.\n    *   **Signature:** `GM_setClipboard(data: string, info?: string | { type?: string, mimetype?: string }): Promise\u003cvoid\u003e`\n    *   **Info:** Can be a simple string representing the type (e.g., 'text') or an object specifying type/mimetype. The runner primarily supports text copying.\n    *   **Examples:**\n        ```javascript\n        // Example 1: Copy selected text\n        const selectedText = window.getSelection().toString();\n        if (selectedText) {\n          GM_setClipboard(selectedText, 'text');\n          console.log('Selected text copied to clipboard.');\n        }\n\n        // Example 2: Copy a specific URL\n        GM_setClipboard(document.location.href); // 'info' is optional for text\n        ```\n\n8.  **`GM_notification(text, title, image, onclick)`**\n    *   **Purpose:** Displays a system notification. *Note: The current implementation logs to the console instead of showing a native OS notification.*\n    *   **Signature:** `GM_notification(text: string, title?: string, image?: string, onclick?: Function): Promise\u003cvoid\u003e`\n    *   **Examples:**\n        ```javascript\n        // Example 1: Show a simple notification message\n        GM_notification(\"Userscript finished processing the page.\", \"Script Complete\");\n\n        // Example 2: Notify about an update (will log to console)\n        GM_notification(\"A new version of the script is available.\", \"Update Check\");\n        ```\n\n9.  **`GM_registerMenuCommand(name, callback)`**\n    *   **Purpose:** Registers a command in the userscript menu. These commands can be triggered externally using the `--run-menu-command` CLI option. The polyfill likely populates a global variable (e.g., `window.__registeredMenuCommands`) which `main.js` uses.\n    *   **Signature:** `GM_registerMenuCommand(name: string, callback: Function): Promise\u003cvoid\u003e`\n    *   **Examples:**\n        ```javascript\n        // Example 1: Register a command to clear settings\n        GM_registerMenuCommand(\"Clear My Settings\", () =\u003e {\n          GM_listValues().then(keys =\u003e {\n            keys.forEach(key =\u003e {\n              if (key.startsWith('myPlugin_')) {\n                GM_deleteValue(key);\n              }\n            });\n            alert('Settings cleared!');\n          });\n        });\n\n        // Example 2: Register a command to toggle a feature\n        GM_registerMenuCommand(\"Toggle Feature X\", () =\u003e {\n          GM_getValue('featureXEnabled', false).then(enabled =\u003e {\n            GM_setValue('featureXEnabled', !enabled).then(() =\u003e {\n               console.log(`Feature X ${!enabled ? 'enabled' : 'disabled'}. Reload may be required.`);\n            });\n          });\n        });\n        ```\n        *To run the first command:* `node main.js -u \u003curl\u003e -m \"Clear My Settings\"`\n\n## Persistent Storage\n\nThe `GM_setValue`, `GM_getValue`, `GM_deleteValue`, and `GM_listValues` functions interact with a persistent JSON file.\n*   By default, this file is `gm_values.json` in the current working directory.\n*   You can specify a different path using the `--storage-path` or `-s` command-line option.\n*   This allows userscript data to persist across multiple runs of the manager.\n\n## Network Interception\n\n*   When the `--intercept-network` or `-i` flag is used, the runner will intercept all network requests made by the page and the userscripts.\n*   Details about each request (URL, method, type) will be logged to the console.\n*   This is useful for debugging userscript network activity or understanding page behavior.\n\n## Menu Commands\n\n*   Userscripts can register menu commands using `GM_registerMenuCommand(commandName, callbackFunction)`. This function is typically provided by the polyfill script.\n*   The polyfill should store these registered commands in a way accessible to the main script (e.g., attaching them to the `window` object like `window.__registeredMenuCommands = window.__registeredMenuCommands || {}; window.__registeredMenuCommands[commandName] = callbackFunction;`).\n*   You can execute a registered command after the page loads by passing its name via the `--run-menu-command` or `-m` CLI option. The runner will find the corresponding callback function and execute it within the page context.\n\n## Browser Extensions (In development, probably doesn't work)\n\n*   You can load unpacked browser extensions (e.g., for testing interactions or providing additional APIs) using the `--extensions` or `-e` option.\n*   Provide a comma-separated list of paths to the directories containing the `manifest.json` file for each extension.\n    ```bash\n    node main.js -u \u003curl\u003e -e ./path/to/ext1,./another/path/to/ext2\n    ```\n\n## Polyfills\n\n*   A polyfill script can be injected into the page *before* any userscripts run using the `--polyfill` or `-p` option.\n*   The default polyfill path is `./polyfill.js`. You can create custom polyfills and place them in a `polyfills/` directory or elsewhere.\n*   Polyfills are useful for:\n    *   Providing implementations for GM\\_ functions not natively supported by the runner's bridge.\n    *   Setting up helper functions or objects needed by userscripts.\n    *   Modifying the page environment in preparation for userscripts.\n## Made With\n\nThis project was developed with assistance from:\n\n*   [GitHub Copilot](https://github.com/features/copilot)\n*   [Roo Code - VScode extention](https://github.com/RooVetGit/Roo-Code)\n*   [Google Gemini](https://gemini.google.com/)\n## License\n\nMIT License\n\nCopyright (c) 2025 Robert Headley\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\nlove \u0026lt;3\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frobertheadley%2Fplaywright-userscript-manager","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frobertheadley%2Fplaywright-userscript-manager","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frobertheadley%2Fplaywright-userscript-manager/lists"}