Repository: pinokiocomputer/pinokio Branch: main Commit: f5e5073c1f19 Files: 33 Total size: 263.5 KB Directory structure: gitextract_4ttzgoi5/ ├── .github/ │ └── workflows/ │ ├── build.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── RELEASE.md ├── after-pack.js ├── build/ │ ├── entitlements.mac.inherit.plist │ ├── entitlements.mac.plist │ ├── icon.icns │ ├── installer.nsh │ └── sign.js ├── chmod.js ├── config.js ├── full.js ├── linux_build.sh ├── main.js ├── minimal.js ├── package.json ├── patch-linux-arm64-natives.js ├── popup-shell.js ├── popup-toolbar.html ├── preload.js ├── prompt.html ├── script/ │ ├── patch.command │ ├── run-update-banner-test.js │ └── zip.js ├── splash.html ├── temp/ │ ├── rebuild.js │ └── yarn.js ├── update-banner.html ├── updater.js └── wrap-linux-launcher.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/build.yml ================================================ name: Build/release on: push: branches: - main #on: workflow_dispatch jobs: create-release: runs-on: ubuntu-latest permissions: contents: write steps: - name: Check out Git repository uses: actions/checkout@v1 - name: Get package.json version id: get_version shell: bash run: | PACKAGE_VERSION=$(node -p "require('./package.json').version") echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> $GITHUB_ENV - name: Create an empty release shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "Releasing version $PACKAGE_VERSION" gh release create "v$PACKAGE_VERSION" --draft \ --title "v$PACKAGE_VERSION" \ --notes-file RELEASE.md # --notes "Pinokio version $PACKAGE_VERSION." windows-unsigned: if: false needs: "create-release" runs-on: windows-latest permissions: contents: write steps: - name: Check out Git repository uses: actions/checkout@v1 - name: Install Node.js, NPM and Yarn uses: actions/setup-node@v1 with: node-version: 22 - name: Build/release Electron app id: electron-builder uses: samuelmeuli/action-electron-builder@v1.6.0 with: github_token: ${{ secrets.github_token }} # If the commit is tagged with a version (e.g. "v1.0.0"), # release the app after building #release: ${{ startsWith(github.ref, 'refs/tags/v') }} release: true #args: --win --dir # Build win-unpacked only args: --win windows: # if: false needs: "create-release" runs-on: windows-latest permissions: contents: write steps: - name: Check out Git repository uses: actions/checkout@v1 - name: Install Node.js, NPM and Yarn uses: actions/setup-node@v1 with: node-version: 22 - name: Build/release Electron app id: electron-builder uses: samuelmeuli/action-electron-builder@v1.6.0 with: github_token: ${{ secrets.github_token }} # If the commit is tagged with a version (e.g. "v1.0.0"), # release the app after building #release: ${{ startsWith(github.ref, 'refs/tags/v') }} #release: true release: false #args: --win --dir # Build win-unpacked only args: --win # - name: Check contents # run: | # dir dist-win32 \ # dir dist-win32\\win-unpacked # shell: cmd ### sign start - name: upload-unsigned-artifact id: upload-unsigned-artifact uses: actions/upload-artifact@v4 with: #path: dist-win32 #path: dist-win32/win-unpacked/Pinokio.exe path: dist-win32/Pinokio.exe retention-days: 1 - id: Sign if: ${{ runner.os == 'Windows' }} uses: signpath/github-action-submit-signing-request@v1.1 with: api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' organization-id: 'd2da0df2-dc12-4516-8222-87178d5ebf3d' project-slug: 'pinokio' #signing-policy-slug: 'test-signing' signing-policy-slug: 'release-signing' github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}' wait-for-completion: true output-artifact-directory: './signed-windows' parameters: | version: ${{ toJSON(github.ref_name) }} - name: Rebuild blockmap and latest.yml from signed installer shell: bash run: | set -euo pipefail shopt -s nullglob files=(signed-windows/*.exe) if [[ ${#files[@]} -ne 1 ]]; then echo "Expected exactly one signed exe, found ${#files[@]}" >&2 exit 1 fi SIGNED_EXE="${files[0]}" EXE_BASENAME=$(basename "$SIGNED_EXE") VERSION=$(node -p "require('./package.json').version") OUT_DIR=dist-win32 mkdir -p "$OUT_DIR" # Use app-builder bundled with electron-builder to regenerate blockmap for the signed binary APP_BUILDER=$(node -p "require('app-builder-bin').appBuilderPath") "$APP_BUILDER" blockmap --input "$SIGNED_EXE" --output "$OUT_DIR/${EXE_BASENAME}.blockmap" EXE="$SIGNED_EXE" BLOCKMAP="$OUT_DIR/${EXE_BASENAME}.blockmap" VERSION="$VERSION" OUT_DIR="$OUT_DIR" node - <<'EOF' const fs = require('fs'); const crypto = require('crypto'); const path = require('path'); const exe = process.env.EXE; const blockmap = process.env.BLOCKMAP; const version = process.env.VERSION; const outDir = process.env.OUT_DIR; const exeStats = fs.statSync(exe); const blockmapStats = fs.statSync(blockmap); const sha512 = crypto.createHash('sha512').update(fs.readFileSync(exe)).digest('base64'); const lines = [ `version: ${version}`, `files:`, ` - url: ${path.basename(exe)}`, ` sha512: ${sha512}`, ` size: ${exeStats.size}`, ` blockMapSize: ${blockmapStats.size}`, `path: ${path.basename(exe)}`, `sha512: ${sha512}`, `releaseDate: "${new Date().toISOString()}"` ]; fs.writeFileSync(path.join(outDir, 'latest.yml'), lines.join('\n')); EOF # # Replace the unsigned exe with the signed exe # - name: Replace with signed exe # run: | # copy /Y ".\signed-windows\Pinokio.exe" ".\dist-win32\win-unpacked\Pinokio.exe" # shell: cmd ### sign end # # Build the final installer from the signed exe # - name: Build final installer # env: # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ## CSC_IDENTITY_AUTO_DISCOVERY: "false" # disable any auto code-sign discovery ## DISABLE_CODE_SIGNING: "true" # electron-builder respects this to skip signing # run: | # #yarn run electron-builder --win --prepackaged dist-win32/win-unpacked --publish never # yarn run electron-builder --win --prepackaged dist-win32/win-unpacked --publish always - name: Get package.json version id: get_version shell: bash run: | PACKAGE_VERSION=$(node -p "require('./package.json').version") echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> $GITHUB_ENV - name: Publish GitHub Release with gh shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "Releasing version $PACKAGE_VERSION" #gh release create "v$PACKAGE_VERSION" ./signed-windows/*.exe \ #gh release upload "v$PACKAGE_VERSION" ./dist-win32/*.exe .dist-win32/latest.yml ./dist-win32/*.exe.blockmap gh release upload "v$PACKAGE_VERSION" ./signed-windows/*.exe ./dist-win32/latest.yml ./dist-win32/*.exe.blockmap --clobber #gh release create "v$PACKAGE_VERSION" ./dist-win32/*.exe \ # --title "Release v$PACKAGE_VERSION" \ # --notes "Pinokio version $PACKAGE_VERSION." mac: # if: false needs: "create-release" runs-on: macos-latest permissions: contents: write steps: - name: Check out Git repository uses: actions/checkout@v1 - name: Install Node.js, NPM and Yarn uses: actions/setup-node@v1 with: node-version: 22 # - name: Prepare for app notarization # if: startsWith(matrix.os, 'macos') # # Import Apple API key for app notarization on macOS # run: | # mkdir -p ~/private_keys/ # echo '${{ secrets.api_key }}' > ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8 - name: Build/release Electron app id: electron-builder uses: samuelmeuli/action-electron-builder@v1.6.0 with: # GitHub token, automatically provided to the action # (No need to define this secret in the repo settings) github_token: ${{ secrets.github_token }} # If the commit is tagged with a version (e.g. "v1.0.0"), # release the app after building #release: ${{ startsWith(github.ref, 'refs/tags/v') }} release: true mac_certs: ${{ secrets.mac_certs }} mac_certs_password: ${{ secrets.mac_certs_password }} env: # macOS notarization API key #API_KEY_ID: ${{ secrets.api_key_id }} #API_KEY_ISSUER_ID: ${{ secrets.api_key_issuer_id }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - name: Show notarization-error.log if: failure() run: cat dist-darwin/**/notarization-error.log || echo "No notarization-error.log found" linux: # if: false needs: "create-release" runs-on: ubuntu-latest strategy: fail-fast: false matrix: arch: [x64, arm64] permissions: contents: write steps: - name: Check out Git repository uses: actions/checkout@v1 - name: Install Node.js, NPM and Yarn uses: actions/setup-node@v1 with: node-version: 22 - name: Install dependencies shell: bash run: | if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then npm ci else npm install fi - name: Constrain Linux targets to matrix arch shell: bash env: TARGET_ARCH: ${{ matrix.arch }} run: | node - <<'EOF' const fs = require('fs'); const packagePath = './package.json'; const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); const targetArch = process.env.TARGET_ARCH; if (!targetArch) { throw new Error('TARGET_ARCH is not set'); } const linux = packageJson.build && packageJson.build.linux; if (!linux || !Array.isArray(linux.target)) { throw new Error('build.linux.target is not configured as an array'); } linux.target = linux.target.map((entry) => { if (typeof entry === 'string') { return { target: entry, arch: [targetArch] }; } if (entry && typeof entry === 'object') { return { ...entry, arch: [targetArch] }; } return entry; }); fs.writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`); EOF - name: Install ARM64 parcel watcher native if: matrix.arch == 'arm64' shell: bash run: | npm_config_os=linux \ npm_config_cpu=arm64 \ npm_config_libc=glibc \ npm_config_force=true \ npm install --no-save @parcel/watcher-linux-arm64-glibc@2.5.1 - name: Build/release Linux app shell: bash env: GITHUB_TOKEN: ${{ secrets.github_token }} run: | ./node_modules/.bin/electron-builder install-app-deps --platform linux --arch ${{ matrix.arch }} ./node_modules/.bin/electron-builder --linux --${{ matrix.arch }} --publish never - name: Validate ARM64 Linux packaged natives if: matrix.arch == 'arm64' shell: bash run: | set -euo pipefail shopt -s nullglob pick_file() { for candidate in "$@"; do if [ -f "$candidate" ]; then printf '%s\n' "$candidate" return 0 fi done return 1 } deb_files=(dist-linux/*arm64*.deb dist-linux/*aarch64*.deb) if [ ${#deb_files[@]} -eq 0 ]; then echo "No ARM64 .deb artifacts found in dist-linux" >&2 ls -la dist-linux || true exit 1 fi for deb in "${deb_files[@]}"; do echo "Validating $deb" workdir="$(mktemp -d)" dpkg-deb -x "$deb" "$workdir/root" if ! grep -R --binary-files=without-match -q '/opt/Pinokio/pinokio-bin' "$workdir/root"; then echo "Could not find /opt/Pinokio/pinokio-bin fallback in extracted package" >&2 exit 1 fi pty_node="$(pick_file \ "$workdir/root/opt/Pinokio/resources/app.asar.unpacked/node_modules/@homebridge/node-pty-prebuilt-multiarch/build/Release/pty.node" \ "$workdir/root/opt/Pinokio/resources/app.asar.unpacked/node_modules/pinokiod/node_modules/@homebridge/node-pty-prebuilt-multiarch/build/Release/pty.node" \ )" || { echo "Missing patched pty.node in ARM64 package" >&2 exit 1 } watcher_node="$(pick_file \ "$workdir/root/opt/Pinokio/resources/app.asar.unpacked/node_modules/@parcel/watcher/build/Release/watcher.node" \ "$workdir/root/opt/Pinokio/resources/app.asar.unpacked/node_modules/pinokiod/node_modules/@parcel/watcher/build/Release/watcher.node" \ )" || { echo "Missing patched watcher.node in ARM64 package" >&2 exit 1 } watcher_platform_node="$(pick_file \ "$workdir/root/opt/Pinokio/resources/app.asar.unpacked/node_modules/@parcel/watcher-linux-arm64-glibc/watcher.node" \ "$workdir/root/opt/Pinokio/resources/app.asar.unpacked/node_modules/pinokiod/node_modules/@parcel/watcher-linux-arm64-glibc/watcher.node" \ )" || { echo "Missing @parcel/watcher-linux-arm64-glibc payload in ARM64 package" >&2 exit 1 } file "$pty_node" file "$watcher_node" file "$watcher_platform_node" file "$pty_node" | grep -qi 'aarch64' || { echo "pty.node is not ARM64 in $deb" >&2 exit 1 } file "$watcher_node" | grep -qi 'aarch64' || { echo "watcher.node is not ARM64 in $deb" >&2 exit 1 } file "$watcher_platform_node" | grep -qi 'aarch64' || { echo "watcher-linux-arm64-glibc payload is not ARM64 in $deb" >&2 exit 1 } rm -rf "$workdir" done - name: Upload Linux release artifacts shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail version="$(node -p "require('./package.json').version")" tag="v${version}" files=() while IFS= read -r -d '' file; do files+=("$file") done < <(find dist-linux -maxdepth 1 -type f \ \( -name '*.AppImage' -o -name '*.deb' -o -name '*.rpm' -o -name '*linux*.yml' -o -name 'latest*.yml' \) \ -print0) if [ "${#files[@]}" -eq 0 ]; then echo "No Linux artifacts found in dist-linux" >&2 ls -la dist-linux || true exit 1 fi gh release upload "$tag" "${files[@]}" --clobber ================================================ FILE: .github/workflows/test.yml ================================================ name: Test #on: push on: workflow_dispatch jobs: print: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Get package.json version id: get_version shell: bash # run: echo "PACKAGE_VERSION=$(node -p 'require("./package.json").version')" >> $GITHUB_ENV #run: echo 'PACKAGE_VERSION=$(node -p "require(\"./package.json\").version")' >> $GITHUB_ENV # run: echo "PACKAGE_VERSION=$(node -p \"require('./package.json').version\")" >> $GITHUB_ENV run: | PACKAGE_VERSION=$(node -p "require('./package.json').version") echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> $GITHUB_ENV - name: Print env shell: bash run: echo $PACKAGE_VERSION ================================================ FILE: .gitignore ================================================ node_modules dist cache package-lock.json .claude ================================================ FILE: LICENSE ================================================ Copyright 2023 Pinokio Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Pinokio Launch Anything. # Script Policy Pinokio is a 1-click launcher for any open-source project. Think of it as a terminal application with a user-friendly interface that can programmatically interact with scripts. This means: 1. **Scripts can run anything:** Just like terminal apps can run shell scripts, Pinokio scripts can run any command, download files, and execute them. Essentially, Pinokio is a user-friendly terminal with a UI. 2. **How scripts can be run:** There are two ways to run scripts on Pinokio: 1. **Write your own:** Just like writing and executing shell scripts in the terminal, you can create your own scripts and run them locally. 2. **Install from the "Discover" page:** Vetted scripts are manually listed in the directory, tracked via Git, and frozen under the official GitHub organization. These are guaranteed to be secure and safe to install. 3. **Verified Scripts:** To be featured on the "Discover" page, scripts must go through the following strict process: 1. **Publisher Verification:** You must be personally verified to submit scripts for consideration. Contact the Pinokio admin (https://x.com/cocktailpeanut) to request verification. 2. **Github Organization Invitation:** Once verified, you'll be invited to the official Pinokio Factory GitHub organization as a contributor. Only members of this organization can publish scripts eligible for the "Discover" page. Abusing publishing privileges may result in removal from the organization. 3. **Repository Transfer and Freeze** To apply for a feature, you must transfer your script repository to the Pinokio Factory GitHub organization. Follow this guide: https://docs.github.com/en/repositories/creating-and-managing-repositories/transferring-a-repository 4. **Feature Application:** Once your repository is fully transferred and controlled by the organization, it is considered "frozen". You can then request to feature it on the "Discover" page by contacting the admin. 5. **Review:** The script will be thoroughly reviewed and tested by the Pinokio admin. If verified as safe, it will be featured on the "Discover" page. 6. **Troubleshooting:** If any issues arise after a script is featured, the Pinokio admin may: - Delist the script from the "Discover" page - Modify the script to resolve the issue. Since the script is under the Pinokio Factory organization, the admin has the rights to make necessary fixes. # Security ## Scripts are isolated by design By default all Pinokio scripts are stored run under an isolated location (at `~/pinokio/api`). Additionally, all binaries installed through the built-in package managers in Pinokio are installed within `~/pinokio/bin`. Basically, everything you do is stored inside `~/pinokio`. The risk factor is when a script intentionally tries to deviatte away from this. The script verification process checks to make sure this doesn't happen. Th Pinokio script syntax was designed to make this process simpler, both by human and machines. ## Scripts are open source All scripts must be downloaded from public git repositories. The scripts are both human readable and machine readable (written in JSON syntax), so you can always check the source code before running it. Here's an example install screen, with an alert letting you know the downloaded 3rd party script is about to be run, as well as the URL to the original script repository where it was downloaded from. ![install.png](install.png) ## Script Verification Verified scripts are scripts that are explicitly reviewed and approved by the Pinokio admin. Because the scripts are designed to run isolated by default, and the syntax makes it easy to detect when a command intentionally tries to run things outside of the isolated environment, it is easy to detect any script that does things out of the ordinary. Here are some of the checks done by the Pinokio admin to make sure each script file is secure: 1. **Path check:** When we verify the scripts, we look at the scripts to see if all commands are run inside each app's path. The script syntax was designed to make this process easy (with the `path` attribute, which declares the folder path from which to run a command, and by default the execution path is each app's path) 2. **Venv check:** We also check to make sure every dependency installation is done within the context of each app using `venv`. This process is again made easy with the script syntax (with the `venv` attribute, which automatically activates a virtual environment and installs all dependencies there, inside each app's folder) 3. **3rd Party Package check:** We also check that any 3rd party packages installed through Pinokio to make sure that they are installed inside the pinokio isolated environment. The built-in package mangagers (Conda, Homebrew, Pip, and NPM) install everything inside the isolated pinokio home path (`~/pinokio`) by default. Since everything runs isolated by default, verifying this is simple (by checking that there are no explicit declaration of additional code that tries to go outside of the isolated environment) Here's an example execution script that installs python dependencies: ```json { "method": "shell.run", "params": { "message": "uv pip install -r requirements.txt", "path": "server", "venv": "venv" } } ``` 1. First of all, by default the entire thing is run isolated in the pinokio activated conda environment, and the execution path is the downloaded app's path (for example `~/pinokio/api/myapp`) 2. second, since the `path` is declared as `server`, the code will be run inside the `server` folder ofr the app (in this case `~/pinokio/api/myapp/server`) 3. Third, the `venv` attribute is included, so the python dependencies are also installed in an app-isolated manner. If the app is located at `~/pinokio/api/myapp`, the The depenencies will be stored at `~/pinokio/api/myapp/venv` The script verification check makes sure that all these components are run locally within the constraints of each app. Of course, there are also additional checks such as: 1. Checking the reputation of the repository and the developer of the original project 2. Trhing out the app personally 3. Making sure that the install and launch instructions actually follow the recommended instructions suggested in the original project's README. No scripts are approved until rigorously tested. ================================================ FILE: RELEASE.md ================================================ # Pinokio Release ## Code Signing Policy Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/). ## Privacy Policy This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it. ================================================ FILE: after-pack.js ================================================ module.exports = async (context) => { const chmodHandler = require('./chmod') const wrapLinuxLauncher = require('./wrap-linux-launcher') const patchLinuxArm64Natives = require('./patch-linux-arm64-natives') await chmodHandler(context) await wrapLinuxLauncher(context) await patchLinuxArm64Natives(context) } ================================================ FILE: build/entitlements.mac.inherit.plist ================================================ com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.disable-library-validation ================================================ FILE: build/entitlements.mac.plist ================================================ com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.disable-library-validation com.apple.security.device.audio-input com.apple.security.device.camera ================================================ FILE: build/installer.nsh ================================================ # https://github.com/electron-userland/electron-builder/issues/6865#issuecomment-1871121350 !macro customInit Delete "$INSTDIR\Uninstall*.exe" !macroend ================================================ FILE: build/sign.js ================================================ module.exports = async function () { // no-op: prevents electron-builder from calling signtool.exe }; ================================================ FILE: chmod.js ================================================ const exec = require('child_process').exec; module.exports = async (context) => { const paths = [ `${context.appOutDir}/resources/app.asar.unpacked/node_modules/go-get-folder-size/dist/go-get-folder-size_linux_386/go-get-folder-size`, `${context.appOutDir}/resources/app.asar.unpacked/node_modules/go-get-folder-size/dist/go-get-folder-size_linux_amd64_v1/go-get-folder-size`, `${context.appOutDir}/resources/app.asar.unpacked/node_modules/go-get-folder-size/dist/go-get-folder-size_linux_arm64/go-get-folder-size`, ] for(let p of paths) { await exec(`chmod +x "${p}"`); } } ================================================ FILE: config.js ================================================ const Store = require('electron-store'); const packagejson = require("./package.json") const store = new Store(); module.exports = { newsfeed: (gitRemote) => { return `https://pinokiocomputer.github.io/home/item?uri=${gitRemote}&display=feed` }, profile: (gitRemote) => { return `https://pinokiocomputer.github.io/home/item?uri=${gitRemote}&display=profile` }, site: "https://pinokio.co", discover_dark: "https://beta.pinokio.co", discover_light: "https://beta.pinokio.co", portal: "https://beta.pinokio.co", docs: "https://pinokio.co/docs", install: "https://pinokiocomputer.github.io/program.pinokio.computer/#/?id=install", agent: "electron", version: packagejson.version, store } ================================================ FILE: full.js ================================================ const {app, screen, shell, BrowserWindow, BrowserView, ipcMain, dialog, clipboard, session, desktopCapturer, systemPreferences, Menu } = require('electron') const windowStateKeeper = require('electron-window-state'); const fs = require('fs') const path = require("path") const Pinokiod = require("pinokiod") const os = require('os') const Updater = require('./updater') const createPopupShellManager = require('./popup-shell') const is_mac = process.platform.startsWith("darwin") const platform = os.platform() var mainWindow; var root_url; var wins = {} var pinned = {} var launched var theme var colors var splashWindow var splashIcon var updateBannerPayload var updateBannerDismissed = false var updateInfo = null var updateDownloadInFlight = false const updateTestMode = (() => { const value = process.env.PINOKIO_TEST_UPDATE_BANNER if (!value) { return false } return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase()) })() let updateTestInterval = null let updateTestTimeout = null const UPDATE_RELEASES_URL = 'https://github.com/peanutcocktail/pinokio/releases' const setWindowTitleBarOverlay = (win, overlay) => { if (!win || !win.setTitleBarOverlay) { return } try { win.setTitleBarOverlay(overlay) } catch (e) { // console.log("ERROR", e) } } const applyTitleBarOverlayToAllWindows = () => { if (!colors) { return } const overlay = titleBarOverlay(colors) const browserWindows = BrowserWindow.getAllWindows() for (const win of browserWindows) { setWindowTitleBarOverlay(win, overlay) } } const updateThemeColors = (payload = {}) => { console.log("updateThemeColors", payload) const nextTheme = payload.theme const nextColors = payload.colors if (nextTheme) { theme = nextTheme } if (nextColors) { colors = nextColors } applyTitleBarOverlayToAllWindows() } const stripHtmlTags = (value) => { if (!value) { return '' } return String(value).replace(/<[^>]*>/g, '') } const buildReleaseNotesPreview = (notes) => { if (!notes) { return '' } let text = '' if (Array.isArray(notes)) { text = notes.map((note) => note && (note.note || note.releaseNotes || note.title || '')).join('\n') } else if (typeof notes === 'string') { text = notes } else { text = String(notes) } const cleaned = stripHtmlTags(text).replace(/\r/g, '') const lines = cleaned.split('\n').map((line) => line.trim()).filter(Boolean) if (!lines.length) { return '' } const firstLine = lines[0] if (firstLine.length > 140) { return `${firstLine.slice(0, 137)}...` } return firstLine } const buildProgressLabel = (progress) => { if (!progress || typeof progress.percent !== 'number') { return '' } const percent = Math.round(progress.percent) if (typeof progress.transferred === 'number' && typeof progress.total === 'number' && progress.total > 0) { const transferred = (progress.transferred / 1024 / 1024).toFixed(1) const total = (progress.total / 1024 / 1024).toFixed(1) return `${percent}% (${transferred} MB of ${total} MB)` } return `${percent}%` } const buildUpdateBannerPayload = (state, info, extra = {}) => { const resolved = info || {} return { state, version: resolved.version || '', notesPreview: buildReleaseNotesPreview(resolved.releaseNotes), releaseUrl: UPDATE_RELEASES_URL, ...extra } } const clearUpdateTestTimers = () => { if (updateTestInterval) { clearInterval(updateTestInterval) updateTestInterval = null } if (updateTestTimeout) { clearTimeout(updateTestTimeout) updateTestTimeout = null } } const showUpdateBannerTestAvailable = () => { updateInfo = { version: '99.9.9-test', releaseNotes: 'Simulated update for banner testing.' } updateDownloadInFlight = false updateBannerDismissed = false showUpdateBanner(buildUpdateBannerPayload('available', updateInfo)) } const startUpdateBannerTestDownload = () => { if (!updateInfo) { showUpdateBannerTestAvailable() } clearUpdateTestTimers() updateDownloadInFlight = true let progress = 0 const tick = () => { progress = Math.min(100, progress + 6 + Math.random() * 12) showUpdateBanner(buildUpdateBannerPayload('downloading', updateInfo, { progressPercent: progress, notesPreview: `${Math.round(progress)}%` })) if (progress >= 100) { clearUpdateTestTimers() updateDownloadInFlight = false showUpdateBanner(buildUpdateBannerPayload('ready', updateInfo)) } } tick() updateTestInterval = setInterval(tick, 320) } const simulateUpdateBannerRestart = () => { clearUpdateTestTimers() hideUpdateBanner() updateTestTimeout = setTimeout(() => { showUpdateBannerTestAvailable() }, 800) } const dispatchUpdateBanner = (payload) => { updateBannerPayload = payload if (!mainWindow || mainWindow.isDestroyed()) { return } if (payload && payload.state === 'available' && updateBannerDismissed) { return } if (mainWindow.webContents && !mainWindow.webContents.isDestroyed()) { if (!mainWindow.webContents.isLoading()) { mainWindow.webContents.send('pinokio:update-banner', payload) } } } const showUpdateBanner = (payload) => { if (payload && payload.state === 'available' && updateBannerDismissed) { updateBannerPayload = payload return } dispatchUpdateBanner(payload || updateBannerPayload) } const hideUpdateBanner = () => { dispatchUpdateBanner({ state: 'hidden' }) } let PORT //let PORT = 42000 //let PORT = (platform === 'linux' ? 42000 : 80) let config = require('./config') const filter = function (item) { return item.browserName === 'Chrome'; }; const updater = new Updater() const pinokiod = new Pinokiod(config) const ENABLE_BROWSER_CONSOLE_LOG = process.env.PINOKIO_BROWSER_LOG === '1' const browserConsoleState = new WeakMap() const attachedConsoleListeners = new WeakSet() const consoleLevelLabels = ['log', 'info', 'warn', 'error', 'debug'] let browserLogFilePath let browserLogFileReady = false let browserLogBuffer = [] let browserLogWritePromise = Promise.resolve() let permissionHandlersInstalled = false let injectorHandlersInstalled = false const frameInjectorSyncState = new Map() const frameInjectTargetRegistry = new Map() const PINOKIO_INJECT_ISOLATED_WORLD_ID = 42000 const permissionPrompted = new Set() const permissionPromptInFlight = new Set() const safeParseUrl = (value, base) => { if (!value) { return null } try { if (base) { return new URL(value, base) } return new URL(value) } catch (err) { return null } } const popupNavigationGuards = new Map() const isRootShellUrl = (value) => { const root = safeParseUrl(root_url) const target = safeParseUrl(value, root ? root.href : undefined) return Boolean(root && target && target.origin === root.origin && (target.pathname || '/') === '/') } const getHttpNavigationTarget = (value, base) => { const target = safeParseUrl(value, base) if (!target || (target.protocol !== 'http:' && target.protocol !== 'https:')) { return null } return target } const openNonPinokioNavigationInPopup = ({ event, owner, url, frame } = {}) => { const target = getHttpNavigationTarget(url, root_url || undefined) if (!target || !owner || owner.isDestroyed?.() || owner.__pinokioPopupShell) { return false } if (popupShellManager.isPinokioWindowUrl(target.href, root_url)) { return false } if (event && typeof event.preventDefault === 'function') { event.preventDefault() } const frameId = frame && (frame.frameToken || frame.frameTreeNodeId || frame.routingId) if (frameId) { const guardKey = `${owner.id}:${frameId}:${target.href}` const now = Date.now() const last = popupNavigationGuards.get(guardKey) || 0 popupNavigationGuards.set(guardKey, now) setTimeout(() => { if (popupNavigationGuards.get(guardKey) === now) { popupNavigationGuards.delete(guardKey) } }, 1500) if (now - last < 1500) { return true } } popupShellManager.openExternalWindow({ url: target.href }) return true } const installForceDestroyOnClose = (win) => { if (!win || win.__pinokioCloseHandlerInstalled) { return } win.__pinokioCloseHandlerInstalled = true win.once('close', (event) => { if (win.isDestroyed()) { return } event.preventDefault() win.destroy() }) } const popupShellManager = createPopupShellManager({ installForceDestroyOnClose }) const installClosePopupOnDownload = (targetSession) => { if (!targetSession || targetSession.__pinokioClosePopupOnDownloadInstalled) { return } targetSession.__pinokioClosePopupOnDownloadInstalled = true targetSession.on('will-download', (_event, _item, webContents) => { if (!webContents || typeof webContents.getOwnerBrowserWindow !== 'function') { return } let owner = null try { owner = webContents.getOwnerBrowserWindow() } catch (_) { owner = null } if (!owner || owner.isDestroyed?.() || !owner.__pinokioCloseOnFirstDownload) { return } owner.__pinokioCloseOnFirstDownload = false setTimeout(() => { if (!owner.isDestroyed()) { owner.close() } }, 0) }) } const resolveConsoleSourceUrl = (sourceId, pageUrl) => { const page = safeParseUrl(pageUrl) const source = safeParseUrl(sourceId, page ? page.href : undefined) if (source && (source.protocol === 'http:' || source.protocol === 'https:' || source.protocol === 'file:')) { return source.href } if (page) { return page.href } return null } const shouldLogUrl = (url) => { if (!ENABLE_BROWSER_CONSOLE_LOG) { return false } if (!url) { return false } const rootParsed = safeParseUrl(root_url) const target = safeParseUrl(url, rootParsed ? rootParsed.origin : undefined) if (!target) { return false } if (rootParsed) { if (target.origin !== rootParsed.origin) { return false } const normalizedTargetPath = (target.pathname || '').replace(/\/+$/, '') const normalizedRootPath = (rootParsed.pathname || '').replace(/\/+$/, '') if (normalizedTargetPath === normalizedRootPath) { return false } } else { const normalizedTargetPath = (target.pathname || '').replace(/\/+$/, '') if (!normalizedTargetPath) { return false } } return true } const getBrowserLogFile = () => { if (!ENABLE_BROWSER_CONSOLE_LOG) { return null } if (!browserLogFilePath) { if (!pinokiod || !pinokiod.kernel || !pinokiod.kernel.homedir) { return null } try { browserLogFilePath = pinokiod.kernel.path('logs/browser.log') } catch (err) { console.error('[BROWSER LOG] Failed to resolve browser log file path', err) return null } } return browserLogFilePath } const ensureBrowserLogFile = () => { if (!ENABLE_BROWSER_CONSOLE_LOG) { return null } const filePath = getBrowserLogFile() if (!filePath) { return null } if (browserLogFileReady) { return filePath } try { fs.mkdirSync(path.dirname(filePath), { recursive: true }) if (fs.existsSync(filePath)) { try { const existingContent = fs.readFileSync(filePath, 'utf8') const existingLines = existingContent.split(/\r?\n/).filter((line) => line.length > 0) const filteredLines = [] for (const line of existingLines) { const parts = line.split('\t') if (parts.length >= 2) { const urlPart = parts[1] if (!shouldLogUrl(urlPart)) { continue } } filteredLines.push(`${line}\n`) if (filteredLines.length > 100) { filteredLines.shift() } } browserLogBuffer = filteredLines fs.writeFileSync(filePath, browserLogBuffer.join('')) } catch (err) { console.error('[BROWSER LOG] Failed to prime existing browser log', err) browserLogBuffer = [] } } browserLogFileReady = true return filePath } catch (err) { console.error('[BROWSER LOG] Failed to prepare browser log file', err) return null } } const titleBarOverlay = (colors) => { if (is_mac) { return false } else { return colors } } const getLogFileHint = () => { try { if (pinokiod && pinokiod.kernel && pinokiod.kernel.homedir) { return path.resolve(pinokiod.kernel.homedir, "logs", "stdout.txt") } } catch (err) { } return path.resolve(os.homedir(), ".pinokio", "logs", "stdout.txt") } const getSplashIcon = () => { if (splashIcon) { return splashIcon } const candidates = [ path.join('assets', 'icon.png'), path.join('assets', 'icon_small@2x.png'), path.join('assets', 'icon_small.png'), 'icon2.png' ] for (const relative of candidates) { const absolute = path.join(__dirname, relative) if (fs.existsSync(absolute)) { splashIcon = relative.split(path.sep).join('/') return splashIcon } } splashIcon = path.join('assets', 'icon_small.png').split(path.sep).join('/') return splashIcon } const ensureSplashWindow = () => { if (splashWindow && !splashWindow.isDestroyed()) { return splashWindow } splashWindow = new BrowserWindow({ width: 420, height: 320, frame: false, resizable: false, transparent: true, show: false, alwaysOnTop: true, skipTaskbar: true, fullscreenable: false, webPreferences: { spellcheck: false, backgroundThrottling: false } }) splashWindow.on('closed', () => { splashWindow = null }) return splashWindow } const updateSplashWindow = ({ state = 'loading', message, detail, logPath, icon } = {}) => { const win = ensureSplashWindow() const query = { state } if (message) { query.message = message } if (detail) { const trimmed = detail.length > 800 ? `${detail.slice(0, 800)}…` : detail query.detail = trimmed } if (logPath) { query.log = logPath } if (icon) { query.icon = icon } win.loadFile(path.join(__dirname, 'splash.html'), { query }).finally(() => { if (!win.isDestroyed()) { win.show() } }) } const closeSplashWindow = () => { if (splashWindow && !splashWindow.isDestroyed()) { splashWindow.close() } } const showStartupError = ({ message, detail, error } = {}) => { const formatted = detail || formatStartupError(error) updateSplashWindow({ state: 'error', message: message || 'Pinokio could not start', detail: formatted, logPath: getLogFileHint(), icon: getSplashIcon() }) } const formatStartupError = (error) => { if (!error) { return '' } if (error.stack) { return `${error.message || 'Unknown error'}\n\n${error.stack}` } if (error.message) { return error.message } if (typeof error === 'string') { return error } try { return JSON.stringify(error, null, 2) } catch (err) { return String(error) } } const SESSION_COOKIE_TTL_DAYS = 90 const SESSION_COOKIE_TTL_SEC = SESSION_COOKIE_TTL_DAYS * 24 * 60 * 60 const SESSION_COOKIE_JAR_FILENAME = 'session-cookies.json' let sessionCookieSavePromise = null let isQuitting = false const getSessionCookieJarPath = () => path.join(app.getPath('userData'), SESSION_COOKIE_JAR_FILENAME) const buildCookieUrl = (cookie) => { if (!cookie || !cookie.domain) { return null } const host = cookie.domain.startsWith('.') ? cookie.domain.slice(1) : cookie.domain if (!host) { return null } const scheme = cookie.secure ? 'https://' : 'http://' const cookiePath = cookie.path && cookie.path.startsWith('/') ? cookie.path : '/' return `${scheme}${host}${cookiePath}` } const serializeSessionCookie = (cookie) => { const url = buildCookieUrl(cookie) if (!url || typeof cookie.name !== 'string') { return null } const entry = { url, name: cookie.name, value: typeof cookie.value === 'string' ? cookie.value : '', path: cookie.path && cookie.path.startsWith('/') ? cookie.path : '/', secure: !!cookie.secure, httpOnly: !!cookie.httpOnly } if (cookie.hostOnly !== true && cookie.domain) { entry.domain = cookie.domain } if (cookie.sameSite) { entry.sameSite = cookie.sameSite } if (cookie.priority) { entry.priority = cookie.priority } if (cookie.sameParty != null) { entry.sameParty = cookie.sameParty } if (cookie.sourceScheme) { entry.sourceScheme = cookie.sourceScheme } if (Number.isInteger(cookie.sourcePort)) { entry.sourcePort = cookie.sourcePort } return entry } const persistSessionCookies = () => { if (sessionCookieSavePromise) { return sessionCookieSavePromise } sessionCookieSavePromise = (async () => { try { const cookies = await session.defaultSession.cookies.get({}) const sessionCookies = cookies.filter((cookie) => cookie && cookie.session) const entries = sessionCookies.map(serializeSessionCookie).filter(Boolean) const jarPath = getSessionCookieJarPath() if (!entries.length) { await fs.promises.unlink(jarPath).catch((err) => { if (err && err.code !== 'ENOENT') { console.warn('[Session Cookies] Failed to remove jar', err) } }) return } const payload = { version: 1, savedAt: Date.now(), cookies: entries } await fs.promises.mkdir(path.dirname(jarPath), { recursive: true }).catch(() => {}) await fs.promises.writeFile(jarPath, JSON.stringify(payload), 'utf8') } catch (err) { console.warn('[Session Cookies] Failed to persist', err) } finally { sessionCookieSavePromise = null } })() return sessionCookieSavePromise } const restoreSessionCookies = async () => { const jarPath = getSessionCookieJarPath() let raw try { raw = await fs.promises.readFile(jarPath, 'utf8') } catch (err) { if (err && err.code !== 'ENOENT') { console.warn('[Session Cookies] Failed to read jar', err) } return } let data try { data = JSON.parse(raw) } catch (err) { console.warn('[Session Cookies] Failed to parse jar', err) return } const entries = Array.isArray(data.cookies) ? data.cookies : [] if (!entries.length) { return } const expirationDate = Math.floor(Date.now() / 1000) + SESSION_COOKIE_TTL_SEC for (const entry of entries) { if (!entry || !entry.url || !entry.name) { continue } const details = { url: entry.url, name: entry.name, value: typeof entry.value === 'string' ? entry.value : '', path: entry.path || '/', secure: !!entry.secure, httpOnly: !!entry.httpOnly, expirationDate } if (entry.domain) { details.domain = entry.domain } if (entry.sameSite) { details.sameSite = entry.sameSite } if (entry.priority) { details.priority = entry.priority } if (entry.sameParty != null) { details.sameParty = entry.sameParty } if (entry.sourceScheme) { details.sourceScheme = entry.sourceScheme } if (Number.isInteger(entry.sourcePort)) { details.sourcePort = entry.sourcePort } try { await session.defaultSession.cookies.set(details) } catch (err) { console.warn('[Session Cookies] Failed to restore cookie', entry.name, err) } } } const clearPersistedSessionCookies = async () => { const jarPath = getSessionCookieJarPath() try { await fs.promises.unlink(jarPath) } catch (err) { if (err && err.code !== 'ENOENT') { console.warn('[Session Cookies] Failed to remove jar', err) } } } const clearSessionCaches = async () => { try { await session.defaultSession.clearCache() } catch (err) { console.warn('[Session Cache] Failed to clear http cache', err) } try { await session.defaultSession.clearStorageData({ storages: ['serviceworkers', 'cachestorage'] }) } catch (err) { console.warn('[Session Cache] Failed to clear service worker/cache storage', err) } } function UpsertKeyValue(obj, keyToChange, value) { const keyToChangeLower = keyToChange.toLowerCase(); for (const key of Object.keys(obj)) { if (key.toLowerCase() === keyToChangeLower) { // Reassign old key obj[key] = value; // Done return; } } // Insert at end instead obj[keyToChange] = value; } const clearBrowserConsoleState = (webContents) => { if (browserConsoleState.has(webContents)) { browserConsoleState.delete(webContents) } } const updateBrowserConsoleTarget = (webContents, url) => { if (!ENABLE_BROWSER_CONSOLE_LOG) { return } if (!root_url) { clearBrowserConsoleState(webContents) return } let parsed try { parsed = new URL(url) } catch (e) { clearBrowserConsoleState(webContents) return } if (parsed.origin !== root_url) { clearBrowserConsoleState(webContents) return } const existing = browserConsoleState.get(webContents) if (existing && existing.url === parsed.href) { return } browserConsoleState.set(webContents, { url: parsed.href }) } const inspectorSessions = new Map() let inspectorHandlersInstalled = false const inspectorLogFile = path.join(os.tmpdir(), 'pinokio-inspector.log') const inspectorMainLog = (label, payload) => { try { const serialized = payload === undefined ? '' : ' ' + JSON.stringify(payload) const line = `[InspectorMain] ${label}${serialized}\n` try { fs.appendFileSync(inspectorLogFile, line) } catch (_) {} process.stdout.write(line) } catch (_) { try { fs.appendFileSync(inspectorLogFile, `[InspectorMain] ${label}\n`) } catch (_) {} process.stdout.write(`[InspectorMain] ${label}\n`) } } const normalizeInspectorUrl = (value) => { if (!value) { return null } try { return new URL(value).href } catch (_) { return value } } const urlsRoughlyMatch = (expected, candidate) => { if (!expected) { return true } if (!candidate) { return false } if (candidate === expected) { return true } return candidate.startsWith(expected) || expected.startsWith(candidate) } const flattenFrameTree = (frame, acc = [], depth = 0) => { if (!frame) { return acc } let frameName = null try { frameName = typeof frame.name === 'string' && frame.name.length ? frame.name : null } catch (_) { frameName = null } acc.push({ frame, depth, url: normalizeInspectorUrl(frame.url || ''), name: frameName }) const children = Array.isArray(frame.frames) ? frame.frames : [] for (const child of children) { flattenFrameTree(child, acc, depth + 1) } return acc } const findDescendantByUrl = (frame, targetUrl) => { if (!frame || !targetUrl) { return null } const normalizedTarget = normalizeInspectorUrl(targetUrl) if (!normalizedTarget) { return null } const stack = [frame] while (stack.length) { const current = stack.pop() try { const currentUrl = normalizeInspectorUrl(current.url || '') if (currentUrl && urlsRoughlyMatch(normalizedTarget, currentUrl)) { return current } } catch (_) {} const children = Array.isArray(current.frames) ? current.frames : [] for (const child of children) { if (child) { stack.push(child) } } } return null } const selectTargetFrame = (webContents, payload = {}) => { if (!webContents || !webContents.mainFrame) { inspectorMainLog('no-webcontents', {}) return null } const frames = flattenFrameTree(webContents.mainFrame, []) if (!frames.length) { inspectorMainLog('no-frames', { webContentsId: webContents.id }) return null } inspectorMainLog('incoming', { frameUrl: payload.frameUrl || null, frameName: payload.frameName || null, frameNodeId: payload.frameNodeId || null, frameCount: frames.length, }) const canonicalUrl = normalizeInspectorUrl(payload.frameUrl) const relativeOrdinal = typeof payload.candidateRelativeOrdinal === 'number' ? payload.candidateRelativeOrdinal : null const globalOrdinal = typeof payload.frameIndex === 'number' ? payload.frameIndex : null const canonicalFrameName = typeof payload.frameName === 'string' && payload.frameName.trim() ? payload.frameName.trim() : null const canonicalFrameNodeId = typeof payload.frameNodeId === 'string' && payload.frameNodeId.trim() ? payload.frameNodeId.trim() : null if (canonicalFrameName || canonicalFrameNodeId) { inspectorMainLog('identifier-search', { frameName: canonicalFrameName || null, frameNodeId: canonicalFrameNodeId || null, names: frames.map((entry) => entry.name || null).slice(0, 12), }) let identifierMatch = null if (canonicalFrameNodeId) { identifierMatch = frames.find((entry) => entry && entry.name === canonicalFrameNodeId) || null if (identifierMatch) { const normalizedUrl = normalizeInspectorUrl(identifierMatch.url || '') if (canonicalUrl && (!normalizedUrl || !urlsRoughlyMatch(canonicalUrl, normalizedUrl))) { const descendant = findDescendantByUrl(identifierMatch.frame, canonicalUrl) if (descendant) { inspectorMainLog('identifier-match-node-descendant', { index: frames.indexOf(identifierMatch), name: identifierMatch.name || null, url: identifierMatch.url || null, descendantUrl: normalizeInspectorUrl(descendant.url || ''), }) return descendant } } inspectorMainLog('identifier-match-node', { index: frames.indexOf(identifierMatch), name: identifierMatch.name || null, url: identifierMatch.url || null, }) return identifierMatch.frame } } if (canonicalFrameName) { identifierMatch = frames.find((entry) => entry && entry.name === canonicalFrameName) || null if (identifierMatch) { const normalizedUrl = normalizeInspectorUrl(identifierMatch.url || '') if (canonicalUrl && (!normalizedUrl || !urlsRoughlyMatch(canonicalUrl, normalizedUrl))) { const descendant = findDescendantByUrl(identifierMatch.frame, canonicalUrl) if (descendant) { inspectorMainLog('identifier-match-name-descendant', { index: frames.indexOf(identifierMatch), name: identifierMatch.name || null, url: identifierMatch.url || null, descendantUrl: normalizeInspectorUrl(descendant.url || ''), }) return descendant } } inspectorMainLog('identifier-match-name', { index: frames.indexOf(identifierMatch), name: identifierMatch.name || null, url: identifierMatch.url || null, }) return identifierMatch.frame } } inspectorMainLog('identifier-miss', {}) } let matches = frames if (canonicalUrl) { matches = frames.filter(({ url }) => urlsRoughlyMatch(canonicalUrl, url)) } if (matches.length) { if (relativeOrdinal !== null) { const filtered = matches.slice().sort((a, b) => a.depth - b.depth || frames.indexOf(a) - frames.indexOf(b)) const targetEntry = filtered[Math.min(Math.max(relativeOrdinal, 0), filtered.length - 1)] if (targetEntry) { inspectorMainLog('relative-ordinal-match', { index: frames.indexOf(targetEntry), name: targetEntry.name || null, url: targetEntry.url || null, }) return targetEntry.frame } } const fallbackEntry = matches[0] if (fallbackEntry) { inspectorMainLog('fallback-match', { index: frames.indexOf(fallbackEntry), name: fallbackEntry.name || null, url: fallbackEntry.url || null, }) return fallbackEntry.frame } } if (globalOrdinal !== null && frames[globalOrdinal]) { inspectorMainLog('global-ordinal-match', { index: globalOrdinal, name: frames[globalOrdinal].name || null, url: frames[globalOrdinal].url || null, }) return frames[globalOrdinal].frame } inspectorMainLog('default-match', { name: frames[0]?.name || null, url: frames[0]?.url || null, }) return frames[0]?.frame || null } const buildInspectorInjection = () => { const source = function () { try { if (window.__PINOKIO_INSPECTOR__ && typeof window.__PINOKIO_INSPECTOR__.stop === 'function') { window.__PINOKIO_INSPECTOR__.stop() } const overlay = document.createElement('div') overlay.style.position = 'fixed' overlay.style.pointerEvents = 'none' overlay.style.border = '2px solid rgba(77,163,255,0.9)' overlay.style.background = 'rgba(77,163,255,0.2)' overlay.style.boxShadow = '0 0 0 1px rgba(23,52,92,0.45)' overlay.style.zIndex = '2147483647' overlay.style.display = 'none' document.documentElement.appendChild(overlay) let active = true const post = (type, payload) => { try { window.parent.postMessage({ pinokioInspector: { type, frameUrl: window.location.href, ...payload } }, '*') } catch (err) { // ignore } } const updateBox = (target) => { if (!active || !target) { overlay.style.display = 'none' return } const rect = target.getBoundingClientRect() if (!rect || rect.width <= 0 || rect.height <= 0) { overlay.style.display = 'none' return } overlay.style.display = 'block' overlay.style.left = `${rect.left}px` overlay.style.top = `${rect.top}px` overlay.style.width = `${rect.width}px` overlay.style.height = `${rect.height}px` } const buildPathKeys = (node) => { if (!node) { return [] } const keys = [] let current = node let depth = 0 while (current && current.nodeType === Node.ELEMENT_NODE && depth < 8) { const tag = current.tagName ? current.tagName.toLowerCase() : 'element' let descriptor = tag if (current.id) { descriptor += `#${current.id}` } else if (current.classList && current.classList.length) { descriptor += `.${Array.from(current.classList).slice(0, 2).join('.')}` } keys.push(descriptor) current = current.parentElement depth += 1 } return keys.reverse() } const handleMove = (event) => { if (!active) { return } const target = event.target updateBox(target) post('update', { nodeName: target && target.tagName ? target.tagName.toLowerCase() : '', pathKeys: buildPathKeys(target), }) } const preventClick = (event) => { if (!active) { return } event.preventDefault() event.stopPropagation() } const handleClick = async (event) => { if (!active) { return } event.preventDefault() event.stopPropagation() const target = event.target const html = target && target.outerHTML ? target.outerHTML : '' let screenshot = null // Hide the overlay before taking screenshot to avoid capturing it if (overlay && overlay.style) { overlay.style.display = 'none' } // Small delay to ensure overlay is hidden before screenshot await new Promise(resolve => setTimeout(resolve, 50)) try { // Use html2canvas-like approach to capture actual element rendering const rect = target.getBoundingClientRect() // Send element bounds for screenshot capture const screenshotRequest = { type: 'screenshot', bounds: { x: Math.round(rect.left), y: Math.round(rect.top), width: Math.max(1, Math.round(rect.width)), height: Math.max(1, Math.round(rect.height)) }, devicePixelRatio: window.devicePixelRatio || 1, frameUrl: window.location.href, __pinokioRelayStage: 0, __pinokioRelayComplete: window === window.top } // Post screenshot request via postMessage to main page try { console.log('Attempting screenshot capture...') console.log('electronAPI available in iframe:', !!window.electronAPI) console.log('Screenshot request:', screenshotRequest) // Send screenshot request to parent page via postMessage const response = await new Promise((resolve, reject) => { const messageId = 'screenshot_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9) const handleResponse = (event) => { if (event.data && event.data.pinokioScreenshotResponse && event.data.messageId === messageId) { window.removeEventListener('message', handleResponse) if (event.data.success) { resolve(event.data.screenshot) } else { reject(new Error(event.data.error || 'Screenshot failed')) } } } window.addEventListener('message', handleResponse) // Send request to parent page window.parent.postMessage({ pinokioScreenshotRequest: screenshotRequest, messageId: messageId }, '*') // Timeout after 3 seconds setTimeout(() => { window.removeEventListener('message', handleResponse) reject(new Error('Screenshot timeout')) }, 3000) }) screenshot = response console.log('Screenshot captured successfully via parent page') } catch (screenshotError) { console.error('Screenshot capture failed:', screenshotError) screenshot = null } } catch (error) { console.warn('Screenshot capture failed:', error) screenshot = null } post('complete', { outerHTML: html, pathKeys: buildPathKeys(target), screenshot: screenshot }) stop() } const handleKey = (event) => { if (!active) { return } if (event.key === 'Escape') { post('cancelled', {}) stop() } } const stop = () => { if (!active) { return } active = false document.removeEventListener('mousemove', handleMove, true) document.removeEventListener('mouseover', handleMove, true) document.removeEventListener('mousedown', preventClick, true) document.removeEventListener('click', handleClick, true) window.removeEventListener('keydown', handleKey, true) if (overlay.parentNode) { overlay.parentNode.removeChild(overlay) } window.__PINOKIO_INSPECTOR__ = null } document.addEventListener('mousemove', handleMove, true) document.addEventListener('mouseover', handleMove, true) document.addEventListener('mousedown', preventClick, true) document.addEventListener('click', handleClick, true) window.addEventListener('keydown', handleKey, true) window.__PINOKIO_INSPECTOR__ = { stop, } post('started', {}) } catch (error) { try { window.parent.postMessage({ pinokioInspector: { type: 'error', frameUrl: window.location.href, message: error && error.message ? error.message : String(error) } }, '*') } catch (_) {} } } return `(${source.toString()})();` } const buildScreenshotRelayInjection = () => { const source = function () { try { if (window.__PINOKIO_SCREENSHOT_RELAY__) { return } window.__PINOKIO_SCREENSHOT_RELAY__ = true const pending = new Map() const EXPIRATION_MS = 5000 const rememberSource = (messageId, sourceWindow) => { if (!messageId || !sourceWindow) { return } pending.set(messageId, sourceWindow) setTimeout(() => { pending.delete(messageId) }, EXPIRATION_MS) } const safeStringify = (value) => { try { return JSON.stringify(value) } catch (_) { return '"[unserializable]"' } } const log = (label, payload) => { try { console.log('[Pinokio Screenshot Relay] ' + label + ' ' + safeStringify(payload)) } catch (_) { // ignore logging failures } } log('relay-installed', { href: window.location.href }) window.addEventListener('message', (event) => { const data = event && event.data log('message-event', { href: window.location.href, hasData: Boolean(data), messageId: data && data.messageId ? data.messageId : null, hasRequest: Boolean(data && data.pinokioScreenshotRequest), hasResponse: Boolean(data && data.pinokioScreenshotResponse) }) if (!data) { return } if (data.pinokioScreenshotRequest) { if (!event.source || event.source === window) { log('request-ignored-no-source', { href: window.location.href, messageId: data.messageId || null }) return } rememberSource(data.messageId, event.source) log('request-processing', { href: window.location.href, messageId: data.messageId || null, originalBounds: data.pinokioScreenshotRequest && data.pinokioScreenshotRequest.bounds ? data.pinokioScreenshotRequest.bounds : null, originalDevicePixelRatio: data.pinokioScreenshotRequest ? data.pinokioScreenshotRequest.devicePixelRatio : null }) let offsetX = 0 let offsetY = 0 let matchedFrame = false try { for (let index = 0; index < window.frames.length; index += 1) { const childWindow = window.frames[index] if (childWindow === event.source) { log('matching-window-frames', { href: window.location.href, messageId: data.messageId || null, frameIndex: index }) try { const frameElement = childWindow.frameElement if (frameElement) { const rect = frameElement.getBoundingClientRect() offsetX = rect ? rect.left || 0 : 0 offsetY = rect ? rect.top || 0 : 0 matchedFrame = true log('matched-window-frames', { href: window.location.href, messageId: data.messageId || null, frameIndex: index, rect: rect ? { left: rect.left, top: rect.top, width: rect.width, height: rect.height } : null }) break } } catch (error) { log('frame-element-access-error', { href: window.location.href, messageId: data.messageId || null, frameIndex: index, error: error && error.message ? error.message : String(error) }) } } } if (!matchedFrame) { const FRAME_SELECTOR = 'iframe, frame' const frames = document.querySelectorAll ? document.querySelectorAll(FRAME_SELECTOR) : [] log('matching-query-selector', { href: window.location.href, messageId: data.messageId || null, selector: FRAME_SELECTOR, count: frames ? frames.length : 0 }) for (const frameEl of frames) { if (!frameEl) { continue } try { if (frameEl.contentWindow === event.source) { const rect = frameEl.getBoundingClientRect() offsetX = rect ? rect.left || 0 : 0 offsetY = rect ? rect.top || 0 : 0 matchedFrame = true log('matched-query-selector', { href: window.location.href, messageId: data.messageId || null, selector: FRAME_SELECTOR, rect: rect ? { left: rect.left, top: rect.top, width: rect.width, height: rect.height } : null }) break } } catch (error) { log('query-selector-access-error', { href: window.location.href, messageId: data.messageId || null, selector: FRAME_SELECTOR, error: error && error.message ? error.message : String(error) }) } } } } catch (error) { log('frame-enumeration-error', { href: window.location.href, messageId: data.messageId || null, error: error && error.message ? error.message : String(error) }) } if (!matchedFrame) { log('frame-match-failed', { href: window.location.href, messageId: data.messageId || null, offsetX, offsetY }) } const request = data.pinokioScreenshotRequest || {} const originalBounds = request.bounds || {} const parentDpr = window.devicePixelRatio || 1 const currentDpr = request.devicePixelRatio && request.devicePixelRatio > 0 ? request.devicePixelRatio : 1 const nextStage = (typeof request.__pinokioRelayStage === 'number' ? request.__pinokioRelayStage : 0) + 1 request.__pinokioRelayStage = nextStage request.__pinokioRelayComplete = window.parent === window if (matchedFrame) { const adjustedBounds = { x: (originalBounds.x || 0) + offsetX, y: (originalBounds.y || 0) + offsetY, width: originalBounds.width || 0, height: originalBounds.height || 0, } request.bounds = adjustedBounds request.devicePixelRatio = Math.max(currentDpr, parentDpr) request.__pinokioAdjusted = true log('request-adjusted', { href: window.location.href, messageId: data.messageId || null, offsetX, offsetY, parentDpr, resultingBounds: adjustedBounds, originalBounds, resultingDevicePixelRatio: request.devicePixelRatio, relayStage: request.__pinokioRelayStage, relayComplete: request.__pinokioRelayComplete }) } else { log('request-forward-unadjusted', { href: window.location.href, messageId: data.messageId || null, relayStage: request.__pinokioRelayStage, relayComplete: request.__pinokioRelayComplete }) } data.pinokioScreenshotRequest = request log('request-forward', { href: window.location.href, messageId: data.messageId || null, matchedFrame, hasParent: Boolean(window.parent && window.parent !== window) }) if (window.parent && window.parent !== window) { window.parent.postMessage(data, '*') if (event && typeof event.stopImmediatePropagation === 'function') { event.stopImmediatePropagation() } return } const targetSource = event.source const messageId = data.messageId const captureRequest = data.pinokioScreenshotRequest log('top-level-capture', { href: window.location.href, messageId, relayStage: captureRequest.__pinokioRelayStage, relayComplete: captureRequest.__pinokioRelayComplete, adjustedFlag: captureRequest.__pinokioAdjusted, bounds: captureRequest.bounds || null }) const captureApi = window.electronAPI && typeof window.electronAPI.captureScreenshot === 'function' ? window.electronAPI.captureScreenshot : null if (!captureApi) { log('top-level-capture-missing-api', { href: window.location.href }) return } Promise.resolve() .then(() => captureApi(captureRequest)) .then((screenshot) => { log('top-level-capture-success', { href: window.location.href, messageId }) try { targetSource.postMessage({ pinokioScreenshotResponse: true, messageId, success: true, screenshot }, '*') } catch (error) { log('top-level-response-error', { href: window.location.href, messageId, error: error && error.message ? error.message : String(error) }) } }) .catch((error) => { log('top-level-capture-error', { href: window.location.href, messageId, error: error && error.message ? error.message : String(error) }) try { targetSource.postMessage({ pinokioScreenshotResponse: true, messageId, success: false, error: error && error.message ? error.message : String(error) }, '*') } catch (responseError) { log('top-level-response-error', { href: window.location.href, messageId, error: responseError && responseError.message ? responseError.message : String(responseError) }) } }) return } if (data.pinokioScreenshotResponse && data.messageId) { log('response-processing', { href: window.location.href, messageId: data.messageId }) const target = pending.get(data.messageId) if (target && target !== event.source) { pending.delete(data.messageId) try { log('response-forwarding-down', { href: window.location.href, messageId: data.messageId }) target.postMessage(data, '*') return } catch (error) { log('response-forwarding-error', { href: window.location.href, messageId: data.messageId, error: error && error.message ? error.message : String(error) }) } } log('response-forwarding-up', { href: window.location.href, messageId: data.messageId, hasParent: Boolean(window.parent && window.parent !== window) }) if (window.parent && window.parent !== window) { window.parent.postMessage(data, '*') } } }, true) } catch (error) { try { console.warn('[Pinokio Screenshot Relay] relay-install-error ' + (error && error.message ? error.message : String(error))) } catch (_) { // ignore logging failures } } } return `(${source.toString()})();` } const installScreenshotRelays = async (frame) => { if (!frame) { return } const topFrame = frame.top || frame const frames = flattenFrameTree(topFrame, []) for (const entry of frames) { const candidate = entry && entry.frame if (!candidate || candidate.isDestroyed && candidate.isDestroyed()) { continue } try { await candidate.executeJavaScript(buildScreenshotRelayInjection(), true) } catch (error) { console.warn('Screenshot relay injection failed:', error && error.message ? error.message : error) } } } const startInspectorSession = async (webContents, payload = {}) => { const existing = inspectorSessions.get(webContents.id) if (existing) { await stopInspectorSession(webContents) } const targetFrame = selectTargetFrame(webContents, payload) if (!targetFrame) { throw new Error('Unable to locate iframe to inspect.') } await installScreenshotRelays(targetFrame) await targetFrame.executeJavaScript(buildInspectorInjection(), true) const navigationHandler = () => { const resultPromise = stopInspectorSession(webContents) Promise.resolve(resultPromise).then((outcome) => { if (!webContents.isDestroyed()) { webContents.send('pinokio:inspector-cancelled', { frameUrl: (outcome && outcome.frameUrl) || targetFrame.url || payload.frameUrl || '' }) } }) } if (!webContents.isDestroyed()) { webContents.on('did-navigate', navigationHandler) webContents.on('did-navigate-in-page', navigationHandler) } inspectorSessions.set(webContents.id, { frame: targetFrame, navigationHandler, }) return { frameUrl: targetFrame.url || payload.frameUrl || '', } } const stopInspectorSession = async (webContents) => { const session = inspectorSessions.get(webContents.id) if (!session) { return { frameUrl: '' } } inspectorSessions.delete(webContents.id) if (session.navigationHandler && !webContents.isDestroyed()) { webContents.removeListener('did-navigate', session.navigationHandler) webContents.removeListener('did-navigate-in-page', session.navigationHandler) } const frameUrl = session.frame && session.frame.url ? session.frame.url : '' try { await session.frame.executeJavaScript('window.__PINOKIO_INSPECTOR__ && window.__PINOKIO_INSPECTOR__.stop()', true) } catch (_) {} return { frameUrl } } const safeCaptureStringify = (value) => { try { return JSON.stringify(value) } catch (_) { return '"[unserializable]"' } } const captureLog = (label, payload) => { try { console.log('[Pinokio Capture] ' + label + ' ' + safeCaptureStringify(payload)) } catch (_) { console.log('[Pinokio Capture] ' + label) } } const installInspectorHandlers = () => { console.log('Installing inspector handlers...') if (inspectorHandlersInstalled) { console.log('Inspector handlers already installed, skipping') return } inspectorHandlersInstalled = true console.log('Installing pinokio:capture-screenshot handler') ipcMain.handle('pinokio:start-inspector', async (event, payload = {}) => { try { const result = await startInspectorSession(event.sender, payload) event.sender.send('pinokio:inspector-started', { frameUrl: result.frameUrl }) return { ok: true } } catch (error) { const message = error && error.message ? error.message : 'Unable to start inspect mode.' event.sender.send('pinokio:inspector-error', { message }) throw new Error(message) } }) ipcMain.handle('pinokio:stop-inspector', async (event) => { try { const result = await stopInspectorSession(event.sender) event.sender.send('pinokio:inspector-cancelled', { frameUrl: result.frameUrl || '' }) return { ok: true } } catch (error) { const message = error && error.message ? error.message : 'Unable to stop inspect mode.' event.sender.send('pinokio:inspector-error', { message }) throw new Error(message) } }) ipcMain.handle('pinokio:capture-screenshot-debug', async (event, payload) => { const { screenshotRequest } = payload const emitDebug = (label, data) => { captureLog(label, data) try { event.sender.send('pinokio:capture-debug-log', { label, payload: data }) } catch (_) { // ignore renderer emit errors } } emitDebug('handler-invoked', { senderId: event && event.sender ? event.sender.id : null, hasRequest: Boolean(screenshotRequest), bounds: screenshotRequest && screenshotRequest.bounds ? { x: screenshotRequest.bounds.x, y: screenshotRequest.bounds.y, width: screenshotRequest.bounds.width, height: screenshotRequest.bounds.height, } : null, devicePixelRatio: screenshotRequest ? screenshotRequest.devicePixelRatio : null, adjustedFlag: Boolean(screenshotRequest && screenshotRequest.__pinokioAdjusted), relayStage: screenshotRequest && typeof screenshotRequest.__pinokioRelayStage !== 'undefined' ? screenshotRequest.__pinokioRelayStage : null, relayComplete: screenshotRequest && typeof screenshotRequest.__pinokioRelayComplete !== 'undefined' ? screenshotRequest.__pinokioRelayComplete : null, frameOffset: screenshotRequest && screenshotRequest.frameOffset ? { x: screenshotRequest.frameOffset.x, y: screenshotRequest.frameOffset.y, } : null }) if (!screenshotRequest || !screenshotRequest.bounds) { throw new Error('Invalid screenshot request') } // Get the inspector session to access the target frame const session = inspectorSessions.get(event.sender.id) if (!session || !session.frame) { throw new Error('No inspector session or frame found') } try { const bounds = screenshotRequest.bounds const dpr = screenshotRequest.devicePixelRatio || 1 const alreadyAdjusted = Boolean(screenshotRequest.__pinokioAdjusted) emitDebug('incoming-bounds', { senderId: event && event.sender ? event.sender.id : null, bounds, devicePixelRatio: dpr, alreadyAdjusted, relayStage: screenshotRequest && typeof screenshotRequest.__pinokioRelayStage !== 'undefined' ? screenshotRequest.__pinokioRelayStage : null, relayComplete: screenshotRequest && typeof screenshotRequest.__pinokioRelayComplete !== 'undefined' ? screenshotRequest.__pinokioRelayComplete : null }) let framePosition = { x: 0, y: 0 } if (!alreadyAdjusted) { try { framePosition = await session.frame.executeJavaScript(` (function() { let x = 0, y = 0; let currentWindow = window; while (currentWindow !== window.top) { try { const frameElement = currentWindow.frameElement; if (frameElement) { const rect = frameElement.getBoundingClientRect(); x += rect.left; y += rect.top; } } catch (error) { return { x, y, crossOriginBlocked: true }; } currentWindow = currentWindow.parent; } return { x, y }; })(); `) if (framePosition && framePosition.crossOriginBlocked) { framePosition = { x: framePosition.x || 0, y: framePosition.y || 0 } } } catch (error) { console.warn('Unable to determine frame offset via DOM script:', error) framePosition = { x: 0, y: 0 } emitDebug('frame-position-fallback', { senderId: event && event.sender ? event.sender.id : null, error: error && error.message ? error.message : String(error) }) } } emitDebug('frame-position-computed', { senderId: event && event.sender ? event.sender.id : null, alreadyAdjusted, framePosition, bounds, devicePixelRatio: dpr, relayStage: screenshotRequest && typeof screenshotRequest.__pinokioRelayStage !== 'undefined' ? screenshotRequest.__pinokioRelayStage : null, relayComplete: screenshotRequest && typeof screenshotRequest.__pinokioRelayComplete !== 'undefined' ? screenshotRequest.__pinokioRelayComplete : null }) // Capture full page and crop to element bounds const fullImage = await event.sender.capturePage() const fullSize = fullImage.getSize() emitDebug('capture-page-size', { senderId: event && event.sender ? event.sender.id : null, fullSize }) // Calculate crop bounds with frame position and device pixel ratio const cropBounds = { x: Math.round((bounds.x + framePosition.x) * dpr), y: Math.round((bounds.y + framePosition.y) * dpr), width: Math.round(bounds.width * dpr), height: Math.round(bounds.height * dpr) } // Validate crop bounds cropBounds.x = Math.max(0, Math.min(cropBounds.x, fullSize.width - 1)) cropBounds.y = Math.max(0, Math.min(cropBounds.y, fullSize.height - 1)) cropBounds.width = Math.min(cropBounds.width, fullSize.width - cropBounds.x) cropBounds.height = Math.min(cropBounds.height, fullSize.height - cropBounds.y) emitDebug('crop-bounds', { senderId: event && event.sender ? event.sender.id : null, framePosition, dpr, validatedCropBounds: cropBounds, fullSize }) const croppedImage = fullImage.crop(cropBounds) const buffer = croppedImage.toPNG() emitDebug('capture-success', { senderId: event && event.sender ? event.sender.id : null, cropWidth: cropBounds.width, cropHeight: cropBounds.height }) return 'data:image/png;base64,' + buffer.toString('base64') } catch (error) { console.error('Screenshot capture failed:', error) emitDebug('capture-error', { senderId: event && event.sender ? event.sender.id : null, error: error && error.message ? error.message : String(error) }) throw error } }) } const getFrameInjectorKey = (frame) => { if (!frame) { return '' } if (typeof frame.frameTreeNodeId === 'number') { return `frame:${frame.frameTreeNodeId}` } const processId = typeof frame.processId === 'number' ? frame.processId : 'unknown' const token = typeof frame.frameToken === 'string' && frame.frameToken ? frame.frameToken : String(typeof frame.routingId === 'number' ? frame.routingId : 'unknown') return `${processId}:${token}` } const getPinokioInjectWebContentsKey = (sender, frame = null) => { if (sender && typeof sender.id === 'number') { return `wc:${sender.id}` } if (frame && frame.hostWebContents && typeof frame.hostWebContents.id === 'number') { return `wc:${frame.hostWebContents.id}` } return '' } const serializeForJavaScript = (value) => JSON.stringify(value) .replace(/\u2028/g, '\\u2028') .replace(/\u2029/g, '\\u2029') const normalizePinokioInjectDescriptor = (descriptor) => { if (!descriptor || typeof descriptor !== 'object' || Array.isArray(descriptor)) { return null } const src = typeof descriptor.src === 'string' ? descriptor.src.trim() : '' if (!src) { return null } const match = Array.isArray(descriptor.match) && descriptor.match.length ? descriptor.match.filter((item) => typeof item === 'string' && item.trim()) : ['*'] const world = typeof descriptor.world === 'string' && descriptor.world.trim().toLowerCase() === 'isolated' ? 'isolated' : 'main' const whenValue = typeof descriptor.when === 'string' ? descriptor.when.trim().toLowerCase() : '' const when = (whenValue === 'start' || whenValue === 'end') ? whenValue : 'idle' const frameValue = typeof descriptor.frame === 'string' ? descriptor.frame.trim().toLowerCase() : '' const frame = frameValue === 'all' ? 'all' : 'self' return { src, match, world, when, frame } } const normalizePinokioInjectTargetRegistrations = (targets) => { const values = Array.isArray(targets) ? targets : [] const normalized = [] for (const target of values) { if (!target || typeof target !== 'object' || Array.isArray(target)) { continue } const name = typeof target.name === 'string' ? target.name.trim() : '' const src = normalizeInspectorUrl(typeof target.src === 'string' ? target.src.trim() : '') if (!name && !src) { continue } normalized.push({ name, src, inject: Array.isArray(target.inject) ? target.inject.map((entry) => normalizePinokioInjectDescriptor(entry)).filter(Boolean) : [] }) } return normalized } const findFramePath = (frame, target, trail = []) => { if (!frame || !target) { return null } const nextTrail = trail.concat(frame) if (frame === target) { return nextTrail } const children = Array.isArray(frame.frames) ? frame.frames : [] for (const child of children) { const result = findFramePath(child, target, nextTrail) if (result) { return result } } return null } const resolvePinokioRelativeMatchTarget = (href) => { try { const parsed = new URL(href) return `${parsed.pathname}${parsed.search}${parsed.hash}` || '/' } catch (_) { return href || '' } } const escapePinokioPattern = (value) => String(value || '').replace(/[|\\{}()[\]^$+?.]/g, '\\$&') const pinokioPatternToExpression = (value) => { const input = String(value || '') let expression = '' for (let index = 0; index < input.length; index += 1) { const char = input[index] if (char === '*') { while (input[index + 1] === '*') { index += 1 } expression += '.*' continue } expression += escapePinokioPattern(char) } return `^${expression}$` } const matchesPinokioInjectPattern = (pattern, currentUrl) => { if (typeof pattern !== 'string') { return false } const normalizedPattern = pattern.trim() if (!normalizedPattern) { return false } const sourceValue = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(normalizedPattern) ? currentUrl : resolvePinokioRelativeMatchTarget(currentUrl) const expression = pinokioPatternToExpression(normalizedPattern) try { return new RegExp(expression).test(sourceValue) } catch (_) { return false } } const matchPinokioInjectTargetToFrame = (targets, frame, hints = {}) => { if (!Array.isArray(targets) || !targets.length) { return null } const frameName = (frame && typeof frame.name === 'string' ? frame.name.trim() : '') || (typeof hints.frameName === 'string' ? hints.frameName.trim() : '') const frameUrl = normalizeInspectorUrl((frame && frame.url) || '') || normalizeInspectorUrl(typeof hints.frameUrl === 'string' ? hints.frameUrl.trim() : '') let matched = null if (frameName) { matched = targets.find((entry) => entry.name && entry.name === frameName && (!entry.src || urlsRoughlyMatch(entry.src, frameUrl))) || targets.find((entry) => entry.name && entry.name === frameName) } if (!matched && frameUrl) { matched = targets.find((entry) => entry.src && urlsRoughlyMatch(entry.src, frameUrl)) || null } return matched } const resolvePinokioInjectTargetMatch = ({ registry, frame, currentUrl, targetHints, descendantDepth = 0 }) => { if (!registry || !Array.isArray(registry.targets) || registry.targets.length === 0) { return null } const target = matchPinokioInjectTargetToFrame(registry.targets, frame, targetHints) if (!target) { return null } const inject = target.inject.filter((descriptor) => { if (descriptor && descriptor.frame !== 'all' && descendantDepth !== 0) { return false } const matches = Array.isArray(descriptor.match) && descriptor.match.length ? descriptor.match : ['*'] return matches.some((pattern) => matchesPinokioInjectPattern(pattern, currentUrl)) }) return { target, inject } } const resolvePinokioInjectorsForFrame = (frame, payload = {}, sender = null) => { if (!frame) { return { inject: [], context: null } } const requestedContext = payload && payload.context && typeof payload.context === 'object' ? payload.context : {} const currentUrl = typeof requestedContext.currentUrl === 'string' && requestedContext.currentUrl.trim() ? requestedContext.currentUrl.trim() : (normalizeInspectorUrl(frame.url || '') || '') let ownerFrame = frame.parent || null let directChildFrame = frame let descendantDepth = 0 const targetHints = { frameName: typeof requestedContext.frameName === 'string' ? requestedContext.frameName.trim() : '', frameUrl: currentUrl } while (ownerFrame) { const ownerKey = getFrameInjectorKey(ownerFrame) const registry = frameInjectTargetRegistry.get(ownerKey) if (!registry || !Array.isArray(registry.targets) || registry.targets.length === 0) { directChildFrame = ownerFrame ownerFrame = ownerFrame.parent || null descendantDepth += 1 continue } const match = resolvePinokioInjectTargetMatch({ registry, frame: directChildFrame, currentUrl, targetHints, descendantDepth }) if (!match) { directChildFrame = ownerFrame ownerFrame = ownerFrame.parent || null descendantDepth += 1 continue } return { inject: match.inject, context: { frameUrl: normalizeInspectorUrl(ownerFrame.url || '') || '', rootFrameUrl: normalizeInspectorUrl(directChildFrame.url || '') || '', currentUrl, pageUrl: normalizeInspectorUrl(frame.url || '') || currentUrl } } } const webContentsKey = getPinokioInjectWebContentsKey(sender, frame) if (webContentsKey) { const registries = Array.from(frameInjectTargetRegistry.entries()) .map(([ownerKey, registry]) => ({ ownerKey, registry })) .filter(({ registry }) => registry && registry.webContentsKey === webContentsKey && Array.isArray(registry.targets) && registry.targets.length > 0) .sort((left, right) => (right.registry.updatedAt || 0) - (left.registry.updatedAt || 0)) for (const entry of registries) { const match = resolvePinokioInjectTargetMatch({ registry: entry.registry, frame, currentUrl, targetHints, descendantDepth: 0 }) if (!match) { continue } return { inject: match.inject, context: { frameUrl: entry.registry.pageUrl || '', rootFrameUrl: normalizeInspectorUrl(frame.url || '') || currentUrl, currentUrl, pageUrl: entry.registry.pageUrl || currentUrl } } } } return { inject: [], context: null } } const PINOKIO_ABSOLUTE_URL_PATTERN = /^[a-zA-Z][a-zA-Z\d+\-.]*:/ const resolvePinokioInjectSourceUrl = (value) => { if (typeof value !== 'string') { return '' } const trimmed = value.trim() if (!trimmed) { return '' } if (!PINOKIO_ABSOLUTE_URL_PATTERN.test(trimmed) && !trimmed.startsWith('/')) { return '' } const baseUrl = root_url || 'http://localhost' const parsed = safeParseUrl(trimmed, baseUrl) if (!parsed) { return '' } if (!['http:', 'https:', 'file:'].includes(parsed.protocol)) { return '' } return parsed.href } const buildPinokioInjectRuntimeBootstrap = () => { const source = function() { const resolveTargetWindow = () => { try { if (window.parent && window.parent !== window) { return window.parent } } catch (_) { } try { if (window.top && window.top !== window) { return window.top } } catch (_) { } return window } const ensureApi = () => { if (!window.$pinokio || typeof window.$pinokio !== 'object') { window.$pinokio = {} } if (typeof window.$pinokio.trigger !== 'function') { window.$pinokio.trigger = function(eventName, payload = {}, context = {}) { if (typeof eventName !== 'string' || !eventName.trim()) { return { ok: false, handled: false, reason: 'invalid_event_name' } } const nextContext = (context && typeof context === 'object') ? { ...context } : {} if (!nextContext.frameUrl) { nextContext.frameUrl = window.location.href } resolveTargetWindow().postMessage({ e: 'pinokio:event', event: eventName.trim(), payload: (payload && typeof payload === 'object') ? payload : {}, context: nextContext }, '*') return { ok: true, handled: true, event: eventName.trim() } } } window.$pinokio.inject = function(definition) { return window.__PINOKIO_INJECT_RUNTIME__.register(definition) } return window.$pinokio } const buildMountContext = (descriptor, sourceContext) => { const currentUrl = (window && window.location && window.location.href) ? window.location.href : '' const baseContext = (sourceContext && typeof sourceContext === 'object') ? { ...sourceContext } : {} if (!baseContext.frameUrl) { baseContext.frameUrl = currentUrl } if (!baseContext.currentUrl) { baseContext.currentUrl = currentUrl } if (!baseContext.rootFrameUrl) { baseContext.rootFrameUrl = currentUrl } return { ...baseContext, descriptor, trigger(eventName, payload = {}, context = {}) { const nextContext = (context && typeof context === 'object') ? { ...baseContext, ...context } : { ...baseContext } return window.$pinokio.trigger(eventName, payload, nextContext) } } } if (!window.__PINOKIO_INJECT_RUNTIME__) { const state = { current: null, cleanups: new Map() } window.__PINOKIO_INJECT_RUNTIME__ = { register(definition) { const current = state.current if (!current) { throw new Error('window.$pinokio.inject() must be called while an injector is loading.') } if (!definition || typeof definition !== 'object' || typeof definition.mount !== 'function') { throw new Error('Pinokio injectors must provide a mount(ctx) function.') } if (current.registered) { throw new Error('Injector registered more than once during a single mount.') } const cleanup = definition.mount(buildMountContext(current.descriptor, current.context)) current.registered = true if (typeof cleanup === 'function') { state.cleanups.set(current.descriptor.runtimeId, cleanup) } else { state.cleanups.delete(current.descriptor.runtimeId) } return { ok: true, id: current.descriptor.runtimeId || '' } }, run(descriptor, context, runSource) { ensureApi() state.current = { descriptor: descriptor || {}, context: (context && typeof context === 'object') ? context : {}, registered: false } try { if (typeof runSource === 'function') { runSource() } if (!state.current.registered) { throw new Error('Injector did not call window.$pinokio.inject(...).') } } finally { state.current = null } }, unmountAll() { for (const cleanup of state.cleanups.values()) { if (typeof cleanup !== 'function') { continue } try { cleanup() } catch (error) { try { console.warn('[pinokio][inject] cleanup failed', error && error.message ? error.message : String(error)) } catch (_) { } } } state.cleanups.clear() } } } ensureApi() } return `(${source.toString()})();` } const buildPinokioInjectUnmountScript = () => `(() => { const runtime = window.__PINOKIO_INJECT_RUNTIME__ if (runtime && typeof runtime.unmountAll === 'function') { runtime.unmountAll() } })();` const buildPinokioInjectExecution = ({ descriptor, context, source }) => { const bootstrap = buildPinokioInjectRuntimeBootstrap() return `(() => { ${bootstrap} window.__PINOKIO_INJECT_RUNTIME__.run(${serializeForJavaScript(descriptor)}, ${serializeForJavaScript(context || {})}, () => { ${source} }) })(); //# sourceURL=${descriptor.src}` } const resetPinokioInjectorsInFrame = async (frame) => { if (!frame || (typeof frame.isDestroyed === 'function' && frame.isDestroyed())) { return } const code = buildPinokioInjectUnmountScript() const tasks = [] if (typeof frame.executeJavaScript === 'function') { tasks.push(frame.executeJavaScript(code, false)) } if (typeof frame.executeJavaScriptInIsolatedWorld === 'function') { tasks.push(frame.executeJavaScriptInIsolatedWorld( PINOKIO_INJECT_ISOLATED_WORLD_ID, [{ code }], false )) } await Promise.allSettled(tasks) } const executePinokioInjectDescriptor = async (frame, descriptor, context) => { if (!frame || (typeof frame.isDestroyed === 'function' && frame.isDestroyed())) { throw new Error('Target frame is not available.') } const sourceDescriptor = descriptor const sourceUrl = resolvePinokioInjectSourceUrl(descriptor.src) if (!sourceUrl) { throw new Error(`Invalid injector source URL: ${descriptor.src}`) } const response = await fetch(sourceUrl, { cache: 'no-store' }) if (!response || !response.ok) { const status = response ? response.status : 'unknown' throw new Error(`Unable to load injector source: ${status}`) } const source = await response.text() const resolvedDescriptor = { ...sourceDescriptor, src: sourceUrl } const code = buildPinokioInjectExecution({ descriptor: resolvedDescriptor, context, source }) if (descriptor.world === 'isolated') { if (typeof frame.executeJavaScriptInIsolatedWorld !== 'function') { throw new Error('Isolated-world frame injection is not supported by this Electron frame API.') } return frame.executeJavaScriptInIsolatedWorld( PINOKIO_INJECT_ISOLATED_WORLD_ID, [{ code, url: sourceUrl }], false ) } return frame.executeJavaScript(code, false) } const installInjectorHandlers = () => { if (injectorHandlersInstalled) { return } injectorHandlersInstalled = true const updatePinokioInjectTargets = (ownerFrame, sender, payload = {}) => { if (!ownerFrame || (typeof ownerFrame.isDestroyed === 'function' && ownerFrame.isDestroyed())) { return { ok: false, reason: 'missing_frame', targets: [] } } const ownerKey = getFrameInjectorKey(ownerFrame) const webContentsKey = getPinokioInjectWebContentsKey(sender, ownerFrame) const targets = normalizePinokioInjectTargetRegistrations(payload && payload.targets) frameInjectTargetRegistry.set(ownerKey, { targets, pageUrl: payload && payload.pageUrl ? payload.pageUrl : '', webContentsKey, updatedAt: Date.now() }) return { ok: true, targets } } ipcMain.on('pinokio:update-inject-targets', (event, payload = {}) => { updatePinokioInjectTargets(event.senderFrame, event.sender, payload) }) ipcMain.on('pinokio:update-inject-targets-sync', (event, payload = {}) => { event.returnValue = updatePinokioInjectTargets(event.senderFrame, event.sender, payload) }) ipcMain.handle('pinokio:resolve-injectors', async (event, payload = {}) => { const frame = event.senderFrame if (!frame || (typeof frame.isDestroyed === 'function' && frame.isDestroyed())) { return { ok: false, reason: 'missing_frame', inject: [], context: null } } const resolved = resolvePinokioInjectorsForFrame(frame, payload, event.sender) return { ok: true, inject: resolved.inject, context: resolved.context } }) ipcMain.handle('pinokio:reset-injectors', async (event, payload = {}) => { const frame = event.senderFrame if (!frame || (typeof frame.isDestroyed === 'function' && frame.isDestroyed())) { return { ok: false, reason: 'missing_frame' } } const frameKey = getFrameInjectorKey(frame) const syncId = typeof payload.syncId === 'number' ? payload.syncId : 0 frameInjectorSyncState.set(frameKey, syncId) await resetPinokioInjectorsInFrame(frame) return { ok: true, syncId } }) ipcMain.handle('pinokio:mount-injectors', async (event, payload = {}) => { const frame = event.senderFrame if (!frame || (typeof frame.isDestroyed === 'function' && frame.isDestroyed())) { return { ok: false, reason: 'missing_frame', applied: [], failed: [] } } const frameKey = getFrameInjectorKey(frame) const syncId = typeof payload.syncId === 'number' ? payload.syncId : 0 if (syncId && frameInjectorSyncState.get(frameKey) !== syncId) { return { ok: true, skipped: true, reason: 'stale_sync', applied: [], failed: [], syncId } } const baseContext = payload && payload.context && typeof payload.context === 'object' ? { ...payload.context } : {} const injectList = Array.isArray(payload.inject) ? payload.inject : [] const applied = [] const failed = [] for (let index = 0; index < injectList.length; index += 1) { if (syncId && frameInjectorSyncState.get(frameKey) !== syncId) { return { ok: true, skipped: true, reason: 'stale_sync', applied, failed, syncId } } const normalizedDescriptor = normalizePinokioInjectDescriptor(injectList[index]) if (!normalizedDescriptor) { continue } const descriptor = { ...normalizedDescriptor, runtimeId: `${frameKey}:${syncId}:${index}:${normalizedDescriptor.src}` } try { await executePinokioInjectDescriptor(frame, descriptor, baseContext) applied.push({ src: descriptor.src, world: descriptor.world, runtimeId: descriptor.runtimeId }) } catch (error) { const message = error && error.message ? error.message : String(error) failed.push({ src: descriptor.src, world: descriptor.world, error: message }) console.warn('[pinokio][main] injector mount failed', { src: descriptor.src, world: descriptor.world, error: message }) } } return { ok: failed.length === 0, applied, failed, syncId } }) } const normalizePermissionList = (value) => { if (!value) return [] const list = Array.isArray(value) ? value : [value] return list.map((item) => typeof item === 'string' ? item.trim() : '').filter(Boolean) } const permissionLabels = { microphone: 'Microphone', camera: 'Camera', screen: 'Screen Recording', screen_capture: 'Screen Recording' } const logPermission = (...args) => { console.log('[PERMISSION]', ...args) } const permissionHints = { darwin: { microphone: 'System Settings → Privacy & Security → Microphone', camera: 'System Settings → Privacy & Security → Camera', screen: 'System Settings → Privacy & Security → Screen Recording', screen_capture: 'System Settings → Privacy & Security → Screen Recording' }, win32: { microphone: 'Settings → Privacy & security → Microphone (allow desktop apps)', camera: 'Settings → Privacy & security → Camera (allow desktop apps)', screen: 'Settings → Privacy & security → Screen recording', screen_capture: 'Settings → Privacy & security → Screen recording' }, linux: { microphone: 'Check your sound settings (PipeWire/PulseAudio) and app permissions.', camera: 'Check your video device permissions in system settings.', screen: 'Check your desktop portal or compositor screen capture permissions.', screen_capture: 'Check your desktop portal or compositor screen capture permissions.' } } const getMediaAccessStatusSafe = (mediaType) => { if (!systemPreferences || typeof systemPreferences.getMediaAccessStatus !== 'function') { return 'unsupported' } try { return systemPreferences.getMediaAccessStatus(mediaType) } catch (_) { return 'unknown' } } const requestMediaPermission = async (permission) => { const platform = process.platform if (permission === 'microphone' || permission === 'camera') { const preStatus = getMediaAccessStatusSafe(permission) const canAsk = platform === 'darwin' && systemPreferences && typeof systemPreferences.askForMediaAccess === 'function' logPermission('requestMediaPermission', permission, { platform, preStatus, canAsk }) let granted = false if (platform === 'darwin' && systemPreferences && typeof systemPreferences.askForMediaAccess === 'function') { granted = await systemPreferences.askForMediaAccess(permission) } const status = getMediaAccessStatusSafe(permission) if (status === 'granted') { granted = true } logPermission('requestMediaPermission result', permission, { status, granted }) return { status, granted } } if (permission === 'screen' || permission === 'screen_capture') { const status = getMediaAccessStatusSafe('screen') logPermission('requestMediaPermission screen', permission, { status }) return { status, granted: status === 'granted' } } logPermission('requestMediaPermission unsupported', permission) return { status: 'unsupported', granted: false } } const buildPermissionMessage = (platform, denied) => { if (!denied.length) return '' const items = denied.map((permission) => permissionLabels[permission] || permission) const label = items.length === 1 ? items[0] : items.join(', ') const hints = permissionHints[platform] || permissionHints.linux const hint = denied.length === 1 ? (hints[denied[0]] || '') : '' if (hint) { return `Pinokio needs ${label} access. Enable it in ${hint}.` } return `Pinokio needs ${label} access. Please enable it in your OS privacy settings.` } const installPermissionHandlers = () => { if (permissionHandlersInstalled) { return } permissionHandlersInstalled = true ipcMain.handle('pinokio:request-permissions', async (event, payload = {}) => { const permissions = normalizePermissionList(payload.permissions) if (permissions.length === 0) { return { ok: true, permissions: [], results: {}, denied: [] } } const results = {} const denied = [] for (const permission of permissions) { const result = await requestMediaPermission(permission) results[permission] = result if (!result.granted) { denied.push(permission) } } return { ok: denied.length === 0, permissions, denied, results, platform: process.platform, message: denied.length ? buildPermissionMessage(process.platform, denied) : '' } }) } const canRequestPermission = (permission) => { if (process.platform !== 'darwin') { return false } return permission === 'microphone' || permission === 'camera' } const promptForProjectPermissions = async (webContents, project, permissions) => { if (!permissions.length) { return } const promptKey = `${project}:${permissions.join(',')}` if (permissionPromptInFlight.has(promptKey) || permissionPrompted.has(promptKey)) { logPermission('prompt skipped (already prompted)', { project, permissions }) return } logPermission('prompt start', { project, permissions }) const pending = [] const blocked = [] const statusInfo = [] for (const permission of permissions) { const statusTarget = (permission === 'screen' || permission === 'screen_capture') ? 'screen' : permission const status = getMediaAccessStatusSafe(statusTarget) if (status === 'granted') { statusInfo.push({ permission, status, action: 'skip' }) continue } if (status === 'denied') { blocked.push(permission) statusInfo.push({ permission, status, action: 'blocked' }) } else if (canRequestPermission(permission)) { pending.push(permission) statusInfo.push({ permission, status, action: 'pending' }) } else { blocked.push(permission) statusInfo.push({ permission, status, action: 'blocked' }) } } logPermission('prompt status', statusInfo) logPermission('prompt lists', { pending, blocked }) if (pending.length === 0 && blocked.length === 0) { return } permissionPromptInFlight.add(promptKey) try { const owner = webContents && !webContents.isDestroyed() ? BrowserWindow.fromWebContents(webContents) : null const denied = blocked.slice() if (pending.length > 0) { const label = pending.map((permission) => permissionLabels[permission] || permission).join(', ') const { response } = await dialog.showMessageBox(owner, { type: 'info', buttons: ['Allow', 'Not now'], defaultId: 0, cancelId: 1, title: 'Permission required', message: `Allow ${label} access?`, detail: `This app requests ${label} access. Click "Allow" to show the OS permission prompt.`, noLink: true }) logPermission('prompt response', { project, permissions: pending, response }) if (response === 0) { for (const permission of pending) { const result = await requestMediaPermission(permission) if (!result.granted) { denied.push(permission) } } } } if (denied.length > 0) { logPermission('prompt denied', { project, denied }) const message = buildPermissionMessage(process.platform, denied) if (message) { await dialog.showMessageBox(owner, { type: 'warning', buttons: ['OK'], defaultId: 0, message, noLink: true }) } } } finally { permissionPromptInFlight.delete(promptKey) permissionPrompted.add(promptKey) } } // Screenshot capture function for inspect mode const captureScreenshotRegion = async (bounds) => { try { const { nativeImage } = require('electron') // Get all displays to find the correct one const displays = screen.getAllDisplays() const primaryDisplay = screen.getPrimaryDisplay() // Get desktop capturer sources with full resolution const sources = await desktopCapturer.getSources({ types: ['screen'], thumbnailSize: { width: primaryDisplay.bounds.width * primaryDisplay.scaleFactor, height: primaryDisplay.bounds.height * primaryDisplay.scaleFactor } }) if (sources.length === 0) { throw new Error('No screen sources available') } // Find the screen source that matches our primary display let screenSource = sources[0] // fallback to first source // Try to find the exact screen source by name or use the first one for (const source of sources) { if (source.name.includes('Entire Screen') || source.name.includes('Screen 1')) { screenSource = source break } } // Get the full resolution screenshot from thumbnail const thumbnailImage = screenSource.thumbnail const fullScreenshotBuffer = thumbnailImage.toPNG() const fullScreenshot = nativeImage.createFromBuffer(fullScreenshotBuffer) // Calculate the actual pixel bounds accounting for device pixel ratio const scaleFactor = primaryDisplay.scaleFactor const actualBounds = { x: Math.max(0, Math.round(bounds.x * scaleFactor)), y: Math.max(0, Math.round(bounds.y * scaleFactor)), width: Math.min( Math.round(bounds.width * scaleFactor), fullScreenshot.getSize().width - Math.round(bounds.x * scaleFactor) ), height: Math.min( Math.round(bounds.height * scaleFactor), fullScreenshot.getSize().height - Math.round(bounds.y * scaleFactor) ) } // Ensure minimum size actualBounds.width = Math.max(1, actualBounds.width) actualBounds.height = Math.max(1, actualBounds.height) // Crop the screenshot to the element bounds const croppedImage = fullScreenshot.crop(actualBounds) // Convert to PNG buffer and then to data URL const croppedBuffer = croppedImage.toPNG() const dataUrl = 'data:image/png;base64,' + croppedBuffer.toString('base64') console.log(`Screenshot captured: ${actualBounds.width}x${actualBounds.height} at (${actualBounds.x},${actualBounds.y})`) return dataUrl } catch (error) { console.warn('Screenshot capture failed:', error) throw error } } //function enable_cors(win) { // win.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => { // details.requestHeaders['Origin'] = null; // details.headers['Origin'] = null; // callback({ requestHeaders: details.requestHeaders }) // }); //// win.webContents.session.webRequest.onBeforeSendHeaders( //// (details, callback) => { //// const { requestHeaders } = details; //// UpsertKeyValue(requestHeaders, 'Access-Control-Allow-Origin', ['*']); //// callback({ requestHeaders }); //// }, //// ); //// //// win.webContents.session.webRequest.onHeadersReceived((details, callback) => { //// const { responseHeaders } = details; //// UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Origin', ['*']); //// UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Headers', ['*']); //// callback({ //// responseHeaders, //// }); //// }); //} const pushContextMenuSeparator = (template) => { if (!template.length) { return } if (template[template.length - 1].type === 'separator') { return } template.push({ type: 'separator' }) } const buildBrowserContextMenuTemplate = (webContents, params = {}) => { const template = [] const linkURL = typeof params.linkURL === 'string' ? params.linkURL : '' const srcURL = typeof params.srcURL === 'string' ? params.srcURL : '' const selectionText = typeof params.selectionText === 'string' ? params.selectionText : '' const hasSelection = selectionText.trim().length > 0 const editFlags = params.editFlags || {} const isEditable = Boolean(params.isEditable) const hasMediaSource = typeof params.mediaType === 'string' && params.mediaType !== 'none' && srcURL const canSuggestSpelling = Array.isArray(params.dictionarySuggestions) && params.dictionarySuggestions.length > 0 const hasMisspelledWord = typeof params.misspelledWord === 'string' && params.misspelledWord.length > 0 const owner = webContents && !webContents.isDestroyed() ? webContents.getOwnerBrowserWindow() : null const canGoBack = Boolean(webContents && webContents.canGoBack && webContents.canGoBack()) const canGoForward = Boolean(webContents && webContents.canGoForward && webContents.canGoForward()) if (linkURL) { template.push({ label: 'Open Link in New Window', click: () => { try { if (typeof loadNewWindow === 'function' && PORT) { if (popupShellManager.isPinokioWindowUrl(linkURL, root_url)) { loadNewWindow(linkURL, PORT) } else { popupShellManager.openExternalWindow({ url: linkURL }) } return } } catch (error) { } shell.openExternal(linkURL).catch(() => {}) } }) template.push({ label: 'Open Link in Browser', click: () => { shell.openExternal(linkURL).catch(() => {}) } }) template.push({ label: 'Copy Link Address', click: () => clipboard.writeText(linkURL) }) pushContextMenuSeparator(template) } if (hasMediaSource) { template.push({ label: 'Open Media in Browser', click: () => { shell.openExternal(srcURL).catch(() => {}) } }) template.push({ label: 'Copy Media Address', click: () => clipboard.writeText(srcURL) }) pushContextMenuSeparator(template) } if (!isEditable) { template.push({ label: 'Back', enabled: canGoBack, click: () => { if (webContents && !webContents.isDestroyed() && webContents.canGoBack()) { webContents.goBack() } } }) template.push({ label: 'Forward', enabled: canGoForward, click: () => { if (webContents && !webContents.isDestroyed() && webContents.canGoForward()) { webContents.goForward() } } }) template.push({ label: 'Reload', click: () => { if (webContents && !webContents.isDestroyed()) { webContents.reload() } } }) pushContextMenuSeparator(template) } if (isEditable) { if (canSuggestSpelling && hasMisspelledWord) { for (const suggestion of params.dictionarySuggestions.slice(0, 5)) { template.push({ label: suggestion, click: () => { if (webContents && !webContents.isDestroyed()) { webContents.replaceMisspelling(suggestion) } } }) } template.push({ label: 'Add to Dictionary', click: () => { try { if (webContents && !webContents.isDestroyed() && webContents.session && typeof webContents.session.addWordToSpellCheckerDictionary === 'function') { webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord) } } catch (error) { } } }) pushContextMenuSeparator(template) } template.push({ role: 'undo', enabled: editFlags.canUndo !== false }) template.push({ role: 'redo', enabled: editFlags.canRedo !== false }) pushContextMenuSeparator(template) template.push({ role: 'cut', enabled: editFlags.canCut !== false }) template.push({ role: 'copy', enabled: editFlags.canCopy !== false }) template.push({ role: 'paste', enabled: editFlags.canPaste !== false }) template.push({ role: 'delete', enabled: editFlags.canDelete !== false }) pushContextMenuSeparator(template) template.push({ role: 'selectAll' }) } else { if (hasSelection) { template.push({ role: 'copy' }) } template.push({ role: 'selectAll' }) } pushContextMenuSeparator(template) template.push({ label: 'Inspect Element', click: () => { if (!webContents || webContents.isDestroyed()) { return } if (!webContents.isDevToolsOpened()) { webContents.openDevTools({ mode: 'detach' }) } const x = typeof params.x === 'number' ? params.x : null const y = typeof params.y === 'number' ? params.y : null if (x !== null && y !== null) { webContents.inspectElement(x, y) } } }) if (template.length && template[template.length - 1].type === 'separator') { template.pop() } if (owner && owner.isDestroyed()) { return [] } return template } const attach = (event, webContents) => { let wc = webContents if (ENABLE_BROWSER_CONSOLE_LOG && !attachedConsoleListeners.has(webContents)) { attachedConsoleListeners.add(webContents) webContents.on('console-message', (event, level, message, line, sourceId) => { if (!root_url) { return } const state = browserConsoleState.get(webContents) let pageUrl = state && state.url ? state.url : '' if (!pageUrl) { try { pageUrl = webContents.getURL() } catch (err) { pageUrl = '' } } if (!pageUrl || !pageUrl.startsWith(root_url)) { return } const targetFile = ensureBrowserLogFile() if (!targetFile) { return } const logUrl = resolveConsoleSourceUrl(sourceId, pageUrl) if (!logUrl || !shouldLogUrl(logUrl)) { return } const timestamp = new Date().toISOString() const levelLabel = consoleLevelLabels[level] || 'log' let location = '' if (sourceId) { location = ` (${sourceId}${line ? `:${line}` : ''})` } else if (line) { location = ` (:${line})` } const entry = `[${timestamp}]\t${logUrl}\t[${levelLabel}] ${message}${location}\n` browserLogBuffer.push(entry) if (browserLogBuffer.length > 100) { browserLogBuffer.shift() } browserLogWritePromise = browserLogWritePromise.then(() => fs.promises.writeFile(targetFile, browserLogBuffer.join(''))).catch((err) => { console.error('[BROWSER LOG] Failed to persist console output', err) }) }) webContents.once('destroyed', () => { clearBrowserConsoleState(webContents) }) } // Enable screen capture permissions for all webContents webContents.session.setPermissionRequestHandler((webContents, permission, callback) => { callback(true) //console.log(`[PERMISSION DEBUG] Permission requested: "${permission}" from webContents`) //if (permission === 'media' || permission === 'display-capture' || permission === 'desktopCapture') { // console.log(`[PERMISSION DEBUG] Granting permission: "${permission}"`) // callback(true) //} else { // console.log(`[PERMISSION DEBUG] Denying permission: "${permission}"`) // callback(false) //} }) webContents.session.setPermissionCheckHandler((webContents, permission) => { return true //console.log(`[PERMISSION DEBUG] Permission check for: "${permission}"`) //return permission === 'media' || permission === 'display-capture' || permission === 'desktopCapture' }) webContents.session.setDisplayMediaRequestHandler((request, callback) => { console.log('[DISPLAY MEDIA DEBUG] Display media request received') desktopCapturer.getSources({ types: ['screen', 'window'] }).then((sources) => { console.log('[DISPLAY MEDIA DEBUG] Available sources:', sources.length) if (sources.length > 0) { callback({ video: sources[0], audio: 'loopback' }) } else { callback({}) } }).catch(err => { console.error('[DISPLAY MEDIA DEBUG] Error getting sources:', err) callback({}) }) }) webContents.on('will-prevent-unload', (event) => { event.preventDefault() }) webContents.on('will-navigate', (event, url) => { if (!webContents.opened) { // The first time this view is being used, set the "opened" to true, and don't do anything // The next time the view navigates, "the "opened" is already true, so trigger the URL open logic // - if the new URL has the same host as the app's url, open in app // - if it's a remote host, open in external browser webContents.opened = true } else { // console.log("will-navigate", { event, url }) const owner = webContents.getOwnerBrowserWindow() if (openNonPinokioNavigationInPopup({ event, owner, url })) { return } const target = safeParseUrl(url, root_url || undefined) if (target && !popupShellManager.isPinokioWindowUrl(target.href, root_url) && target.protocol !== 'http:' && target.protocol !== 'https:') { event.preventDefault() shell.openExternal(target.href) } } }) webContents.on('will-frame-navigate', (event) => { const owner = webContents.getOwnerBrowserWindow() const frame = event && event.frame const isDirectChildFrame = Boolean( frame && webContents.mainFrame && frame.parent && !frame.parent.parent && frame.parent === webContents.mainFrame ) if (!isDirectChildFrame) { return } const currentUrl = (() => { try { return webContents.getURL() } catch (_) { return '' } })() if (!isRootShellUrl(currentUrl)) { return } openNonPinokioNavigationInPopup({ event, owner, url: event && event.url, frame }) }) // webContents.session.defaultSession.loadExtension('path/to/unpacked/extension').then(({ id }) => { // }) webContents.session.webRequest.onHeadersReceived((details, callback) => { // console.log("details", details) // console.log("responseHeaders", JSON.stringify(details.responseHeaders, null, 2)) // 1. Remove X-Frame-Options if (details.responseHeaders["X-Frame-Options"]) { delete details.responseHeaders["X-Frame-Options"] } else if (details.responseHeaders["x-frame-options"]) { delete details.responseHeaders["x-frame-options"] } // 2. Remove Content-Security-Policy "frame-ancestors" attribute let csp let csp_type; if (details.responseHeaders["Content-Security-Policy"]) { csp = details.responseHeaders["Content-Security-Policy"] csp_type = 0 } else if (details.responseHeaders['content-security-policy']) { csp = details.responseHeaders["content-security-policy"] csp_type = 1 } if (details.responseHeaders["cross-origin-opener-policy-report-only"]) { delete details.responseHeaders["cross-origin-opener-policy-report-only"] } else if (details.responseHeaders["Cross-Origin-Opener-Policy-Report-Only"]) { delete details.responseHeaders["Cross-Origin-Opener-Policy-Report-Only"] } if (csp) { // console.log("CSP", csp) // find /frame-ancestors ;$/ let new_csp = csp.map((c) => { return c.replaceAll(/frame-ancestors[^;]+;?/gi, "") }) // console.log("new_csp = ", new_csp) const r = { responseHeaders: details.responseHeaders } if (csp_type === 0) { r.responseHeaders["Content-Security-Policy"] = new_csp } else if (csp_type === 1) { r.responseHeaders["content-security-policy"] = new_csp } // console.log("R", JSON.stringify(r, null, 2)) callback(r) } else { // console.log("RH", details.responseHeaders) callback({ responseHeaders: details.responseHeaders }) } }) webContents.session.webRequest.onBeforeSendHeaders((details, callback) => { let ua = details.requestHeaders['User-Agent'] // console.log("User Agent Before", ua) if (ua) { ua = ua.replace(/ pinokio\/[0-9.]+/i, ''); ua = ua.replace(/Electron\/.+ /i,''); // console.log("User Agent After", ua) details.requestHeaders['User-Agent'] = ua; } // console.log("REQ", details) // console.log("HEADER BEFORE", details.requestHeaders) // // Remove all sec-fetch-* headers // for(let key in details.requestHeaders) { // if (key.toLowerCase().startsWith("sec-")) { // delete details.requestHeaders[key] // } // } // console.log("HEADER AFTER", details.requestHeaders) callback({ cancel: false, requestHeaders: details.requestHeaders }); }); // webContents.session.webRequest.onBeforeSendHeaders( // (details, callback) => { // const { requestHeaders } = details; // UpsertKeyValue(requestHeaders, 'Access-Control-Allow-Origin', ['*']); // callback({ requestHeaders }); // }, // ); // // webContents.session.webRequest.onHeadersReceived((details, callback) => { // const { responseHeaders } = details; // UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Origin', ['*']); // UpsertKeyValue(responseHeaders, 'Access-Control-Allow-Headers', ['*']); // callback({ // responseHeaders, // }); // }); // webContents.session.webRequest.onBeforeSendHeaders((details, callback) => { // //console.log("Before", { details }) // if (details.requestHeaders) details.requestHeaders['Origin'] = null; // if (details.requestHeaders) details.requestHeaders['Referer'] = null; // if (details.requestHeaders) details.requestHeaders['referer'] = null; // if (details.headers) details.headers['Origin'] = null; // if (details.headers) details.headers['Referer'] = null; // if (details.headers) details.headers['referer'] = null; // // if (details.referrer) details.referrer = null // //console.log("After", { details }) // callback({ requestHeaders: details.requestHeaders }) // }); // webContents.on("did-create-window", (parentWindow, details) => { // const view = new BrowserView(); // parentWindow.setBrowserView(view); // view.setBounds({ x: 0, y: 30, width: parentWindow.getContentBounds().width, height: parentWindow.getContentBounds().height - 30 }); // view.setAutoResize({ width: true, height: true }); // view.webContents.loadURL(details.url); // }) webContents.on('did-navigate', (event, url) => { let win = webContents.getOwnerBrowserWindow() if (win && typeof win.setTitleBarOverlay === "function") { const overlay = titleBarOverlay(colors) setWindowTitleBarOverlay(win, overlay) } launched = true updateBrowserConsoleTarget(webContents, url) }) webContents.on('did-navigate-in-page', (event, url) => { updateBrowserConsoleTarget(webContents, url) }) webContents.on('context-menu', (event, params) => { const template = buildBrowserContextMenuTemplate(webContents, params) if (!template.length) { return } const menu = Menu.buildFromTemplate(template) const win = webContents.getOwnerBrowserWindow() if (win && !win.isDestroyed()) { menu.popup({ window: win }) return } menu.popup() }) webContents.setWindowOpenHandler((config) => { let url = config.url let features = config.features || "" let disposition = config.disposition || "" let params = new URLSearchParams(features.split(",").join("&")) let win = wc.getOwnerBrowserWindow() let [width, height] = win.getSize() let [x,y] = win.getPosition() // if the origin is the same as the pinokio host, // always open in new window // if not, check the features // if features exists and it's app or self, open in pinokio // otherwise if it's file, if (/(^|,)\s*pinokio\s*(,|$)/i.test(features)) { const targetUrl = popupShellManager.resolveTargetUrl({ url, openerWebContents: wc, rootUrl: root_url }) if (targetUrl) { if (popupShellManager.isPinokioWindowUrl(targetUrl, root_url)) { loadNewWindow(targetUrl, PORT) } else { popupShellManager.openExternalWindow({ url: targetUrl }) } } return { action: 'deny' }; } if (features === "browser") { shell.openExternal(url); return { action: 'deny' }; } else if (disposition === "foreground-tab" || disposition === "background-tab") { const targetUrl = popupShellManager.resolveTargetUrl({ url, openerWebContents: wc, rootUrl: root_url }) if (targetUrl) { popupShellManager.openExternalWindow({ url: targetUrl }) } return { action: 'deny' }; } else if (popupShellManager.isPinokioWindowUrl(url, root_url)) { return { action: 'allow', outlivesOpener: true, overrideBrowserWindowOptions: { width: (params.get("width") ? parseInt(params.get("width")) : width), height: (params.get("height") ? parseInt(params.get("height")) : height), x: x + 30, y: y + 30, parent: null, titleBarStyle : "hidden", titleBarOverlay : titleBarOverlay(colors), webPreferences: { session: session.defaultSession, webSecurity: false, spellcheck: false, nativeWindowOpen: true, contextIsolation: false, nodeIntegrationInSubFrames: true, preload: path.join(__dirname, 'preload.js') }, } } } else { if (features.startsWith("app") || features.startsWith("self")) { return popupShellManager.createPopupResponse({ params, width, height, x, y }) } if (features.startsWith("file")) { let u = features.replace("file://", "") shell.showItemInFolder(u) return { action: 'deny' }; } const targetUrl = popupShellManager.resolveTargetUrl({ url, openerWebContents: wc, rootUrl: root_url }) if (targetUrl) { popupShellManager.openExternalWindow({ url: targetUrl }) } return { action: 'deny' }; } // if (origin === root_url) { // // if the origin is the same as pinokio, open in pinokio // // otherwise open in external browser // if (features) { // if (features.startsWith("app") || features.startsWith("self")) { // return { // action: 'allow', // outlivesOpener: true, // overrideBrowserWindowOptions: { // width: (params.get("width") ? parseInt(params.get("width")) : width), // height: (params.get("height") ? parseInt(params.get("height")) : height), // x: x + 30, // y: y + 30, // // parent: null, // titleBarStyle : "hidden", // titleBarOverlay : titleBarOverlay("default"), // } // } // } else if (features.startsWith("file")) { // let u = features.replace("file://", "") // shell.showItemInFolder(u) // return { action: 'deny' }; // } else { // return { action: 'deny' }; // } // } else { // if (features.startsWith("file")) { // let u = features.replace("file://", "") // shell.showItemInFolder(u) // return { action: 'deny' }; // } else { // shell.openExternal(url); // return { action: 'deny' }; // } // } // } else { // if (features.startsWith("file")) { // let u = features.replace("file://", "") // shell.showItemInFolder(u) // return { action: 'deny' }; // } else { // shell.openExternal(url); // return { action: 'deny' }; // } // } }); } const getWinState = (url, options) => { let filename try { let pathname = new URL(url).pathname.slice(1) filename = pathname.slice("/").join("-") } catch { filename = "index.json" } let state = windowStateKeeper({ file: filename, ...options }); return state } const createWindow = (port) => { let mainWindowState = windowStateKeeper({ // file: "index.json", defaultWidth: 1000, defaultHeight: 800 }); mainWindow = new BrowserWindow({ titleBarStyle : "hidden", titleBarOverlay : titleBarOverlay(colors), x: mainWindowState.x, y: mainWindowState.y, width: mainWindowState.width, height: mainWindowState.height, minWidth: 190, webPreferences: { session: session.defaultSession, webSecurity: false, spellcheck: false, nativeWindowOpen: true, contextIsolation: false, nodeIntegrationInSubFrames: true, enableRemoteModule: false, experimentalFeatures: true, preload: path.join(__dirname, 'preload.js') }, }) mainWindow.on('closed', () => { mainWindow = null }) // Debug media device availability mainWindow.webContents.once('did-finish-load', () => { console.log('[MEDIA DEBUG] Main window loaded, checking media devices availability...') mainWindow.webContents.executeJavaScript(` console.log('[MEDIA DEBUG] navigator.mediaDevices available:', !!navigator.mediaDevices); console.log('[MEDIA DEBUG] getDisplayMedia available:', !!(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia)); console.log('[MEDIA DEBUG] getUserMedia available:', !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)); if (navigator.mediaDevices && navigator.mediaDevices.getSupportedConstraints) { console.log('[MEDIA DEBUG] Supported constraints:', navigator.mediaDevices.getSupportedConstraints()); } `).catch(err => console.error('[MEDIA DEBUG] Error checking media devices:', err)) if (updateBannerPayload && !(updateBannerPayload.state === 'available' && updateBannerDismissed)) { mainWindow.webContents.send('pinokio:update-banner', updateBannerPayload) } }) // Enable screen capture permissions mainWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => { callback(true) //console.log(`[PERMISSION DEBUG] MainWindow permission requested: "${permission}"`) //if (permission === 'media' || permission === 'display-capture' || permission === 'desktopCapture') { // console.log(`[PERMISSION DEBUG] MainWindow granting permission: "${permission}"`) // callback(true) //} else { // console.log(`[PERMISSION DEBUG] MainWindow denying permission: "${permission}"`) // callback(false) //} }) // enable_cors(mainWindow) if("" + port === "80") { root_url = `http://localhost` } else { root_url = `http://localhost:${port}` } mainWindow.loadURL(root_url) // mainWindow.maximize(); mainWindowState.manage(mainWindow); } const loadNewWindow = (url, port) => { let winState = windowStateKeeper({ // file: "index.json", defaultWidth: 1000, defaultHeight: 800 }); let win = new BrowserWindow({ titleBarStyle : "hidden", titleBarOverlay : titleBarOverlay(colors), x: winState.x, y: winState.y, width: winState.width, height: winState.height, minWidth: 190, webPreferences: { session: session.defaultSession, webSecurity: false, spellcheck: false, nativeWindowOpen: true, contextIsolation: false, nodeIntegrationInSubFrames: true, enableRemoteModule: false, experimentalFeatures: true, preload: path.join(__dirname, 'preload.js') }, }) installForceDestroyOnClose(win) // Enable screen capture permissions win.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => { callback(true) //console.log(`[PERMISSION DEBUG] New window permission requested: "${permission}"`) //if (permission === 'media' || permission === 'display-capture' || permission === 'desktopCapture') { // console.log(`[PERMISSION DEBUG] New window granting permission: "${permission}"`) // callback(true) //} else { // console.log(`[PERMISSION DEBUG] New window denying permission: "${permission}"`) // callback(false) //} }) // enable_cors(win) win.focus() win.loadURL(url) winState.manage(win) } popupShellManager.setPinokioHomeWindowOpener(() => { if (root_url && PORT) { loadNewWindow(root_url, PORT) } }) if (process.defaultApp) { if (process.argv.length >= 2) { app.setAsDefaultProtocolClient('pinokio', process.execPath, [path.resolve(process.argv[1])]) } } else { app.setAsDefaultProtocolClient('pinokio') } const gotTheLock = app.requestSingleInstanceLock() if (!gotTheLock) { app.quit() } else { app.on('certificate-error', (event, webContents, url, error, certificate, callback) => { // Prevent having error event.preventDefault() // and continue callback(true) }) app.on('second-instance', (event, argv) => { if (!mainWindow || mainWindow.isDestroyed()) { createWindow(PORT) } else { if (mainWindow.isMinimized()) mainWindow.restore() mainWindow.focus() } const url = [...argv].reverse().find(arg => typeof arg === 'string' && arg.startsWith('pinokio:')) if (!url) { return } //let u = new URL(url).search let u = url.replace(/pinokio:[\/]+/, "") loadNewWindow(`${root_url}/pinokio/${u}`, PORT) // if (BrowserWindow.getAllWindows().length === 0 || !mainWindow) createWindow(PORT) // mainWindow.focus() // mainWindow.loadURL(`${root_url}/pinokio/${u}`) }) // Create mainWindow, load the rest of the app, etc... // Enable desktop capture for getDisplayMedia support (must be before app ready) app.commandLine.appendSwitch('disable-features', 'LazyImageLoading') app.commandLine.appendSwitch('enable-experimental-web-platform-features'); app.commandLine.appendSwitch('enable-features', 'GetDisplayMediaSet,GetDisplayMediaSetAutoSelectAllScreens'); app.whenReady().then(async () => { console.log('App is ready, about to install inspector handlers...') app.userAgentFallback = "Pinokio" installInspectorHandlers() installInjectorHandlers() installPermissionHandlers() installClosePopupOnDownload(session.defaultSession) installClosePopupOnDownload(popupShellManager.getPopupBrowserSession()) ipcMain.on('pinokio:update-banner-action', (_event, payload = {}) => { const action = payload && payload.action if (!action) { return } if (updateTestMode) { if (action === 'update') { startUpdateBannerTestDownload() return } if (action === 'restart') { simulateUpdateBannerRestart() return } if (action === 'dismiss') { updateBannerDismissed = true hideUpdateBanner() return } if (action === 'release-notes') { const target = payload && payload.releaseUrl ? payload.releaseUrl : UPDATE_RELEASES_URL shell.openExternal(target) return } } if (action === 'update') { if (updateDownloadInFlight) { return } updateDownloadInFlight = true updateBannerDismissed = false showUpdateBanner(buildUpdateBannerPayload('downloading', updateInfo, { progressPercent: 0 })) updater.downloadUpdate().catch((err) => { updateDownloadInFlight = false const message = err && err.message ? err.message : 'Update failed' showUpdateBanner(buildUpdateBannerPayload('error', updateInfo, { errorMessage: message })) }) return } if (action === 'restart') { updater.quitAndInstall() return } if (action === 'dismiss') { updateBannerDismissed = true hideUpdateBanner() return } if (action === 'release-notes') { const target = payload && payload.releaseUrl ? payload.releaseUrl : UPDATE_RELEASES_URL shell.openExternal(target) } }) // PROMPT let promptResponse ipcMain.on('prompt', function(eventRet, arg) { promptResponse = null const point = screen.getCursorScreenPoint() const display = screen.getDisplayNearestPoint(point) const bounds = display.bounds // const bounds = focused.getBounds() let promptWindow = new BrowserWindow({ x: bounds.x + bounds.width/2 - 200, y: bounds.y + bounds.height/2 - 60, width: 400, height: 120, //width: 1000, //height: 500, show: false, resizable: false, // movable: false, // alwaysOnTop: true, frame: false, webPreferences: { session: session.defaultSession, webSecurity: false, spellcheck: false, nativeWindowOpen: true, contextIsolation: false, nodeIntegrationInSubFrames: true, preload: path.join(__dirname, 'preload.js') }, }) arg.val = arg.val || '' const promptHtml = `
` // promptWindow.loadFile("prompt.html") promptWindow.loadURL('data:text/html,' + encodeURIComponent(promptHtml)) promptWindow.show() promptWindow.on('closed', function() { console.log({ promptResponse }) debugger eventRet.returnValue = promptResponse promptWindow = null }) }) ipcMain.on('prompt-response', function(event, arg) { if (arg === ''){ arg = null } console.log("prompt-response", { arg}) promptResponse = arg }) updateSplashWindow({ state: 'loading', message: 'Starting Pinokio…', icon: getSplashIcon() }) try { await restoreSessionCookies() await clearSessionCaches() try { const portInUse = await pinokiod.running(pinokiod.port) if (portInUse) { showStartupError({ message: 'Pinokio is already running', detail: `Pinokio detected another instance listening on port ${pinokiod.port}. Please close the other instance before launching a new one.` }) return } } catch (checkError) { console.warn('Failed to verify pinokio port availability', checkError) } await pinokiod.start({ onquit: () => { app.quit() }, onrestart: () => { persistSessionCookies().finally(() => { app.relaunch() app.exit() }) }, onrefresh: (payload) => { try { updateThemeColors(payload || { theme: pinokiod.theme, colors: pinokiod.colors }) } catch (err) { console.error('Failed to sync title bar theme', err) } }, browser: { clearCache: async () => { console.log('clear cache from all sessions') // Clear default session await session.defaultSession.clearStorageData() // Clear all custom sessions from active windows const windows = BrowserWindow.getAllWindows() for (const window of windows) { if (window.webContents && window.webContents.session) { await window.webContents.session.clearStorageData() } } await clearPersistedSessionCookies() console.log("cleared all sessions") }, requestPermissions: async (payload = {}) => { try { const project = typeof payload.name === 'string' ? payload.name.trim() : '' const permissions = normalizePermissionList(payload.permissions) logPermission('callback received', { project, permissions }) if (!project || permissions.length === 0) { logPermission('callback skipped (missing project or permissions)', { project, permissions }) return { ok: true, skipped: true } } const owner = BrowserWindow.getFocusedWindow() || mainWindow || BrowserWindow.getAllWindows()[0] || null const webContents = owner && owner.webContents ? owner.webContents : null if (!webContents || webContents.isDestroyed()) { logPermission('callback failed (no webContents)', { project, permissions }) return { ok: false, error: 'no-webcontents' } } await promptForProjectPermissions(webContents, project, permissions) return { ok: true } } catch (err) { console.error('[PERMISSION] Failed to prompt via callback', err) return { ok: false, error: err && err.message ? err.message : String(err) } } } } }) } catch (error) { console.error('Failed to start pinokiod', error) showStartupError({ error }) return } closeSplashWindow() PORT = pinokiod.port app.on('web-contents-created', attach) app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) createWindow(PORT) }) app.on('before-quit', function(e) { if (pinokiod.kernel.kill) { if (isQuitting) { return } e.preventDefault() isQuitting = true persistSessionCookies().finally(() => { console.log('Cleaning up before quit', process.pid) pinokiod.kernel.kill() }) } }); app.on('window-all-closed', function () { console.log("window-all-closed") if (process.platform !== 'darwin') { // Reset all shells before quitting pinokiod.kernel.shell.reset() // wait 1 second before quitting the app // otherwise the app.quit() fails because the subprocesses are running setTimeout(() => { console.log("app.quit()") app.quit() }, 1000) } }) app.on('browser-window-created', (event, win) => { const parentWindow = (win && typeof win.getParentWindow === 'function') ? win.getParentWindow() : null if (parentWindow && !parentWindow.isDestroyed()) installForceDestroyOnClose(win) if (win.type !== "splash") { if (win && typeof win.setTitleBarOverlay === 'function') { const overlay = titleBarOverlay(colors) setWindowTitleBarOverlay(win, overlay) } } }) app.on('open-url', (event, url) => { let u = url.replace(/pinokio:[\/]+/, "") // let u = new URL(url).search // console.log("u", u) loadNewWindow(`${root_url}/pinokio/${u}`, PORT) // if (BrowserWindow.getAllWindows().length === 0 || !mainWindow) createWindow(PORT) // const topWindow = BrowserWindow.getFocusedWindow(); // console.log("top window", topWindow) // //mainWindow.focus() // //mainWindow.loadURL(`${root_url}/pinokio/${u}`) // topWindow.focus() // topWindow.loadURL(`${root_url}/pinokio/${u}`) }) // app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors') let all = BrowserWindow.getAllWindows() for(win of all) { try { if (win && typeof win.setTitleBarOverlay === 'function') { const overlay = titleBarOverlay(colors) setWindowTitleBarOverlay(win, overlay) } } catch (e) { // console.log("E2", e) } } createWindow(PORT) if (updateTestMode) { setTimeout(() => { showUpdateBannerTestAvailable() }, 400) } else { updater.setHandlers({ onUpdateAvailable: (info) => { updateInfo = info updateDownloadInFlight = false updateBannerDismissed = false showUpdateBanner(buildUpdateBannerPayload('available', info)) }, onUpdateNotAvailable: () => { updateInfo = null updateDownloadInFlight = false hideUpdateBanner() }, onDownloadProgress: (progress) => { const payload = buildUpdateBannerPayload('downloading', updateInfo, { progressPercent: progress && typeof progress.percent === 'number' ? progress.percent : 0, notesPreview: buildProgressLabel(progress) }) showUpdateBanner(payload) }, onUpdateDownloaded: (info) => { updateInfo = info updateDownloadInFlight = false showUpdateBanner(buildUpdateBannerPayload('ready', info)) }, onError: (err) => { const wasDownloading = updateDownloadInFlight updateDownloadInFlight = false if (!wasDownloading) { console.warn('Update check error:', err) return } const message = err && err.message ? err.message : 'Update error' showUpdateBanner(buildUpdateBannerPayload('error', updateInfo, { notesPreview: message })) } }) updater.run(mainWindow) } }) } ================================================ FILE: linux_build.sh ================================================ docker run --rm -ti \ -v "$PWD:/project" \ -w /project \ -e SNAPCRAFT_BUILD_ENVIRONMENT=host \ -e SNAP_DESTRUCTIVE_MODE=true \ electronuserland/builder \ bash -lc "rm -rf node_modules && npm install && npm run dist" ================================================ FILE: main.js ================================================ const { app } = require('electron') const Pinokiod = require("pinokiod") const config = require('./config') const pinokiod = new Pinokiod(config) if (process.platform === 'linux') { console.log('[PINOKIO DEBUG] Linux startup') console.log('[PINOKIO DEBUG] ELECTRON_OZONE_PLATFORM_HINT:', process.env.ELECTRON_OZONE_PLATFORM_HINT || '') console.log('[PINOKIO DEBUG] ELECTRON_DISABLE_GPU:', process.env.ELECTRON_DISABLE_GPU || '') console.log('[PINOKIO DEBUG] DISPLAY:', process.env.DISPLAY || '') console.log('[PINOKIO DEBUG] WAYLAND_DISPLAY:', process.env.WAYLAND_DISPLAY || '') console.log('[PINOKIO DEBUG] argv:', process.argv.join(' ')) app.disableHardwareAcceleration() } let mode = pinokiod.kernel.store.get("mode") || "full" //iprocess.env.PINOKIO_MODE = process.env.PINOKIO_MODE || 'desktop'; if (mode === 'minimal' || mode === 'background') { require('./minimal'); } else { require('./full'); } ================================================ FILE: minimal.js ================================================ const { app, Tray, Menu, shell, nativeImage, BrowserWindow, session, Notification } = require('electron'); const path = require('path') const os = require('os') const fs = require('fs') const Pinokiod = require("pinokiod") const config = require('./config') const Updater = require('./updater') const pinokiod = new Pinokiod(config) const updater = new Updater() let tray let hiddenWindow let rootUrl let splashWindow let splashIcon const getLogFileHint = () => { try { if (pinokiod && pinokiod.kernel && pinokiod.kernel.homedir) { return path.resolve(pinokiod.kernel.homedir, "logs", "stdout.txt") } } catch (err) { } return path.resolve(os.homedir(), ".pinokio", "logs", "stdout.txt") } const ensureSplashWindow = () => { if (splashWindow && !splashWindow.isDestroyed()) { return splashWindow } splashWindow = new BrowserWindow({ width: 420, height: 320, frame: false, resizable: false, transparent: true, show: false, alwaysOnTop: true, skipTaskbar: true, fullscreenable: false, webPreferences: { backgroundThrottling: false } }) splashWindow.on('closed', () => { splashWindow = null }) return splashWindow } const getSplashIcon = () => { if (splashIcon) { return splashIcon } const candidates = [ path.join('assets', 'icon.png'), path.join('assets', 'icon_small@2x.png'), path.join('assets', 'icon_small.png'), 'icon2.png' ] for (const relative of candidates) { const absolute = path.join(__dirname, relative) if (fs.existsSync(absolute)) { splashIcon = relative.split(path.sep).join('/') return splashIcon } } splashIcon = path.join('assets', 'icon_small.png').split(path.sep).join('/') return splashIcon } const updateSplashWindow = ({ state = 'loading', message, detail, logPath, icon } = {}) => { const win = ensureSplashWindow() const query = { state } if (message) { query.message = message } if (detail) { const trimmed = detail.length > 800 ? `${detail.slice(0, 800)}…` : detail query.detail = trimmed } if (logPath) { query.log = logPath } if (icon) { query.icon = icon } win.loadFile(path.join(__dirname, 'splash.html'), { query }).finally(() => { if (!win.isDestroyed()) { win.show() } }) } const closeSplashWindow = () => { if (splashWindow && !splashWindow.isDestroyed()) { splashWindow.close() } } const showStartupError = ({ message, detail, error } = {}) => { const formatted = detail || formatStartupError(error) updateSplashWindow({ state: 'error', message: message || 'Pinokio could not start', detail: formatted, logPath: getLogFileHint(), icon: getSplashIcon() }) } const formatStartupError = (error) => { if (!error) return '' if (error.stack) { return `${error.message || 'Unknown error'}\n\n${error.stack}` } if (error.message) return error.message if (typeof error === 'string') return error try { return JSON.stringify(error, null, 2) } catch (err) { return String(error) } } const gotTheLock = app.requestSingleInstanceLock() if (!gotTheLock) { app.quit() } app.on('second-instance', () => { if (rootUrl) { shell.openExternal(rootUrl) } }) app.whenReady().then(async () => { if (!gotTheLock) { return } updateSplashWindow({ state: 'loading', message: 'Starting Pinokio…', icon: getSplashIcon() }) try { try { const portInUse = await pinokiod.running(pinokiod.port) if (portInUse) { showStartupError({ message: 'Pinokio is already running', detail: `An existing Pinokio instance is using port ${pinokiod.port}. Please close it before launching another.` }) return } } catch (checkError) { console.warn('Failed to verify pinokio port availability', checkError) } await pinokiod.start({ onquit: () => { app.quit() }, onrestart: () => { app.relaunch(); app.exit() }, browser: { clearCache: async () => { console.log('clear cache', session.defaultSession) await session.defaultSession.clearStorageData() console.log("cleared") } } }) } catch (error) { console.error('Failed to start pinokiod', error) showStartupError({ error }) return } let quitting = false app.on('before-quit', (e) => { if (quitting) { return } if (pinokiod && pinokiod.kernel && typeof pinokiod.kernel.kill === 'function') { quitting = true e.preventDefault() try { pinokiod.kernel.kill() } catch (err) { console.warn('Failed to terminate pinokiod on quit', err) } } }) closeSplashWindow() rootUrl = `http://localhost:${pinokiod.port}` if (process.platform === 'darwin') app.dock.hide(); const assetsRoot = app.isPackaged ? process.resourcesPath : __dirname const iconPath = path.resolve(assetsRoot, "assets/icon_small.png") let icon = nativeImage.createFromPath(iconPath) icon = icon.resize({ height: 24, width: 24 }); console.log('Tray icon path:', iconPath, 'isEmpty:', icon.isEmpty()); // if true, image failed to load tray = new Tray(icon) const contextMenu = Menu.buildFromTemplate([ { label: 'Open in Browser', click: () => shell.openExternal(rootUrl) }, { label: 'Restart', click: () => { app.relaunch(); app.exit(); } }, { label: 'Quit', click: () => app.quit() } ]); tray.setToolTip('Pinokio'); tray.setContextMenu(contextMenu); const showNotification = (options = {}) => { try { new Notification({ title: 'Pinokio', body: 'Running in background', ...options }).show() } catch (err) { console.warn('Failed to show background notification', err) } } const announceTray = () => { const platformHandlers = { darwin: () => { try { tray.setHighlightMode('always') tray.setTitle('Pinokio running') setTimeout(() => tray.setHighlightMode('selection'), 4000) setTimeout(() => tray.popUpContextMenu(contextMenu), 150) } catch (err) { console.warn('Failed to signal tray/notification on macOS', err) } showNotification() }, win32: () => { try { app.setAppUserModelId('Pinokio') } catch (err) { console.warn('Failed to set AppUserModelID', err) } showNotification({ icon: iconPath }) }, default: () => { showNotification() } } const handler = platformHandlers[process.platform] || platformHandlers.default handler() } announceTray() tray.on('click', () => { tray.popUpContextMenu(contextMenu); }); shell.openExternal(rootUrl); hiddenWindow = new BrowserWindow({ show: false }); updater.run(hiddenWindow) }); ================================================ FILE: package.json ================================================ { "name": "Pinokio", "private": true, "version": "7.0.0", "homepage": "https://pinokio.co", "description": "pinokio", "main": "main.js", "email": "cocktailpeanuts@proton.me", "author": "https://twitter.com/cocktailpeanut", "scripts": { "start": "electron .", "test:update-banner": "node script/run-update-banner-test.js", "pack": "./node_modules/.bin/electron-builder --dir", "eject": "hdiutil info | grep '/dev/disk' | awk '{print $1}' | xargs -I {} hdiutil detach {}", "l": "docker run --rm -ti -v $PWD:/project -w /project -e SNAPCRAFT_BUILD_ENVIRONMENT=host -e SNAP_DESTRUCTIVE_MODE=true electronuserland/builder bash -lc 'rm -rf node_modules && npm install && npm run monkeypatch && ./node_modules/.bin/electron-builder install-app-deps && ./node_modules/.bin/electron-builder -l'", "mw": "rm -rf node_modules && npm install && npm run monkeypatch && ./node_modules/.bin/electron-builder install-app-deps && ./node_modules/.bin/electron-builder -mw && npm run zip", "build2": "npm run l && npm run mw", "dist": "npm run monkeypatch && ./node_modules/.bin/electron-builder install-app-deps && export SNAPCRAFT_BUILD_ENVIRONMENT=host && export SNAP_DESTRUCTIVE_MODE='true' && ./node_modules/.bin/electron-builder -l && npm run zip", "dist2": "npm run monkeypatch && export USE_SYSTEM_FPM=true && ./node_modules/.bin/electron-builder install-app-deps && export SNAPCRAFT_BUILD_ENVIRONMENT=host && export SNAP_DESTRUCTIVE_MODE='true' && ./node_modules/.bin/electron-builder -mwl && npm run zip", "zip": "node script/zip", "monkeypatch": "cp temp/yarn.js node_modules/app-builder-lib/out/util/yarn.js && cp temp/rebuild.js node_modules/@electron/rebuild/lib/src/rebuild.js", "postinstall2": "npm run monkeypatch && ./node_modules/.bin/electron-builder install-app-deps", "fix": "brew install fpm" }, "build": { "appId": "computer.pinokio", "afterPack": "after-pack.js", "afterSign": "electron-builder-notarize", "directories": { "output": "dist-${platform}" }, "publish": [ { "provider": "github", "owner": "pinokiocomputer", "repo": "pinokio" } ], "asarUnpack": [ "node_modules/go-get-folder-size/**/*", "node_modules/7zip-bin/**/*", "node_modules/sweetalert2/**/*", "node_modules/@homebridge/**/*", "node_modules/pinokiod/server/public/**/*", "node_modules/pinokiod/server/scripts/**/*", "node_modules/toasted-notifier/vendor/mac.noindex/**/*" ], "nsis": { "include": "build/installer.nsh" }, "extraResources": [ "./script/**", { "from": "assets/icon_small.png", "to": "assets/icon_small.png" } ], "protocols": [ { "name": "pinokio", "schemes": [ "pinokio" ] } ], "mac": { "category": "utility", "target": [ { "target": "default", "arch": [ "x64", "arm64" ] } ], "hardenedRuntime": true, "entitlements": "build/entitlements.mac.plist", "entitlementsInherit": "build/entitlements.mac.inherit.plist", "extendInfo": { "NSMicrophoneUsageDescription": "Pinokio needs microphone access for apps that use audio.", "NSCameraUsageDescription": "Pinokio needs camera access for apps that use video." } }, "dmg": { "backgroundColor": "#ffffff", "icon": null, "window": { "width": 520, "height": 320 }, "iconSize": 120, "contents": [ { "type": "file", "x": 160, "y": 170 }, { "type": "link", "name": "Applications", "path": "/Applications", "x": 360, "y": 170 } ] }, "linux": { "maintainer": "Cocktail Peanut ", "target": [ { "target": "deb", "arch": [ "x64", "arm64" ] }, { "target": "rpm", "arch": [ "x64", "arm64" ] }, { "target": "AppImage", "arch": [ "x64", "arm64" ] } ] }, "win": { "artifactName": "Pinokio.${ext}", "signtoolOptions": { "sign": "./build/sign.js" }, "target": [ { "target": "nsis", "arch": [ "x64" ] } ] } }, "license": "MIT", "dependencies": { "electron-progressbar": "^2.2.1", "electron-store": "^8.1.0", "electron-updater": "^6.6.2", "electron-window-state": "^5.0.3", "pinokiod": "^7.0.0" }, "devDependencies": { "@electron/rebuild": "3.2.10", "electron": "39.2.3", "electron-builder": "26.2.0", "electron-builder-notarize": "^1.5.2" } } ================================================ FILE: patch-linux-arm64-natives.js ================================================ const fs = require('fs') const path = require('path') const ARCH_BY_ENUM = { 0: 'ia32', 1: 'x64', 2: 'armv7l', 3: 'arm64', 4: 'universal' } const ELF_MACHINE_AARCH64 = 183 const resolveArch = (context) => { if (typeof context.arch === 'string') { return context.arch.toLowerCase() } if (typeof context.arch === 'number') { const mapped = ARCH_BY_ENUM[context.arch] if (mapped) { return mapped } } if (typeof context.appOutDir === 'string' && /arm64/i.test(context.appOutDir)) { return 'arm64' } return '' } const ensureAarch64Elf = (filePath, label) => { const data = fs.readFileSync(filePath) if (data.length < 20) { throw new Error(`[linux-arm64-native-fix] ${label} is too small to be an ELF binary: ${filePath}`) } if (!(data[0] === 0x7f && data[1] === 0x45 && data[2] === 0x4c && data[3] === 0x46)) { throw new Error(`[linux-arm64-native-fix] ${label} is not an ELF binary: ${filePath}`) } const isLittleEndian = data[5] !== 2 const machine = isLittleEndian ? data.readUInt16LE(18) : data.readUInt16BE(18) if (machine !== ELF_MACHINE_AARCH64) { throw new Error(`[linux-arm64-native-fix] ${label} is not aarch64 (e_machine=${machine}): ${filePath}`) } } const copyWithValidation = (source, destination, label) => { if (!fs.existsSync(source)) { throw new Error(`[linux-arm64-native-fix] Missing ${label} source file: ${source}`) } ensureAarch64Elf(source, `${label} source`) fs.mkdirSync(path.dirname(destination), { recursive: true }) fs.copyFileSync(source, destination) ensureAarch64Elf(destination, `${label} destination`) console.log(`[linux-arm64-native-fix] Patched ${label}: ${destination}`) } const existingDirectories = (candidates) => candidates.filter((candidate) => fs.existsSync(candidate)) module.exports = async (context) => { if (context.electronPlatformName !== 'linux') { return } const arch = resolveArch(context) if (arch !== 'arm64') { return } const unpackedRoot = path.join(context.appOutDir, 'resources', 'app.asar.unpacked') const ptyBases = existingDirectories([ path.join(unpackedRoot, 'node_modules', '@homebridge', 'node-pty-prebuilt-multiarch'), path.join(unpackedRoot, 'node_modules', 'pinokiod', 'node_modules', '@homebridge', 'node-pty-prebuilt-multiarch') ]) if (ptyBases.length === 0) { throw new Error('[linux-arm64-native-fix] Could not find @homebridge/node-pty-prebuilt-multiarch in app.asar.unpacked') } for (const ptyBase of ptyBases) { const source = path.join(ptyBase, 'prebuilds', 'linux-arm64', 'node.abi131.node') const destination = path.join(ptyBase, 'build', 'Release', 'pty.node') copyWithValidation(source, destination, 'node-pty') } const watcherBases = existingDirectories([ path.join(unpackedRoot, 'node_modules', '@parcel', 'watcher'), path.join(unpackedRoot, 'node_modules', 'pinokiod', 'node_modules', '@parcel', 'watcher') ]) if (watcherBases.length === 0) { throw new Error('[linux-arm64-native-fix] Could not find @parcel/watcher in app.asar.unpacked') } for (const watcherBase of watcherBases) { const source = path.join(path.dirname(watcherBase), 'watcher-linux-arm64-glibc', 'watcher.node') const destination = path.join(watcherBase, 'build', 'Release', 'watcher.node') copyWithValidation(source, destination, 'parcel-watcher') } } ================================================ FILE: popup-shell.js ================================================ const path = require('path') const windowStateKeeper = require('electron-window-state') const { BrowserWindow, WebContentsView, session } = require('electron') const parseUrl = (value, base) => { try { return new URL(value, base) } catch (_) { return null } } const isHttpUrl = (value) => { return Boolean(value && (value.protocol === 'http:' || value.protocol === 'https:')) } const getFeatureDimension = (params, key, fallback) => { const value = parseInt(params.get(key), 10) return Number.isFinite(value) ? value : fallback } module.exports = ({ contentPreloadPath = path.join(__dirname, 'preload.js'), toolbarHtmlPath = path.join(__dirname, 'popup-toolbar.html'), toolbarHeight = 46, installForceDestroyOnClose } = {}) => { let openPinokioHomeWindow = null const popupBrowserPartition = 'persist:pinokio-popup-browser' const buildBrowserLikeUserAgent = () => { const chromeVersion = process.versions.chrome || '140.0.0.0' if (process.platform === 'darwin') { return `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36` } if (process.platform === 'win32') { const arch = process.arch === 'arm64' ? 'ARM64' : 'x64' return `Mozilla/5.0 (Windows NT 10.0; Win64; ${arch}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36` } const arch = process.arch === 'arm64' ? 'aarch64' : 'x86_64' return `Mozilla/5.0 (X11; Linux ${arch}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36` } const browserLikeUserAgent = buildBrowserLikeUserAgent() const getPopupBrowserSession = () => { const popupSession = session.fromPartition(popupBrowserPartition) if (!popupSession.__pinokioPopupBrowserConfigured) { popupSession.__pinokioPopupBrowserConfigured = true popupSession.setUserAgent(browserLikeUserAgent, 'en-US,en') } return popupSession } const buildAppPopupContentWebPreferences = (overrides = {}) => { const next = (overrides && typeof overrides === 'object') ? { ...overrides } : {} return { ...next, session: session.defaultSession, webSecurity: false, spellcheck: false, nativeWindowOpen: true, contextIsolation: false, nodeIntegrationInSubFrames: true, enableRemoteModule: false, experimentalFeatures: true, preload: contentPreloadPath } } const buildBrowserPopupContentWebPreferences = (overrides = {}) => { const next = (overrides && typeof overrides === 'object') ? { ...overrides } : {} delete next.session delete next.preload delete next.partition delete next.nodeIntegration delete next.nodeIntegrationInSubFrames delete next.contextIsolation delete next.experimentalFeatures delete next.webSecurity return { ...next, partition: popupBrowserPartition, sandbox: true, webSecurity: true, allowRunningInsecureContent: false, nativeWindowOpen: true, contextIsolation: true, nodeIntegration: false, nodeIntegrationInSubFrames: false, enableRemoteModule: false, experimentalFeatures: false } } const unwrapContainerTarget = (target, rootParsed) => { let next = target while (next && next.pathname === '/container') { const innerUrl = next.searchParams.get('url') if (!innerUrl) { break } const unwrapped = parseUrl(innerUrl, rootParsed ? rootParsed.origin : undefined) if (!isHttpUrl(unwrapped) || unwrapped.href === next.href) { break } next = unwrapped } return next } const isPinokioWindowUrl = (value, rootUrl) => { const rootParsed = parseUrl(rootUrl) const target = unwrapContainerTarget( parseUrl(value, rootParsed ? rootParsed.origin : undefined), rootParsed ) if (!rootParsed || !isHttpUrl(target)) { return false } return target.origin === rootParsed.origin } const resolveTargetUrl = ({ url, openerWebContents, rootUrl } = {}) => { const openerUrl = (() => { try { return openerWebContents && !openerWebContents.isDestroyed() ? openerWebContents.getURL() : (rootUrl || '') } catch (_) { return rootUrl || '' } })() const target = parseUrl(url, openerUrl || (rootUrl || undefined)) return isHttpUrl(target) ? target.href : '' } const buildRegularWindowOptions = ({ x, y, width, height, overlay } = {}) => { const options = { x, y, width: width || 1000, height: height || 800, minWidth: 190, parent: null, titleBarStyle: 'hidden', webPreferences: buildAppPopupContentWebPreferences() } if (overlay) { options.titleBarOverlay = overlay } return options } const createRegularWindow = ({ x, y, width, height, overlay } = {}) => { const win = new BrowserWindow(buildRegularWindowOptions({ x, y, width, height, overlay })) installForceDestroyOnClose(win) return win } const layoutPopupShell = (shellState) => { if (!shellState || !shellState.win || shellState.win.isDestroyed()) { return } const bounds = shellState.win.getContentBounds() const width = Math.max(bounds.width || 0, 0) const height = Math.max(bounds.height || 0, 0) shellState.toolbarView.setBounds({ x: 0, y: 0, width, height: toolbarHeight }) shellState.contentView.setBounds({ x: 0, y: toolbarHeight, width, height: Math.max(height - toolbarHeight, 0) }) } const buildPopupShellState = (shellState) => { const target = shellState && shellState.contentView ? shellState.contentView.webContents : null let url = '' let title = '' try { if (target && !target.isDestroyed()) { url = target.getURL() || '' title = target.getTitle() || '' } } catch (_) { } return { url, title: title || url || 'Pinokio', canGoBack: Boolean(target && !target.isDestroyed() && target.canGoBack()), canGoForward: Boolean(target && !target.isDestroyed() && target.canGoForward()) } } const sendPopupShellState = (shellState) => { if (!shellState || !shellState.toolbarView || !shellState.contentView) { return } const toolbarContents = shellState.toolbarView.webContents if (!toolbarContents || toolbarContents.isDestroyed()) { return } const state = buildPopupShellState(shellState) toolbarContents.send('pinokio:popup-shell-state', state) if (shellState.win && !shellState.win.isDestroyed()) { shellState.win.setTitle(state.title) } } const createPopupShellWindow = ({ x, y, width, height, adoptedWebContents = null, contentWebPreferences = {}, browserLike = false, initialUrl = '' } = {}) => { const win = new BrowserWindow({ frame: true, x, y, width: width || 1000, height: height || 800, minWidth: 190, backgroundColor: '#ffffff' }) win.__pinokioPopupShell = true win.__pinokioCloseOnFirstDownload = Boolean(browserLike && initialUrl) installForceDestroyOnClose(win) const toolbarView = new WebContentsView({ webPreferences: { nodeIntegration: true, contextIsolation: false, spellcheck: false, backgroundThrottling: false } }) const contentView = adoptedWebContents ? new WebContentsView({ webContents: adoptedWebContents }) : new WebContentsView({ webPreferences: browserLike ? buildBrowserPopupContentWebPreferences(contentWebPreferences) : buildAppPopupContentWebPreferences(contentWebPreferences) }) const shellState = { win, toolbarView, contentView } win.contentView.addChildView(contentView) win.contentView.addChildView(toolbarView) layoutPopupShell(shellState) const syncShellState = () => { layoutPopupShell(shellState) sendPopupShellState(shellState) } const focusContent = () => { if (contentView.webContents && !contentView.webContents.isDestroyed()) { contentView.webContents.focus() } } toolbarView.webContents.on('did-finish-load', () => { sendPopupShellState(shellState) }) toolbarView.webContents.on('ipc-message', (_event, channel) => { const target = contentView.webContents if (!target || target.isDestroyed()) { return } if (channel === 'pinokio:popup-shell-back') { if (target.canGoBack()) { target.goBack() } return } if (channel === 'pinokio:popup-shell-forward') { if (target.canGoForward()) { target.goForward() } return } if (channel === 'pinokio:popup-shell-refresh') { target.reload() return } if (channel === 'pinokio:popup-shell-open-home') { if (typeof openPinokioHomeWindow === 'function') { openPinokioHomeWindow() } } }) if (browserLike && contentView.webContents && !contentView.webContents.isDestroyed()) { getPopupBrowserSession() contentView.webContents.setUserAgent(browserLikeUserAgent) } contentView.webContents.on('did-finish-load', () => { if (shellState.win && !shellState.win.isDestroyed()) { shellState.win.__pinokioCloseOnFirstDownload = false } syncShellState() focusContent() }) contentView.webContents.on('did-navigate', syncShellState) contentView.webContents.on('did-navigate-in-page', syncShellState) contentView.webContents.on('page-title-updated', (event) => { event.preventDefault() sendPopupShellState(shellState) }) win.on('focus', focusContent) win.on('resize', syncShellState) toolbarView.webContents.loadFile(toolbarHtmlPath).catch((error) => { console.error('[pinokio][popup-shell] failed to load toolbar', error) }) if (initialUrl) { contentView.webContents.loadURL(initialUrl).catch((error) => { console.error('[pinokio][popup-shell] failed to load content url', { initialUrl, error }) }) } return shellState } const allowPermissions = (targetSession) => { if (!targetSession) { return } targetSession.setPermissionRequestHandler((_webContents, _permission, callback) => { callback(true) }) } const createPopupWindowState = () => { if (typeof windowStateKeeper !== 'function') { return { x: undefined, y: undefined, width: 1000, height: 800, manage: () => {} } } return windowStateKeeper({ // file: "index.json", defaultWidth: 1000, defaultHeight: 800 }) } const createPopupResponse = ({ params, width, height, x, y } = {}) => { return { action: 'allow', outlivesOpener: true, overrideBrowserWindowOptions: { webPreferences: buildBrowserPopupContentWebPreferences() }, createWindow: (options = {}) => { const shellState = createPopupShellWindow({ width: getFeatureDimension(params, 'width', width), height: getFeatureDimension(params, 'height', height), x: x + 30, y: y + 30, adoptedWebContents: options.webContents || null, contentWebPreferences: options.webPreferences || {}, browserLike: true }) return shellState.contentView.webContents } } } const openExternalWindow = ({ url, windowState } = {}) => { const nextWindowState = windowState || createPopupWindowState() const shellState = createPopupShellWindow({ x: nextWindowState.x, y: nextWindowState.y, width: nextWindowState.width, height: nextWindowState.height, browserLike: true, initialUrl: url }) const win = shellState.win allowPermissions(shellState.contentView.webContents.session) win.focus() nextWindowState.manage(win) return win } return { createPopupResponse, getPopupBrowserSession, isPinokioWindowUrl, resolveTargetUrl, openExternalWindow, setPinokioHomeWindowOpener: (nextOpener) => { openPinokioHomeWindow = typeof nextOpener === 'function' ? nextOpener : null } } } ================================================ FILE: popup-toolbar.html ================================================ Popup Toolbar ================================================ FILE: preload.js ================================================ // put this preload for main-window to give it prompt() const { ipcRenderer, } = require('electron') window.prompt = function(title, val){ return ipcRenderer.sendSync('prompt', {title, val}) } try { } catch (_) { } const sendPinokio = (action) => { if (!action) { return } try { if (window.parent === window.top) { window.parent.postMessage({ action }, "*") } } catch (_) { } } // Only apply frame bridge hooks inside embedded pages. let isEmbeddedFrame = false let isDirectChildFrame = false try { isEmbeddedFrame = window.parent !== window isDirectChildFrame = isEmbeddedFrame && window.parent === window.top } catch (_) { isEmbeddedFrame = false isDirectChildFrame = false } let previousFrameUrl = isEmbeddedFrame ? document.location.href : '' const publishFrameLocation = () => { if (!isEmbeddedFrame) { return } const currentUrl = document.location.href if (currentUrl === previousFrameUrl) { return } previousFrameUrl = currentUrl if (isDirectChildFrame) { sendPinokio({ type: 'location', url: currentUrl }) } syncPinokioInjectors('location').catch(() => {}) } if (isEmbeddedFrame) { if (isDirectChildFrame) { sendPinokio({ type: 'location', url: previousFrameUrl }) } const originalPushState = history.pushState history.pushState = function pushStateWithPinokioLocation(...args) { const result = originalPushState.apply(this, args) publishFrameLocation() return result } const originalReplaceState = history.replaceState history.replaceState = function replaceStateWithPinokioLocation(...args) { const result = originalReplaceState.apply(this, args) publishFrameLocation() return result } window.addEventListener('popstate', publishFrameLocation) window.addEventListener('hashchange', publishFrameLocation) window.addEventListener('beforeunload', () => { resetPinokioInjectors('unload').catch(() => {}) }, { once: true }) window.addEventListener('message', (event) => { if (event.data) { if (event.data.action === 'back') { history.back() } else if (event.data.action === 'forward') { history.forward() } else if (event.data.action === 'refresh') { location.reload() } } }) } //document.addEventListener("DOMContentLoaded", (e) => { // if (window.parent === window.top) { // window.parent.postMessage({ // action: { // type: "title", // text: document.title // } // }, "*") // } //}) window.electronAPI = { send: (type, msg) => { ipcRenderer.send(type, msg) }, sendSync: (type, msg) => ipcRenderer.sendSync(type, msg), requestPermissions: (payload) => ipcRenderer.invoke('pinokio:request-permissions', payload || {}), startInspector: (payload) => ipcRenderer.invoke('pinokio:start-inspector', payload || {}), stopInspector: () => ipcRenderer.invoke('pinokio:stop-inspector'), captureScreenshot: (screenshotRequest) => { return ipcRenderer.invoke('pinokio:capture-screenshot-debug', { screenshotRequest }) } } const resolvePinokioTargetWindow = () => { try { if (window.parent && window.parent !== window) { return window.parent } } catch (_) { } try { if (window.top && window.top !== window) { return window.top } } catch (_) { } return window } const postPinokioEvent = (eventName, payload = {}, context = {}) => { const target = resolvePinokioTargetWindow() const nextContext = (context && typeof context === 'object') ? { ...context } : {} if (!nextContext.frameUrl) { nextContext.frameUrl = window.location.href } if (!nextContext.workspace) { const workspaceHint = resolvePinokioWorkspaceHint() if (workspaceHint) { nextContext.workspace = workspaceHint } } target.postMessage({ e: 'pinokio:event', event: eventName, payload: (payload && typeof payload === 'object') ? payload : {}, context: nextContext }, '*') } const ensurePinokioApi = () => { const api = (window.$pinokio && typeof window.$pinokio === 'object') ? window.$pinokio : {} api.trigger = (eventName, payload = {}, context = {}) => { if (typeof eventName !== 'string' || !eventName.trim()) { return { ok: false, handled: false, reason: 'invalid_event_name' } } const normalizedEvent = eventName.trim() postPinokioEvent( normalizedEvent, (payload && typeof payload === 'object') ? payload : {}, (context && typeof context === 'object') ? context : {} ) return { ok: true, handled: true, event: normalizedEvent } } window.$pinokio = api return api } ensurePinokioApi() const extractWorkspaceFromPathname = (pathname) => { if (typeof pathname !== 'string') { return '' } const value = pathname.trim() if (!value) { return '' } const patterns = [ /^\/pinokio\/([^/?#]+)/i, /^\/p\/([^/?#]+)/i, /^\/api\/([^/?#]+)/i, /^\/_api\/([^/?#]+)/i, /^\/raw\/api\/([^/?#]+)/i, /^\/asset\/api\/([^/?#]+)/i, /^\/files\/api\/([^/?#]+)/i, /^\/env\/api\/([^/?#]+)/i, /^\/run\/api\/([^/?#]+)/i, ] for (const pattern of patterns) { const match = value.match(pattern) if (!match || !match[1]) { continue } try { return decodeURIComponent(match[1]).trim() } catch (_) { return String(match[1] || '').trim() } } return '' } const resolvePinokioWorkspaceHint = () => { const candidates = [] try { const ref = (typeof document !== 'undefined' && document.referrer) ? document.referrer : '' if (ref) { candidates.push(ref) } } catch (_) { } try { candidates.push(window.location.href) } catch (_) { } for (const candidate of candidates) { if (typeof candidate !== 'string' || !candidate.trim()) { continue } try { const parsed = new URL(candidate, 'http://localhost') const workspaceQueryHint = (parsed.searchParams.get('__pinokio_workspace') || parsed.searchParams.get('workspace') || '').trim() if (workspaceQueryHint) { return workspaceQueryHint } const workspace = extractWorkspaceFromPathname(parsed.pathname || '') if (workspace) { return workspace } } catch (_) { const workspace = extractWorkspaceFromPathname(candidate) if (workspace) { return workspace } } } return '' } let pinokioInjectSyncId = 0 const buildPinokioContext = (reason = 'load', responseContext = {}) => { const currentUrl = window.location.href const referrerUrl = (typeof document !== 'undefined' && document.referrer) ? document.referrer : '' const workspaceHint = resolvePinokioWorkspaceHint() const frameName = typeof window.name === 'string' ? window.name.trim() : '' const rootFrameUrl = responseContext && typeof responseContext.frameUrl === 'string' && responseContext.frameUrl.trim() ? responseContext.frameUrl.trim() : currentUrl return { frameUrl: currentUrl, rootFrameUrl, currentUrl, pageUrl: referrerUrl || rootFrameUrl, referrerUrl, frameName: frameName || undefined, workspace: workspaceHint || undefined, reason } } const waitForPinokioDocumentEnd = () => { if (document.readyState === 'loading') { return new Promise((resolve) => { window.addEventListener('DOMContentLoaded', resolve, { once: true }) }) } return Promise.resolve() } const waitForPinokioDocumentIdle = async () => { await waitForPinokioDocumentEnd() await new Promise((resolve) => { if (typeof window.requestIdleCallback === 'function') { window.requestIdleCallback(() => resolve(), { timeout: 120 }) return } setTimeout(resolve, 32) }) } const requestPinokioInjectDescriptors = (reason = 'load') => { if (!isEmbeddedFrame) { return Promise.resolve(null) } const context = buildPinokioContext(reason) return ipcRenderer.invoke('pinokio:resolve-injectors', { reason, context }).then((result) => result && typeof result === 'object' ? result : null).catch(() => null) } const resetPinokioInjectors = async (reason = 'sync', syncId = pinokioInjectSyncId) => { if (!isEmbeddedFrame) { return } try { await ipcRenderer.invoke('pinokio:reset-injectors', { syncId, reason, context: buildPinokioContext(reason) }) } catch (error) { try { console.warn('[pinokio][preload] injector reset failed', { reason, error: error && error.message ? error.message : String(error) }) } catch (_) { } } } const mountPinokioInjectGroup = async (syncId, descriptors, responseContext, reason) => { if (!descriptors.length || syncId !== pinokioInjectSyncId) { return } try { await ipcRenderer.invoke('pinokio:mount-injectors', { syncId, reason, inject: descriptors, context: buildPinokioContext(reason, responseContext) }) } catch (error) { try { console.warn('[pinokio][preload] injector mount failed', { reason, descriptors: descriptors.map((item) => item && item.src).filter(Boolean), error: error && error.message ? error.message : String(error) }) } catch (_) { } } } async function syncPinokioInjectors(reason = 'load') { if (!isEmbeddedFrame) { return } const syncId = ++pinokioInjectSyncId const response = await requestPinokioInjectDescriptors(reason) if (syncId !== pinokioInjectSyncId) { return } const descriptors = Array.isArray(response && response.inject) ? response.inject : [] const groups = { start: [], end: [], idle: [] } for (const descriptor of descriptors) { const when = descriptor && (descriptor.when === 'start' || descriptor.when === 'end') ? descriptor.when : 'idle' groups[when].push(descriptor) } await resetPinokioInjectors(reason, syncId) if (syncId !== pinokioInjectSyncId) { return } await mountPinokioInjectGroup(syncId, groups.start, response && response.context, reason) await waitForPinokioDocumentEnd() await mountPinokioInjectGroup(syncId, groups.end, response && response.context, reason) await waitForPinokioDocumentIdle() await mountPinokioInjectGroup(syncId, groups.idle, response && response.context, reason) } if (isEmbeddedFrame) { syncPinokioInjectors('load').catch(() => {}) } ;(function initUpdateBanner() { if (typeof document === 'undefined') { return } if (window !== window.top) { return } const BANNER_HEIGHT = 72 const state = { payload: null, banner: null, style: null, layoutActive: false, layoutTick: null, ready: false } const ensureStyle = () => { if (state.style) { return } const style = document.createElement('style') style.id = 'pinokio-update-banner-style' style.textContent = ` body.pinokio-update-banner-active[data-pinokio-update-layout-root="1"] #layout-root { height: calc(100% - var(--layout-dragger-height, 0px) - var(--pinokio-update-banner-height, 0px)); } body.pinokio-update-banner-active:not([data-pinokio-update-layout-root="1"]) { padding-bottom: var(--pinokio-update-banner-height, 0px) !important; } #pinokio-update-banner { position: fixed; bottom: 0; left: 0; right: 0; height: var(--pinokio-update-banner-height, 72px); display: none; align-items: center; justify-content: space-between; gap: 16px; padding: 10px 16px 12px; box-sizing: border-box; background: linear-gradient(90deg, rgba(24, 24, 30, 0.94), rgba(30, 30, 38, 0.98)); border-top: 1px solid rgba(255, 255, 255, 0.12); box-shadow: 0 -12px 26px rgba(0, 0, 0, 0.35); z-index: 2147483646; color: #f5f5f7; font-family: "SF Pro Text", "Segoe UI", "Helvetica Neue", Arial, sans-serif; border-radius: 0; } #pinokio-update-banner .pinokio-update-left { min-width: 0; display: flex; flex-direction: column; gap: 4px; } #pinokio-update-banner .pinokio-update-title { font-size: 15px; font-weight: 600; letter-spacing: 0.1px; } #pinokio-update-banner .pinokio-update-title.danger { color: #ff7b72; } #pinokio-update-banner .pinokio-update-details { font-size: 12px; color: rgba(255, 255, 255, 0.68); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 520px; } #pinokio-update-banner .pinokio-update-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; } #pinokio-update-banner button { appearance: none; border: 1px solid transparent; border-radius: 0; padding: 6px 12px; font-size: 12px; font-weight: 600; cursor: pointer; transition: background 120ms ease, border-color 120ms ease, transform 120ms ease; } #pinokio-update-banner button:disabled { opacity: 0.6; cursor: default; } #pinokio-update-banner .pinokio-update-primary { color: #1b1b1f; background: #f6c251; } #pinokio-update-banner .pinokio-update-primary:hover:not(:disabled) { background: #ffcc66; transform: translateY(-1px); } #pinokio-update-banner .pinokio-update-ghost { color: #f5f5f7; background: transparent; border-color: rgba(255, 255, 255, 0.18); } #pinokio-update-banner .pinokio-update-ghost:hover:not(:disabled) { border-color: rgba(255, 255, 255, 0.35); } #pinokio-update-banner .pinokio-update-progress { position: absolute; left: 0; top: 0; width: 100%; height: 3px; background: rgba(255, 255, 255, 0.15); } #pinokio-update-banner .pinokio-update-progress-bar { height: 100%; width: 0%; background: #f6c251; transition: width 150ms ease; } #pinokio-update-banner .pinokio-update-hidden { display: none !important; } ` const target = document.head || document.documentElement target.appendChild(style) state.style = style } const ensureBanner = () => { if (state.banner) { return state.banner } if (!document.body) { return null } ensureStyle() const container = document.createElement('div') container.id = 'pinokio-update-banner' container.innerHTML = `
Update available
` container.addEventListener('click', (event) => { const button = event.target.closest('button') if (!button) { return } const action = button.getAttribute('data-action') if (!action) { return } if (action === 'release-notes' && state.payload && state.payload.releaseUrl) { ipcRenderer.send('pinokio:update-banner-action', { action, releaseUrl: state.payload.releaseUrl }) return } ipcRenderer.send('pinokio:update-banner-action', { action }) }) document.body.appendChild(container) state.banner = container return container } const notifyLayoutResize = () => { if (state.layoutTick) { cancelAnimationFrame(state.layoutTick) } state.layoutTick = requestAnimationFrame(() => { state.layoutTick = null try { window.dispatchEvent(new CustomEvent('pinokio:viewport-change', { detail: { height: window.innerHeight } })) } catch (_) { window.dispatchEvent(new Event('resize')) } }) } const applyLayoutOffset = (active) => { if (!document.body) { return } const hasLayoutRoot = Boolean(document.getElementById('layout-root')) if (hasLayoutRoot) { document.body.setAttribute('data-pinokio-update-layout-root', '1') } else { document.body.removeAttribute('data-pinokio-update-layout-root') } document.documentElement.style.setProperty('--pinokio-update-banner-height', `${BANNER_HEIGHT}px`) document.body.classList.toggle('pinokio-update-banner-active', Boolean(active)) if (state.layoutActive !== Boolean(active)) { state.layoutActive = Boolean(active) notifyLayoutResize() } } const setHidden = (node, hidden) => { if (!node) return node.classList.toggle('pinokio-update-hidden', Boolean(hidden)) } const render = (payload) => { state.payload = payload if (!payload || payload.state === 'hidden') { if (state.banner) { state.banner.style.display = 'none' } applyLayoutOffset(false) return } const banner = ensureBanner() if (!banner) { return } banner.style.display = 'flex' applyLayoutOffset(true) const title = banner.querySelector('.pinokio-update-title') const details = banner.querySelector('.pinokio-update-details') const updateNow = banner.querySelector('[data-action="update"]') const restartNow = banner.querySelector('[data-action="restart"]') const releaseNotes = banner.querySelector('[data-action="release-notes"]') const progress = banner.querySelector('.pinokio-update-progress') const progressBar = banner.querySelector('.pinokio-update-progress-bar') const stateKey = payload.state || 'available' const version = payload.version ? `Version ${payload.version}` : '' const notes = payload.notesPreview || '' const detail = [version, notes].filter(Boolean).join(' - ') let titleText = 'Update available' if (stateKey === 'downloading') titleText = 'Downloading update' if (stateKey === 'ready') titleText = 'Update ready' if (stateKey === 'error') titleText = 'Update failed' if (title) { title.textContent = titleText if (stateKey === 'error') { title.classList.add('danger') } else { title.classList.remove('danger') } } if (details) { details.textContent = detail } if (updateNow) { updateNow.textContent = stateKey === 'error' ? 'Retry' : 'Update now' updateNow.disabled = stateKey === 'downloading' } setHidden(updateNow, stateKey === 'ready') setHidden(restartNow, stateKey !== 'ready') setHidden(progress, stateKey !== 'downloading') setHidden(releaseNotes, !payload.releaseUrl) if (progressBar) { if (stateKey === 'downloading' && typeof payload.progressPercent === 'number') { const percent = Math.max(0, Math.min(100, payload.progressPercent)) progressBar.style.width = `${percent}%` } else { progressBar.style.width = '0%' } } } ipcRenderer.on('pinokio:update-banner', (_event, payload) => { render(payload) }) const ready = () => { state.ready = true if (state.payload) { render(state.payload) } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', ready, { once: true }) } else { ready() } })() ;(function initInspector() { if (typeof document === 'undefined') { return } const log = (message) => { try { console.log(`[Inspector] ${message}`) } catch (_) { // ignore } } const state = { active: false, button: null, lastFrameOrdinal: null, lastRelativeOrdinal: null, lastUrl: null, lastDomPath: null, displayUrl: null, overlay: null, instructionsVisible: false, closing: false, } const normalizeUrl = (value) => { if (!value) { return null } try { return new URL(value, window.location.href).toString() } catch (err) { return value } } const findIframeCandidates = () => { const list = Array.from(document.querySelectorAll('iframe')).map((iframe, index) => { const style = window.getComputedStyle ? window.getComputedStyle(iframe) : null const rect = iframe.getBoundingClientRect() const area = rect ? Math.max(rect.width, 0) * Math.max(rect.height, 0) : 0 const visible = Boolean( rect && rect.width > 2 && rect.height > 2 && !iframe.classList.contains('hidden') && !iframe.hasAttribute('hidden') && (!style || (style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || '1') > 0)) ) return { element: iframe, index, rect, area, visible, src: normalizeUrl(iframe.getAttribute('src') || iframe.src || ''), } }) return list } const selectVisibleIframe = () => { const candidates = findIframeCandidates() if (!candidates.length) { log('no iframe candidates discovered') return null } candidates.slice(0, 3).forEach((candidate) => { const rect = candidate.rect || { width: 0, height: 0 } log(`candidate[${candidate.index}] src=${candidate.src || ''} visible=${candidate.visible ? 'yes' : 'no'} size=${Math.round(rect.width)}x${Math.round(rect.height)}`) }) const visible = candidates.filter((candidate) => candidate.visible) const ranked = (visible.length ? visible : candidates).slice().sort((a, b) => { if (b.area === a.area) { return (b.rect ? b.rect.width : 0) - (a.rect ? a.rect.width : 0) } return b.area - a.area }) const chosen = ranked[0] if (!chosen) { log('no suitable iframe found after ranking') return null } log(`selected iframe src=${chosen.src || ''} visible=${chosen.visible ? 'yes' : 'no'} size=${Math.round(chosen.rect?.width || 0)}x${Math.round(chosen.rect?.height || 0)}`) const siblingsWithSameUrl = candidates.filter((candidate) => candidate.src === chosen.src) const relativeOrdinal = siblingsWithSameUrl.indexOf(chosen) return { element: chosen.element, index: chosen.index, relativeOrdinal: relativeOrdinal >= 0 ? relativeOrdinal : null, url: chosen.src, } } const ensureOverlay = () => { if (state.overlay && state.overlay.container && document.body.contains(state.overlay.container)) { return state.overlay } // Remove stale overlays left over from previous script executions const orphaned = Array.from(document.querySelectorAll('.pinokio-inspector-overlay')) for (const node of orphaned) { if (!state.overlay || node !== state.overlay.container) { node.remove() } } const container = document.createElement('div') container.className = 'pinokio-inspector-overlay' Object.assign(container.style, { position: 'fixed', right: '16px', bottom: '16px', maxWidth: 'min(420px, 92vw)', maxHeight: '70vh', padding: '12px 14px', borderRadius: '10px', border: '1px solid rgba(255, 255, 255, 0.12)', background: 'rgba(9, 12, 20, 0.92)', color: '#fefefe', boxShadow: '0 20px 42px rgba(0,0,0,0.45)', display: 'none', flexDirection: 'column', gap: '8px', zIndex: '2147483646', fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", fontSize: '12px', }) const header = document.createElement('div') Object.assign(header.style, { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', }) const title = document.createElement('strong') title.textContent = 'Inspect Mode' Object.assign(title.style, { fontSize: '12px', letterSpacing: '0.06em', textTransform: 'uppercase', }) const closeButton = document.createElement('button') closeButton.type = 'button' closeButton.textContent = '×' closeButton.dataset.role = 'close' Object.assign(closeButton.style, { background: 'transparent', border: 'none', color: '#fefefe', fontSize: '18px', lineHeight: '1', cursor: 'pointer', }) header.append(title, closeButton) const status = document.createElement('div') status.dataset.role = 'status' status.style.color = '#ccd5ff' const urlRow = document.createElement('div') urlRow.dataset.role = 'url' Object.assign(urlRow.style, { color: '#9aa7c2', fontSize: '11px', wordBreak: 'break-all', }) const htmlSection = document.createElement('div') htmlSection.dataset.role = 'html-container' Object.assign(htmlSection.style, { display: 'none', margin: '8px 0', padding: '8px', borderRadius: '8px', background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.12)', }) const htmlHeader = document.createElement('div') Object.assign(htmlHeader.style, { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', marginBottom: '6px', }) const htmlLabel = document.createElement('div') htmlLabel.textContent = 'Element Snippet' Object.assign(htmlLabel.style, { fontSize: '11px', color: '#9aa7c2', textTransform: 'uppercase', letterSpacing: '0.06em', }) const buttonBaseStyle = { display: 'none', background: 'rgba(77,163,255,0.2)', border: '1px solid rgba(77,163,255,0.4)', borderRadius: '6px', padding: '4px 12px', fontSize: '11px', cursor: 'pointer', color: '#ccd5ff', fontWeight: '600', } const copyButton = document.createElement('button') copyButton.dataset.role = 'copy' copyButton.type = 'button' copyButton.textContent = 'Copy snippet' Object.assign(copyButton.style, buttonBaseStyle) htmlHeader.append(htmlLabel, copyButton) const htmlBlock = document.createElement('textarea') htmlBlock.dataset.role = 'html' Object.assign(htmlBlock.style, { margin: '0', padding: '10px', maxHeight: '28vh', overflow: 'auto', borderRadius: '8px', background: 'rgba(255,255,255,0.08)', display: 'none', fontFamily: "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace", fontSize: '11px', border: '1px solid rgba(255,255,255,0.18)', color: '#fefefe', resize: 'vertical', minHeight: '140px', width: '100%', boxSizing: 'border-box', }) htmlBlock.spellcheck = false htmlSection.append(htmlHeader, htmlBlock) const screenshotBlock = document.createElement('div') screenshotBlock.dataset.role = 'screenshot-container' Object.assign(screenshotBlock.style, { margin: '8px 0', padding: '8px', borderRadius: '8px', background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.12)', display: 'none', textAlign: 'center', }) const screenshotHeader = document.createElement('div') Object.assign(screenshotHeader.style, { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', marginBottom: '8px', }) const screenshotImg = document.createElement('img') screenshotImg.dataset.role = 'screenshot' Object.assign(screenshotImg.style, { maxWidth: '100%', maxHeight: '200px', borderRadius: '4px', boxShadow: '0 2px 8px rgba(0,0,0,0.3)', }) const screenshotLabel = document.createElement('div') screenshotLabel.textContent = 'Element Screenshot' Object.assign(screenshotLabel.style, { fontSize: '11px', color: '#9aa7c2', textTransform: 'uppercase', letterSpacing: '0.06em', }) const copyScreenshotButton = document.createElement('button') copyScreenshotButton.dataset.role = 'copy-screenshot' copyScreenshotButton.type = 'button' copyScreenshotButton.textContent = 'Copy screenshot' Object.assign(copyScreenshotButton.style, buttonBaseStyle) screenshotHeader.append(screenshotLabel, copyScreenshotButton) screenshotBlock.append(screenshotHeader, screenshotImg) container.append(header, status, urlRow, htmlSection, screenshotBlock) document.body.appendChild(container) const overlay = { container, status, urlRow, htmlSection, htmlBlock, screenshotBlock, screenshotImg, copyButton, copyScreenshotButton, closeButton, } closeButton.addEventListener('click', () => { stopInspector() }) copyButton.addEventListener('click', async () => { const text = overlay.htmlBlock.value || '' if (!text) { return } try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text) } else { const textarea = document.createElement('textarea') textarea.value = text textarea.setAttribute('readonly', '') textarea.style.position = 'absolute' textarea.style.left = '-9999px' document.body.appendChild(textarea) textarea.select() document.execCommand('copy') document.body.removeChild(textarea) } overlay.copyButton.textContent = 'Copied' setTimeout(() => { overlay.copyButton.textContent = 'Copy snippet' }, 1500) } catch (err) { overlay.copyButton.textContent = 'Copy failed' setTimeout(() => { overlay.copyButton.textContent = 'Copy snippet' }, 1500) } }) copyScreenshotButton.addEventListener('click', async () => { const img = overlay.screenshotImg if (!img.src) { return } try { // Convert data URL to blob const response = await fetch(img.src) const blob = await response.blob() if (navigator.clipboard && navigator.clipboard.write) { const clipboardItem = new ClipboardItem({ 'image/png': blob }) await navigator.clipboard.write([clipboardItem]) } else { throw new Error('Clipboard API not available') } overlay.copyScreenshotButton.textContent = 'Copied' setTimeout(() => { overlay.copyScreenshotButton.textContent = 'Copy screenshot' }, 1500) } catch (err) { console.warn('Screenshot copy failed:', err) overlay.copyScreenshotButton.textContent = 'Copy failed' setTimeout(() => { overlay.copyScreenshotButton.textContent = 'Copy screenshot' }, 1500) } }) state.overlay = overlay return overlay } const showOverlay = (message, frameUrl, html, screenshot) => { const overlay = ensureOverlay() if (overlay.container.parentNode) { overlay.container.parentNode.appendChild(overlay.container) } for (const node of document.querySelectorAll('.pinokio-inspector-overlay')) { if (node !== overlay.container) { node.style.display = 'none' } } overlay.container.style.display = 'flex' overlay.status.textContent = message || '' overlay.urlRow.textContent = frameUrl ? `Page: ${frameUrl}` : '' state.displayUrl = frameUrl || null // Handle HTML content if (html) { const pageUrl = frameUrl || state.displayUrl || state.lastUrl || '' const domPath = state.lastDomPath || '' const lines = [] if (pageUrl) { lines.push(`Page: ${pageUrl}`) } if (domPath) { lines.push(`DOM: ${domPath}`) } lines.push(`HTML: ${html}`) overlay.htmlSection.style.display = 'block' overlay.htmlBlock.style.display = 'block' overlay.htmlBlock.value = lines.join('\n') overlay.copyButton.style.display = 'inline-flex' overlay.copyButton.textContent = 'Copy snippet' } else { overlay.htmlSection.style.display = 'none' overlay.htmlBlock.style.display = 'none' overlay.htmlBlock.value = '' overlay.copyButton.style.display = 'none' } // Handle screenshot content if (screenshot) { overlay.screenshotImg.src = screenshot overlay.screenshotBlock.style.display = 'block' overlay.copyScreenshotButton.style.display = 'inline-flex' overlay.copyScreenshotButton.textContent = 'Copy screenshot' } else { overlay.screenshotImg.src = '' overlay.screenshotBlock.style.display = 'none' overlay.copyScreenshotButton.style.display = 'none' } } const hideOverlay = () => { const overlay = state.overlay if (overlay && overlay.container) { overlay.container.style.display = 'none' overlay.status.textContent = '' overlay.urlRow.textContent = '' overlay.htmlBlock.value = '' overlay.htmlSection.style.display = 'none' overlay.htmlBlock.style.display = 'none' overlay.copyButton.style.display = 'none' overlay.screenshotImg.src = '' overlay.screenshotBlock.style.display = 'none' overlay.copyScreenshotButton.style.display = 'none' } state.instructionsVisible = false state.closing = false state.displayUrl = null } const startInspector = async (button) => { if (state.active) { log('inspector already active') return } const target = selectVisibleIframe() if (!target) { showOverlay('No visible iframe found to inspect.', '', null) log('startInspector aborted: no target iframe') return } hideOverlay() state.active = true state.button = button || null state.lastFrameOrdinal = target.index state.lastRelativeOrdinal = target.relativeOrdinal state.lastUrl = target.url state.lastDomPath = null if (state.button) { state.button.classList.add('inspector-active') state.button.setAttribute('aria-pressed', 'true') } showOverlay('Inspect mode enabled – hover items and click to capture.', target.url || '', null) try { await window.electronAPI.startInspector({ frameIndex: target.index, frameUrl: target.url, candidateOrdinal: target.index, candidateRelativeOrdinal: target.relativeOrdinal, }) log('startInspector IPC resolved') } catch (error) { const message = error && error.message ? error.message : 'Unable to start inspect mode.' showOverlay(message, target.url || '', null) log(`startInspector IPC error: ${message}`) resetState() } } const stopInspector = () => { if (!state.active) { hideOverlay() return } window.electronAPI.stopInspector().catch(() => {}) resetState() hideOverlay() } const resetState = () => { state.active = false state.lastFrameOrdinal = null state.lastRelativeOrdinal = null state.lastUrl = null state.lastDomPath = null if (state.button) { state.button.classList.remove('inspector-active') state.button.removeAttribute('aria-pressed') state.button = null } } const handleToggleClick = (event) => { const button = event.target.closest('button') if (!button) { return } const isTrigger = ( button.id === 'inspector' || button.hasAttribute('data-pinokio-inspector') || button.classList.contains('pinokio-inspector-button') || (button.dataset && button.dataset.tippyContent === 'Switch to inspect mode') ) if (!isTrigger) { return } event.preventDefault() event.stopPropagation() if (state.active) { stopInspector() } else { startInspector(button) } } const handleInspectorMessage = (event) => { const data = event && event.data && event.data.pinokioInspector if (!data) { return } const frameUrl = typeof data.frameUrl === 'string' ? data.frameUrl : state.lastUrl if (data.type === 'started') { showOverlay('Inspect mode enabled – hover items and click to capture.', frameUrl || '', null) return } if (data.type === 'update') { const label = data.nodeName ? `<${String(data.nodeName).toLowerCase()}>` : '' if (Array.isArray(data.pathKeys) && data.pathKeys.length) { state.lastDomPath = data.pathKeys.join(' > ') } showOverlay(label ? `Hovering ${label}` : 'Inspect mode enabled – hover items and click to capture.', frameUrl || '', null) return } if (data.type === 'complete') { const html = typeof data.outerHTML === 'string' ? data.outerHTML : '' const screenshot = typeof data.screenshot === 'string' ? data.screenshot : null if (Array.isArray(data.pathKeys) && data.pathKeys.length) { state.lastDomPath = data.pathKeys.join(' > ') } showOverlay('Element captured. Inspect again or close.', frameUrl || '', html, screenshot) state.closing = true window.electronAPI.stopInspector().catch(() => {}).finally(() => { state.closing = false }) resetState() return } if (data.type === 'cancelled') { window.electronAPI.stopInspector().catch(() => {}) resetState() hideOverlay() return } if (data.type === 'error') { const message = data.message || 'Failed to inspect element.' if (Array.isArray(data.pathKeys) && data.pathKeys.length) { state.lastDomPath = data.pathKeys.join(' > ') } showOverlay(message, frameUrl || '', null) window.electronAPI.stopInspector().catch(() => {}) resetState() return } } ipcRenderer.on('pinokio:inspector-cancelled', () => { if (state.closing) { state.closing = false return } resetState() hideOverlay() }) ipcRenderer.on('pinokio:inspector-error', (_event, payload) => { const message = payload && payload.message ? payload.message : 'Inspect mode ended.' hideOverlay() showOverlay(message, payload && payload.frameUrl ? payload.frameUrl : '', null) resetState() }) ipcRenderer.on('pinokio:inspector-started', (_event, payload) => { const url = payload && payload.frameUrl ? payload.frameUrl : state.lastUrl showOverlay('Inspect mode enabled – hover items and click to capture.', url || '', null) }) ipcRenderer.on('pinokio:capture-debug-log', (_event, payload) => { try { const serialized = JSON.stringify(payload) console.log('[Pinokio Capture]', serialized) } catch (error) { console.log('[Pinokio Capture]', payload) } }) const logCaptureEvent = (label, payload) => { try { console.log('[Pinokio Capture]', JSON.stringify({ label, payload })) } catch (error) { console.log('[Pinokio Capture]', label) } } const processScreenshotRequest = async (screenshotRequest, messageId, source) => { logCaptureEvent('renderer-process-start', { messageId, relayStage: screenshotRequest && screenshotRequest.__pinokioRelayStage, relayComplete: screenshotRequest && screenshotRequest.__pinokioRelayComplete, adjustedFlag: screenshotRequest && screenshotRequest.__pinokioAdjusted, bounds: screenshotRequest && screenshotRequest.bounds ? screenshotRequest.bounds : null }) try { const screenshot = await window.electronAPI.captureScreenshot(screenshotRequest) source.postMessage({ pinokioScreenshotResponse: true, messageId: messageId, success: true, screenshot: screenshot }, '*') } catch (error) { console.error('Screenshot capture failed:', error) source.postMessage({ pinokioScreenshotResponse: true, messageId, success: false, error: error.message || 'Screenshot failed' }, '*') } } // Handle screenshot requests from iframes const handleScreenshotMessage = async (event) => { if (event.data && event.data.pinokioScreenshotRequest) { if (window !== window.top) { logCaptureEvent('renderer-ignored-non-top', { currentHref: window.location.href }) return } const screenshotRequest = event.data.pinokioScreenshotRequest const messageId = event.data.messageId const source = event.source logCaptureEvent('renderer-message-received', { messageId, relayStage: screenshotRequest.__pinokioRelayStage, relayComplete: screenshotRequest.__pinokioRelayComplete, adjustedFlag: screenshotRequest.__pinokioAdjusted, bounds: screenshotRequest && screenshotRequest.bounds ? screenshotRequest.bounds : null }) logCaptureEvent('renderer-skip-delegated', { messageId }) return } } window.addEventListener('message', handleInspectorMessage) window.addEventListener('message', handleScreenshotMessage) window.addEventListener('message', (event) => { if (!event || !event.data || event.source === window) { return } if (event.data.e !== 'pinokio-start-inspector') { return } try { console.log('[Inspector] start-request ' + JSON.stringify({ url: event.data.frameUrl || null, name: event.data.frameName || null, nodeId: event.data.frameNodeId || null, active: state.active, })) } catch (_) {} const payload = {} if (typeof event.data.frameUrl === 'string' && event.data.frameUrl.trim()) { payload.frameUrl = event.data.frameUrl.trim() } if (typeof event.data.frameName === 'string' && event.data.frameName.trim()) { payload.frameName = event.data.frameName.trim() } if (typeof event.data.frameNodeId === 'string' && event.data.frameNodeId.trim()) { payload.frameNodeId = event.data.frameNodeId.trim() } if (!payload.frameUrl && !payload.frameName && !payload.frameNodeId) { return } if (state.active) { try { console.log('[Inspector] stopping-current-before-start') } catch (_) {} stopInspector() } hideOverlay() state.active = true state.button = null state.lastFrameOrdinal = null state.lastRelativeOrdinal = null state.lastUrl = payload.frameUrl || null state.lastDomPath = null showOverlay('Inspect mode enabled – hover items and click to capture.', payload.frameUrl || '', null) window.electronAPI.startInspector(payload).then(() => { try { console.log('[Inspector] ipc-start-success ' + JSON.stringify(payload)) } catch (_) {} }).catch((error) => { const message = error && error.message ? error.message : 'Unable to start inspect mode.' showOverlay(message, payload.frameUrl || '', null) try { console.log('[Inspector] ipc-start-error ' + JSON.stringify({ message })) } catch (_) {} resetState() }) }) document.addEventListener('click', handleToggleClick, true) window.addEventListener('keydown', (event) => { if (event.key === 'Escape' && state.active) { stopInspector() } }) })() ================================================ FILE: prompt.html ================================================
================================================ FILE: script/patch.command ================================================ sudo -s xattr -d com.apple.quarantine /Applications/Pinokio.app ================================================ FILE: script/run-update-banner-test.js ================================================ const { spawn } = require('child_process') const cmd = process.platform === 'win32' ? 'npm.cmd' : 'npm' const child = spawn(cmd, ['run', 'start'], { stdio: 'inherit', env: { ...process.env, PINOKIO_TEST_UPDATE_BANNER: '1' } }) child.on('exit', (code) => { process.exit(code || 0) }) ================================================ FILE: script/zip.js ================================================ const { exec } = require('child_process'); const path = require('path') const fs = require('fs') const version = process.env.npm_package_version // Windows let exePath = path.resolve(__dirname, `../dist/Pinokio Setup ${version}.exe`) let zipPath = path.resolve(__dirname, `../dist/Pinokio-${version}-win32.zip`) exec(`zip -j "${zipPath}" "${exePath}"`, (error, stdout, stderr) => { if (error) { console.error(`Error executing command: ${error}`); return; } console.log('Command executed successfully.'); console.log('stdout:', stdout); console.log('stderr:', stderr); }); // Mac // find dmg files const macPaths = [{ dmg: path.resolve(__dirname, `../dist/Pinokio-${version}-arm64.dmg`), //temp: path.resolve(__dirname, `../dist/Pinokio-${version}-darwin-arm64-temp`), temp: `Pinokio-${version}-darwin-arm64`, //zip: path.resolve(__dirname, `../dist/Pinokio-${version}-darwin-arm64.zip`), zip: `Pinokio-${version}-darwin-arm64.zip` }, { dmg: path.resolve(__dirname, `../dist/Pinokio-${version}.dmg`), //temp: path.resolve(__dirname, `../dist/Pinokio-${version}-darwin-intel-temp`), temp: `Pinokio-${version}-darwin-intel`, //zip: path.resolve(__dirname, `../dist/Pinokio-${version}-darwin-intel.zip`) zip: `Pinokio-${version}-darwin-intel.zip` }] let sentinelPath = path.resolve(__dirname, `../assets/Sentinel.app`) for(let macPath of macPaths) { const zipPath = macPath.zip try { console.log("mkdirSync", path.resolve(__dirname, "../dist", macPath.temp)) fs.mkdirSync(path.resolve(__dirname, "../dist", macPath.temp), { recursive: true }) } catch (e) { console.log("E1", e) } try { fs.cpSync(macPath.dmg, path.resolve(__dirname, "../dist", macPath.temp, "install.dmg"), { force: true, recursive: true }) } catch (e) { console.log("E2", e) } try { fs.cpSync(sentinelPath, path.resolve(__dirname, "../dist", macPath.temp, "Sentinel.app"), { force: true, recursive: true }) } catch (e) { console.log("E3", e) } const cmd = `zip -r "${zipPath}" "${macPath.temp}"` console.log({ cmd }) exec(cmd, { cwd: path.resolve(__dirname, "../dist") }, (error, stdout, stderr) => { if (error) { console.error(`Error executing command: ${error}`); return; } console.log('Command executed successfully.'); console.log('stdout:', stdout); console.log('stderr:', stderr); }); // try { // fs.rmSync(path.resolve(__dirname, "../dist", macPath.temp), { recursive: true }) // } catch (e) { // } } let rmFiles = [ `Pinokio-${version}-arm64-mac.zip`, `Pinokio-${version}-mac.zip`, // `Pinokio-${version}-darwin-arm64`, // `Pinokio-${version}-darwin-intel`, ] for(let f of rmFiles) { try { fs.rmSync(path.resolve(__dirname, "../dist", f), { recursive: true }) } catch (e) { } } ================================================ FILE: splash.html ================================================

Starting Pinokio…

This should only take a moment.

================================================ FILE: temp/rebuild.js ================================================ "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.rebuild = exports.Rebuilder = void 0; const debug_1 = __importDefault(require("debug")); const events_1 = require("events"); const fs = __importStar(require("fs-extra")); const nodeAbi = __importStar(require("node-abi")); const os = __importStar(require("os")); const path = __importStar(require("path")); const cache_1 = require("./cache"); const types_1 = require("./types"); const module_rebuilder_1 = require("./module-rebuilder"); const module_walker_1 = require("./module-walker"); const d = (0, debug_1.default)('electron-rebuild'); const defaultMode = 'sequential'; const defaultTypes = ['prod', 'optional']; class Rebuilder { constructor(options) { console.log("options", options) var _a; this.platform = options.platform || process.platform; this.lifecycle = options.lifecycle; this.buildPath = options.buildPath; this.electronVersion = options.electronVersion; this.arch = options.arch || process.arch; this.force = options.force || false; this.headerURL = options.headerURL || 'https://www.electronjs.org/headers'; this.mode = options.mode || defaultMode; this.debug = options.debug || false; this.useCache = options.useCache || false; this.useElectronClang = options.useElectronClang || false; this.cachePath = options.cachePath || path.resolve(os.homedir(), '.electron-rebuild-cache'); this.prebuildTagPrefix = options.prebuildTagPrefix || 'v'; this.msvsVersion = process.env.GYP_MSVS_VERSION; this.disablePreGypCopy = options.disablePreGypCopy || false; if (this.useCache && this.force) { console.warn('[WARNING]: Electron Rebuild has force enabled and cache enabled, force take precedence and the cache will not be used.'); this.useCache = false; } if (typeof this.electronVersion === 'number') { if (`${this.electronVersion}`.split('.').length === 1) { this.electronVersion = `${this.electronVersion}.0.0`; } else { this.electronVersion = `${this.electronVersion}.0`; } } if (typeof this.electronVersion !== 'string') { throw new Error(`Expected a string version for electron version, got a "${typeof this.electronVersion}"`); } this.ABIVersion = (_a = options.forceABI) === null || _a === void 0 ? void 0 : _a.toString(); const onlyModules = options.onlyModules || null; const extraModules = (options.extraModules || []).reduce((acc, x) => acc.add(x), new Set()); const types = options.types || defaultTypes; this.moduleWalker = new module_walker_1.ModuleWalker(this.buildPath, options.projectRootPath, types, extraModules, onlyModules); this.rebuilds = []; d('rebuilding with args:', this.buildPath, this.electronVersion, this.arch, extraModules, this.force, this.headerURL, types, this.debug); console.log("THIS>PLATFORM", this.platform) } get ABI() { if (this.ABIVersion === undefined) { this.ABIVersion = nodeAbi.getAbi(this.electronVersion, 'electron'); } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this.ABIVersion; } get buildType() { return this.debug ? types_1.BuildType.Debug : types_1.BuildType.Release; } async rebuild() { if (!path.isAbsolute(this.buildPath)) { throw new Error('Expected buildPath to be an absolute path'); } this.lifecycle.emit('start'); await this.moduleWalker.walkModules(); for (const nodeModulesPath of await this.moduleWalker.nodeModulesPaths) { await this.moduleWalker.findAllModulesIn(nodeModulesPath); } for (const modulePath of this.moduleWalker.modulesToRebuild) { this.rebuilds.push(() => this.rebuildModuleAt(modulePath)); } this.rebuilds.push(() => this.rebuildModuleAt(this.buildPath)); if (this.mode !== 'sequential') { await Promise.all(this.rebuilds.map(fn => fn())); } else { for (const rebuildFn of this.rebuilds) { await rebuildFn(); } } } async rebuildModuleAt(modulePath) { if (!(await fs.pathExists(path.resolve(modulePath, 'binding.gyp')))) { return; } const moduleRebuilder = new module_rebuilder_1.ModuleRebuilder(this, modulePath); this.lifecycle.emit('module-found', path.basename(modulePath)); if (!this.force && await moduleRebuilder.alreadyBuiltByRebuild()) { d(`skipping: ${path.basename(modulePath)} as it is already built`); this.lifecycle.emit('module-done'); this.lifecycle.emit('module-skip'); return; } if (await moduleRebuilder.prebuildInstallNativeModuleExists()) { d(`skipping: ${path.basename(modulePath)} as it was prebuilt`); return; } let cacheKey; if (this.useCache) { cacheKey = await (0, cache_1.generateCacheKey)({ ABI: this.ABI, arch: this.arch, debug: this.debug, electronVersion: this.electronVersion, headerURL: this.headerURL, modulePath, }); const applyDiffFn = await (0, cache_1.lookupModuleState)(this.cachePath, cacheKey); if (typeof applyDiffFn === 'function') { await applyDiffFn(modulePath); this.lifecycle.emit('module-done'); return; } } if (await moduleRebuilder.rebuild(cacheKey)) { this.lifecycle.emit('module-done'); } } } exports.Rebuilder = Rebuilder; function rebuild(options) { console.log("(rebuild)", options) // eslint-disable-next-line prefer-rest-params d('rebuilding with args:', arguments); const lifecycle = new events_1.EventEmitter(); const rebuilderOptions = { ...options, lifecycle }; const rebuilder = new Rebuilder(rebuilderOptions); const ret = rebuilder.rebuild(); ret.lifecycle = lifecycle; return ret; } exports.rebuild = rebuild; //# sourceMappingURL=rebuild.js.map ================================================ FILE: temp/yarn.js ================================================ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.rebuild = exports.nodeGypRebuild = exports.getGypEnv = exports.installOrRebuild = void 0; const builder_util_1 = require("builder-util"); const fs_extra_1 = require("fs-extra"); const os_1 = require("os"); const path = require("path"); const electronVersion_1 = require("../electron/electronVersion"); const electronRebuild = require("@electron/rebuild"); const searchModule = require("@electron/rebuild/lib/src/search-module"); async function installOrRebuild(config, appDir, options, forceInstall = false) { console.log("install or rebuild", { config, options }) let isDependenciesInstalled = false; for (const fileOrDir of ["node_modules", ".pnp.js"]) { if (await (0, fs_extra_1.pathExists)(path.join(appDir, fileOrDir))) { isDependenciesInstalled = true; break; } } if (forceInstall || !isDependenciesInstalled) { const effectiveOptions = { buildFromSource: config.buildDependenciesFromSource === true, additionalArgs: (0, builder_util_1.asArray)(config.npmArgs), ...options, }; await installDependencies(appDir, effectiveOptions); } else { await rebuild(appDir, config.buildDependenciesFromSource === true, options); } } exports.installOrRebuild = installOrRebuild; function getElectronGypCacheDir() { return path.join((0, os_1.homedir)(), ".electron-gyp"); } function getGypEnv(frameworkInfo, platform, arch, buildFromSource) { const npmConfigArch = arch === "armv7l" ? "arm" : arch; const common = { ...process.env, npm_config_arch: npmConfigArch, npm_config_target_arch: npmConfigArch, npm_config_platform: platform, npm_config_build_from_source: buildFromSource, // required for node-pre-gyp npm_config_target_platform: platform, npm_config_update_binary: true, npm_config_fallback_to_build: true, }; if (platform !== process.platform) { common.npm_config_force = "true"; } if (platform === "win32" || platform === "darwin") { common.npm_config_target_libc = "unknown"; } if (!frameworkInfo.useCustomDist) { return common; } // https://github.com/nodejs/node-gyp/issues/21 return { ...common, npm_config_disturl: "https://electronjs.org/headers", npm_config_target: frameworkInfo.version, npm_config_runtime: "electron", npm_config_devdir: getElectronGypCacheDir(), }; } exports.getGypEnv = getGypEnv; function checkYarnBerry() { var _a; const npmUserAgent = process.env["npm_config_user_agent"] || ""; const regex = /yarn\/(\d+)\./gm; const yarnVersionMatch = regex.exec(npmUserAgent); const yarnMajorVersion = Number((_a = yarnVersionMatch === null || yarnVersionMatch === void 0 ? void 0 : yarnVersionMatch[1]) !== null && _a !== void 0 ? _a : 0); return yarnMajorVersion >= 2; } function installDependencies(appDir, options) { const platform = options.platform || process.platform; const arch = options.arch || process.arch; const additionalArgs = options.additionalArgs; builder_util_1.log.info({ platform, arch, appDir }, `installing production dependencies`); let execPath = process.env.npm_execpath || process.env.NPM_CLI_JS; const execArgs = ["install"]; const isYarnBerry = checkYarnBerry(); if (!isYarnBerry) { if (process.env.NPM_NO_BIN_LINKS === "true") { execArgs.push("--no-bin-links"); } execArgs.push("--production"); } if (!isRunningYarn(execPath)) { execArgs.push("--prefer-offline"); } if (execPath == null) { execPath = getPackageToolPath(); } else if (!isYarnBerry) { execArgs.unshift(execPath); execPath = process.env.npm_node_execpath || process.env.NODE_EXE || "node"; } if (additionalArgs != null) { execArgs.push(...additionalArgs); } return (0, builder_util_1.spawn)(execPath, execArgs, { cwd: appDir, env: getGypEnv(options.frameworkInfo, platform, arch, options.buildFromSource === true), }); } async function nodeGypRebuild(arch) { return rebuild(process.cwd(), false, arch); } exports.nodeGypRebuild = nodeGypRebuild; function getPackageToolPath() { if (process.env.FORCE_YARN === "true") { return process.platform === "win32" ? "yarn.cmd" : "yarn"; } else { return process.platform === "win32" ? "npm.cmd" : "npm"; } } function isRunningYarn(execPath) { const userAgent = process.env.npm_config_user_agent; return process.env.FORCE_YARN === "true" || (execPath != null && path.basename(execPath).startsWith("yarn")) || (userAgent != null && /\byarn\b/.test(userAgent)); } /** @internal */ async function rebuild(appDir, buildFromSource, options) { builder_util_1.log.info({ appDir, arch: options.arch, platform: options.platform }, "executing @electron/rebuild"); const effectiveOptions = { buildPath: appDir, electronVersion: await (0, electronVersion_1.getElectronVersion)(appDir), arch: options.arch, platform: options.platform, force: true, debug: builder_util_1.log.isDebugEnabled, projectRootPath: await searchModule.getProjectRootPath(appDir), }; if (buildFromSource) { effectiveOptions.prebuildTagPrefix = "totally-not-a-real-prefix-to-force-rebuild"; } return electronRebuild.rebuild(effectiveOptions); } exports.rebuild = rebuild; //# sourceMappingURL=yarn.js.map ================================================ FILE: update-banner.html ================================================ Pinokio Update ================================================ FILE: updater.js ================================================ const { autoUpdater } = require("electron-updater"); const ProgressBar = require('electron-progressbar'); const { dialog } = require('electron'); class Updater { constructor(handlers = {}) { this.handlers = handlers || {}; this.mainWindow = null; this.progressBar = null; } setHandlers(handlers = {}) { this.handlers = handlers || {}; } run(mainWindow, handlers = null) { if (handlers) { this.setHandlers(handlers); } this.mainWindow = mainWindow || null; autoUpdater.autoDownload = false; autoUpdater.on('checking-for-update', () => { console.log('Checking for update...'); }); autoUpdater.on('update-available', (info) => { console.log('Update available:', info.version); if (this.handlers && typeof this.handlers.onUpdateAvailable === 'function') { this.handlers.onUpdateAvailable(info); return; } this.showDefaultUpdatePrompt(info); }); autoUpdater.on('update-not-available', () => { console.log('No update available.'); if (this.handlers && typeof this.handlers.onUpdateNotAvailable === 'function') { this.handlers.onUpdateNotAvailable(); } }); autoUpdater.on("download-progress", (progress) => { console.log(`Downloaded ${Math.round(progress.percent)}%`); if (this.handlers && typeof this.handlers.onDownloadProgress === 'function') { this.handlers.onDownloadProgress(progress); return; } this.updateDefaultProgress(progress); }); autoUpdater.on("update-downloaded", (info) => { console.log("Update downloaded:", info.version); if (this.handlers && typeof this.handlers.onUpdateDownloaded === 'function') { this.handlers.onUpdateDownloaded(info); return; } this.showDefaultRestartPrompt(info); }); autoUpdater.on("error", (err) => { console.error("Update error:", err); if (this.handlers && typeof this.handlers.onError === 'function') { this.handlers.onError(err); return; } this.closeProgressBar(); }); autoUpdater.checkForUpdates().catch((err) => { // The updater promise rejects on recoverable network errors; log and continue. console.error('Failed to check for updates:', err) }); } downloadUpdate() { return autoUpdater.downloadUpdate(); } quitAndInstall() { autoUpdater.quitAndInstall(); } showDefaultUpdatePrompt(info) { const targetWindow = this.mainWindow; dialog.showMessageBox(targetWindow, { type: 'question', buttons: ['Yes', 'No'], defaultId: 0, cancelId: 1, title: 'Update Available', message: `Version ${info.version} is available. Do you want to download it now?` }).then(result => { if (result.response === 0) { this.startDefaultDownload(); } }); } startDefaultDownload() { if (this.progressBar) { this.progressBar.close(); this.progressBar = null; } this.progressBar = new ProgressBar({ indeterminate: false, text: "Downloading update...", detail: "Please wait...", browserWindow: { parent: this.mainWindow, modal: true, closable: false, minimizable: false, maximizable: false, width: 400, height: 120 } }); autoUpdater.downloadUpdate(); } updateDefaultProgress(progress) { if (this.progressBar && !this.progressBar.isCompleted()) { this.progressBar.value = Math.floor(progress.percent); this.progressBar.detail = `Downloaded ${Math.round(progress.percent)}% (${(progress.transferred / 1024 / 1024).toFixed(2)} MB of ${(progress.total / 1024 / 1024).toFixed(2)} MB)`; } } showDefaultRestartPrompt(info) { if (this.progressBar && !this.progressBar.isCompleted()) { this.progressBar.setCompleted(); this.progressBar = null; } dialog.showMessageBox(this.mainWindow, { type: "info", buttons: ["Restart Now", "Later"], title: "Update Ready", message: "A new version has been downloaded. Restart the application to apply the updates?" }).then((result) => { if (result.response === 0) { autoUpdater.quitAndInstall(); } }); } closeProgressBar() { if (this.progressBar && !this.progressBar.isCompleted()) { this.progressBar.close(); this.progressBar = null; } } } module.exports = Updater; ================================================ FILE: wrap-linux-launcher.js ================================================ const fs = require('fs') const path = require('path') module.exports = async (context) => { const { appOutDir, electronPlatformName, packager } = context if (electronPlatformName !== 'linux') { return } const exeName = packager.executableName || packager.appInfo.productFilename const exePath = path.join(appOutDir, exeName) const wrappedExePath = path.join(appOutDir, `${exeName}-bin`) if (!fs.existsSync(exePath)) { console.warn(`[wrap-linux-launcher] Executable not found at ${exePath}, skipping wrapper`) return } const originalStat = fs.statSync(exePath) fs.renameSync(exePath, wrappedExePath) const wrapperScript = `#!/usr/bin/env sh export ELECTRON_OZONE_PLATFORM_HINT=x11 export ELECTRON_DISABLE_GPU=1 SCRIPT_PATH="$0" RESOLVED_PATH="" if command -v readlink >/dev/null 2>&1; then RESOLVED_PATH="$(readlink -f "$SCRIPT_PATH" 2>/dev/null || true)" fi if [ -z "$RESOLVED_PATH" ] && command -v realpath >/dev/null 2>&1; then RESOLVED_PATH="$(realpath "$SCRIPT_PATH" 2>/dev/null || true)" fi if [ -n "$RESOLVED_PATH" ]; then SCRIPT_DIR="$(dirname "$RESOLVED_PATH")" else SCRIPT_DIR="$(dirname "$SCRIPT_PATH")" fi OPT_BIN="/opt/Pinokio/${exeName}-bin" LOCAL_BIN="$SCRIPT_DIR/${exeName}-bin" if [ -x "$OPT_BIN" ]; then TARGET_BIN="$OPT_BIN" else TARGET_BIN="$LOCAL_BIN" fi exec "$TARGET_BIN" --ozone-platform=x11 --disable-gpu --disable-gpu-sandbox "$@" ` fs.writeFileSync(exePath, wrapperScript, { mode: originalStat.mode || 0o755 }) }