This commit is contained in:
Raven Scott
2026-07-11 11:39:59 -04:00
parent fa99bd8c6e
commit 2526ebb99b
23 changed files with 10113 additions and 158 deletions
+74 -65
View File
@@ -1,12 +1,23 @@
/**
* Pear-side Holesail client using the **real `holesail` package**.
* Local Holesail client helper for the peardock UI.
*
* Architecture (Holepunch pear-electron):
* - Bare main (`index.js`) runs `holesailBareControl.cjs` with real `require('holesail')`
* (require.addon works there). Exposes http://127.0.0.1:<port> + token in app storage.
* - UI renderer never loads bare-tcp; it POSTs to that control API.
* - Node (tests / non-Pear): in-process `require('holesail')`.
* Architecture:
* - Electron/Pear main runs `holesailBareControl.cjs` (real `require('holesail')`)
* and writes http://127.0.0.1:<port> + token into app storage.
* - UI talks to that control API over fetch — never loads bare-tcp in the renderer.
* - Node tests: optional in-process `require('holesail')`.
*
* Electron packaged client: NEVER use dynamic `import('fs')` / `import('path')`
* etc. inside the esbuild CJS GUI bundle. Node ESM `import()` in the renderer
* crashes Chromium (exit 5 → grey frozen window). Static imports become
* `require()` in the bundle and work with nodeIntegration.
*/
import fs from 'fs'
import path from 'path'
import net from 'net'
import { createRequire } from 'module'
import { exec } from 'child_process'
/** @type {Map<string, { info: object, localPort: number, host: string, url: string, via?: string }>} */
const localClients = new Map()
@@ -18,32 +29,52 @@ let holesailLoadError = null
let controlCache = null
/**
* @returns {boolean}
* Lazy-load npm packages without static-importing natives into every UI load.
* Prefer ambient require (Electron CJS bundle); fall back to createRequire (pear ESM).
* @param {string} id
*/
function isPearGui() {
return Boolean(globalThis.Pear?.config)
function nodeRequire(id) {
try {
if (typeof require === 'function') return require(id)
} catch {
// not resolvable
}
try {
const metaUrl =
typeof import.meta !== 'undefined' &&
import.meta &&
typeof import.meta.url === 'string' &&
import.meta.url.startsWith('file:')
? import.meta.url
: null
if (metaUrl) return createRequire(metaUrl)(id)
} catch {
// ignore
}
return createRequire(path.join(process.cwd(), 'package.json'))(id)
}
/**
* Resolve Bare control endpoint written by index.js.
* @returns {boolean}
*/
function isPearGui() {
// pear run + Electron peardock-client both set Pear.config.storage
return Boolean(globalThis.Pear?.config?.storage)
}
/**
* Resolve control endpoint written by main process.
* @returns {Promise<{ baseUrl: string, token: string } | null>}
*/
async function getControlEndpoint() {
if (controlCache) return controlCache
const Pear = globalThis.Pear
const storage = Pear?.config?.storage
const storage = globalThis.Pear?.config?.storage
if (!storage) return null
try {
const fs = await import('fs')
const path = await import('path')
const join = path.join || path.default?.join
const readFileSync = fs.readFileSync || fs.default?.readFileSync
if (typeof join !== 'function' || typeof readFileSync !== 'function') return null
const p = join(storage, STATE_FILE)
const raw = readFileSync(p, 'utf8')
const p = path.join(storage, STATE_FILE)
const raw = fs.readFileSync(p, 'utf8')
const meta = JSON.parse(raw)
if (!meta?.baseUrl || !meta?.token) return null
controlCache = { baseUrl: meta.baseUrl, token: meta.token }
@@ -54,7 +85,7 @@ async function getControlEndpoint() {
}
/**
* Poll storage briefly — Bare main may still be writing the state file.
* Poll storage briefly — main may still be writing the state file.
* @param {number} [ms]
*/
async function waitForControlEndpoint(ms = 8000) {
@@ -62,7 +93,6 @@ async function waitForControlEndpoint(ms = 8000) {
while (Date.now() - start < ms) {
const ep = await getControlEndpoint()
if (ep) {
// Verify health
try {
const res = await fetch(`${ep.baseUrl}/health`, { method: 'GET' })
if (res.ok) return ep
@@ -85,7 +115,7 @@ async function controlRpc(method, body, timeoutMs = 55000) {
const ep = await waitForControlEndpoint()
if (!ep) {
throw new Error(
'Holesail local control service not found. Restart peardock (Bare main starts the control API).'
'Holesail local control service not found. Restart peardock so the main process can start the control API.'
)
}
@@ -119,35 +149,19 @@ async function controlRpc(method, body, timeoutMs = 55000) {
}
/**
* Load the real holesail constructor in-process (Node / Bare with require.addon).
* Load the real holesail constructor in-process (Node tests only).
* @returns {Promise<Function>}
*/
export async function loadHolesailCtor() {
if (HolesailCtor) return HolesailCtor
if (holesailLoadError) throw holesailLoadError
try {
try {
const { createRequire } = await import('module')
const require = createRequire(import.meta.url)
const Ctor = require('holesail')
if (typeof Ctor === 'function') {
HolesailCtor = Ctor
return HolesailCtor
}
} catch {
// createRequire missing
}
const mod = await import('holesail')
const Ctor =
(typeof mod === 'function' && mod) ||
mod?.default ||
mod?.Holesail ||
(mod?.default && mod.default.default) ||
null
if (typeof Ctor !== 'function') {
const Ctor = nodeRequire('holesail')
const Fn = typeof Ctor === 'function' ? Ctor : Ctor?.default
if (typeof Fn !== 'function') {
throw new Error('holesail package loaded but no constructor export was found')
}
HolesailCtor = Ctor
HolesailCtor = Fn
return HolesailCtor
} catch (err) {
holesailLoadError = err
@@ -160,16 +174,13 @@ export async function loadHolesailCtor() {
}
/**
* Pick a free TCP port on 127.0.0.1 (Node / in-process path only).
* Pick a free TCP port on 127.0.0.1 (in-process path only).
* @returns {Promise<number>}
*/
export async function findFreePort() {
try {
const net = await import('net')
const createServer = net.createServer || net.default?.createServer
if (typeof createServer !== 'function') throw new Error('net.createServer missing')
return await new Promise((resolve, reject) => {
const s = createServer()
const s = net.createServer()
s.listen(0, '127.0.0.1', () => {
const addr = s.address()
const port = typeof addr === 'object' && addr ? addr.port : 0
@@ -183,7 +194,7 @@ export async function findFreePort() {
}
/**
* Connect to an hs:// URL and listen locally (real holesail).
* Connect to an hs:// URL and listen locally.
* @param {string} urlOrKey
* @param {{ localPort?: number, host?: string, openBrowser?: boolean }} [opts]
*/
@@ -195,7 +206,7 @@ export async function connectLocalHolesail(urlOrKey, opts = {}) {
return localClients.get(url)
}
// Pear GUI → Bare main control HTTP (real holesail there)
// Pear GUI / Electron client → main control HTTP
if (isPearGui()) {
const result = await controlRpc('/connect', {
url,
@@ -298,20 +309,18 @@ function tryOpenBrowser(href) {
} catch {
// ignore
}
import('child_process')
.then((cp) => {
const exec = cp.exec || cp.default?.exec
if (typeof exec !== 'function') return
const platform = globalThis.process?.platform || ''
const cmd =
platform === 'darwin'
? `open "${href}"`
: platform === 'win32'
? `start "" "${href}"`
: `xdg-open "${href}"`
exec(cmd)
})
.catch(() => {})
try {
const platform = globalThis.process?.platform || ''
const cmd =
platform === 'darwin'
? `open "${href}"`
: platform === 'win32'
? `start "" "${href}"`
: `xdg-open "${href}"`
exec(cmd)
} catch {
// ignore
}
}
export default {