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

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:
2026-07-11 00:24:26 -04:00
parent 0e097308ce
commit 08b6e8be6d
15 changed files with 8917 additions and 511 deletions
+10 -2
View File
@@ -26,11 +26,19 @@ jobs:
- name: Unit + RPC tests
run: npm test
- name: Smoke-pack server (linux-x64)
- name: Smoke-pack server (linux-x64 Bare)
run: npm run make:server:linux-x64
- name: Verify binary exists
- name: Verify server binary
run: |
test -x out/server/linux-x64/peardock-server
file out/server/linux-x64/peardock-server || true
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
+108 -94
View File
@@ -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:
# RELEASE_TOKEN — personal access token with repository release write scope
#
# 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.
# Secret: RELEASE_TOKEN (Gitea PAT with release write)
# Optional: GITEA_URL
name: Rolling release
@@ -27,134 +21,153 @@ env:
RELEASE_TAG: rolling
jobs:
build:
name: ${{ matrix.product }} / ${{ matrix.host }}
build-server:
name: server / ${{ matrix.host }}
strategy:
fail-fast: false
matrix:
include:
# Linux x64 — primary server target
- os: ubuntu-latest
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
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
host: darwin-arm64
product: server
- os: ubuntu-latest
host: darwin-x64
product: server
- os: ubuntu-latest
host: win32-x64
product: server
- os: ubuntu-latest
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 }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
- name: Install dependencies
run: npm ci
- name: Build standalone binary
run: npm run make:${{ matrix.product }}:${{ matrix.host }}
- name: List artifact
- run: npm ci
- run: npm run make:server:${{ matrix.host }}
- name: Stage server binary
run: |
set -e
dir="out/${{ matrix.product }}/${{ matrix.host }}"
dir="out/server/${{ matrix.host }}"
ls -la "$dir"
# Normalize name for upload
if [ -f "$dir/peardock-${{ matrix.product }}.exe" ]; then
cp "$dir/peardock-${{ matrix.product }}.exe" \
"peardock-${{ matrix.product }}-${{ matrix.host }}.exe"
if [ -f "$dir/peardock-server.exe" ]; then
cp "$dir/peardock-server.exe" "peardock-server-${{ matrix.host }}.exe"
else
cp "$dir/peardock-${{ matrix.product }}" \
"peardock-${{ matrix.product }}-${{ matrix.host }}"
cp "$dir/peardock-server" "peardock-server-${{ matrix.host }}"
fi
ls -la peardock-${{ matrix.product }}-${{ matrix.host }}*
- name: Upload build artifact
uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v4
with:
name: peardock-${{ matrix.product }}-${{ matrix.host }}
path: peardock-${{ matrix.product }}-${{ matrix.host }}*
name: peardock-server-${{ 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
retention-days: 7
publish:
name: Publish rolling release
needs: [build]
needs: [build-server, build-client]
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'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: Download all artifacts
uses: actions/download-artifact@v4
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Stage binaries for release
- name: Stage for release script
run: |
set -euo pipefail
mkdir -p out/server out/client dist
# Flatten artifact dirs into out/{product}/{host}/peardock-*
find artifacts -type f -name 'peardock-*' | while read -r f; do
base="$(basename "$f")"
# peardock-server-linux-x64[.exe]
if [[ "$base" =~ ^peardock-(server|client)-([a-z0-9]+-[a-z0-9]+)(\.exe)?$ ]]; then
product="${BASH_REMATCH[1]}"
host="${BASH_REMATCH[2]}"
ext="${BASH_REMATCH[3]}"
mkdir -p "out/${product}/${host}"
cp -a "$f" "out/${product}/${host}/peardock-${product}${ext}"
echo "staged $base → out/${product}/${host}/"
mkdir -p out/server out/client dist/rolling-stage
find artifacts -type f | sort
# Servers
find artifacts -type f -name 'peardock-server-*' | while read -r f; do
base=$(basename "$f")
# peardock-server-linux-x64
host=${base#peardock-server-}
host=${host%.exe}
mkdir -p "out/server/$host"
if [[ "$base" == *.exe ]]; then
cp "$f" "out/server/$host/peardock-server.exe"
else
echo "skip unrecognized: $base"
cp "$f" "out/server/$host/peardock-server"
fi
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
ls -la artifacts || true
ls -la dist/rolling-stage/ || true
- name: Publish to Gitea (rolling)
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
@@ -163,10 +176,11 @@ jobs:
GITHUB_REPOSITORY: ${{ github.repository }}
RELEASE_TAG: ${{ env.RELEASE_TAG }}
ARTIFACT_DIR: ${{ github.workspace }}/out
CLIENT_STAGE: ${{ github.workspace }}/dist/rolling-stage
run: |
set -euo pipefail
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
fi
export GITEA_URL="${GITEA_URL_SECRET:-${GITHUB_SERVER_URL}}"
+19 -8
View File
@@ -76,22 +76,33 @@ pear release .
pear run pear://<your-app-key>
```
### Standalone Bare binaries
### Standalone binaries
Self-contained ELFs/Mach-O/PE with **all modules and native addons embedded**
([bare-build --standalone](https://github.com/holepunchto/bare-build) pattern):
| Binary | Kind | Command |
|--------|------|---------|
| **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
npm ci
npm run make:server:linux-x64 # → out/server/linux-x64/peardock-server
npm run make:client:linux-x64 # → out/client/linux-x64/peardock-client
npm run make:server:linux-x64
# → 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/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
`RELEASE_TOKEN`. See [docs/RELEASE.md](docs/RELEASE.md).
Dev UI (unchanged):
```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
View File
@@ -1,146 +1,12 @@
/**
* Bare standalone peardock client agent.
* peardock-client entry — full Pear GUI application.
*
* Built with bare-build / scripts/bare-standalone.cjs:
* npm run make:client:linux-x64
* Preferred run modes:
* 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:
* - Self-contained Bare ELF with embedded JS graph + native addons
* - 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
* This module is the Pear platform entry (same as package "main" / index.js).
* Electron packaging uses electron/main.cjs as the process entry instead.
*/
/* global Bare */
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
}
}
await import('../index.js')
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

+2 -1
View File
@@ -21,7 +21,8 @@ let controlCache = null
* @returns {boolean}
*/
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
View File
@@ -1,194 +1,117 @@
# peardock release process
## Standalone Bare binaries (recommended)
## Two products
peardock ships **two separate self-contained binaries** built with the Holepunch
stack (`bare-pack` + `bare-build --standalone`):
| Artifact | What it is | Build |
|----------|------------|--------|
| **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`
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).
## peardock-server (Bare binary)
### Local build
Self-contained ELF/Mach-O/PE with the full JS graph and native addons embedded.
```bash
npm ci
# Native host (server + client)
npm run make
# Explicit targets
npm run make:server:linux-x64
npm run make:client:linux-x64
npm run make:all # all desktop hosts × both products
```
# → out/server/linux-x64/peardock-server
Outputs:
```
out/server/<host>/peardock-server[.exe]
out/client/<host>/peardock-client[.exe]
./out/server/linux-x64/peardock-server
# Identity: PEARDOCK_HOME or next to the binary (.env)
```
Hosts: `linux-x64`, `linux-arm64`, `darwin-arm64`, `darwin-x64`, `win32-x64`, `win32-arm64`.
### Why `scripts/bare-standalone.cjs` instead of plain `bare-build`?
`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.)
Builder: `scripts/bare-standalone.cjs` (passes `bare-node-runtime` imports + Docker socket shims).
---
## 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) + peardocks existing pear-electron UI.
---
## Gitea CI rolling release
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 |
|------|--------|
| `RELEASE_TOKEN` | Personal access token with **repository** write (create/delete releases + upload assets) |
1. Matrix-build **servers** (Bare) for all desktop hosts
2. Matrix-build **clients** (Electron Forge) on Linux/macOS/Windows runners
3. Publish/replace tag **`rolling`** with all assets + `SHA256SUMS`
Optional:
| 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)
Manual:
```bash
export RELEASE_TOKEN=...
export GITEA_URL=https://your.gitea.host
export GITEA_OWNER=snxraven
export GITEA_REPO=peardock
npm run make:all # or copy CI artifacts into out/
export RELEASE_TOKEN=... GITEA_URL=https://your.gitea GITEA_OWNER=... GITEA_REPO=peardock
npm run make:server:linux-x64
npm run make:client
npm run release:rolling
```
---
## Source tarball + checksums (legacy / Node install)
```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:
## systemd (server)
```ini
ExecStart=/opt/peardock/peardock-server
Environment=PEARDOCK_HOME=/opt/peardock
```
See `deploy/peardock.service`.
---
## Pear desktop app (GUI OTA)
## Pear OTA (optional)
```bash
npm ci
pear stage .
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
# Terminal 1
./out/server/linux-x64/peardock-server
# or: npm run server
# Terminal 2
SOAK_DURATION_MS=86400000 npm run soak
./scripts/release-checksums.sh dist/
```
---
## Encoding profile
Handshake returns `schemaVersion` and features. Default encoding remains JSON
(`shared/encodings.js`); binary bulk uses `binaryStream*` + `push:binaryChunk`.
+303
View File
@@ -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()
}
})
}
+89
View File
@@ -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)
+108
View File
@@ -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')
},
},
}
+8004 -4
View File
File diff suppressed because it is too large Load Diff
+55 -39
View File
@@ -500,7 +500,7 @@
"sbom:notes": "node -e \"console.log('See docs/SBOM.md')\"",
"make": "node scripts/make.cjs both",
"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: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",
@@ -508,31 +508,59 @@
"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-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-arm64": "node scripts/bare-standalone.cjs --product client --host linux-arm64",
"make:client:darwin-arm64": "node scripts/bare-standalone.cjs --product client --host darwin-arm64",
"make:client:darwin-x64": "node scripts/bare-standalone.cjs --product client --host darwin-x64",
"make:client:win32-x64": "node scripts/bare-standalone.cjs --product client --host win32-x64",
"make:client:win32-arm64": "node scripts/bare-standalone.cjs --product client --host win32-arm64",
"make:client:linux-x64": "electron-forge make --platform linux --arch x64",
"make:client:linux-arm64": "electron-forge make --platform linux --arch arm64",
"make:client:darwin-arm64": "electron-forge make --platform darwin --arch arm64",
"make:client:darwin-x64": "electron-forge make --platform darwin --arch x64",
"make:client:win32-x64": "electron-forge make --platform win32 --arch x64",
"make:client:win32-arm64": "electron-forge make --platform win32 --arch arm64",
"release:rolling": "bash scripts/gitea-rolling-release.sh",
"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": {
"node": ">=20"
},
"dependencies": {
"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-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-fetch": "^3.0.0",
"bare-fs": "^4.7.4",
"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-node-runtime": "^1.5.0",
"bare-os": "^3.9.3",
"bare-path": "^3.1.1",
"bare-performance": "^2.0.0",
"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-timers": "^3.0.0",
"bare-tls": "^3.0.0",
"bare-tty": "^5.0.0",
"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",
"dockerode": "^5.0.1",
"dotenv": "^17.4.2",
@@ -549,39 +577,27 @@
"protomux-rpc": "^1.10.0",
"safety-catch": "^1.0.3",
"which-runtime": "^1.4.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"
"z32": "^1.1.0"
},
"devDependencies": {
"brittle": "^4.1.0",
"pear-interface": "^1.1.0",
"@electron-forge/cli": "^7.11.2",
"@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-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"
}
}
+8 -6
View File
@@ -42,11 +42,8 @@ const PRODUCTS = {
entry: path.join(root, 'bin/peardock-server.mjs'),
outPrefix: path.join(root, 'out/server'),
},
client: {
name: 'peardock-client',
entry: path.join(root, 'bin/peardock-client.mjs'),
outPrefix: path.join(root, 'out/client'),
},
// peardock-client (full Pear GUI) is built with Electron Forge — not bare-build.
// See: npm run make:client / electron-forge make
}
const DEFAULT_DEFER = [
@@ -154,8 +151,13 @@ async function embedStandalone(bundle, hosts, name, outDir) {
}
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]
if (!product) throw new Error(`Unknown product '${productKey}' (server|client)`)
if (!product) throw new Error(`Unknown product '${productKey}' (server)`)
const imports = loadImports()
console.log(
+74 -38
View File
@@ -1,24 +1,21 @@
#!/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:
# RELEASE_TOKEN Gitea personal access token (repo write + package/release)
# GITEA_URL e.g. https://git.example.com (no trailing slash)
# GITEA_OWNER e.g. snxraven
# GITEA_REPO e.g. peardock
# RELEASE_TOKEN, GITEA_URL, GITEA_OWNER, GITEA_REPO
#
# Optional:
# RELEASE_TAG default: rolling
# RELEASE_TITLE default: "Rolling release"
# ARTIFACT_DIR default: ./out
# GITEA_API override full API base (default: $GITEA_URL/api/v1)
# RELEASE_TAG (default rolling), ARTIFACT_DIR (default ./out), CLIENT_STAGE
#
# Usage (after make):
# ./scripts/gitea-rolling-release.sh
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
ARTIFACT_DIR="${ARTIFACT_DIR:-$ROOT/out}"
CLIENT_STAGE="${CLIENT_STAGE:-}"
RELEASE_TAG="${RELEASE_TAG:-rolling}"
RELEASE_TITLE="${RELEASE_TITLE:-Rolling release}"
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] 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}"
mkdir -p "$STAGE"
for f in "${FILES[@]}"; do
# out/server/linux-x64/peardock-serverpeardock-server-linux-x64
# --- Server Bare binaries ---
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"/}"
product="$(echo "$rel" | cut -d/ -f1)" # server|client
host="$(echo "$rel" | cut -d/ -f2)" # linux-x64
base="$(basename "$f")"
base="${base%.exe}"
host="$(echo "$rel" | cut -d/ -f2)"
ext=""
[[ "$f" == *.exe ]] && ext=".exe"
dest="${base}-${host}${ext}"
dest="peardock-server-${host}${ext}"
cp -a "$f" "$STAGE/$dest"
echo "[release] staged $dest"
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"
sha256sum peardock-* > SHA256SUMS
sha256sum peardock-* > SHA256SUMS 2>/dev/null || sha256sum * > SHA256SUMS
)
BODY=$(cat <<EOF
Automated **rolling** release of peardock standalone Bare binaries.
Automated **rolling** release of peardock.
| Field | Value |
|-------|-------|
| Version | \`${VERSION}\` |
| Commit | \`${COMMIT}\` |
| Built | \`${STAMP}\` |
| Tooling | \`bare-build --standalone\` (embeds JS graph + native addons) |
## Binaries
## Artifacts
- **peardock-server-\<host\>** — HyperDHT Docker control plane
- **peardock-client-\<host\>** — Pear/Bare desktop client entry
| Product | Tooling | Contents |
|---------|---------|----------|
| **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
sha256sum -c SHA256SUMS
@@ -93,13 +130,11 @@ sha256sum -c SHA256SUMS
EOF
)
# Escape body for JSON
json_escape() {
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)
# Find existing release by tag
EXISTING_ID=$(curl -fsSL -H "$AUTH" -H "$ACCEPT" \
"${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)
@@ -108,7 +143,6 @@ if [[ -n "${EXISTING_ID}" ]]; then
echo "[release] deleting existing release id=${EXISTING_ID} tag=${RELEASE_TAG}"
curl -fsSL -X DELETE -H "$AUTH" \
"${API}/repos/${GITEA_OWNER}/${GITEA_REPO}/releases/${EXISTING_ID}" >/dev/null
# Delete tag so we can recreate (rolling)
curl -fsSL -X DELETE -H "$AUTH" \
"${API}/repos/${GITEA_OWNER}/${GITEA_REPO}/tags/${RELEASE_TAG}" >/dev/null 2>&1 || true
fi
@@ -126,9 +160,11 @@ upload() {
local name
name="$(basename "$file")"
echo "[release] upload ${name}"
local enc
enc=$(node -e "console.log(encodeURIComponent(process.argv[1]))" "$name")
curl -fsSL -X POST -H "$AUTH" \
-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
}
+62 -33
View File
@@ -1,14 +1,15 @@
#!/usr/bin/env node
/**
* Host-aware Bare standalone builder for peardock.
* Delegates to scripts/bare-standalone.cjs (bare-pack + bare-build embed).
* peardock build orchestrator
*
* server → Bare standalone (bare-pack + bare-build embed)
* client → full Pear GUI via Electron Forge (hello-pear-electron pattern)
*
* Usage:
* node scripts/make.cjs server # native host
* node scripts/make.cjs client
* node scripts/make.cjs server [host]
* node scripts/make.cjs client [platform arch]
* node scripts/make.cjs both
* node scripts/make.cjs server linux-x64
* node scripts/make.cjs all # all desktop hosts × both products
* node scripts/make.cjs all
*/
'use strict'
@@ -18,9 +19,9 @@ const { spawnSync } = require('child_process')
const root = path.resolve(__dirname, '..')
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-x64',
'linux-arm64',
@@ -29,16 +30,13 @@ const HOSTS = [
'win32-x64',
]
function run(product, hosts) {
const args = [builder, '--product', product]
for (const h of hosts) {
args.push('--host', h)
}
console.log(`[make] node ${args.map((a) => (a.includes(' ') ? JSON.stringify(a) : a)).join(' ')}`)
const res = spawnSync(process.execPath, args, {
function run(cmd, args, opts = {}) {
console.log(`[make] ${cmd} ${args.join(' ')}`)
const res = spawnSync(cmd, args, {
cwd: root,
stdio: 'inherit',
env: process.env,
shell: opts.shell || false,
})
if (res.error) {
console.error(res.error.message)
@@ -47,33 +45,64 @@ function run(product, hosts) {
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() {
const argv = process.argv.slice(2)
const target = argv[0] || 'both'
let products = []
let hosts = []
if (target === 'server') {
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') {
products = ['server', 'client']
hosts = [...HOSTS]
} else if (target === 'both') {
products = ['server', 'client']
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)
makeServer(SERVER_HOSTS)
// Client forge packages for the runner OS only (cross-compile Electron is limited)
makeClient()
return
}
for (const product of products) {
run(product, hosts)
if (SERVER_HOSTS.includes(target)) {
makeServer([target])
return
}
console.error('Usage: node scripts/make.cjs <server|client|both|all|host> [host|platform]')
process.exit(1)
}
main()