Add Bare standalone binaries and Gitea rolling release CI
Rolling release / server / linux-x64 (push) Has been cancelled
Rolling release / client / win32-arm64 (push) Has been cancelled
Rolling release / server / win32-arm64 (push) Has been cancelled
Rolling release / client / win32-x64 (push) Has been cancelled
Rolling release / server / win32-x64 (push) Has been cancelled
Rolling release / Publish rolling release (push) Has been cancelled
Rolling release / client / darwin-arm64 (push) Has been cancelled
Rolling release / server / darwin-arm64 (push) Has been cancelled
Rolling release / client / darwin-x64 (push) Has been cancelled
Rolling release / server / darwin-x64 (push) Has been cancelled
Rolling release / client / linux-arm64 (push) Has been cancelled
Rolling release / server / linux-arm64 (push) Has been cancelled
Rolling release / client / linux-x64 (push) Has been cancelled
CI / test (push) Has been cancelled

Package peardock-server and peardock-client with bare-pack + bare-build
--standalone (hello-pear-bare pattern), embedding the full module graph and
native addons. Server uses bare-node-runtime imports plus Docker socket
shims; client is a Bare agent with holesail control. Gitea Actions builds
multi-host artifacts and publishes a rolling release via RELEASE_TOKEN.
This commit is contained in:
2026-07-11 00:19:39 -04:00
parent fa99bd8c6e
commit 0e097308ce
21 changed files with 2920 additions and 69 deletions
+36
View File
@@ -0,0 +1,36 @@
# peardock CI (Gitea Actions)
name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
- name: Install
run: npm ci
- name: Unit + RPC tests
run: npm test
- name: Smoke-pack server (linux-x64)
run: npm run make:server:linux-x64
- name: Verify binary exists
run: |
test -x out/server/linux-x64/peardock-server
file out/server/linux-x64/peardock-server || true
ls -lh out/server/linux-x64/
+176
View File
@@ -0,0 +1,176 @@
# peardock — rolling multi-arch Bare standalone binaries
#
# 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.
name: Rolling release
on:
push:
branches: [main, master]
workflow_dispatch:
concurrency:
group: rolling-release-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: '22'
RELEASE_TAG: rolling
jobs:
build:
name: ${{ matrix.product }} / ${{ 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
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: |
set -e
dir="out/${{ matrix.product }}/${{ 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"
else
cp "$dir/peardock-${{ matrix.product }}" \
"peardock-${{ matrix.product }}-${{ matrix.host }}"
fi
ls -la peardock-${{ matrix.product }}-${{ matrix.host }}*
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: peardock-${{ matrix.product }}-${{ matrix.host }}
path: peardock-${{ matrix.product }}-${{ matrix.host }}*
if-no-files-found: error
retention-days: 7
publish:
name: Publish rolling release
needs: [build]
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
with:
node-version: ${{ env.NODE_VERSION }}
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Stage binaries for release
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}/"
else
echo "skip unrecognized: $base"
fi
done
find out -type f | sort
ls -la artifacts || true
- name: Publish to Gitea (rolling)
env:
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITEA_URL_SECRET: ${{ secrets.GITEA_URL }}
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_REPOSITORY: ${{ github.repository }}
RELEASE_TAG: ${{ env.RELEASE_TAG }}
ARTIFACT_DIR: ${{ github.workspace }}/out
run: |
set -euo pipefail
if [ -z "${RELEASE_TOKEN:-}" ]; then
echo "ERROR: secret RELEASE_TOKEN is not set on this repository"
exit 1
fi
export GITEA_URL="${GITEA_URL_SECRET:-${GITHUB_SERVER_URL}}"
export GITEA_OWNER="${GITHUB_REPOSITORY%%/*}"
export GITEA_REPO="${GITHUB_REPOSITORY##*/}"
chmod +x scripts/gitea-rolling-release.sh
bash scripts/gitea-rolling-release.sh
+2
View File
@@ -5,6 +5,8 @@ server/.env
peardock-audit.log
peardock-vault.json
peardock-peers.json
peardock-tunnels.json
peardock-sbom.json
dist/
out/
.DS_Store
+17
View File
@@ -76,6 +76,23 @@ pear release .
pear run pear://<your-app-key>
```
### Standalone Bare binaries
Self-contained ELFs/Mach-O/PE with **all modules and native addons embedded**
([bare-build --standalone](https://github.com/holepunchto/bare-build) pattern):
```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
./out/server/linux-x64/peardock-server
./out/client/linux-x64/peardock-client --connect <server-public-key>
```
Gitea CI publishes a **rolling** release on every `main` push using secret
`RELEASE_TOKEN`. See [docs/RELEASE.md](docs/RELEASE.md).
---
## Architecture
+146
View File
@@ -0,0 +1,146 @@
/**
* Bare standalone peardock client agent.
*
* Built with bare-build / scripts/bare-standalone.cjs:
* npm run make:client:linux-x64
*
* 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
*/
/* 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
}
}
+20
View File
@@ -0,0 +1,20 @@
/**
* Bare / Node entry for the peardock server standalone binary.
*
* Built with bare-build --standalone (embeds JS graph + native addons):
* bare-build --name peardock-server --standalone --host linux-x64 --out ./out/server/linux-x64 bin/peardock-server.mjs
*
* Under Bare, load bare-node-runtime so Node-style deps (dockerode, dotenv, …)
* resolve via package.json imports maps to bare-* modules.
*/
/* global Bare */
const isBare = typeof globalThis.Bare !== 'undefined'
if (isBare) {
// Install process / crypto / stream / worker globals used by Node packages
await import('bare-node-runtime/global')
}
// Side-effect import boots HyperDHT + protomux-rpc + dockerode
await import('../server/server.js')
+93
View File
@@ -0,0 +1,93 @@
/**
* bare-http1 wrapper with Unix domain socket (socketPath) support for dockerode.
* bare-http1's default Agent only opens TCP sockets.
*/
'use strict'
const http = require('bare-http1')
const net = require('bare-net')
class UnixAwareAgent extends http.Agent {
createConnection(opts) {
if (opts && opts.socketPath) {
const socket = net.createConnection({ path: opts.socketPath })
// bare-pipe IPC sockets don't implement TCP keep-alive APIs
if (typeof socket.setKeepAlive !== 'function') {
socket.setKeepAlive = function setKeepAlive() {
return this
}
}
if (typeof socket.ref !== 'function') {
socket.ref = function ref() {
return this
}
}
if (typeof socket.unref !== 'function') {
socket.unref = function unref() {
return this
}
}
return socket
}
return super.createConnection(opts)
}
getName(opts) {
if (opts && opts.socketPath) return `unix:${opts.socketPath}`
return super.getName(opts)
}
keepSocketAlive(socket) {
try {
return super.keepSocketAlive(socket)
} catch {
return false
}
}
}
// Disable keep-alive pooling for Unix sockets — safer with bare-pipe
const unixAgent = new UnixAwareAgent({ keepAlive: false })
function withUnixAgent(opts) {
if (!opts || typeof opts !== 'object') return opts
if (opts.socketPath && (opts.agent === undefined || opts.agent === null)) {
return { ...opts, agent: unixAgent, host: opts.host || 'localhost' }
}
return opts
}
function request(url, opts, onresponse) {
if (typeof opts === 'function') {
onresponse = opts
opts = undefined
}
if (typeof url === 'string' || (url && typeof url.protocol === 'string')) {
opts = withUnixAgent(opts || {})
return http.request(url, opts, onresponse)
}
// Node style: request(options, cb)
return http.request(withUnixAgent(url), opts)
}
function get(url, opts, onresponse) {
const req = request(url, opts, onresponse)
req.end()
return req
}
module.exports = {
...http,
Agent: UnixAwareAgent,
globalAgent: unixAgent,
request,
get,
createServer: http.createServer,
Server: http.Server,
IncomingMessage: http.IncomingMessage,
ServerResponse: http.ServerResponse,
ClientRequest: http.ClientRequest,
METHODS: http.METHODS,
STATUS_CODES: http.STATUS_CODES,
constants: http.constants,
}
+154
View File
@@ -0,0 +1,154 @@
/**
* Node-compatible `url` module for Bare (docker-modem uses legacy url.resolve/parse).
* Built on bare-url WHATWG URL + small legacy helpers.
*/
'use strict'
const bare = require('bare-url')
const URL = bare.URL || bare.default?.URL || bare
function parse(urlStr, parseQueryString, slashesDenoteHost) {
if (typeof bare.parse === 'function') {
try {
const u = bare.parse(String(urlStr))
if (u) return legacyFromWhatwg(u, parseQueryString)
} catch {
// fall through
}
}
try {
const u = new URL(String(urlStr))
return legacyFromWhatwg(u, parseQueryString)
} catch {
// Relative or invalid — return minimal shape docker-modem tolerates
return {
protocol: null,
slashes: null,
auth: null,
host: null,
port: null,
hostname: null,
hash: null,
search: null,
query: parseQueryString ? {} : null,
pathname: String(urlStr),
path: String(urlStr),
href: String(urlStr),
}
}
}
function legacyFromWhatwg(u, parseQueryString) {
const search = u.search || ''
const query = parseQueryString
? Object.fromEntries(new URLSearchParams(search))
: search.startsWith('?')
? search.slice(1)
: search || null
return {
protocol: u.protocol || null,
slashes: true,
auth: u.username ? (u.password ? `${u.username}:${u.password}` : u.username) : null,
host: u.host || null,
port: u.port || null,
hostname: u.hostname || null,
hash: u.hash || null,
search: search || null,
query,
pathname: u.pathname || null,
path: (u.pathname || '') + (search || ''),
href: u.href,
}
}
function format(parts) {
if (typeof parts === 'string') return parts
if (parts && typeof parts.href === 'string' && !parts.protocol && !parts.host) {
return parts.href
}
if (typeof bare.format === 'function') {
try {
return bare.format(parts)
} catch {
// fall through
}
}
if (parts instanceof URL || (parts && parts.href && parts.protocol)) {
try {
return String(parts.href || parts)
} catch {
// fall through
}
}
const protocol = parts.protocol || ''
const slashes = protocol && parts.slashes !== false ? '//' : ''
const auth = parts.auth ? `${parts.auth}@` : ''
const host = parts.host || (parts.hostname || '') + (parts.port ? `:${parts.port}` : '')
const pathname = parts.pathname || ''
const search =
parts.search ||
(parts.query
? typeof parts.query === 'string'
? (parts.query.startsWith('?') ? parts.query : `?${parts.query}`)
: `?${new URLSearchParams(parts.query)}`
: '')
const hash = parts.hash || ''
if (protocol === 'http:' || protocol === 'https:' || protocol === 'ws:' || protocol === 'wss:') {
return `${protocol}${slashes}${auth}${host}${pathname}${search}${hash}`
}
// unix / path style
return `${protocol || ''}${pathname || host || ''}${search}${hash}`
}
/**
* Node.js url.resolve(from, to) — resolve `to` against `from`.
* https://nodejs.org/api/url.html#urlresolvefrom-to
*/
function resolve(from, to) {
from = String(from || '')
to = String(to || '')
if (!from) return to
if (!to) return from
try {
// Absolute URL in `to`
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(to)) return to
const base = from.endsWith('/') || from.includes('?') || from.includes('#') ? from : from + '/'
// WHATWG URL needs absolute base; synthesize for path-only docker paths
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(from)) {
return new URL(to, from).href
}
// Relative base (e.g. /v1.41/containers/json)
const fake = new URL(from, 'http://docker.invalid')
const resolved = new URL(to, fake)
// Preserve original style: if from was path-like, return path+search
if (from.startsWith('/') || !from.includes('://')) {
return resolved.pathname + resolved.search + resolved.hash
}
return resolved.href
} catch {
// Last resort join
if (to.startsWith('/')) {
try {
const u = new URL(from)
return `${u.protocol}//${u.host}${to}`
} catch {
return to
}
}
const base = from.endsWith('/') ? from : from.replace(/[^/]*$/, '')
return base + to
}
}
module.exports = {
URL,
URLSearchParams: bare.URLSearchParams || globalThis.URLSearchParams,
parse,
format,
resolve,
pathToFileURL: bare.pathToFileURL,
fileURLToPath: bare.fileURLToPath,
domainToASCII: bare.domainToASCII,
domainToUnicode: bare.domainToUnicode,
}
+39
View File
@@ -0,0 +1,39 @@
/**
* Stub for @grpc/grpc-js — only used by dockerode BuildKit sessions.
* Bare standalone peardock-server uses the Docker Engine HTTP API over the Unix socket.
*/
'use strict'
class Server {
addService() {}
bindAsync(_addr, _creds, cb) {
if (typeof cb === 'function') cb(new Error('gRPC sessions not available in Bare peardock-server'))
}
start() {}
tryShutdown(cb) {
if (typeof cb === 'function') cb()
}
forceShutdown() {}
}
const ServerCredentials = {
createInsecure() {
return {}
},
}
function loadPackageDefinition() {
return {}
}
module.exports = {
Server,
ServerCredentials,
loadPackageDefinition,
credentials: ServerCredentials,
status: {},
Metadata: function Metadata() {},
makeClientConstructor() {
return function StubClient() {}
},
}
+9
View File
@@ -0,0 +1,9 @@
'use strict'
module.exports = {
loadSync() {
return {}
},
load() {
return Promise.resolve({})
},
}
+26
View File
@@ -0,0 +1,26 @@
/**
* Minimal ssh2 stub for Bare standalone builds.
* docker-modem requires ssh2 at load time even when only the local Unix socket
* is used. Remote Docker-over-SSH is not supported in the Bare binary.
*/
'use strict'
class Client {
on() {
return this
}
once() {
return this
}
connect() {
throw new Error(
'Docker-over-SSH is not available in the peardock Bare standalone binary. Use the local Docker socket or run the Node server.'
)
}
end() {}
exec() {
throw new Error('Docker-over-SSH is not available in this build')
}
}
module.exports = { Client, default: { Client } }
+5
View File
@@ -0,0 +1,5 @@
'use strict'
module.exports = false
module.exports.stdout = false
module.exports.stderr = false
module.exports.supportsColor = false
+5 -1
View File
@@ -8,7 +8,10 @@ Requires=docker.service
[Service]
Type=simple
WorkingDirectory=/opt/peardock
ExecStart=/usr/bin/node server/server.js
# Prefer the Bare standalone binary (rolling release artifact):
ExecStart=/opt/peardock/peardock-server
# Node fallback (source install):
# ExecStart=/usr/bin/node server/server.js
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
@@ -25,6 +28,7 @@ ReadWritePaths=/opt/peardock /var/run/docker.sock
# Environment
Environment=NODE_ENV=production
Environment=PEARDOCK_HOME=/opt/peardock
EnvironmentFile=-/opt/peardock/.env
# Example production knobs (uncomment / set in .env):
# Environment=PEARDOCK_DEFAULT_ROLE=operator
+153 -27
View File
@@ -1,6 +1,137 @@
# peardock release process
## Server tarball + checksums
## Standalone Bare binaries (recommended)
peardock ships **two separate self-contained binaries** built with the Holepunch
stack (`bare-pack` + `bare-build --standalone`):
| 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).
### Local build
```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
```
Outputs:
```
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`.
### 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.)
---
## Gitea CI — rolling release (`RELEASE_TOKEN`)
Workflow: `.gitea/workflows/release-rolling.yml`
### Secret
In the Gitea repo **Settings → Secrets**:
| Name | Value |
|------|--------|
| `RELEASE_TOKEN` | Personal access token with **repository** write (create/delete releases + upload assets) |
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)
```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/
npm run release:rolling
```
---
## Source tarball + checksums (legacy / Node install)
```bash
chmod +x scripts/release-checksums.sh
@@ -9,60 +140,55 @@ chmod +x scripts/release-checksums.sh
GPG_KEY_ID=YOUR_KEY_ID ./scripts/release-checksums.sh dist/
```
Artifacts:
| File | Purpose |
|------|---------|
| `peardock-<ver>-<stamp>.tar.gz` | Source/runtime tree (no node_modules) |
| `*.sha256` | SHA-256 checksum |
| `*.asc` | Detached GPG signature (if keyed) |
Verify:
```bash
cd dist
sha256sum -c peardock-*.sha256
gpg --verify peardock-*.tar.gz.asc peardock-*.tar.gz # if signed
```
## Server install from release
```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
```
## Pear desktop app
Update `deploy/peardock.service` `ExecStart` to the standalone binary when using
Bare builds:
```ini
ExecStart=/opt/peardock/peardock-server
Environment=PEARDOCK_HOME=/opt/peardock
```
---
## Pear desktop app (GUI OTA)
```bash
npm ci
pear stage .
pear release .
# Distribute pear:// link or channel per Pear docs
# Distribute pear:// link per Pear docs
```
---
## Certification soak (24h)
```bash
# Terminal 1
npm run server
./out/server/linux-x64/peardock-server
# or: npm run server
# Terminal 2 — 24 hours
# Terminal 2
SOAK_DURATION_MS=86400000 npm run soak
# or: node scripts/soak.js --hours 24
```
Exit 0 = Docker remained reachable within failure threshold.
## Load / fuzz in CI
Included in `npm test`:
- `test/load.test.js` — concurrent HyperDHT pings
- `test/fuzz.test.js` — schema/role fuzz
---
## Encoding profile
Handshake returns `schemaVersion` and features. Default encoding remains JSON (`shared/encodings.js`); binary bulk uses `binaryStream*` + `push:binaryChunk`. Hyperschema can replace JSON value encodings without renaming methods.
Handshake returns `schemaVersion` and features. Default encoding remains JSON
(`shared/encodings.js`); binary bulk uses `binaryStream*` + `push:binaryChunk`.
+1077 -7
View File
File diff suppressed because it is too large Load Diff
+500 -28
View File
@@ -6,45 +6,469 @@
"license": "Apache-2.0",
"main": "index.js",
"imports": {
"crypto": {
"bare": "bare-crypto",
"default": "crypto"
"assert": {
"bare": "bare-assert",
"default": "assert"
},
"fs": {
"bare": "bare-fs",
"default": "fs"
"node:assert": {
"bare": "bare-assert",
"default": "assert"
},
"path": {
"bare": "bare-path",
"default": "path"
"assert/strict": {
"bare": "bare-assert/strict",
"default": "assert/strict"
},
"http": {
"bare": "bare-http1",
"default": "http"
"node:assert/strict": {
"bare": "bare-assert/strict",
"default": "assert/strict"
},
"net": {
"bare": "bare-net",
"default": "net"
"async_hooks": {
"bare": "bare-async-hooks",
"default": "async_hooks"
},
"process": {
"bare": "bare-process",
"default": "process"
"node:async_hooks": {
"bare": "bare-async-hooks",
"default": "async_hooks"
},
"events": {
"bare": "bare-events",
"default": "events"
"buffer": {
"bare": "bare-buffer",
"default": "buffer"
},
"os": {
"bare": "bare-os",
"default": "os"
"node:buffer": {
"bare": "bare-buffer",
"default": "buffer"
},
"child_process": {
"bare": "bare-subprocess",
"default": "child_process"
},
"node:child_process": {
"bare": "bare-subprocess",
"default": "child_process"
},
"cluster": {
"bare": "bare-node-runtime/unsupported",
"default": "cluster"
},
"node:cluster": {
"bare": "bare-node-runtime/unsupported",
"default": "cluster"
},
"console": {
"bare": "bare-console",
"default": "console"
},
"node:console": {
"bare": "bare-console",
"default": "console"
},
"constants": {
"bare": "bare-node-runtime/unsupported",
"default": "constants"
},
"node:constants": {
"bare": "bare-node-runtime/unsupported",
"default": "constants"
},
"crypto": {
"bare": "bare-crypto",
"default": "crypto"
},
"node:crypto": {
"bare": "bare-crypto",
"default": "crypto"
},
"dgram": {
"bare": "bare-dgram",
"default": "dgram"
},
"node:dgram": {
"bare": "bare-dgram",
"default": "dgram"
},
"diagnostics_channel": {
"bare": "bare-diagnostics-channel",
"default": "diagnostics_channel"
},
"node:diagnostics_channel": {
"bare": "bare-diagnostics-channel",
"default": "diagnostics_channel"
},
"dns": {
"bare": "bare-dns",
"default": "dns"
},
"node:dns": {
"bare": "bare-dns",
"default": "dns"
},
"dns/promises": {
"bare": "bare-dns/promises",
"default": "dns/promises"
},
"node:dns/promises": {
"bare": "bare-dns/promises",
"default": "dns/promises"
},
"domain": {
"bare": "bare-node-runtime/unsupported",
"default": "domain"
},
"node:domain": {
"bare": "bare-node-runtime/unsupported",
"default": "domain"
},
"events": {
"bare": "bare-events",
"default": "events"
},
"node:events": {
"bare": "bare-events",
"default": "events"
},
"fs": {
"bare": "bare-fs",
"default": "fs"
},
"node:fs": {
"bare": "bare-fs",
"default": "fs"
},
"fs/promises": {
"bare": "bare-fs/promises",
"default": "fs/promises"
},
"node:fs/promises": {
"bare": "bare-fs/promises",
"default": "fs/promises"
},
"http": {
"bare": "bare-http1",
"default": "http"
},
"node:http": {
"bare": "bare-http1",
"default": "http"
},
"http2": {
"bare": "bare-node-runtime/unsupported",
"default": "http2"
},
"node:http2": {
"bare": "bare-node-runtime/unsupported",
"default": "http2"
},
"https": {
"bare": "bare-https",
"default": "https"
},
"node:https": {
"bare": "bare-https",
"default": "https"
},
"inspector": {
"bare": "bare-inspector",
"default": "inspector"
},
"node:inspector": {
"bare": "bare-inspector",
"default": "inspector"
},
"inspector/promises": {
"bare": "bare-inspector/promises",
"default": "inspector/promises"
},
"node:inspector/promises": {
"bare": "bare-inspector/promises",
"default": "inspector/promises"
},
"module": {
"bare": "bare-module",
"default": "module"
},
"node:module": {
"bare": "bare-module",
"default": "module"
},
"net": {
"bare": "bare-net",
"default": "net"
},
"node:net": {
"bare": "bare-net",
"default": "net"
},
"os": {
"bare": "bare-os",
"default": "os"
},
"node:os": {
"bare": "bare-os",
"default": "os"
},
"path": {
"bare": "bare-path",
"default": "path"
},
"node:path": {
"bare": "bare-path",
"default": "path"
},
"path/posix": {
"bare": "bare-path/posix",
"default": "path/posix"
},
"node:path/posix": {
"bare": "bare-path/posix",
"default": "path/posix"
},
"path/win32": {
"bare": "bare-path/win32",
"default": "path/win32"
},
"node:path/win32": {
"bare": "bare-path/win32",
"default": "path/win32"
},
"perf_hooks": {
"bare": "bare-performance",
"default": "perf_hooks"
},
"node:perf_hooks": {
"bare": "bare-performance",
"default": "perf_hooks"
},
"process": {
"bare": "bare-process",
"default": "process"
},
"node:process": {
"bare": "bare-process",
"default": "process"
},
"punycode": {
"bare": "bare-punycode",
"default": "punycode"
},
"node:punycode": {
"bare": "bare-punycode",
"default": "punycode"
},
"querystring": {
"bare": "bare-querystring",
"default": "querystring"
},
"node:querystring": {
"bare": "bare-querystring",
"default": "querystring"
},
"readline": {
"bare": "bare-readline",
"default": "readline"
},
"node:readline": {
"bare": "bare-readline",
"default": "readline"
},
"readline/promises": {
"bare": "bare-readline/promises",
"default": "readline/promises"
},
"node:readline/promises": {
"bare": "bare-readline/promises",
"default": "readline/promises"
},
"repl": {
"bare": "bare-repl",
"default": "repl"
},
"node:repl": {
"bare": "bare-repl",
"default": "repl"
},
"sea": {
"bare": "bare-node-runtime/unsupported",
"default": "sea"
},
"node:sea": {
"bare": "bare-node-runtime/unsupported",
"default": "sea"
},
"sqlite": {
"bare": "bare-sqlite",
"default": "sqlite"
},
"node:sqlite": {
"bare": "bare-sqlite",
"default": "sqlite"
},
"stream": {
"bare": "bare-stream",
"default": "stream"
},
"node:stream": {
"bare": "bare-stream",
"default": "stream"
},
"stream/consumers": {
"bare": "bare-stream/consumers",
"default": "stream/consumers"
},
"node:stream/consumers": {
"bare": "bare-stream/consumers",
"default": "stream/consumers"
},
"stream/promises": {
"bare": "bare-stream/promises",
"default": "stream/promises"
},
"node:stream/promises": {
"bare": "bare-stream/promises",
"default": "stream/promises"
},
"stream/web": {
"bare": "bare-stream/web",
"default": "stream/web"
},
"node:stream/web": {
"bare": "bare-stream/web",
"default": "stream/web"
},
"string_decoder": {
"bare": "bare-string-decoder",
"default": "string_decoder"
},
"node:string_decoder": {
"bare": "bare-string-decoder",
"default": "string_decoder"
},
"sys": {
"bare": "bare-node-runtime/unsupported",
"default": "sys"
},
"node:sys": {
"bare": "bare-node-runtime/unsupported",
"default": "sys"
},
"test": {
"bare": "bare-node-runtime/unsupported",
"default": "test"
},
"node:test": {
"bare": "bare-node-runtime/unsupported",
"default": "test"
},
"test/reporters": {
"bare": "bare-node-runtime/unsupported",
"default": "test/reporters"
},
"node:test/reporters": {
"bare": "bare-node-runtime/unsupported",
"default": "test/reporters"
},
"timers": {
"bare": "bare-timers",
"default": "timers"
},
"node:timers": {
"bare": "bare-timers",
"default": "timers"
},
"timers/promises": {
"bare": "bare-timers/promises",
"default": "timers/promises"
},
"node:timers/promises": {
"bare": "bare-timers/promises",
"default": "timers/promises"
},
"tls": {
"bare": "bare-tls",
"default": "tls"
},
"node:tls": {
"bare": "bare-tls",
"default": "tls"
},
"trace_events": {
"bare": "bare-node-runtime/unsupported",
"default": "trace_events"
},
"node:trace_events": {
"bare": "bare-node-runtime/unsupported",
"default": "trace_events"
},
"tty": {
"bare": "bare-tty",
"default": "tty"
},
"node:tty": {
"bare": "bare-tty",
"default": "tty"
},
"url": {
"bare": "bare-url",
"default": "url"
},
"node:url": {
"bare": "bare-url",
"default": "url"
},
"util": {
"bare": "bare-utils",
"default": "util"
},
"node:util": {
"bare": "bare-utils",
"default": "util"
},
"util/types": {
"bare": "bare-utils/types",
"default": "util/types"
},
"node:util/types": {
"bare": "bare-utils/types",
"default": "util/types"
},
"v8": {
"bare": "bare-v8",
"default": "v8"
},
"node:v8": {
"bare": "bare-v8",
"default": "v8"
},
"vm": {
"bare": "bare-vm",
"default": "vm"
},
"node:vm": {
"bare": "bare-vm",
"default": "vm"
},
"wasi": {
"bare": "bare-node-runtime/unsupported",
"default": "wasi"
},
"node:wasi": {
"bare": "bare-node-runtime/unsupported",
"default": "wasi"
},
"worker_threads": {
"bare": "bare-worker",
"default": "worker_threads"
},
"node:worker_threads": {
"bare": "bare-worker",
"default": "worker_threads"
},
"zlib": {
"bare": "bare-zlib",
"default": "zlib"
},
"node:zlib": {
"bare": "bare-zlib",
"default": "zlib"
}
},
"pear": {
@@ -73,7 +497,26 @@
"soak": "node scripts/soak.js",
"soak:24h": "SOAK_DURATION_MS=86400000 node scripts/soak.js",
"release:checksums": "bash scripts/release-checksums.sh",
"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:server": "node scripts/make.cjs server",
"make:client": "node scripts/make.cjs client",
"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",
"make:server:darwin-arm64": "node scripts/bare-standalone.cjs --product server --host darwin-arm64",
"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",
"release:rolling": "bash scripts/gitea-rolling-release.sh",
"start:server:bin": "node bin/peardock-server.mjs",
"make:bin": "node scripts/bare-standalone.cjs"
},
"engines": {
"node": ">=20"
@@ -106,10 +549,39 @@
"protomux-rpc": "^1.10.0",
"safety-catch": "^1.0.3",
"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": {
"brittle": "^4.1.0",
"pear-interface": "^1.1.0"
}
"pear-interface": "^1.1.0",
"bare-build": "^1.0.2",
"bare-runtime": "1.30.3"
},
"productName": "peardock"
}
+213
View File
@@ -0,0 +1,213 @@
#!/usr/bin/env node
/**
* Build a self-contained Bare executable (JS graph + native addons embedded).
*
* Why not plain `bare-build` CLI?
* bare-build 1.0.x packs with bare-module-traverse but does not expose
* `--imports` global overrides. Node packages like dockerode require
* `bare-node-runtime/imports.json` so builtins (events, stream, …) map to bare-*.
*
* This script mirrors holepunchto/bare-build --standalone + bare-pack --imports.
*
* Usage:
* node scripts/bare-standalone.js --product server --host linux-x64
* node scripts/bare-standalone.js --product client --host linux-x64
* node scripts/bare-standalone.js --product server --host linux-x64 --host linux-arm64
*/
'use strict'
const path = require('path')
const fs = require('fs')
const { pathToFileURL } = require('url')
const root = path.resolve(__dirname, '..')
// bare-build exports only "." and "./constants" — resolve main then walk up
const bareBuildMain = require.resolve('bare-build')
const bareBuildRoot = path.dirname(bareBuildMain)
const pack = require('bare-pack')
const { readModule, listPrefix } = require('bare-pack/fs')
const traverse = require('bare-module-traverse')
const bundleId = require('bare-bundle-id')
const buildPkg = require(path.join(bareBuildRoot, 'package.json'))
// Platform embedders (same as bare-build)
const platforms = {
linux: require(path.join(bareBuildRoot, 'lib/platform/linux.js')),
darwin: require(path.join(bareBuildRoot, 'lib/platform/apple.js')),
win32: require(path.join(bareBuildRoot, 'lib/platform/windows.js')),
}
const PRODUCTS = {
server: {
name: 'peardock-server',
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'),
},
}
const DEFAULT_DEFER = [
// Optional debug pretty-printing; not required at runtime
'supports-color',
]
function loadImports() {
const p = path.join(root, 'node_modules/bare-node-runtime/imports.json')
if (!fs.existsSync(p)) {
throw new Error(
'bare-node-runtime not installed — run npm ci (imports map required for Node deps under Bare)'
)
}
const imports = { ...require(p) }
// Optional / incompatible Node-native stacks — not needed for local Docker socket.
// docker-modem always requires ssh2; dockerode always requires @grpc/* for BuildKit sessions.
const stub = (rel) => pathToFileURL(path.join(root, rel)).href
imports.ssh2 = stub('build/stubs/ssh2.cjs')
imports['supports-color'] = stub('build/stubs/supports-color.cjs')
imports['@grpc/grpc-js'] = stub('build/stubs/grpc-js.cjs')
imports['@grpc/proto-loader'] = stub('build/stubs/grpc-proto-loader.cjs')
// docker-modem needs legacy Node url.resolve / url.parse / url.format
const urlShim = stub('build/shims/url.cjs')
imports.url = { bare: urlShim, default: 'url' }
imports['node:url'] = { bare: urlShim, default: 'url' }
// bare-http1 Agent is TCP-only; shim adds socketPath → bare-net IPC for Docker
const httpShim = stub('build/shims/http.cjs')
imports.http = { bare: httpShim, default: 'http' }
imports['node:http'] = { bare: httpShim, default: 'http' }
return imports
}
function hostPlatform(host) {
if (host.startsWith('linux-')) return 'linux'
if (host.startsWith('darwin-') || host.startsWith('ios-')) return 'darwin'
if (host.startsWith('win32-')) return 'win32'
if (host.startsWith('android-')) return 'android'
throw new Error(`Unknown host platform for '${host}'`)
}
async function packEntry(entry, hosts, imports, defer) {
const base = pathToFileURL(root + '/')
const bundle = await pack(
pathToFileURL(entry),
{
resolve: traverse.resolve.bare,
hosts,
imports,
defer,
linked: false, // standalone embeds prebuilds
base,
},
readModule,
listPrefix
)
bundle.id = bundleId(bundle).toString('hex')
return bundle
}
async function embedStandalone(bundle, hosts, name, outDir) {
const byPlatform = new Map()
for (const host of hosts) {
const p = hostPlatform(host)
if (!byPlatform.has(p)) byPlatform.set(p, [])
byPlatform.get(p).push(host)
}
const results = []
for (const [platform, platformHosts] of byPlatform) {
const impl = platforms[platform]
if (!impl) throw new Error(`No bare-build platform embedder for ${platform}`)
// When multiple hosts for one platform, bare-build nests by arch
const multi = platformHosts.length > 1
for (const host of platformHosts) {
const out = multi ? path.join(outDir, host) : outDir
// Re-run per host so output path matches out/{product}/{host}/
const gen = impl(
root,
bundle,
null,
{
name,
hosts: [host],
standalone: true,
package: false,
out,
version: require(path.join(root, 'package.json')).version,
description: require(path.join(root, 'package.json')).description,
author: require(path.join(root, 'package.json')).author || '',
}
)
for await (const resource of gen) {
results.push(resource)
console.log(`[bare-standalone] wrote ${resource}`)
}
}
}
return results
}
async function buildProduct(productKey, hosts) {
const product = PRODUCTS[productKey]
if (!product) throw new Error(`Unknown product '${productKey}' (server|client)`)
const imports = loadImports()
console.log(
`[bare-standalone] packing ${product.name} entry=${path.relative(root, product.entry)} hosts=${hosts.join(',')}`
)
console.log(
`[bare-standalone] imports=bare-node-runtime (${Object.keys(imports).length} keys) bare-build@${buildPkg.version}`
)
const bundle = await packEntry(product.entry, hosts, imports, DEFAULT_DEFER)
console.log(`[bare-standalone] bundle id=${bundle.id} keys≈${Object.keys(bundle.files || bundle).length || 'n/a'}`)
for (const host of hosts) {
const out = path.join(product.outPrefix, host)
fs.mkdirSync(out, { recursive: true })
await embedStandalone(bundle, [host], product.name, out)
}
}
async function main(argv) {
let product = 'server'
const hosts = []
for (let i = 0; i < argv.length; i++) {
const a = argv[i]
if (a === '--help' || a === '-h') {
console.log('Usage: node scripts/bare-standalone.cjs --product server|client|both --host <host> [...]')
console.log('Hosts: linux-x64 linux-arm64 darwin-arm64 darwin-x64 win32-x64 win32-arm64')
return
}
if (a === '--product') {
product = argv[++i]
continue
}
if (a === '--host') {
hosts.push(argv[++i])
continue
}
console.error('Unknown arg:', a)
process.exit(1)
}
if (hosts.length === 0) hosts.push(`${process.platform}-${process.arch}`)
const products =
product === 'both' || product === 'all' ? ['server', 'client'] : [product]
for (const p of products) {
await buildProduct(p, hosts)
}
console.log('[bare-standalone] done')
}
main(process.argv.slice(2)).catch((err) => {
console.error(err)
process.exit(1)
})
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
# Publish / refresh a rolling Gitea release with standalone bare-build binaries.
#
# 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
#
# 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)
#
# Usage (after make):
# ./scripts/gitea-rolling-release.sh
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
ARTIFACT_DIR="${ARTIFACT_DIR:-$ROOT/out}"
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)"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
COMMIT="$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo unknown)"
: "${RELEASE_TOKEN:?RELEASE_TOKEN is required}"
: "${GITEA_URL:?GITEA_URL is required (e.g. https://git.example.com)}"
: "${GITEA_OWNER:?GITEA_OWNER is required}"
: "${GITEA_REPO:?GITEA_REPO is required}"
API="${GITEA_API:-${GITEA_URL%/}/api/v1}"
AUTH="Authorization: token ${RELEASE_TOKEN}"
ACCEPT="Accept: application/json"
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-server → peardock-server-linux-x64
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}"
ext=""
[[ "$f" == *.exe ]] && ext=".exe"
dest="${base}-${host}${ext}"
cp -a "$f" "$STAGE/$dest"
echo "[release] staged $dest"
done
# Checksums
(
cd "$STAGE"
sha256sum peardock-* > SHA256SUMS
)
BODY=$(cat <<EOF
Automated **rolling** release of peardock standalone Bare binaries.
| Field | Value |
|-------|-------|
| Version | \`${VERSION}\` |
| Commit | \`${COMMIT}\` |
| Built | \`${STAMP}\` |
| Tooling | \`bare-build --standalone\` (embeds JS graph + native addons) |
## Binaries
- **peardock-server-\<host\>** — HyperDHT Docker control plane
- **peardock-client-\<host\>** — Pear/Bare desktop client entry
Hosts follow Bare addon naming: \`linux-x64\`, \`linux-arm64\`, \`darwin-arm64\`, \`darwin-x64\`, \`win32-x64\`, \`win32-arm64\`.
Verify:
\`\`\`bash
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)
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
echo "[release] creating release ${RELEASE_TAG}"
CREATE_RESP=$(curl -fsSL -X POST -H "$AUTH" -H "$ACCEPT" -H "$CT" \
"${API}/repos/${GITEA_OWNER}/${GITEA_REPO}/releases" \
-d "{\"tag_name\":\"${RELEASE_TAG}\",\"name\":\"${RELEASE_TITLE} (${VERSION} ${COMMIT})\",\"body\":${BODY_JSON},\"draft\":false,\"prerelease\":true,\"target_commitish\":\"$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || echo main)\"}")
RELEASE_ID=$(printf '%s' "$CREATE_RESP" | node -e 'let s="";process.stdin.on("data",d=>s+=d);process.stdin.on("end",()=>console.log(JSON.parse(s).id))')
echo "[release] release id=${RELEASE_ID}"
upload() {
local file="$1"
local name
name="$(basename "$file")"
echo "[release] upload ${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")" \
--data-binary @"$file" >/dev/null
}
for f in "$STAGE"/*; do
[[ -f "$f" ]] || continue
upload "$f"
done
echo "[release] done → ${GITEA_URL%/}/${GITEA_OWNER}/${GITEA_REPO}/releases/tag/${RELEASE_TAG}"
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env node
/**
* Host-aware Bare standalone builder for peardock.
* Delegates to scripts/bare-standalone.cjs (bare-pack + bare-build embed).
*
* Usage:
* node scripts/make.cjs server # native host
* node scripts/make.cjs client
* node scripts/make.cjs both
* node scripts/make.cjs server linux-x64
* node scripts/make.cjs all # all desktop hosts × both products
*/
'use strict'
const os = require('os')
const path = require('path')
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 HOSTS = [
'darwin-arm64',
'darwin-x64',
'linux-arm64',
'linux-x64',
'win32-arm64',
'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, {
cwd: root,
stdio: 'inherit',
env: process.env,
})
if (res.error) {
console.error(res.error.message)
process.exit(1)
}
if (res.status !== 0) process.exit(res.status || 1)
}
function main() {
const argv = process.argv.slice(2)
const target = argv[0] || 'both'
let products = []
let hosts = []
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)
}
for (const product of products) {
run(product, hosts)
}
}
main()
+24 -2
View File
@@ -11,7 +11,29 @@ import crypto from 'hypercore-crypto'
import dotenv from 'dotenv'
import logger from '../utils/logger.js'
dotenv.config()
/**
* Resolve .env path for both Node (cwd) and Bare standalone binaries.
* Prefer PEARDOCK_ENV / PEARDOCK_HOME, then cwd, then next to the executable.
*/
function defaultEnvPath() {
if (process.env.PEARDOCK_ENV) return process.env.PEARDOCK_ENV
if (process.env.PEARDOCK_HOME) {
return path.join(process.env.PEARDOCK_HOME, '.env')
}
// Bare standalone: keep state next to binary when cwd is ephemeral
try {
const execPath = process.execPath || globalThis.Bare?.argv?.[0]
if (execPath && /peardock-server/i.test(String(execPath))) {
return path.join(path.dirname(execPath), '.env')
}
} catch {
// ignore
}
return path.resolve(process.cwd(), '.env')
}
const resolvedEnvPath = defaultEnvPath()
dotenv.config({ path: resolvedEnvPath, quiet: true })
const log = logger.child('keys')
@@ -19,7 +41,7 @@ const log = logger.child('keys')
* @param {string} [envPath]
* @returns {{ seed: Uint8Array, keyPair: { publicKey: Uint8Array, secretKey: Uint8Array }, publicKeyHex: string, seedHex: string }}
*/
export function loadOrCreateKeyPair(envPath = '.env') {
export function loadOrCreateKeyPair(envPath = resolvedEnvPath) {
let seedHex = process.env.SERVER_SEED || process.env.SERVER_KEY
// SERVER_KEY historically was a 32-byte topic seed; reuse as DHT seed if present
+6 -4
View File
@@ -10,13 +10,12 @@
* @see https://github.com/holesail/holesail
* @see docs/HOLESAIL.md
*/
import { createRequire } from 'module'
import { randomBytes } from 'crypto'
import fs from 'fs'
import path from 'path'
import logger from '../utils/logger.js'
const require = createRequire(import.meta.url)
// Static import so bare-pack embeds holesail (createRequire is fragile under Bare standalone).
import HolesailPackage from 'holesail'
/** @typedef {{
* id: string,
@@ -95,7 +94,10 @@ function loadHolesail() {
if (HolesailCtor) return HolesailCtor
if (holesailLoadError) throw holesailLoadError
try {
HolesailCtor = require('holesail')
HolesailCtor = HolesailPackage?.default || HolesailPackage
if (typeof HolesailCtor !== 'function' && typeof HolesailCtor !== 'object') {
throw new Error('holesail package did not export a constructor')
}
return HolesailCtor
} catch (err) {
holesailLoadError = err