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
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:
@@ -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/
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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() {}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
'use strict'
|
||||
module.exports = {
|
||||
loadSync() {
|
||||
return {}
|
||||
},
|
||||
load() {
|
||||
return Promise.resolve({})
|
||||
},
|
||||
}
|
||||
@@ -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 } }
|
||||
@@ -0,0 +1,5 @@
|
||||
'use strict'
|
||||
module.exports = false
|
||||
module.exports.stdout = false
|
||||
module.exports.stderr = false
|
||||
module.exports.supportsColor = false
|
||||
@@ -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
@@ -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`.
|
||||
|
||||
Generated
+1077
-7
File diff suppressed because it is too large
Load Diff
+500
-28
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
Executable
+140
@@ -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}"
|
||||
Executable
+79
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user