Showing preview only (275K chars total). Download the full file or copy to clipboard to get everything.
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.

## 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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>
================================================
FILE: build/entitlements.mac.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>
================================================
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 = `<html><body><form><label for="val">${arg.title}</label>
<input id="val" value="${arg.val}" autofocus />
<button id='ok'>OK</button>
<button id='cancel'>Cancel</button></form>
<style>body {font-family: sans-serif;} form {padding: 5px; } button {float:right; margin-left: 10px;} label { display: block; margin-bottom: 5px; width: 100%; } input {margin-bottom: 10px; padding: 5px; width: 100%; display:block;}</style>
<script>
document.querySelector("#cancel").addEventListener("click", (e) => {
debugger
e.preventDefault()
e.stopPropagation()
window.close()
})
document.querySelector("form").addEventListener("submit", (e) => {
e.preventDefault()
e.stopPropagation()
debugger
window.electronAPI.send('prompt-response', document.querySelector("#val").value)
window.close()
})
</script></body></html>`
// 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 || '<unset>')
console.log('[PINOKIO DEBUG] ELECTRON_DISABLE_GPU:', process.env.ELECTRON_DISABLE_GPU || '<unset>')
console.log('[PINOKIO DEBUG] DISPLAY:', process.env.DISPLAY || '<unset>')
console.log('[PINOKIO DEBUG] WAYLAND_DISPLAY:', process.env.WAYLAND_DISPLAY || '<unset>')
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 <cocktailpeanuts@proton.me>",
"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
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self' 'unsafe-inline'; img-src 'self' data:;"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Popup Toolbar</title>
<style>
:root {
color-scheme: light;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
html, body {
margin: 0;
padding: 0;
background: #f8fafc;
color: #0f172a;
overflow: hidden;
}
body {
height: 100vh;
border-bottom: 1px solid rgba(15, 23, 42, 0.08);
}
.popup-toolbar {
height: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
box-sizing: border-box;
}
.popup-toolbar__group {
display: flex;
align-items: center;
gap: 8px;
flex: 0 0 auto;
}
.popup-toolbar__logo {
width: 18px;
height: 18px;
display: block;
}
.popup-toolbar__url {
flex: 1 1 auto;
min-width: 0;
padding: 7px 10px;
border-radius: 8px;
background: #ffffff;
border: 1px solid rgba(15, 23, 42, 0.12);
font-size: 12px;
line-height: 1.3;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
user-select: text;
}
.popup-toolbar__button {
flex: 0 0 auto;
border: 0;
border-radius: 6px;
padding: 6px 8px;
background: transparent;
color: #0f172a;
cursor: pointer;
font: inherit;
font-size: 12px;
font-weight: 500;
}
.popup-toolbar__button:hover {
background: rgba(15, 23, 42, 0.06);
}
.popup-toolbar__button--icon {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 30px;
padding: 6px;
}
.popup-toolbar__button:disabled {
cursor: default;
opacity: 0.55;
}
</style>
</head>
<body>
<div class="popup-toolbar">
<div class="popup-toolbar__group">
<button class="popup-toolbar__button popup-toolbar__button--icon" id="open-home" type="button" title="Open Pinokio Home">
<img class="popup-toolbar__logo" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAIKADAAQAAAABAAAAIAAAAABfvA/wAAAACXBIWXMAAAsTAAALEwEAmpwYAAACyGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj42NDwvZXhpZjpQaXhlbFhEaW1lbnNpb24+CiAgICAgICAgIDxleGlmOkNvbG9yU3BhY2U+MTwvZXhpZjpDb2xvclNwYWNlPgogICAgICAgICA8ZXhpZjpQaXhlbFlEaW1lbnNpb24+NjQ8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4Kiv76YwAABgZJREFUWAmtV21I1VcYP/dmojNlihDOLF2ZOVEaZmVlmZLULEJbkVYQNAQHMjYYUxesj/s4kMYYNDJoflgTpGBzqB8cK8zKUVhZrRfNd2umWOh9Ofv9Hv/n31/tXnX0wLnnf57zvD/Pec65LrVwcIHUjcHZj6ExCAbPtRMvm2/jh0qXLEIQacmzIKD1gcB45iNBWVlZ7JMnT7bevn07q6enJwmoSItxbNWqVY9SU1PbV69e/dfp06efWfgQzF7re9GTCbU6cOBAclpaWg0kDGAwzHr79u26tLRUl5SU6JycHMFZe/0ZGRnfged9rAnGienVAn/t8G3evPlr8Exg6JMnT+qWlhZfd3e3Z3x83Ds1NSVjbGzMi8h4mpubfdXV1caYcfB+5dBny3Tg3vgphHv27IlKT0v/jYorKip0V1eXR2vtxwgKfsCdO3c85eXlYgii0bBv3753LE3zGiH1cOzYsYjk5OQ2Kq+rq5vyeDy2Yp/Pp6FDhrHErLlnANHxnzt3booyIOvP48ePh1lGiA7re84kFqanp/9CxosXL04agU7hBsfZKOdswElbX18/SVmQed7SFjAKcsy2bNlSToYzZ85MORUY4f9nNpHYmp19wjJCdFnfMklYUNGxWPWxul+9eiXxdHpmlBvc6Oiovnv3rr5//75++PChdno+MTEhe8TfunXLl52dzZrohuxoS/GMVPC8qg0bNnyJSV+5coUFN0OgUe7Eg85UvC4sLKTRNhmK1t7DkdQXLlzwUDbq4TPqAohOfjAnXnC6rl27dujw4cNq/fr1kie3O2C6yKfMPpqQWrZsmXK5XjvFvfDwcJWYmKieP3+uEAEXClEhWoeE2dGgRMvRo0dTsZEBT1RYWJgLBll0gSejMDo6WoWEhCgnD9KkEBHBDw4OUqZ7586dFPbhwYMHUyypYrGEAs0lA8jQlJQUXiZuCjMKLGJ7Mp6jstXQ0JDQERcaGmrTrF27VqFByR6NiYqKcqEnUHY48EmYuzIzM0OuX7/uEQP6+vriyR0TEyMGBFJOGgPwip6ZpcwMd3t7u1q6dKlasmS62OkMDURBCg265on8/PwOdM5BIKbzDOu/iYuL0729vVKAptLBHBRIx2FOACreLj4In/MNo3ixEd+LyysZ83Q1Io9eHCsFQcQtGEykzLxmzRrV398v3sPyGXK4hgFuRGBy48aN74HuUxB8LiGA98MsmhcvXkhhzGaeISnIgqFm+FmUnM3gminBWiPafq4BrxsSqp8lqhsbG6WnmpAGjb9j09DPkwL/8uXLTVqGcMV/QCvElJUrV/6N7/62tra4goICDU8kEiRYCNBzQnx8vLp06ZIUHaPIlFrR1DU1Na6mpqYRKP4+IiLi/NWrV++BRRglFGCuBWLRhTg5OalR/TKQQv3y5UsZbMcGhoeHPbGxsRqp/oGGWiDKp00HBuf0J240NDQIzrLcop07mX0eL/YE3PsqLy9P4XyrTZs2qaSkJHXvHp1Uqra21j0yMsI9Oqlyc3MZeR55G0TpunXrfgVG37x5U46j18su/WYwRzVY3p8+farxQJF3AUL/s6XtdfHZ6q1c7N69OxG4Zzt27NDocqLdFNibzdAanU3jocpXk9yAvCE7Ozv148eP9YMHD7yUBZmDe/fulWaHbzvq+J4BUpDo2R8Bq4uKijTOqhhhmo3xOpAxTjzy7i0uLpaq37VrV76lSXTM0DprIQTbtm0rpRG4G/Tly5f5OLGfPMYYRsY5jHLg/K2trRJ2yPAj3x9bOuZVbmwRQvTrHCAeYciLuKOjw4MKZ0RojG2QWXPvxo0bnqqqKvEa3fEfyMieT3mg804jvHzBIKfVEPwJ1u/u379f8VpNSEjgDSeyeevhNlV4siu8I4n7Nysr60cU3bdnz54dxVpkCfEif+xqxUMlwXox/QEZ/IPCxyb/9XDwm7jf0eO/OHLkyAp8G7BlGMTsOVAEDB33WbX2LVVZWRk9MDCwAs1HQhAZGTmGp1bPqVOn6K0BKuY5ZzreCtAQhjKYwQuhmWNMMIFziC0EeWbzSeEFYgiG/w8u2Su57ZNoZAAAAABJRU5ErkJggg==" alt="Pinokio" />
</button>
<button class="popup-toolbar__button popup-toolbar__button--icon" id="go-back" type="button" title="Back" disabled>←</button>
<button class="popup-toolbar__button popup-toolbar__button--icon" id="go-forward" type="button" title="Forward" disabled>→</button>
<button class="popup-toolbar__button popup-toolbar__button--icon" id="refresh" type="button" title="Refresh">↻</button>
</div>
<div class="popup-toolbar__url" id="popup-url" title="Loading...">Loading...</div>
<button class="popup-toolbar__button" id="open-browser" type="button" disabled>Open in Browser</button>
</div>
<script>
const { ipcRenderer, shell } = require('electron')
const state = { url: '', canGoBack: false, canGoForward: false }
const urlNode = document.getElementById('popup-url')
const homeButton = document.getElementById('open-home')
const backButton = document.getElementById('go-back')
const forwardButton = document.getElementById('go-forward')
const refreshButton = document.getElementById('refresh')
const openButton = document.getElementById('open-browser')
const render = () => {
const value = state.url || 'Loading...'
urlNode.textContent = value
urlNode.title = value
backButton.disabled = !state.canGoBack
forwardButton.disabled = !state.canGoForward
openButton.disabled = !state.url
}
ipcRenderer.on('pinokio:popup-shell-state', (_event, payload = {}) => {
state.url = typeof payload.url === 'string' ? payload.url : ''
state.canGoBack = Boolean(payload.canGoBack)
state.canGoForward = Boolean(payload.canGoForward)
render()
})
homeButton.addEventListener('click', () => {
ipcRenderer.send('pinokio:popup-shell-open-home')
})
backButton.addEventListener('click', () => {
ipcRenderer.send('pinokio:popup-shell-back')
})
forwardButton.addEventListener('click', () => {
ipcRenderer.send('pinokio:popup-shell-forward')
})
refreshButton.addEventListener('click', () => {
ipcRenderer.send('pinokio:popup-shell-refresh')
})
openButton.addEventListener('click', () => {
if (!state.url) {
return
}
shell.openExternal(state.url).catch(() => {})
})
render()
</script>
</body>
</html>
================================================
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:
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
SYMBOL INDEX (43 symbols across 6 files)
FILE: full.js
constant UPDATE_RELEASES_URL (line 33) | const UPDATE_RELEASES_URL = 'https://github.com/peanutcocktail/pinokio/r...
constant PORT (line 189) | let PORT
constant ENABLE_BROWSER_CONSOLE_LOG (line 201) | const ENABLE_BROWSER_CONSOLE_LOG = process.env.PINOKIO_BROWSER_LOG === '1'
constant PINOKIO_INJECT_ISOLATED_WORLD_ID (line 213) | const PINOKIO_INJECT_ISOLATED_WORLD_ID = 42000
constant SESSION_COOKIE_TTL_DAYS (line 531) | const SESSION_COOKIE_TTL_DAYS = 90
constant SESSION_COOKIE_TTL_SEC (line 532) | const SESSION_COOKIE_TTL_SEC = SESSION_COOKIE_TTL_DAYS * 24 * 60 * 60
constant SESSION_COOKIE_JAR_FILENAME (line 533) | const SESSION_COOKIE_JAR_FILENAME = 'session-cookies.json'
function UpsertKeyValue (line 700) | function UpsertKeyValue(obj, keyToChange, value) {
constant PINOKIO_ABSOLUTE_URL_PATTERN (line 2073) | const PINOKIO_ABSOLUTE_URL_PATTERN = /^[a-zA-Z][a-zA-Z\d+\-.]*:/
method trigger (line 2158) | trigger(eventName, payload = {}, context = {}) {
method register (line 2173) | register(definition) {
method run (line 2193) | run(descriptor, context, runSource) {
method unmountAll (line 2211) | unmountAll() {
FILE: patch-linux-arm64-natives.js
constant ARCH_BY_ENUM (line 4) | const ARCH_BY_ENUM = {
constant ELF_MACHINE_AARCH64 (line 12) | const ELF_MACHINE_AARCH64 = 183
FILE: preload.js
function syncPinokioInjectors (line 328) | async function syncPinokioInjectors(reason = 'load') {
FILE: temp/rebuild.js
class Rebuilder (line 43) | class Rebuilder {
method constructor (line 44) | constructor(options) {
method ABI (line 86) | get ABI() {
method buildType (line 93) | get buildType() {
method rebuild (line 96) | async rebuild() {
method rebuildModuleAt (line 118) | async rebuildModuleAt(modulePath) {
function rebuild (line 157) | function rebuild(options) {
FILE: temp/yarn.js
function installOrRebuild (line 11) | async function installOrRebuild(config, appDir, options, forceInstall = ...
function getElectronGypCacheDir (line 33) | function getElectronGypCacheDir() {
function getGypEnv (line 36) | function getGypEnv(frameworkInfo, platform, arch, buildFromSource) {
function checkYarnBerry (line 68) | function checkYarnBerry() {
function installDependencies (line 76) | function installDependencies(appDir, options) {
function nodeGypRebuild (line 108) | async function nodeGypRebuild(arch) {
function getPackageToolPath (line 112) | function getPackageToolPath() {
function isRunningYarn (line 120) | function isRunningYarn(execPath) {
function rebuild (line 125) | async function rebuild(appDir, buildFromSource, options) {
FILE: updater.js
class Updater (line 5) | class Updater {
method constructor (line 6) | constructor(handlers = {}) {
method setHandlers (line 12) | setHandlers(handlers = {}) {
method run (line 16) | run(mainWindow, handlers = null) {
method downloadUpdate (line 76) | downloadUpdate() {
method quitAndInstall (line 80) | quitAndInstall() {
method showDefaultUpdatePrompt (line 84) | showDefaultUpdatePrompt(info) {
method startDefaultDownload (line 100) | startDefaultDownload() {
method updateDefaultProgress (line 122) | updateDefaultProgress(progress) {
method showDefaultRestartPrompt (line 129) | showDefaultRestartPrompt(info) {
method closeProgressBar (line 146) | closeProgressBar() {
Condensed preview — 33 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (281K chars).
[
{
"path": ".github/workflows/build.yml",
"chars": 15404,
"preview": "name: Build/release\n\non:\n push:\n branches:\n - main\n\n#on: workflow_dispatch\n\njobs:\n\n create-release:\n runs-o"
},
{
"path": ".github/workflows/test.yml",
"chars": 778,
"preview": "name: Test\n\n#on: push\non: workflow_dispatch\n\njobs:\n print:\n runs-on: ubuntu-latest\n steps:\n - name: Checkout"
},
{
"path": ".gitignore",
"chars": 50,
"preview": "node_modules\ndist\ncache\npackage-lock.json\n.claude\n"
},
{
"path": "LICENSE",
"chars": 1047,
"preview": "Copyright 2023 Pinokio\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software an"
},
{
"path": "README.md",
"chars": 6437,
"preview": "# Pinokio\n\nLaunch Anything.\n\n# Script Policy\n\nPinokio is a 1-click launcher for any open-source project. Think of it as "
},
{
"path": "RELEASE.md",
"chars": 349,
"preview": "# Pinokio Release\n\n## Code Signing Policy\n\nFree code signing provided by [SignPath.io](https://signpath.io/), certificat"
},
{
"path": "after-pack.js",
"chars": 321,
"preview": "module.exports = async (context) => {\n const chmodHandler = require('./chmod')\n const wrapLinuxLauncher = require('./w"
},
{
"path": "build/entitlements.mac.inherit.plist",
"chars": 409,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "build/entitlements.mac.plist",
"chars": 534,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "build/installer.nsh",
"chars": 155,
"preview": "# https://github.com/electron-userland/electron-builder/issues/6865#issuecomment-1871121350\n!macro customInit\n Delete \""
},
{
"path": "build/sign.js",
"chars": 104,
"preview": "module.exports = async function () {\n // no-op: prevents electron-builder from calling signtool.exe\n};\n"
},
{
"path": "chmod.js",
"chars": 599,
"preview": "const exec = require('child_process').exec;\nmodule.exports = async (context) => {\n const paths = [\n `${context.appOu"
},
{
"path": "config.js",
"chars": 718,
"preview": "const Store = require('electron-store');\nconst packagejson = require(\"./package.json\")\nconst store = new Store();\nmodule"
},
{
"path": "full.js",
"chars": 128509,
"preview": "const {app, screen, shell, BrowserWindow, BrowserView, ipcMain, dialog, clipboard, session, desktopCapturer, systemPrefe"
},
{
"path": "linux_build.sh",
"chars": 228,
"preview": "docker run --rm -ti \\\n -v \"$PWD:/project\" \\\n -w /project \\\n -e SNAPCRAFT_BUILD_ENVIRONMENT=host \\\n -e SNAP_DESTRUCTI"
},
{
"path": "main.js",
"chars": 951,
"preview": "const { app } = require('electron')\nconst Pinokiod = require(\"pinokiod\")\nconst config = require('./config')\nconst pinoki"
},
{
"path": "minimal.js",
"chars": 6924,
"preview": "const { app, Tray, Menu, shell, nativeImage, BrowserWindow, session, Notification } = require('electron');\nconst path = "
},
{
"path": "package.json",
"chars": 4994,
"preview": "{\n \"name\": \"Pinokio\",\n \"private\": true,\n \"version\": \"7.0.0\",\n \"homepage\": \"https://pinokio.co\",\n \"description\": \"pi"
},
{
"path": "patch-linux-arm64-natives.js",
"chars": 3415,
"preview": "const fs = require('fs')\nconst path = require('path')\n\nconst ARCH_BY_ENUM = {\n 0: 'ia32',\n 1: 'x64',\n 2: 'armv7l',\n "
},
{
"path": "popup-shell.js",
"chars": 12469,
"preview": "const path = require('path')\nconst windowStateKeeper = require('electron-window-state')\nconst { BrowserWindow, WebConten"
},
{
"path": "popup-toolbar.html",
"chars": 8330,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta\n http-equiv=\"Content-Security-Poli"
},
{
"path": "preload.js",
"chars": 43737,
"preview": "// put this preload for main-window to give it prompt()\nconst { ipcRenderer, } = require('electron')\nwindow.prompt = fun"
},
{
"path": "prompt.html",
"chars": 904,
"preview": "<html>\n<head>\n<style>body {font-family: sans-serif;} button {float:right; margin-left: 10px;} label { display: block; ma"
},
{
"path": "script/patch.command",
"chars": 64,
"preview": "sudo -s xattr -d com.apple.quarantine /Applications/Pinokio.app\n"
},
{
"path": "script/run-update-banner-test.js",
"chars": 301,
"preview": "const { spawn } = require('child_process')\n\nconst cmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'\nconst child = s"
},
{
"path": "script/zip.js",
"chars": 2804,
"preview": "const { exec } = require('child_process');\nconst path = require('path')\nconst fs = require('fs')\nconst version = process"
},
{
"path": "splash.html",
"chars": 3302,
"preview": "<html>\n<head>\n<style>\nhtml, body {\n width: 100%;\n height: 100%;\n margin: 0;\n font-family: -apple-system, BlinkMacSys"
},
{
"path": "temp/rebuild.js",
"chars": 7572,
"preview": "\"use strict\";\nvar __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {\n if ("
},
{
"path": "temp/yarn.js",
"chars": 5649,
"preview": "\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.rebuild = exports.nodeGypRebuild = "
},
{
"path": "update-banner.html",
"chars": 6768,
"preview": "<!doctype html>\n<html>\n <head>\n <meta charset=\"utf-8\" />\n <title>Pinokio Update</title>\n <style>\n :root {"
},
{
"path": "updater.js",
"chars": 4478,
"preview": "const { autoUpdater } = require(\"electron-updater\");\nconst ProgressBar = require('electron-progressbar');\nconst { dialog"
},
{
"path": "wrap-linux-launcher.js",
"chars": 1500,
"preview": "const fs = require('fs')\nconst path = require('path')\nmodule.exports = async (context) => {\n const { appOutDir, electro"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the pinokiocomputer/pinokio GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 33 files (263.5 KB), approximately 66.9k tokens, and a symbol index with 43 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.