Make peardock-client the full Pear GUI via Electron Forge
Rolling release / server / win32-arm64 (push) Failing after 9s
Rolling release / server / darwin-arm64 (push) Has been cancelled
Rolling release / server / darwin-x64 (push) Has been cancelled
Rolling release / server / linux-arm64 (push) Has been cancelled
Rolling release / server / linux-x64 (push) Has been cancelled
Rolling release / server / win32-x64 (push) Has been cancelled
Rolling release / client / macos-latest (push) Has been cancelled
Rolling release / client / windows-latest (push) Has been cancelled
Rolling release / Publish rolling release (push) Has been cancelled
Rolling release / client / ubuntu-latest (push) Has been cancelled
CI / test (push) Has been cancelled
Rolling release / server / win32-arm64 (push) Failing after 9s
Rolling release / server / darwin-arm64 (push) Has been cancelled
Rolling release / server / darwin-x64 (push) Has been cancelled
Rolling release / server / linux-arm64 (push) Has been cancelled
Rolling release / server / linux-x64 (push) Has been cancelled
Rolling release / server / win32-x64 (push) Has been cancelled
Rolling release / client / macos-latest (push) Has been cancelled
Rolling release / client / windows-latest (push) Has been cancelled
Rolling release / Publish rolling release (push) Has been cancelled
Rolling release / client / ubuntu-latest (push) Has been cancelled
CI / test (push) Has been cancelled
Replace the headless Bare agent client with a real desktop app: Electron shell loads the same index.html/app.js UI as pear run, with a Pear polyfill, Holesail control, and Node ESM bootstrap for HyperDHT. Server remains bare-build standalone; CI rolling release publishes both product types.
This commit is contained in:
+10
-2
@@ -26,11 +26,19 @@ jobs:
|
|||||||
- name: Unit + RPC tests
|
- name: Unit + RPC tests
|
||||||
run: npm test
|
run: npm test
|
||||||
|
|
||||||
- name: Smoke-pack server (linux-x64)
|
- name: Smoke-pack server (linux-x64 Bare)
|
||||||
run: npm run make:server:linux-x64
|
run: npm run make:server:linux-x64
|
||||||
|
|
||||||
- name: Verify binary exists
|
- name: Verify server binary
|
||||||
run: |
|
run: |
|
||||||
test -x out/server/linux-x64/peardock-server
|
test -x out/server/linux-x64/peardock-server
|
||||||
file out/server/linux-x64/peardock-server || true
|
file out/server/linux-x64/peardock-server || true
|
||||||
ls -lh out/server/linux-x64/
|
ls -lh out/server/linux-x64/
|
||||||
|
|
||||||
|
- name: Smoke-package client (Electron full Pear GUI)
|
||||||
|
run: npx electron-forge package --platform linux --arch x64
|
||||||
|
|
||||||
|
- name: Verify client package
|
||||||
|
run: |
|
||||||
|
test -x out/peardock-linux-x64/peardock-client
|
||||||
|
ls -lh out/peardock-linux-x64/peardock-client
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
# peardock — rolling multi-arch Bare standalone binaries
|
# peardock — rolling release
|
||||||
|
# - peardock-server: Bare standalone (bare-build --standalone, embeds all modules)
|
||||||
|
# - peardock-client: full Pear GUI (Electron Forge package / AppImage / zip)
|
||||||
#
|
#
|
||||||
# Secret required in the Gitea repo:
|
# Secret: RELEASE_TOKEN (Gitea PAT with release write)
|
||||||
# RELEASE_TOKEN — personal access token with repository release write scope
|
# Optional: GITEA_URL
|
||||||
#
|
|
||||||
# Optional secrets / vars:
|
|
||||||
# GITEA_URL — defaults to the forge this workflow runs on
|
|
||||||
#
|
|
||||||
# Builds use bare-build --standalone (embeds full JS module graph + native addons)
|
|
||||||
# Pattern: holepunchto/hello-pear-bare + bare-build README
|
|
||||||
#
|
|
||||||
# Gitea Actions is Actions-compatible; runner labels may differ on your forge.
|
|
||||||
|
|
||||||
name: Rolling release
|
name: Rolling release
|
||||||
|
|
||||||
@@ -27,134 +21,153 @@ env:
|
|||||||
RELEASE_TAG: rolling
|
RELEASE_TAG: rolling
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build-server:
|
||||||
name: ${{ matrix.product }} / ${{ matrix.host }}
|
name: server / ${{ matrix.host }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
# Linux x64 — primary server target
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
host: linux-x64
|
host: linux-x64
|
||||||
product: server
|
|
||||||
- os: ubuntu-latest
|
|
||||||
host: linux-x64
|
|
||||||
product: client
|
|
||||||
# Linux arm64 (label may be ubuntu-latest on multiarch runners)
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
host: linux-arm64
|
host: linux-arm64
|
||||||
product: server
|
|
||||||
- os: ubuntu-latest
|
|
||||||
host: linux-arm64
|
|
||||||
product: client
|
|
||||||
# Cross-pack from Linux where prebuilds exist (darwin/win32)
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
host: darwin-arm64
|
host: darwin-arm64
|
||||||
product: server
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
host: darwin-x64
|
host: darwin-x64
|
||||||
product: server
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
host: win32-x64
|
host: win32-x64
|
||||||
product: server
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
host: win32-arm64
|
host: win32-arm64
|
||||||
product: server
|
|
||||||
- os: ubuntu-latest
|
|
||||||
host: darwin-arm64
|
|
||||||
product: client
|
|
||||||
- os: ubuntu-latest
|
|
||||||
host: darwin-x64
|
|
||||||
product: client
|
|
||||||
- os: ubuntu-latest
|
|
||||||
host: win32-x64
|
|
||||||
product: client
|
|
||||||
- os: ubuntu-latest
|
|
||||||
host: win32-arm64
|
|
||||||
product: client
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
- uses: actions/setup-node@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: npm
|
cache: npm
|
||||||
|
- run: npm ci
|
||||||
- name: Install dependencies
|
- run: npm run make:server:${{ matrix.host }}
|
||||||
run: npm ci
|
- name: Stage server binary
|
||||||
|
|
||||||
- name: Build standalone binary
|
|
||||||
run: npm run make:${{ matrix.product }}:${{ matrix.host }}
|
|
||||||
|
|
||||||
- name: List artifact
|
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
dir="out/${{ matrix.product }}/${{ matrix.host }}"
|
dir="out/server/${{ matrix.host }}"
|
||||||
ls -la "$dir"
|
ls -la "$dir"
|
||||||
# Normalize name for upload
|
if [ -f "$dir/peardock-server.exe" ]; then
|
||||||
if [ -f "$dir/peardock-${{ matrix.product }}.exe" ]; then
|
cp "$dir/peardock-server.exe" "peardock-server-${{ matrix.host }}.exe"
|
||||||
cp "$dir/peardock-${{ matrix.product }}.exe" \
|
|
||||||
"peardock-${{ matrix.product }}-${{ matrix.host }}.exe"
|
|
||||||
else
|
else
|
||||||
cp "$dir/peardock-${{ matrix.product }}" \
|
cp "$dir/peardock-server" "peardock-server-${{ matrix.host }}"
|
||||||
"peardock-${{ matrix.product }}-${{ matrix.host }}"
|
|
||||||
fi
|
fi
|
||||||
ls -la peardock-${{ matrix.product }}-${{ matrix.host }}*
|
- uses: actions/upload-artifact@v4
|
||||||
|
|
||||||
- name: Upload build artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: peardock-${{ matrix.product }}-${{ matrix.host }}
|
name: peardock-server-${{ matrix.host }}
|
||||||
path: peardock-${{ matrix.product }}-${{ matrix.host }}*
|
path: peardock-server-${{ matrix.host }}*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
build-client:
|
||||||
|
name: client / ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: linux
|
||||||
|
arch: x64
|
||||||
|
host: linux-x64
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: linux
|
||||||
|
arch: arm64
|
||||||
|
host: linux-arm64
|
||||||
|
# macOS / Windows need matching runners for native Electron rebuild
|
||||||
|
- os: macos-latest
|
||||||
|
platform: darwin
|
||||||
|
arch: arm64
|
||||||
|
host: darwin-arm64
|
||||||
|
- os: windows-latest
|
||||||
|
platform: win32
|
||||||
|
arch: x64
|
||||||
|
host: win32-x64
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 45
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
cache: npm
|
||||||
|
- run: npm ci
|
||||||
|
- name: Electron Forge make (full Pear GUI)
|
||||||
|
run: npx electron-forge make --platform ${{ matrix.platform }} --arch ${{ matrix.arch }}
|
||||||
|
- name: Stage client artifacts
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p staged
|
||||||
|
# Prefer AppImage / zip / dir from forge make + package output
|
||||||
|
if compgen -G "out/make/**/*.AppImage" > /dev/null; then
|
||||||
|
cp out/make/**/*.AppImage "staged/peardock-client-${{ matrix.host }}.AppImage" || true
|
||||||
|
# flatten globs
|
||||||
|
find out/make -name '*.AppImage' -exec cp {} "staged/peardock-client-${{ matrix.host }}.AppImage" \;
|
||||||
|
fi
|
||||||
|
if compgen -G "out/make/**/*.zip" > /dev/null; then
|
||||||
|
find out/make -name '*.zip' -exec cp {} "staged/peardock-client-${{ matrix.host }}.zip" \;
|
||||||
|
fi
|
||||||
|
if compgen -G "out/make/**/*.deb" > /dev/null; then
|
||||||
|
find out/make -name '*.deb' -exec cp {} "staged/peardock-client-${{ matrix.host }}.deb" \;
|
||||||
|
fi
|
||||||
|
# Packaged app directory (always present after package/make)
|
||||||
|
dir=$(find out -maxdepth 1 -type d -name 'peardock-*' | head -1)
|
||||||
|
if [ -n "$dir" ] && [ ! -f "staged/peardock-client-${{ matrix.host }}.zip" ]; then
|
||||||
|
(cd out && zip -ry "../staged/peardock-client-${{ matrix.host }}.zip" "$(basename "$dir")")
|
||||||
|
fi
|
||||||
|
ls -la staged/
|
||||||
|
test -n "$(ls -A staged)"
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: peardock-client-${{ matrix.host }}
|
||||||
|
path: staged/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Publish rolling release
|
name: Publish rolling release
|
||||||
needs: [build]
|
needs: [build-server, build-client]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Only publish from the default branch pushes / manual dispatch
|
|
||||||
if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
|
if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
- uses: actions/setup-node@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
- name: Download all artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
|
- name: Stage for release script
|
||||||
- name: Stage binaries for release
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
mkdir -p out/server out/client dist
|
mkdir -p out/server out/client dist/rolling-stage
|
||||||
# Flatten artifact dirs into out/{product}/{host}/peardock-*
|
find artifacts -type f | sort
|
||||||
find artifacts -type f -name 'peardock-*' | while read -r f; do
|
# Servers
|
||||||
base="$(basename "$f")"
|
find artifacts -type f -name 'peardock-server-*' | while read -r f; do
|
||||||
# peardock-server-linux-x64[.exe]
|
base=$(basename "$f")
|
||||||
if [[ "$base" =~ ^peardock-(server|client)-([a-z0-9]+-[a-z0-9]+)(\.exe)?$ ]]; then
|
# peardock-server-linux-x64
|
||||||
product="${BASH_REMATCH[1]}"
|
host=${base#peardock-server-}
|
||||||
host="${BASH_REMATCH[2]}"
|
host=${host%.exe}
|
||||||
ext="${BASH_REMATCH[3]}"
|
mkdir -p "out/server/$host"
|
||||||
mkdir -p "out/${product}/${host}"
|
if [[ "$base" == *.exe ]]; then
|
||||||
cp -a "$f" "out/${product}/${host}/peardock-${product}${ext}"
|
cp "$f" "out/server/$host/peardock-server.exe"
|
||||||
echo "staged $base → out/${product}/${host}/"
|
|
||||||
else
|
else
|
||||||
echo "skip unrecognized: $base"
|
cp "$f" "out/server/$host/peardock-server"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
# Clients — copy into dist stage with original names
|
||||||
|
find artifacts -type f \( -name 'peardock-client-*' -o -name '*.AppImage' -o -name '*.zip' -o -name '*.deb' \) | while read -r f; do
|
||||||
|
cp -a "$f" dist/rolling-stage/
|
||||||
|
done
|
||||||
find out -type f | sort
|
find out -type f | sort
|
||||||
ls -la artifacts || true
|
ls -la dist/rolling-stage/ || true
|
||||||
|
|
||||||
- name: Publish to Gitea (rolling)
|
- name: Publish to Gitea (rolling)
|
||||||
env:
|
env:
|
||||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
@@ -163,10 +176,11 @@ jobs:
|
|||||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||||
RELEASE_TAG: ${{ env.RELEASE_TAG }}
|
RELEASE_TAG: ${{ env.RELEASE_TAG }}
|
||||||
ARTIFACT_DIR: ${{ github.workspace }}/out
|
ARTIFACT_DIR: ${{ github.workspace }}/out
|
||||||
|
CLIENT_STAGE: ${{ github.workspace }}/dist/rolling-stage
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
if [ -z "${RELEASE_TOKEN:-}" ]; then
|
if [ -z "${RELEASE_TOKEN:-}" ]; then
|
||||||
echo "ERROR: secret RELEASE_TOKEN is not set on this repository"
|
echo "ERROR: secret RELEASE_TOKEN is not set"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
export GITEA_URL="${GITEA_URL_SECRET:-${GITHUB_SERVER_URL}}"
|
export GITEA_URL="${GITEA_URL_SECRET:-${GITHUB_SERVER_URL}}"
|
||||||
|
|||||||
@@ -76,22 +76,33 @@ pear release .
|
|||||||
pear run pear://<your-app-key>
|
pear run pear://<your-app-key>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Standalone Bare binaries
|
### Standalone binaries
|
||||||
|
|
||||||
Self-contained ELFs/Mach-O/PE with **all modules and native addons embedded**
|
| Binary | Kind | Command |
|
||||||
([bare-build --standalone](https://github.com/holepunchto/bare-build) pattern):
|
|--------|------|---------|
|
||||||
|
| **peardock-server** | Bare standalone (all modules embedded) | `npm run make:server:linux-x64` |
|
||||||
|
| **peardock-client** | **Full Pear GUI** (Electron; same UI as `pear run`) | `npm run make:client` |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm ci
|
npm ci
|
||||||
npm run make:server:linux-x64 # → out/server/linux-x64/peardock-server
|
npm run make:server:linux-x64
|
||||||
npm run make:client:linux-x64 # → out/client/linux-x64/peardock-client
|
# → out/server/linux-x64/peardock-server
|
||||||
|
|
||||||
|
npm run make:client
|
||||||
|
# → out/peardock-linux-x64/peardock-client (+ AppImage/zip under out/make/)
|
||||||
|
|
||||||
./out/server/linux-x64/peardock-server
|
./out/server/linux-x64/peardock-server
|
||||||
./out/client/linux-x64/peardock-client --connect <server-public-key>
|
./out/peardock-linux-x64/peardock-client # full desktop app
|
||||||
```
|
```
|
||||||
|
|
||||||
Gitea CI publishes a **rolling** release on every `main` push using secret
|
Dev UI (unchanged):
|
||||||
`RELEASE_TOKEN`. See [docs/RELEASE.md](docs/RELEASE.md).
|
|
||||||
|
```bash
|
||||||
|
npm run dev # pear run -d .
|
||||||
|
npm run start:client # Electron shell of the same GUI
|
||||||
|
```
|
||||||
|
|
||||||
|
Gitea CI publishes a **rolling** release (`RELEASE_TOKEN`). See [docs/RELEASE.md](docs/RELEASE.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+8
-142
@@ -1,146 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* Bare standalone peardock client agent.
|
* peardock-client entry — full Pear GUI application.
|
||||||
*
|
*
|
||||||
* Built with bare-build / scripts/bare-standalone.cjs:
|
* Preferred run modes:
|
||||||
* npm run make:client:linux-x64
|
* 1) Electron packaged binary (CI / make:client): peardock-client
|
||||||
|
* 2) Electron dev: npm run start:client
|
||||||
|
* 3) Pear platform (pear-electron + bridge): npm run dev / pear run -d .
|
||||||
*
|
*
|
||||||
* What this binary is:
|
* This module is the Pear platform entry (same as package "main" / index.js).
|
||||||
* - Self-contained Bare ELF with embedded JS graph + native addons
|
* Electron packaging uses electron/main.cjs as the process entry instead.
|
||||||
* - Local Holesail client control plane (real `holesail` package)
|
|
||||||
* - Optional headless HyperDHT connect for health/RPC smoke tests
|
|
||||||
*
|
|
||||||
* What this binary is not:
|
|
||||||
* - The full pear-electron GUI. Desktop UI still uses:
|
|
||||||
* pear run -d .
|
|
||||||
* (see hello-pear-electron for Electron-forge installers later)
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* peardock-client
|
|
||||||
* peardock-client --connect <64-hex-server-key>
|
|
||||||
* peardock-client --storage /var/lib/peardock-client
|
|
||||||
*/
|
*/
|
||||||
/* global Bare */
|
await import('../index.js')
|
||||||
|
|
||||||
const isBare = typeof globalThis.Bare !== 'undefined'
|
|
||||||
|
|
||||||
if (isBare) {
|
|
||||||
try {
|
|
||||||
await import('bare-node-runtime/global')
|
|
||||||
} catch {
|
|
||||||
// optional
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
import process from 'process'
|
|
||||||
import path from 'path'
|
|
||||||
import os from 'os'
|
|
||||||
import bareControl from '../client/holesailBareControl.cjs'
|
|
||||||
import { PearDockConnection } from '../client/connection.js'
|
|
||||||
|
|
||||||
function parseArgs(argv) {
|
|
||||||
const out = { connect: null, storage: null, help: false }
|
|
||||||
for (let i = 0; i < argv.length; i++) {
|
|
||||||
const a = argv[i]
|
|
||||||
if (a === '--help' || a === '-h') out.help = true
|
|
||||||
else if (a === '--connect') out.connect = argv[++i]
|
|
||||||
else if (a === '--storage') out.storage = argv[++i]
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
const argv = isBare
|
|
||||||
? globalThis.Bare.argv.slice(2)
|
|
||||||
: process.argv.slice(2)
|
|
||||||
const args = parseArgs(argv)
|
|
||||||
|
|
||||||
if (args.help) {
|
|
||||||
console.log(`peardock-client — Bare standalone agent
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
peardock-client [--storage <dir>] [--connect <server-public-key>]
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--storage <dir> State directory (default: OS temp / peardock-client)
|
|
||||||
--connect <key> Dial HyperDHT server and print handshake, then keep running
|
|
||||||
--help Show this help
|
|
||||||
|
|
||||||
Full desktop GUI:
|
|
||||||
pear run -d . # from the peardock source tree with pear-electron
|
|
||||||
`)
|
|
||||||
if (isBare) Bare.exit(0)
|
|
||||||
else process.exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const storage =
|
|
||||||
args.storage ||
|
|
||||||
path.join(os.tmpdir(), 'peardock-client')
|
|
||||||
|
|
||||||
const sep = storage.includes('\\') ? '\\' : '/'
|
|
||||||
const statePath = storage.endsWith(sep)
|
|
||||||
? `${storage}peardock-holesail-local.json`
|
|
||||||
: `${storage}${sep}peardock-holesail-local.json`
|
|
||||||
|
|
||||||
console.log('peardock-client (Bare standalone agent)')
|
|
||||||
console.log(` storage: ${storage}`)
|
|
||||||
|
|
||||||
let holesailControl = null
|
|
||||||
try {
|
|
||||||
const start = bareControl.start || bareControl.default?.start
|
|
||||||
if (typeof start !== 'function') throw new Error('holesailBareControl missing start()')
|
|
||||||
holesailControl = await start({ statePath })
|
|
||||||
console.log(` holesail control: ${holesailControl.baseUrl}`)
|
|
||||||
console.log(` state: ${statePath}`)
|
|
||||||
} catch (err) {
|
|
||||||
console.error(' holesail control failed:', err.message || err)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {import('../client/connection.js').PearDockConnection|null} */
|
|
||||||
let conn = null
|
|
||||||
if (args.connect) {
|
|
||||||
try {
|
|
||||||
conn = new PearDockConnection(args.connect)
|
|
||||||
await conn.connect()
|
|
||||||
console.log(` connected: ${conn.id}… role=${conn.role || '?'}`)
|
|
||||||
try {
|
|
||||||
const pong = await conn.ping()
|
|
||||||
console.log(' ping:', JSON.stringify(pong))
|
|
||||||
} catch (e) {
|
|
||||||
console.log(' ping failed:', e.message || e)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(' connect failed:', err.message || err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('')
|
|
||||||
console.log('Agent running. Ctrl+C to stop.')
|
|
||||||
console.log('GUI: install Pear and run `pear run -d .` from the peardock tree.')
|
|
||||||
|
|
||||||
const shutdown = async (code = 0) => {
|
|
||||||
try {
|
|
||||||
await holesailControl?.close?.()
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await conn?.close?.()
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
if (isBare) Bare.exit(code)
|
|
||||||
else process.exit(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
process.on?.('SIGINT', () => shutdown(130))
|
|
||||||
process.on?.('SIGTERM', () => shutdown(143))
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
if (isBare) {
|
|
||||||
try {
|
|
||||||
Bare.on?.('suspend', () => {})
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 9.5 KiB |
@@ -21,7 +21,8 @@ let controlCache = null
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
function isPearGui() {
|
function isPearGui() {
|
||||||
return Boolean(globalThis.Pear?.config)
|
// pear run (pear-electron) and Electron peardock-client both set Pear.config.storage
|
||||||
|
return Boolean(globalThis.Pear?.config?.storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+67
-144
@@ -1,194 +1,117 @@
|
|||||||
# peardock release process
|
# peardock release process
|
||||||
|
|
||||||
## Standalone Bare binaries (recommended)
|
## Two products
|
||||||
|
|
||||||
peardock ships **two separate self-contained binaries** built with the Holepunch
|
| Artifact | What it is | Build |
|
||||||
stack (`bare-pack` + `bare-build --standalone`):
|
|----------|------------|--------|
|
||||||
|
| **peardock-server** | Bare standalone daemon (HyperDHT + Docker) | `bare-pack` + `bare-build --standalone` |
|
||||||
|
| **peardock-client** | **Full Pear GUI app** (same UI as `pear run -d .`) | Electron Forge (hello-pear-electron pattern) |
|
||||||
|
|
||||||
| Binary | Role |
|
---
|
||||||
|--------|------|
|
|
||||||
| `peardock-server-<host>` | HyperDHT Docker control plane (dockerode over Unix socket) |
|
|
||||||
| `peardock-client-<host>` | Bare agent: Holesail local control + optional headless HyperDHT connect |
|
|
||||||
|
|
||||||
Each binary embeds the full JS module graph and native addons (no `node_modules`
|
## peardock-server (Bare binary)
|
||||||
on the target host). Pattern matches [hello-pear-bare](https://github.com/holepunchto/hello-pear-bare)
|
|
||||||
and [bare-build](https://github.com/holepunchto/bare-build).
|
|
||||||
|
|
||||||
### Local build
|
Self-contained ELF/Mach-O/PE with the full JS graph and native addons embedded.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm ci
|
npm ci
|
||||||
# Native host (server + client)
|
|
||||||
npm run make
|
|
||||||
|
|
||||||
# Explicit targets
|
|
||||||
npm run make:server:linux-x64
|
npm run make:server:linux-x64
|
||||||
npm run make:client:linux-x64
|
# → out/server/linux-x64/peardock-server
|
||||||
npm run make:all # all desktop hosts × both products
|
|
||||||
```
|
|
||||||
|
|
||||||
Outputs:
|
./out/server/linux-x64/peardock-server
|
||||||
|
# Identity: PEARDOCK_HOME or next to the binary (.env)
|
||||||
```
|
|
||||||
out/server/<host>/peardock-server[.exe]
|
|
||||||
out/client/<host>/peardock-client[.exe]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Hosts: `linux-x64`, `linux-arm64`, `darwin-arm64`, `darwin-x64`, `win32-x64`, `win32-arm64`.
|
Hosts: `linux-x64`, `linux-arm64`, `darwin-arm64`, `darwin-x64`, `win32-x64`, `win32-arm64`.
|
||||||
|
|
||||||
### Why `scripts/bare-standalone.cjs` instead of plain `bare-build`?
|
Builder: `scripts/bare-standalone.cjs` (passes `bare-node-runtime` imports + Docker socket shims).
|
||||||
|
|
||||||
`bare-build` 1.x packs with Bare resolution but does **not** expose
|
|
||||||
`bare-pack --imports`. Node packages such as `dockerode` need
|
|
||||||
`bare-node-runtime/imports.json` so builtins (`events`, `stream`, …) map to
|
|
||||||
`bare-*`. The peardock builder:
|
|
||||||
|
|
||||||
1. `bare-pack` with bare-node-runtime imports + small shims/stubs
|
|
||||||
2. Embeds the bundle into a portable Bare runtime ELF/Mach-O/PE (same as
|
|
||||||
`bare-build --standalone`)
|
|
||||||
|
|
||||||
Shims under `build/`:
|
|
||||||
|
|
||||||
| Path | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `shims/http.cjs` | Unix `socketPath` for Docker (bare-http1 is TCP-only) |
|
|
||||||
| `shims/url.cjs` | Legacy `url.resolve` / `parse` for docker-modem |
|
|
||||||
| `stubs/ssh2.cjs` | docker-modem optional SSH (local socket only) |
|
|
||||||
| `stubs/grpc-js.cjs` | dockerode BuildKit gRPC sessions (not used for Engine API) |
|
|
||||||
|
|
||||||
### Run server binary
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Identity lives next to the binary by default (.env)
|
|
||||||
./out/server/linux-x64/peardock-server
|
|
||||||
|
|
||||||
# Or point at a home directory
|
|
||||||
PEARDOCK_HOME=/var/lib/peardock ./peardock-server
|
|
||||||
# / PEARDOCK_ENV=/etc/peardock.env
|
|
||||||
```
|
|
||||||
|
|
||||||
Requires: Docker Engine socket (`/var/run/docker.sock`) and network for HyperDHT.
|
|
||||||
|
|
||||||
### Run client binary (agent)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./out/client/linux-x64/peardock-client
|
|
||||||
./out/client/linux-x64/peardock-client --connect <server-public-key-hex>
|
|
||||||
```
|
|
||||||
|
|
||||||
Full **desktop GUI** still uses Pear + pear-electron:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev # pear run -d .
|
|
||||||
```
|
|
||||||
|
|
||||||
(Electron-forge installers can follow the [hello-pear-electron](https://github.com/holepunchto/hello-pear-electron)
|
|
||||||
pattern later.)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Gitea CI — rolling release (`RELEASE_TOKEN`)
|
## peardock-client (full Pear GUI)
|
||||||
|
|
||||||
|
The desktop app is the **same** peardock UI as development (`index.html` + `app.js` + HyperDHT client + Holesail).
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pear platform (recommended for day-to-day UI work)
|
||||||
|
npm run dev # pear run -d . → index.js (pear-electron + bridge)
|
||||||
|
|
||||||
|
# Electron shell (what the released client binary uses)
|
||||||
|
npm run start:client # electron electron/main.cjs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Package / make
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run package:client # → out/peardock-<platform>-<arch>/peardock-client
|
||||||
|
npm run make:client # zip / deb / AppImage under out/make/
|
||||||
|
npm run make:client:linux-x64
|
||||||
|
```
|
||||||
|
|
||||||
|
Architecture:
|
||||||
|
|
||||||
|
- `electron/main.cjs` — window, local static server, Holesail control, Pear polyfill
|
||||||
|
- `electron/preload.cjs` — `globalThis.Pear.config.storage` etc. for UI + holesailLocal
|
||||||
|
- App entry remains `app.js` (Node ESM via dynamic `import(fileURL)` so npm deps resolve)
|
||||||
|
- Packaged `package.json` main rewritten to `electron/main.cjs` (source tree keeps `main: index.js` for `pear run`)
|
||||||
|
|
||||||
|
Pattern: [hello-pear-electron](https://github.com/holepunchto/hello-pear-electron) + peardock’s existing pear-electron UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gitea CI rolling release
|
||||||
|
|
||||||
Workflow: `.gitea/workflows/release-rolling.yml`
|
Workflow: `.gitea/workflows/release-rolling.yml`
|
||||||
|
|
||||||
### Secret
|
| Secret | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `RELEASE_TOKEN` | Gitea PAT with release write |
|
||||||
|
| `GITEA_URL` | Optional forge base URL |
|
||||||
|
|
||||||
In the Gitea repo **Settings → Secrets**:
|
On push to `main`:
|
||||||
|
|
||||||
| Name | Value |
|
1. Matrix-build **servers** (Bare) for all desktop hosts
|
||||||
|------|--------|
|
2. Matrix-build **clients** (Electron Forge) on Linux/macOS/Windows runners
|
||||||
| `RELEASE_TOKEN` | Personal access token with **repository** write (create/delete releases + upload assets) |
|
3. Publish/replace tag **`rolling`** with all assets + `SHA256SUMS`
|
||||||
|
|
||||||
Optional:
|
Manual:
|
||||||
|
|
||||||
| Name | Value |
|
|
||||||
|------|--------|
|
|
||||||
| `GITEA_URL` | Forge base URL if not the same as the Actions host (e.g. `https://git.example.com`) |
|
|
||||||
|
|
||||||
### Behaviour
|
|
||||||
|
|
||||||
1. On every push to `main` / `master` (and manual dispatch), matrix-builds
|
|
||||||
server + client for desktop hosts via `npm run make:<product>:<host>`.
|
|
||||||
2. `publish` job downloads artifacts and runs
|
|
||||||
`scripts/gitea-rolling-release.sh`.
|
|
||||||
3. That script **replaces** the release tagged `rolling` (delete + recreate) and
|
|
||||||
uploads:
|
|
||||||
|
|
||||||
- `peardock-server-<host>[.exe]`
|
|
||||||
- `peardock-client-<host>[.exe]`
|
|
||||||
- `SHA256SUMS`
|
|
||||||
|
|
||||||
### Manual publish (from a machine with artifacts)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export RELEASE_TOKEN=...
|
export RELEASE_TOKEN=... GITEA_URL=https://your.gitea GITEA_OWNER=... GITEA_REPO=peardock
|
||||||
export GITEA_URL=https://your.gitea.host
|
npm run make:server:linux-x64
|
||||||
export GITEA_OWNER=snxraven
|
npm run make:client
|
||||||
export GITEA_REPO=peardock
|
|
||||||
npm run make:all # or copy CI artifacts into out/
|
|
||||||
npm run release:rolling
|
npm run release:rolling
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Source tarball + checksums (legacy / Node install)
|
## systemd (server)
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x scripts/release-checksums.sh
|
|
||||||
./scripts/release-checksums.sh dist/
|
|
||||||
# Optional GPG:
|
|
||||||
GPG_KEY_ID=YOUR_KEY_ID ./scripts/release-checksums.sh dist/
|
|
||||||
```
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `peardock-<ver>-<stamp>.tar.gz` | Source/runtime tree (no node_modules) |
|
|
||||||
| `*.sha256` | SHA-256 checksum |
|
|
||||||
| `*.asc` | Detached GPG signature (if keyed) |
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tar -xzf peardock-*.tar.gz -C /opt/peardock
|
|
||||||
cd /opt/peardock && npm ci --omit=dev
|
|
||||||
# Prefer binary: copy peardock-server into place and set ExecStart
|
|
||||||
cp deploy/peardock.service /etc/systemd/system/
|
|
||||||
systemctl enable --now peardock
|
|
||||||
```
|
|
||||||
|
|
||||||
Update `deploy/peardock.service` `ExecStart` to the standalone binary when using
|
|
||||||
Bare builds:
|
|
||||||
|
|
||||||
```ini
|
```ini
|
||||||
ExecStart=/opt/peardock/peardock-server
|
ExecStart=/opt/peardock/peardock-server
|
||||||
Environment=PEARDOCK_HOME=/opt/peardock
|
Environment=PEARDOCK_HOME=/opt/peardock
|
||||||
```
|
```
|
||||||
|
|
||||||
|
See `deploy/peardock.service`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Pear desktop app (GUI OTA)
|
## Pear OTA (optional)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm ci
|
|
||||||
pear stage .
|
pear stage .
|
||||||
pear release .
|
pear release .
|
||||||
# Distribute pear:// link per Pear docs
|
# pear-install pear://...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Electron client can later embed `pear-runtime` OTA like hello-pear-electron (`upgrade` field in package.json).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Certification soak (24h)
|
## Source tarball (legacy Node server)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Terminal 1
|
./scripts/release-checksums.sh dist/
|
||||||
./out/server/linux-x64/peardock-server
|
|
||||||
# or: npm run server
|
|
||||||
|
|
||||||
# Terminal 2
|
|
||||||
SOAK_DURATION_MS=86400000 npm run soak
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Encoding profile
|
|
||||||
|
|
||||||
Handshake returns `schemaVersion` and features. Default encoding remains JSON
|
|
||||||
(`shared/encodings.js`); binary bulk uses `binaryStream*` + `push:binaryChunk`.
|
|
||||||
|
|||||||
@@ -0,0 +1,303 @@
|
|||||||
|
/**
|
||||||
|
* peardock desktop client — Electron shell (full Pear GUI app).
|
||||||
|
*
|
||||||
|
* Architecture (hello-pear-electron + peardock pear-electron layout):
|
||||||
|
* - Electron owns the window chrome
|
||||||
|
* - A Bare worker (pear-runtime) runs holesail control (require.addon)
|
||||||
|
* - Renderer loads the real peardock UI (index.html + app.js) with Node integration
|
||||||
|
* so HyperDHT / protomux client modules work like under pear run
|
||||||
|
* - Pear polyfill provides config.storage, exit, teardown for UI code
|
||||||
|
*
|
||||||
|
* Dev: npm run start:client
|
||||||
|
* Pack: npm run make:client
|
||||||
|
*/
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const { app, BrowserWindow, ipcMain, session } = require('electron')
|
||||||
|
const path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
|
const os = require('os')
|
||||||
|
const http = require('http')
|
||||||
|
const { pathToFileURL } = require('url')
|
||||||
|
|
||||||
|
const pkg = require('../package.json')
|
||||||
|
const appName = pkg.productName || pkg.name || 'peardock'
|
||||||
|
|
||||||
|
// ---- CLI flags (paparam optional) ----
|
||||||
|
const argv = app.isPackaged ? process.argv.slice(1) : process.argv.slice(2)
|
||||||
|
let storageOverride = null
|
||||||
|
let noSandbox = false
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
if (argv[i] === '--storage') storageOverride = argv[++i]
|
||||||
|
if (argv[i] === '--no-sandbox') noSandbox = true
|
||||||
|
}
|
||||||
|
if (noSandbox || process.platform === 'linux') {
|
||||||
|
app.commandLine.appendSwitch('no-sandbox')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storageOverride) {
|
||||||
|
app.setPath('userData', storageOverride)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userData = () => app.getPath('userData')
|
||||||
|
const storageDir = () => path.join(userData(), 'storage')
|
||||||
|
|
||||||
|
/** @type {import('http').Server|null} */
|
||||||
|
let staticServer = null
|
||||||
|
/** @type {number} */
|
||||||
|
let staticPort = 0
|
||||||
|
/** @type {{ close?: () => Promise<void> }|null} */
|
||||||
|
let holesailControl = null
|
||||||
|
|
||||||
|
function ensureDir(p) {
|
||||||
|
fs.mkdirSync(p, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal static file server rooted at the app directory.
|
||||||
|
* Serves the full peardock UI tree (index.html, app.js, client/, ui/, …).
|
||||||
|
*
|
||||||
|
* index.html is rewritten so app.js loads via Node ESM (`import(fileURL)`),
|
||||||
|
* which resolves hyperdht etc. from node_modules — same graph as pear run.
|
||||||
|
*/
|
||||||
|
function startStaticServer(rootDir) {
|
||||||
|
const appJsFileUrl = pathToFileURL(path.join(rootDir, 'app.js')).href
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const mime = {
|
||||||
|
'.html': 'text/html; charset=utf-8',
|
||||||
|
'.js': 'text/javascript; charset=utf-8',
|
||||||
|
'.mjs': 'text/javascript; charset=utf-8',
|
||||||
|
'.cjs': 'text/javascript; charset=utf-8',
|
||||||
|
'.css': 'text/css; charset=utf-8',
|
||||||
|
'.json': 'application/json',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.ico': 'image/x-icon',
|
||||||
|
'.woff': 'font/woff',
|
||||||
|
'.woff2': 'font/woff2',
|
||||||
|
'.map': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
try {
|
||||||
|
const u = new URL(req.url || '/', 'http://127.0.0.1')
|
||||||
|
let rel = decodeURIComponent(u.pathname)
|
||||||
|
if (rel === '/') rel = '/index.html'
|
||||||
|
// block path escape
|
||||||
|
const filePath = path.normalize(path.join(rootDir, rel))
|
||||||
|
if (!filePath.startsWith(rootDir)) {
|
||||||
|
res.writeHead(403)
|
||||||
|
res.end('Forbidden')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
|
||||||
|
res.writeHead(404)
|
||||||
|
res.end('Not found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const ext = path.extname(filePath).toLowerCase()
|
||||||
|
|
||||||
|
// Full Pear GUI bootstrap: Node dynamic import of app.js (nodeIntegration)
|
||||||
|
if (rel === '/index.html' || filePath.endsWith(`${path.sep}index.html`)) {
|
||||||
|
let html = fs.readFileSync(filePath, 'utf8')
|
||||||
|
// Remove browser ESM entry (cannot resolve npm packages over HTTP)
|
||||||
|
html = html.replace(
|
||||||
|
/<script\s+type=["']module["']\s+src=["'][^"']*app\.js["']\s*>\s*<\/script>/i,
|
||||||
|
''
|
||||||
|
)
|
||||||
|
const boot = `
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const href = ${JSON.stringify(appJsFileUrl)};
|
||||||
|
console.log('[peardock] loading full app via Node ESM:', href);
|
||||||
|
import(href).catch(function (err) {
|
||||||
|
console.error('[peardock] failed to load app.js', err);
|
||||||
|
var el = document.createElement('pre');
|
||||||
|
el.style.cssText = 'color:#f88;padding:2rem;white-space:pre-wrap;font:14px monospace';
|
||||||
|
el.textContent = 'peardock failed to start:\\n' + (err && err.stack ? err.stack : err);
|
||||||
|
document.body.appendChild(el);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>`
|
||||||
|
if (html.includes('</body>')) {
|
||||||
|
html = html.replace('</body>', boot + '\n</body>')
|
||||||
|
} else {
|
||||||
|
html += boot
|
||||||
|
}
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'text/html; charset=utf-8',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
})
|
||||||
|
res.end(html)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': mime[ext] || 'application/octet-stream',
|
||||||
|
'Cache-Control': 'no-cache',
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
})
|
||||||
|
fs.createReadStream(filePath).pipe(res)
|
||||||
|
} catch (err) {
|
||||||
|
res.writeHead(500)
|
||||||
|
res.end(String(err && err.message ? err.message : err))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
server.listen(0, '127.0.0.1', () => {
|
||||||
|
const addr = server.address()
|
||||||
|
staticPort = typeof addr === 'object' && addr ? addr.port : 0
|
||||||
|
staticServer = server
|
||||||
|
resolve(staticPort)
|
||||||
|
})
|
||||||
|
server.on('error', reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inject Pear polyfill + bootstrap Node ESM load of app.js before other scripts.
|
||||||
|
* Pear-electron uses pear-bridge/script-linker; here Electron provides Node and we
|
||||||
|
* polyfill the small Pear surface peardock needs.
|
||||||
|
*/
|
||||||
|
function buildPreloadPath() {
|
||||||
|
return path.join(__dirname, 'preload.cjs')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startHolesailControl() {
|
||||||
|
ensureDir(storageDir())
|
||||||
|
const statePath = path.join(storageDir(), 'peardock-holesail-local.json')
|
||||||
|
try {
|
||||||
|
// Prefer CJS control (works under Node main with native addons when available)
|
||||||
|
const bareControl = require('../client/holesailBareControl.cjs')
|
||||||
|
const start = bareControl.start || bareControl.default?.start
|
||||||
|
if (typeof start !== 'function') throw new Error('holesailBareControl missing start()')
|
||||||
|
holesailControl = await start({ statePath })
|
||||||
|
console.log(`[peardock] Holesail local control at ${holesailControl.baseUrl}`)
|
||||||
|
return holesailControl
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[peardock] Holesail local control failed (tunnels UI may be limited):', err.message || err)
|
||||||
|
// Write a stub endpoint file so UI can detect absence cleanly
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(
|
||||||
|
statePath,
|
||||||
|
JSON.stringify({ error: String(err.message || err), at: new Date().toISOString() }, null, 2)
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
width: pkg.pear?.gui?.width || 1280,
|
||||||
|
height: pkg.pear?.gui?.height || 800,
|
||||||
|
backgroundColor: pkg.pear?.gui?.backgroundColor || '#0a0c10',
|
||||||
|
title: appName,
|
||||||
|
webPreferences: {
|
||||||
|
preload: buildPreloadPath(),
|
||||||
|
// Full peardock UI imports HyperDHT etc. as ESM node packages — same as pear UI.
|
||||||
|
nodeIntegration: true,
|
||||||
|
contextIsolation: false,
|
||||||
|
sandbox: false,
|
||||||
|
spellcheck: false,
|
||||||
|
webSecurity: true,
|
||||||
|
},
|
||||||
|
show: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
win.once('ready-to-show', () => win.show())
|
||||||
|
|
||||||
|
const url = `http://127.0.0.1:${staticPort}/index.html`
|
||||||
|
win.loadURL(url).catch((err) => {
|
||||||
|
console.error('Failed to load UI:', err)
|
||||||
|
app.quit()
|
||||||
|
})
|
||||||
|
|
||||||
|
win.webContents.on('did-fail-load', (_e, code, desc) => {
|
||||||
|
console.error('did-fail-load', code, desc)
|
||||||
|
})
|
||||||
|
|
||||||
|
return win
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPC for Pear polyfill
|
||||||
|
ipcMain.on('peardock:get-pear-config', (evt) => {
|
||||||
|
evt.returnValue = {
|
||||||
|
storage: storageDir(),
|
||||||
|
name: appName,
|
||||||
|
version: pkg.version,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('peardock:exit', () => {
|
||||||
|
app.quit()
|
||||||
|
})
|
||||||
|
|
||||||
|
const teardownFns = []
|
||||||
|
ipcMain.on('peardock:teardown-register', () => {
|
||||||
|
// Renderer registers via preload bridge; actual teardown on before-quit
|
||||||
|
})
|
||||||
|
|
||||||
|
app.whenReady().then(async () => {
|
||||||
|
ensureDir(storageDir())
|
||||||
|
|
||||||
|
// App root: project root in dev; Electron app path (app.asar or app/) when packaged
|
||||||
|
const appRoot = app.isPackaged ? app.getAppPath() : path.resolve(__dirname, '..')
|
||||||
|
|
||||||
|
await startStaticServer(appRoot)
|
||||||
|
await startHolesailControl()
|
||||||
|
|
||||||
|
// Expose storage path for renderer polyfill via process.env (nodeIntegration)
|
||||||
|
process.env.PEARDOCK_STORAGE = storageDir()
|
||||||
|
process.env.PEARDOCK_APP_NAME = appName
|
||||||
|
|
||||||
|
createWindow()
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
if (process.platform !== 'darwin') app.quit()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('before-quit', async (e) => {
|
||||||
|
try {
|
||||||
|
await holesailControl?.close?.()
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
if (staticServer) {
|
||||||
|
try {
|
||||||
|
staticServer.close()
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const fn of teardownFns) {
|
||||||
|
try {
|
||||||
|
await fn()
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Identity for single-instance
|
||||||
|
const gotLock = app.requestSingleInstanceLock()
|
||||||
|
if (!gotLock) {
|
||||||
|
app.quit()
|
||||||
|
} else {
|
||||||
|
app.on('second-instance', () => {
|
||||||
|
const wins = BrowserWindow.getAllWindows()
|
||||||
|
if (wins[0]) {
|
||||||
|
if (wins[0].isMinimized()) wins[0].restore()
|
||||||
|
wins[0].focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* Preload + Pear polyfill for the full peardock GUI under Electron.
|
||||||
|
* Runs with contextIsolation: false / nodeIntegration: true so app.js ESM
|
||||||
|
* can import HyperDHT packages (pear-electron UI equivalent).
|
||||||
|
*/
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const { ipcRenderer } = require('electron')
|
||||||
|
const path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
|
const os = require('os')
|
||||||
|
|
||||||
|
const cfg = ipcRenderer.sendSync('peardock:get-pear-config') || {}
|
||||||
|
const storage =
|
||||||
|
cfg.storage ||
|
||||||
|
process.env.PEARDOCK_STORAGE ||
|
||||||
|
path.join(os.homedir(), '.config', 'peardock', 'storage')
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(storage, { recursive: true })
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
const teardownHooks = []
|
||||||
|
|
||||||
|
/** Pear surface used by peardock index/holesailLocal/UI */
|
||||||
|
const Pear = {
|
||||||
|
config: {
|
||||||
|
storage,
|
||||||
|
name: cfg.name || 'peardock',
|
||||||
|
version: cfg.version || '0.0.0',
|
||||||
|
},
|
||||||
|
// alias some runtimes use
|
||||||
|
app: {
|
||||||
|
storage,
|
||||||
|
name: cfg.name || 'peardock',
|
||||||
|
},
|
||||||
|
exit(code = 0) {
|
||||||
|
try {
|
||||||
|
ipcRenderer.invoke('peardock:exit', code)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
},
|
||||||
|
teardown(fn) {
|
||||||
|
if (typeof fn === 'function') teardownHooks.push(fn)
|
||||||
|
},
|
||||||
|
constructor: {
|
||||||
|
IPC: null,
|
||||||
|
UI: null,
|
||||||
|
CUTOVER: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.Pear = Pear
|
||||||
|
// Also on window for any non-module scripts
|
||||||
|
try {
|
||||||
|
window.Pear = Pear
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
// pear-ctrl custom element is a no-op stub outside pear platform chrome
|
||||||
|
try {
|
||||||
|
if (typeof customElements !== 'undefined' && !customElements.get('pear-ctrl')) {
|
||||||
|
class PearCtrl extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
this.style.display = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('pear-ctrl', PearCtrl)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
for (const fn of teardownHooks) {
|
||||||
|
try {
|
||||||
|
const r = fn()
|
||||||
|
if (r && typeof r.then === 'function') r.catch(() => {})
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('[peardock] Pear polyfill ready, storage=', storage)
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* Electron Forge config for peardock-client (full Pear GUI).
|
||||||
|
* Pattern: holepunchto/hello-pear-electron
|
||||||
|
*/
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const path = require('path')
|
||||||
|
const fs = require('fs')
|
||||||
|
|
||||||
|
const pkg = require('./package.json')
|
||||||
|
const appName = pkg.productName || pkg.name || 'peardock'
|
||||||
|
|
||||||
|
/** Files/dirs to ignore when packaging (keep client UI + deps, drop server bulk if possible) */
|
||||||
|
const ignore = [
|
||||||
|
/^\/\.git($|\/)/,
|
||||||
|
/^\/\.gitea($|\/)/,
|
||||||
|
/^\/\.github($|\/)/,
|
||||||
|
/^\/out($|\/)/,
|
||||||
|
/^\/dist($|\/)/,
|
||||||
|
/^\/screenshots($|\/)/,
|
||||||
|
/^\/test($|\/)/,
|
||||||
|
/^\/docs($|\/)/,
|
||||||
|
// Server is a separate bare binary — still include shared protocol used by client
|
||||||
|
// Keep server/ out of client package to slim the app
|
||||||
|
/^\/server($|\/)/,
|
||||||
|
/^\/deploy($|\/)/,
|
||||||
|
/^\/build\/stubs($|\/)/,
|
||||||
|
/^\/build\/shims($|\/)/,
|
||||||
|
/\.md$/,
|
||||||
|
/^\/peardock-.*\.json$/,
|
||||||
|
/^\/\.env$/,
|
||||||
|
]
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
packagerConfig: {
|
||||||
|
name: appName,
|
||||||
|
executableName: 'peardock-client',
|
||||||
|
appBundleId: 'com.peardock.app',
|
||||||
|
icon: fs.existsSync(path.join(__dirname, 'build', 'icon.png'))
|
||||||
|
? path.join(__dirname, 'build', 'icon')
|
||||||
|
: undefined,
|
||||||
|
// Unpack native addons so HyperDHT / holesail work
|
||||||
|
asar: {
|
||||||
|
unpack: '**/{*.node,*.bare,prebuilds/**}',
|
||||||
|
},
|
||||||
|
ignore: (file) => {
|
||||||
|
if (!file) return false
|
||||||
|
return ignore.some((re) => re.test(file))
|
||||||
|
},
|
||||||
|
derefSymlinks: true,
|
||||||
|
prune: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
rebuildConfig: {},
|
||||||
|
|
||||||
|
makers: [
|
||||||
|
{
|
||||||
|
name: '@electron-forge/maker-zip',
|
||||||
|
platforms: ['darwin', 'linux', 'win32'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '@electron-forge/maker-deb',
|
||||||
|
platforms: ['linux'],
|
||||||
|
config: {
|
||||||
|
options: {
|
||||||
|
maintainer: 'peardock',
|
||||||
|
homepage: 'https://github.com/snxraven/peardock',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pear-electron-forge-maker-appimage',
|
||||||
|
platforms: ['linux'],
|
||||||
|
config: {
|
||||||
|
icons: fs.existsSync(path.join(__dirname, 'build', 'icon.png'))
|
||||||
|
? [{ file: 'build/icon.png', size: 256 }]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: 'electron-forge-plugin-universal-prebuilds',
|
||||||
|
config: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'electron-forge-plugin-prune-prebuilds',
|
||||||
|
config: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
hooks: {
|
||||||
|
preMake: async () => {
|
||||||
|
fs.rmSync(path.join(__dirname, 'out', 'make'), { recursive: true, force: true })
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Pear keeps package.json "main" as index.js (pear run).
|
||||||
|
* Packaged Electron must boot electron/main.cjs instead.
|
||||||
|
*/
|
||||||
|
packageAfterCopy: async (_forgeConfig, buildPath) => {
|
||||||
|
const pkgPath = path.join(buildPath, 'package.json')
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
||||||
|
pkg.main = 'electron/main.cjs'
|
||||||
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
Generated
+8004
-4
File diff suppressed because it is too large
Load Diff
+55
-39
@@ -500,7 +500,7 @@
|
|||||||
"sbom:notes": "node -e \"console.log('See docs/SBOM.md')\"",
|
"sbom:notes": "node -e \"console.log('See docs/SBOM.md')\"",
|
||||||
"make": "node scripts/make.cjs both",
|
"make": "node scripts/make.cjs both",
|
||||||
"make:server": "node scripts/make.cjs server",
|
"make:server": "node scripts/make.cjs server",
|
||||||
"make:client": "node scripts/make.cjs client",
|
"make:client": "electron-forge make",
|
||||||
"make:all": "node scripts/make.cjs all",
|
"make:all": "node scripts/make.cjs all",
|
||||||
"make:server:linux-x64": "node scripts/bare-standalone.cjs --product server --host linux-x64",
|
"make:server:linux-x64": "node scripts/bare-standalone.cjs --product server --host linux-x64",
|
||||||
"make:server:linux-arm64": "node scripts/bare-standalone.cjs --product server --host linux-arm64",
|
"make:server:linux-arm64": "node scripts/bare-standalone.cjs --product server --host linux-arm64",
|
||||||
@@ -508,31 +508,59 @@
|
|||||||
"make:server:darwin-x64": "node scripts/bare-standalone.cjs --product server --host darwin-x64",
|
"make:server:darwin-x64": "node scripts/bare-standalone.cjs --product server --host darwin-x64",
|
||||||
"make:server:win32-x64": "node scripts/bare-standalone.cjs --product server --host win32-x64",
|
"make:server:win32-x64": "node scripts/bare-standalone.cjs --product server --host win32-x64",
|
||||||
"make:server:win32-arm64": "node scripts/bare-standalone.cjs --product server --host win32-arm64",
|
"make:server:win32-arm64": "node scripts/bare-standalone.cjs --product server --host win32-arm64",
|
||||||
"make:client:linux-x64": "node scripts/bare-standalone.cjs --product client --host linux-x64",
|
"make:client:linux-x64": "electron-forge make --platform linux --arch x64",
|
||||||
"make:client:linux-arm64": "node scripts/bare-standalone.cjs --product client --host linux-arm64",
|
"make:client:linux-arm64": "electron-forge make --platform linux --arch arm64",
|
||||||
"make:client:darwin-arm64": "node scripts/bare-standalone.cjs --product client --host darwin-arm64",
|
"make:client:darwin-arm64": "electron-forge make --platform darwin --arch arm64",
|
||||||
"make:client:darwin-x64": "node scripts/bare-standalone.cjs --product client --host darwin-x64",
|
"make:client:darwin-x64": "electron-forge make --platform darwin --arch x64",
|
||||||
"make:client:win32-x64": "node scripts/bare-standalone.cjs --product client --host win32-x64",
|
"make:client:win32-x64": "electron-forge make --platform win32 --arch x64",
|
||||||
"make:client:win32-arm64": "node scripts/bare-standalone.cjs --product client --host win32-arm64",
|
"make:client:win32-arm64": "electron-forge make --platform win32 --arch arm64",
|
||||||
"release:rolling": "bash scripts/gitea-rolling-release.sh",
|
"release:rolling": "bash scripts/gitea-rolling-release.sh",
|
||||||
"start:server:bin": "node bin/peardock-server.mjs",
|
"start:server:bin": "node bin/peardock-server.mjs",
|
||||||
"make:bin": "node scripts/bare-standalone.cjs"
|
"make:bin": "node scripts/bare-standalone.cjs",
|
||||||
|
"start:client": "electron electron/main.cjs",
|
||||||
|
"package:client": "electron-forge package"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"b4a": "^1.8.1",
|
"b4a": "^1.8.1",
|
||||||
|
"bare-abort-controller": "^1.0.0",
|
||||||
|
"bare-assert": "^1.1.0",
|
||||||
|
"bare-async-hooks": "^0.0.0",
|
||||||
|
"bare-buffer": "^3.3.1",
|
||||||
|
"bare-console": "^6.0.1",
|
||||||
"bare-crypto": "^1.15.3",
|
"bare-crypto": "^1.15.3",
|
||||||
|
"bare-dgram": "^1.0.1",
|
||||||
|
"bare-diagnostics-channel": "^1.1.0",
|
||||||
|
"bare-dns": "^2.1.4",
|
||||||
|
"bare-encoding": "^1.0.0",
|
||||||
"bare-events": "^2.9.1",
|
"bare-events": "^2.9.1",
|
||||||
|
"bare-fetch": "^3.0.0",
|
||||||
"bare-fs": "^4.7.4",
|
"bare-fs": "^4.7.4",
|
||||||
"bare-http1": "^4.5.7",
|
"bare-http1": "^4.5.7",
|
||||||
|
"bare-https": "^3.0.0",
|
||||||
|
"bare-inspector": "^6.0.1",
|
||||||
|
"bare-module": "^6.1.2",
|
||||||
"bare-net": "^2.3.2",
|
"bare-net": "^2.3.2",
|
||||||
|
"bare-node-runtime": "^1.5.0",
|
||||||
"bare-os": "^3.9.3",
|
"bare-os": "^3.9.3",
|
||||||
"bare-path": "^3.1.1",
|
"bare-path": "^3.1.1",
|
||||||
|
"bare-performance": "^2.0.0",
|
||||||
"bare-process": "^4.5.1",
|
"bare-process": "^4.5.1",
|
||||||
|
"bare-punycode": "^0.0.0",
|
||||||
|
"bare-querystring": "^1.0.0",
|
||||||
|
"bare-stream": "^2.7.0",
|
||||||
|
"bare-string-decoder": "^1.0.0",
|
||||||
"bare-subprocess": "^5.2.3",
|
"bare-subprocess": "^5.2.3",
|
||||||
|
"bare-timers": "^3.0.0",
|
||||||
|
"bare-tls": "^3.0.0",
|
||||||
|
"bare-tty": "^5.0.0",
|
||||||
"bare-url": "^2.4.5",
|
"bare-url": "^2.4.5",
|
||||||
|
"bare-utils": "^1.5.1",
|
||||||
|
"bare-worker": "^4.0.0",
|
||||||
|
"bare-ws": "^3.0.0",
|
||||||
|
"bare-zlib": "^1.3.1",
|
||||||
"compact-encoding": "^3.3.0",
|
"compact-encoding": "^3.3.0",
|
||||||
"dockerode": "^5.0.1",
|
"dockerode": "^5.0.1",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
@@ -549,39 +577,27 @@
|
|||||||
"protomux-rpc": "^1.10.0",
|
"protomux-rpc": "^1.10.0",
|
||||||
"safety-catch": "^1.0.3",
|
"safety-catch": "^1.0.3",
|
||||||
"which-runtime": "^1.4.0",
|
"which-runtime": "^1.4.0",
|
||||||
"z32": "^1.1.0",
|
"z32": "^1.1.0"
|
||||||
"bare-assert": "^1.1.0",
|
|
||||||
"bare-async-hooks": "^0.0.0",
|
|
||||||
"bare-buffer": "^3.3.1",
|
|
||||||
"bare-console": "^6.0.1",
|
|
||||||
"bare-dgram": "^1.0.1",
|
|
||||||
"bare-diagnostics-channel": "^1.1.0",
|
|
||||||
"bare-dns": "^2.1.4",
|
|
||||||
"bare-fetch": "^3.0.0",
|
|
||||||
"bare-https": "^3.0.0",
|
|
||||||
"bare-inspector": "^6.0.1",
|
|
||||||
"bare-module": "^6.1.2",
|
|
||||||
"bare-performance": "^2.0.0",
|
|
||||||
"bare-punycode": "^0.0.0",
|
|
||||||
"bare-querystring": "^1.0.0",
|
|
||||||
"bare-stream": "^2.7.0",
|
|
||||||
"bare-string-decoder": "^1.0.0",
|
|
||||||
"bare-timers": "^3.0.0",
|
|
||||||
"bare-tls": "^3.0.0",
|
|
||||||
"bare-tty": "^5.0.0",
|
|
||||||
"bare-utils": "^1.5.1",
|
|
||||||
"bare-worker": "^4.0.0",
|
|
||||||
"bare-ws": "^3.0.0",
|
|
||||||
"bare-zlib": "^1.3.1",
|
|
||||||
"bare-node-runtime": "^1.5.0",
|
|
||||||
"bare-abort-controller": "^1.0.0",
|
|
||||||
"bare-encoding": "^1.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"brittle": "^4.1.0",
|
"@electron-forge/cli": "^7.11.2",
|
||||||
"pear-interface": "^1.1.0",
|
"@electron-forge/maker-deb": "^7.11.2",
|
||||||
|
"@electron-forge/maker-squirrel": "^7.11.2",
|
||||||
|
"@electron-forge/maker-zip": "^7.11.2",
|
||||||
"bare-build": "^1.0.2",
|
"bare-build": "^1.0.2",
|
||||||
"bare-runtime": "1.30.3"
|
"bare-runtime": "1.30.3",
|
||||||
|
"brittle": "^4.1.0",
|
||||||
|
"electron": "^33.4.11",
|
||||||
|
"electron-forge-plugin-prune-prebuilds": "^1.0.1",
|
||||||
|
"electron-forge-plugin-universal-prebuilds": "^1.0.0",
|
||||||
|
"framed-stream": "^1.0.1",
|
||||||
|
"paparam": "^1.10.1",
|
||||||
|
"pear-electron-forge-maker-appimage": "^2.0.0",
|
||||||
|
"pear-interface": "^1.1.0",
|
||||||
|
"pear-runtime": "^1.3.1"
|
||||||
},
|
},
|
||||||
"productName": "peardock"
|
"productName": "peardock",
|
||||||
|
"config": {
|
||||||
|
"forge": "./forge.config.cjs"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,11 +42,8 @@ const PRODUCTS = {
|
|||||||
entry: path.join(root, 'bin/peardock-server.mjs'),
|
entry: path.join(root, 'bin/peardock-server.mjs'),
|
||||||
outPrefix: path.join(root, 'out/server'),
|
outPrefix: path.join(root, 'out/server'),
|
||||||
},
|
},
|
||||||
client: {
|
// peardock-client (full Pear GUI) is built with Electron Forge — not bare-build.
|
||||||
name: 'peardock-client',
|
// See: npm run make:client / electron-forge make
|
||||||
entry: path.join(root, 'bin/peardock-client.mjs'),
|
|
||||||
outPrefix: path.join(root, 'out/client'),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_DEFER = [
|
const DEFAULT_DEFER = [
|
||||||
@@ -154,8 +151,13 @@ async function embedStandalone(bundle, hosts, name, outDir) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function buildProduct(productKey, hosts) {
|
async function buildProduct(productKey, hosts) {
|
||||||
|
if (productKey === 'client') {
|
||||||
|
throw new Error(
|
||||||
|
'peardock-client is the full Pear GUI — build with: npm run make:client (Electron Forge), not bare-standalone'
|
||||||
|
)
|
||||||
|
}
|
||||||
const product = PRODUCTS[productKey]
|
const product = PRODUCTS[productKey]
|
||||||
if (!product) throw new Error(`Unknown product '${productKey}' (server|client)`)
|
if (!product) throw new Error(`Unknown product '${productKey}' (server)`)
|
||||||
|
|
||||||
const imports = loadImports()
|
const imports = loadImports()
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Publish / refresh a rolling Gitea release with standalone bare-build binaries.
|
# Publish / refresh a rolling Gitea release.
|
||||||
|
#
|
||||||
|
# Artifacts:
|
||||||
|
# peardock-server-* — Bare standalone (out/server/<host>/)
|
||||||
|
# peardock-client-* — full Pear GUI (Electron: .AppImage / .zip / .deb from CLIENT_STAGE or out/make)
|
||||||
#
|
#
|
||||||
# Required env:
|
# Required env:
|
||||||
# RELEASE_TOKEN Gitea personal access token (repo write + package/release)
|
# RELEASE_TOKEN, GITEA_URL, GITEA_OWNER, GITEA_REPO
|
||||||
# GITEA_URL e.g. https://git.example.com (no trailing slash)
|
|
||||||
# GITEA_OWNER e.g. snxraven
|
|
||||||
# GITEA_REPO e.g. peardock
|
|
||||||
#
|
#
|
||||||
# Optional:
|
# Optional:
|
||||||
# RELEASE_TAG default: rolling
|
# RELEASE_TAG (default rolling), ARTIFACT_DIR (default ./out), CLIENT_STAGE
|
||||||
# RELEASE_TITLE default: "Rolling release"
|
|
||||||
# ARTIFACT_DIR default: ./out
|
|
||||||
# GITEA_API override full API base (default: $GITEA_URL/api/v1)
|
|
||||||
#
|
#
|
||||||
# Usage (after make):
|
|
||||||
# ./scripts/gitea-rolling-release.sh
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
ARTIFACT_DIR="${ARTIFACT_DIR:-$ROOT/out}"
|
ARTIFACT_DIR="${ARTIFACT_DIR:-$ROOT/out}"
|
||||||
|
CLIENT_STAGE="${CLIENT_STAGE:-}"
|
||||||
RELEASE_TAG="${RELEASE_TAG:-rolling}"
|
RELEASE_TAG="${RELEASE_TAG:-rolling}"
|
||||||
RELEASE_TITLE="${RELEASE_TITLE:-Rolling release}"
|
RELEASE_TITLE="${RELEASE_TITLE:-Rolling release}"
|
||||||
VERSION="$(node -p "require('$ROOT/package.json').version" 2>/dev/null || echo 0.0.0)"
|
VERSION="$(node -p "require('$ROOT/package.json').version" 2>/dev/null || echo 0.0.0)"
|
||||||
@@ -38,54 +35,94 @@ CT="Content-Type: application/json"
|
|||||||
echo "[release] tag=${RELEASE_TAG} version=${VERSION} commit=${COMMIT} stamp=${STAMP}"
|
echo "[release] tag=${RELEASE_TAG} version=${VERSION} commit=${COMMIT} stamp=${STAMP}"
|
||||||
echo "[release] api=${API}/repos/${GITEA_OWNER}/${GITEA_REPO}"
|
echo "[release] api=${API}/repos/${GITEA_OWNER}/${GITEA_REPO}"
|
||||||
|
|
||||||
# Collect artifacts: out/{server,client}/<host>/peardock-*
|
|
||||||
mapfile -t FILES < <(find "$ARTIFACT_DIR" -type f \( -name 'peardock-server' -o -name 'peardock-server.exe' -o -name 'peardock-client' -o -name 'peardock-client.exe' \) 2>/dev/null | sort)
|
|
||||||
if [[ ${#FILES[@]} -eq 0 ]]; then
|
|
||||||
echo "[release] no binaries under $ARTIFACT_DIR — run: npm run make:all (or matrix CI builds)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
STAGE="$ROOT/dist/rolling-${STAMP}"
|
STAGE="$ROOT/dist/rolling-${STAMP}"
|
||||||
mkdir -p "$STAGE"
|
mkdir -p "$STAGE"
|
||||||
|
|
||||||
for f in "${FILES[@]}"; do
|
# --- Server Bare binaries ---
|
||||||
# out/server/linux-x64/peardock-server → peardock-server-linux-x64
|
mapfile -t SERVER_FILES < <(find "$ARTIFACT_DIR/server" -type f \( -name 'peardock-server' -o -name 'peardock-server.exe' \) 2>/dev/null | sort || true)
|
||||||
|
for f in "${SERVER_FILES[@]+"${SERVER_FILES[@]}"}"; do
|
||||||
|
[[ -z "${f:-}" ]] && continue
|
||||||
rel="${f#"$ARTIFACT_DIR"/}"
|
rel="${f#"$ARTIFACT_DIR"/}"
|
||||||
product="$(echo "$rel" | cut -d/ -f1)" # server|client
|
host="$(echo "$rel" | cut -d/ -f2)"
|
||||||
host="$(echo "$rel" | cut -d/ -f2)" # linux-x64
|
|
||||||
base="$(basename "$f")"
|
|
||||||
base="${base%.exe}"
|
|
||||||
ext=""
|
ext=""
|
||||||
[[ "$f" == *.exe ]] && ext=".exe"
|
[[ "$f" == *.exe ]] && ext=".exe"
|
||||||
dest="${base}-${host}${ext}"
|
dest="peardock-server-${host}${ext}"
|
||||||
cp -a "$f" "$STAGE/$dest"
|
cp -a "$f" "$STAGE/$dest"
|
||||||
echo "[release] staged $dest"
|
echo "[release] staged $dest"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Checksums
|
# --- Client full Pear GUI packages ---
|
||||||
|
collect_clients() {
|
||||||
|
local dir="$1"
|
||||||
|
[[ -d "$dir" ]] || return 0
|
||||||
|
find "$dir" -type f \( \
|
||||||
|
-name 'peardock-client-*' -o \
|
||||||
|
-name '*.AppImage' -o \
|
||||||
|
-name '*.zip' -o \
|
||||||
|
-name '*.deb' -o \
|
||||||
|
-name '*.dmg' -o \
|
||||||
|
-name '*.msix' \
|
||||||
|
\) 2>/dev/null | sort
|
||||||
|
}
|
||||||
|
|
||||||
|
mapfile -t CLIENT_FILES < <({
|
||||||
|
collect_clients "${CLIENT_STAGE}"
|
||||||
|
collect_clients "$ARTIFACT_DIR/make"
|
||||||
|
collect_clients "$ARTIFACT_DIR/client"
|
||||||
|
# Packaged dirs: zip if not already staged
|
||||||
|
find "$ARTIFACT_DIR" -maxdepth 1 -type d -name 'peardock-*' 2>/dev/null | while read -r d; do
|
||||||
|
base=$(basename "$d")
|
||||||
|
zipname="peardock-client-${base#peardock-}.zip"
|
||||||
|
if [[ ! -f "$STAGE/$zipname" ]]; then
|
||||||
|
(cd "$ARTIFACT_DIR" && zip -qry "$STAGE/$zipname" "$base")
|
||||||
|
echo "[release] zipped $zipname" >&2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
} | sort -u)
|
||||||
|
|
||||||
|
for f in "${CLIENT_FILES[@]+"${CLIENT_FILES[@]}"}"; do
|
||||||
|
[[ -z "${f:-}" || ! -f "$f" ]] && continue
|
||||||
|
dest=$(basename "$f")
|
||||||
|
# Normalize forge names
|
||||||
|
if [[ "$dest" != peardock-client-* ]]; then
|
||||||
|
dest="peardock-client-${dest}"
|
||||||
|
fi
|
||||||
|
cp -a "$f" "$STAGE/$dest"
|
||||||
|
echo "[release] staged $dest"
|
||||||
|
done
|
||||||
|
|
||||||
|
count=$(find "$STAGE" -type f ! -name 'SHA256SUMS' | wc -l)
|
||||||
|
if [[ "$count" -eq 0 ]]; then
|
||||||
|
echo "[release] no artifacts staged — build server (bare) and/or client (electron-forge) first"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
(
|
(
|
||||||
cd "$STAGE"
|
cd "$STAGE"
|
||||||
sha256sum peardock-* > SHA256SUMS
|
sha256sum peardock-* > SHA256SUMS 2>/dev/null || sha256sum * > SHA256SUMS
|
||||||
)
|
)
|
||||||
|
|
||||||
BODY=$(cat <<EOF
|
BODY=$(cat <<EOF
|
||||||
Automated **rolling** release of peardock standalone Bare binaries.
|
Automated **rolling** release of peardock.
|
||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|-------|-------|
|
|-------|-------|
|
||||||
| Version | \`${VERSION}\` |
|
| Version | \`${VERSION}\` |
|
||||||
| Commit | \`${COMMIT}\` |
|
| Commit | \`${COMMIT}\` |
|
||||||
| Built | \`${STAMP}\` |
|
| Built | \`${STAMP}\` |
|
||||||
| Tooling | \`bare-build --standalone\` (embeds JS graph + native addons) |
|
|
||||||
|
|
||||||
## Binaries
|
## Artifacts
|
||||||
|
|
||||||
- **peardock-server-\<host\>** — HyperDHT Docker control plane
|
| Product | Tooling | Contents |
|
||||||
- **peardock-client-\<host\>** — Pear/Bare desktop client entry
|
|---------|---------|----------|
|
||||||
|
| **peardock-server-\<host\>** | \`bare-build --standalone\` | HyperDHT Docker control plane (all JS + addons embedded) |
|
||||||
|
| **peardock-client-\<host\>** | Electron Forge (full Pear GUI) | Desktop UI + HyperDHT client + Holesail control |
|
||||||
|
|
||||||
Hosts follow Bare addon naming: \`linux-x64\`, \`linux-arm64\`, \`darwin-arm64\`, \`darwin-x64\`, \`win32-x64\`, \`win32-arm64\`.
|
### Server hosts
|
||||||
|
\`linux-x64\`, \`linux-arm64\`, \`darwin-arm64\`, \`darwin-x64\`, \`win32-x64\`, \`win32-arm64\`
|
||||||
|
|
||||||
Verify:
|
### Client
|
||||||
|
AppImage / zip of the packaged Electron app (same UI as \`pear run -d .\`).
|
||||||
|
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
sha256sum -c SHA256SUMS
|
sha256sum -c SHA256SUMS
|
||||||
@@ -93,13 +130,11 @@ sha256sum -c SHA256SUMS
|
|||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
# Escape body for JSON
|
|
||||||
json_escape() {
|
json_escape() {
|
||||||
node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>process.stdout.write(JSON.stringify(s)))'
|
node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>process.stdout.write(JSON.stringify(s)))'
|
||||||
}
|
}
|
||||||
BODY_JSON=$(printf '%s' "$BODY" | json_escape)
|
BODY_JSON=$(printf '%s' "$BODY" | json_escape)
|
||||||
|
|
||||||
# Find existing release by tag
|
|
||||||
EXISTING_ID=$(curl -fsSL -H "$AUTH" -H "$ACCEPT" \
|
EXISTING_ID=$(curl -fsSL -H "$AUTH" -H "$ACCEPT" \
|
||||||
"${API}/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/tags/${RELEASE_TAG}" \
|
"${API}/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/tags/${RELEASE_TAG}" \
|
||||||
2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{try{console.log(JSON.parse(s).id||"")}catch{console.log("")}}') || true)
|
2>/dev/null | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>{try{console.log(JSON.parse(s).id||"")}catch{console.log("")}}') || true)
|
||||||
@@ -108,7 +143,6 @@ if [[ -n "${EXISTING_ID}" ]]; then
|
|||||||
echo "[release] deleting existing release id=${EXISTING_ID} tag=${RELEASE_TAG}"
|
echo "[release] deleting existing release id=${EXISTING_ID} tag=${RELEASE_TAG}"
|
||||||
curl -fsSL -X DELETE -H "$AUTH" \
|
curl -fsSL -X DELETE -H "$AUTH" \
|
||||||
"${API}/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/${EXISTING_ID}" >/dev/null
|
"${API}/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/${EXISTING_ID}" >/dev/null
|
||||||
# Delete tag so we can recreate (rolling)
|
|
||||||
curl -fsSL -X DELETE -H "$AUTH" \
|
curl -fsSL -X DELETE -H "$AUTH" \
|
||||||
"${API}/repos/${GITEA_OWNER}/${GITEA_REPO}/tags/${RELEASE_TAG}" >/dev/null 2>&1 || true
|
"${API}/repos/${GITEA_OWNER}/${GITEA_REPO}/tags/${RELEASE_TAG}" >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
@@ -126,9 +160,11 @@ upload() {
|
|||||||
local name
|
local name
|
||||||
name="$(basename "$file")"
|
name="$(basename "$file")"
|
||||||
echo "[release] upload ${name}"
|
echo "[release] upload ${name}"
|
||||||
|
local enc
|
||||||
|
enc=$(node -e "console.log(encodeURIComponent(process.argv[1]))" "$name")
|
||||||
curl -fsSL -X POST -H "$AUTH" \
|
curl -fsSL -X POST -H "$AUTH" \
|
||||||
-H "Content-Type: application/octet-stream" \
|
-H "Content-Type: application/octet-stream" \
|
||||||
"${API}/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/${RELEASE_ID}/assets?name=$(printf '%s' "$name" | jq -sRr @uri 2>/dev/null || node -e "console.log(encodeURIComponent(process.argv[1]))" "$name")" \
|
"${API}/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/${RELEASE_ID}/assets?name=${enc}" \
|
||||||
--data-binary @"$file" >/dev/null
|
--data-binary @"$file" >/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+62
-33
@@ -1,14 +1,15 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* Host-aware Bare standalone builder for peardock.
|
* peardock build orchestrator
|
||||||
* Delegates to scripts/bare-standalone.cjs (bare-pack + bare-build embed).
|
*
|
||||||
|
* server → Bare standalone (bare-pack + bare-build embed)
|
||||||
|
* client → full Pear GUI via Electron Forge (hello-pear-electron pattern)
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* node scripts/make.cjs server # native host
|
* node scripts/make.cjs server [host]
|
||||||
* node scripts/make.cjs client
|
* node scripts/make.cjs client [platform arch]
|
||||||
* node scripts/make.cjs both
|
* node scripts/make.cjs both
|
||||||
* node scripts/make.cjs server linux-x64
|
* node scripts/make.cjs all
|
||||||
* node scripts/make.cjs all # all desktop hosts × both products
|
|
||||||
*/
|
*/
|
||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
@@ -18,9 +19,9 @@ const { spawnSync } = require('child_process')
|
|||||||
|
|
||||||
const root = path.resolve(__dirname, '..')
|
const root = path.resolve(__dirname, '..')
|
||||||
const nativeHost = `${os.platform()}-${os.arch()}`
|
const nativeHost = `${os.platform()}-${os.arch()}`
|
||||||
const builder = path.join(root, 'scripts/bare-standalone.cjs')
|
const serverBuilder = path.join(root, 'scripts/bare-standalone.cjs')
|
||||||
|
|
||||||
const HOSTS = [
|
const SERVER_HOSTS = [
|
||||||
'darwin-arm64',
|
'darwin-arm64',
|
||||||
'darwin-x64',
|
'darwin-x64',
|
||||||
'linux-arm64',
|
'linux-arm64',
|
||||||
@@ -29,16 +30,13 @@ const HOSTS = [
|
|||||||
'win32-x64',
|
'win32-x64',
|
||||||
]
|
]
|
||||||
|
|
||||||
function run(product, hosts) {
|
function run(cmd, args, opts = {}) {
|
||||||
const args = [builder, '--product', product]
|
console.log(`[make] ${cmd} ${args.join(' ')}`)
|
||||||
for (const h of hosts) {
|
const res = spawnSync(cmd, args, {
|
||||||
args.push('--host', h)
|
|
||||||
}
|
|
||||||
console.log(`[make] node ${args.map((a) => (a.includes(' ') ? JSON.stringify(a) : a)).join(' ')}`)
|
|
||||||
const res = spawnSync(process.execPath, args, {
|
|
||||||
cwd: root,
|
cwd: root,
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
env: process.env,
|
env: process.env,
|
||||||
|
shell: opts.shell || false,
|
||||||
})
|
})
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
console.error(res.error.message)
|
console.error(res.error.message)
|
||||||
@@ -47,33 +45,64 @@ function run(product, hosts) {
|
|||||||
if (res.status !== 0) process.exit(res.status || 1)
|
if (res.status !== 0) process.exit(res.status || 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeServer(hosts) {
|
||||||
|
const args = [serverBuilder, '--product', 'server']
|
||||||
|
for (const h of hosts) {
|
||||||
|
args.push('--host', h)
|
||||||
|
}
|
||||||
|
run(process.execPath, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeClient(platform, arch) {
|
||||||
|
// electron-forge make on current OS; optional platform/arch for cross where supported
|
||||||
|
const args = ['electron-forge', 'make']
|
||||||
|
if (platform) args.push('--platform', platform)
|
||||||
|
if (arch) args.push('--arch', arch)
|
||||||
|
run('npx', args, { shell: os.platform() === 'win32' })
|
||||||
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
const argv = process.argv.slice(2)
|
const argv = process.argv.slice(2)
|
||||||
const target = argv[0] || 'both'
|
const target = argv[0] || 'both'
|
||||||
|
|
||||||
let products = []
|
if (target === 'server') {
|
||||||
let hosts = []
|
const host = argv[1] || nativeHost
|
||||||
|
makeServer([host])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target === 'client') {
|
||||||
|
// argv[1] may be host like linux-x64 or platform
|
||||||
|
const spec = argv[1] || `${os.platform()}-${os.arch()}`
|
||||||
|
if (spec.includes('-')) {
|
||||||
|
const [platform, arch] = spec.split('-')
|
||||||
|
makeClient(platform, arch)
|
||||||
|
} else {
|
||||||
|
makeClient(spec, argv[2])
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target === 'both') {
|
||||||
|
makeServer([argv[1] || nativeHost])
|
||||||
|
makeClient()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (target === 'all') {
|
if (target === 'all') {
|
||||||
products = ['server', 'client']
|
makeServer(SERVER_HOSTS)
|
||||||
hosts = [...HOSTS]
|
// Client forge packages for the runner OS only (cross-compile Electron is limited)
|
||||||
} else if (target === 'both') {
|
makeClient()
|
||||||
products = ['server', 'client']
|
return
|
||||||
hosts = [argv[1] || nativeHost]
|
|
||||||
} else if (target === 'server' || target === 'client') {
|
|
||||||
products = [target]
|
|
||||||
hosts = [argv[1] || nativeHost]
|
|
||||||
} else if (HOSTS.includes(target)) {
|
|
||||||
products = ['server', 'client']
|
|
||||||
hosts = [target]
|
|
||||||
} else {
|
|
||||||
console.error('Usage: node scripts/make.cjs <server|client|both|all|host> [host]')
|
|
||||||
process.exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const product of products) {
|
if (SERVER_HOSTS.includes(target)) {
|
||||||
run(product, hosts)
|
makeServer([target])
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.error('Usage: node scripts/make.cjs <server|client|both|all|host> [host|platform]')
|
||||||
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user