Files
bare-operating-system/packages/bare-os-booter/lib/p2p/holesail-cli.js
T
2026-08-18 18:11:28 -04:00

529 lines
19 KiB
JavaScript

/**
* Drive-resident /bin/holesail → ctx.bareOsRunHolesailCli (booter).
* Manages persisted tunnels under **`BARE_OS_HOLESAIL_STATE`** or **`~/.holesail/state.json`** (per **`$HOME`**).
* Live start/stop uses the **bare-holesail** initd unit after login (managed mode is the stock default).
*/
import {
bareHolesailManagedEnvForConnectionId,
bareHolesailManagedNormalizeEntry,
bareHolesailManagedReadState,
bareHolesailManagedRuntimeRunning,
bareHolesailManagedRuntimeUrl,
bareHolesailManagedServiceIsRunning,
bareHolesailManagedStartOne,
bareHolesailManagedStatePath,
bareHolesailManagedStopOne,
bareHolesailManagedSyncPersistedServerKey,
bareHolesailManagedValidateId,
bareHolesailManagedWriteState
} from './bare-holesail-managed.js'
/**
* @param {Record<string, unknown>} ctx
*/
function ctxEnv(ctx) {
return ctx.env && typeof ctx.env === 'object'
? /** @type {Record<string, string | undefined>} */ (ctx.env)
: /** @type {Record<string, string | undefined>} */ ({})
}
/**
* @param {Record<string, unknown>} ctx
* @param {Record<string, string | undefined>} env
*/
async function holesailCliReadStateMergedForList(ctx, env) {
return bareHolesailManagedReadState(ctx, env)
}
function printHelp(ctx, prog) {
ctx.console.log(
`Usage: ${prog} help|path|list|status
${prog} show ID
${prog} add ID --server|--client [--key KEY] [--port N] [--host H] [--udp] [--secure] [--no-secure] [--log N|true|false]
${prog} edit ID [--port N] [--host H] [--clear-host] [--udp|--no-udp|--tcp] [--secure|--no-secure] [--key KEY] [--log N|true|false]
${prog} remove ID
${prog} start ID | stop ID | restart ID
${prog} enable ID | disable ID
State file: BARE_OS_HOLESAIL_STATE or ${bareHolesailManagedStatePath({})}
Stock boot enables bare-holesail in managed mode (BARE_OS_HOLESAIL_INITD/MANAGED default on).
Disable with BARE_OS_HOLESAIL_INITD=0 or systemctl disable bare-holesail.
show never prints seed material (ctor secret). list/show print the shareable hs:// URL.
edit updates fields in place (does not change server/client or seed). Live tunnels restart.
Upstream holesail is AGPL-3.0; see handbook/04-the-booter-runtime.md.`
)
}
/**
* @param {Record<string, unknown>} entry
* @param {string} id
* @param {boolean} live
* @param {string} runtimeUrl
*/
export function holesailCliFormatListRow(id, entry, live, runtimeUrl) {
const mode = entry.server ? 'server' : entry.client ? 'client' : '?'
const persistedUrl = String(entry.key ?? '').trim()
const url = (live ? String(runtimeUrl || '').trim() : '') || persistedUrl
const port = entry.port != null && entry.port !== '' ? String(entry.port) : ''
const host = String(entry.host ?? '').trim()
const udp = entry.udp != null ? String(!!entry.udp) : ''
const secure = entry.secure != null ? String(!!entry.secure) : ''
const bits = [
id,
mode,
`enabled=${String(!!entry.enabled)}`,
`live=${String(!!live)}`
]
if (port) bits.push(`port=${port}`)
if (host) bits.push(`host=${host}`)
if (udp) bits.push(`udp=${udp}`)
if (secure) bits.push(`secure=${secure}`)
if (url) bits.push(`url=${url.slice(0, 120)}`)
return bits.join('\t')
}
/**
* Accept `hs://…` without `--key` (otherwise parsed as an unknown flag).
* @param {string} a
*/
function holesailCliTokenLooksLikeHolesailConnectionString(a) {
return typeof a === 'string' && /^hs:\/\//i.test(a)
}
export function holesailCliParseAdd(flagArgs) {
if (flagArgs.length < 1) {
return { ok: false, err: 'holesail add: missing ID' }
}
const idOk = bareHolesailManagedValidateId(flagArgs[0])
if (!idOk.ok) return { ok: false, err: `holesail add: ${idOk.err}` }
/** @type {Record<string, unknown>} */
const raw = {}
for (let i = 1; i < flagArgs.length; i++) {
const a = flagArgs[i]
if (a === '--server') raw.server = true
else if (a === '--client') raw.client = true
else if (holesailCliTokenLooksLikeHolesailConnectionString(a)) {
if (raw.key) {
return { ok: false, err: 'holesail add: duplicate key' }
}
raw.key = a
} else if (a === '--key' && flagArgs[i + 1]) raw.key = flagArgs[++i]
else if (a === '--port' && flagArgs[i + 1]) raw.port = flagArgs[++i]
else if (a === '--host' && flagArgs[i + 1]) raw.host = flagArgs[++i]
else if (a === '--udp') raw.udp = true
else if (a === '--secure') raw.secure = true
else if (a === '--no-secure') raw.secure = false
else if (a === '--log' && flagArgs[i + 1]) {
const v = flagArgs[++i]
if (v === 'true' || v === 'false') raw.log = v
else raw.log = v
} else {
return { ok: false, err: `holesail add: unknown or incomplete flag: ${a}` }
}
}
const norm = bareHolesailManagedNormalizeEntry(raw)
if (!norm.ok) return { ok: false, err: `holesail add: ${norm.err}` }
return { ok: true, id: idOk.id, entry: norm.entry }
}
/**
* @param {string[]} flagArgs tokens including ID for "edit"
* @returns {{ ok: true, id: string, patch: Record<string, unknown> } | { ok: false, err: string }}
*/
export function holesailCliParseEdit(flagArgs) {
if (flagArgs.length < 1) {
return { ok: false, err: 'holesail edit: missing ID' }
}
const idOk = bareHolesailManagedValidateId(flagArgs[0])
if (!idOk.ok) return { ok: false, err: `holesail edit: ${idOk.err}` }
/** @type {Record<string, unknown>} */
const patch = {}
let touched = false
for (let i = 1; i < flagArgs.length; i++) {
const a = flagArgs[i]
if (holesailCliTokenLooksLikeHolesailConnectionString(a)) {
if (patch.key) return { ok: false, err: 'holesail edit: duplicate key' }
patch.key = a
touched = true
} else if (a === '--key' && flagArgs[i + 1]) {
patch.key = flagArgs[++i]
touched = true
} else if (a === '--port' && flagArgs[i + 1]) {
patch.port = flagArgs[++i]
touched = true
} else if (a === '--host' && flagArgs[i + 1]) {
patch.host = flagArgs[++i]
touched = true
} else if (a === '--clear-host') {
patch.host = ''
touched = true
} else if (a === '--udp') {
patch.udp = true
touched = true
} else if (a === '--no-udp' || a === '--tcp') {
patch.udp = false
touched = true
} else if (a === '--secure') {
patch.secure = true
touched = true
} else if (a === '--no-secure') {
patch.secure = false
touched = true
} else if (a === '--log' && flagArgs[i + 1]) {
patch.log = flagArgs[++i]
touched = true
} else if (a === '--server' || a === '--client') {
return { ok: false, err: 'holesail edit: cannot change server/client (remove and add)' }
} else {
return { ok: false, err: `holesail edit: unknown or incomplete flag: ${a}` }
}
}
if (!touched) return { ok: false, err: 'holesail edit: no fields to change' }
return { ok: true, id: idOk.id, patch }
}
/**
* @param {Record<string, unknown>} ctx
* @param {string[]} argv argv[0] is holesail
*/
export async function runHolesailCli(ctx, argv) {
const env = ctxEnv(ctx)
const args = argv.slice(1)
const prog = 'holesail'
if (
args.length === 0 ||
args[0] === 'help' ||
args[0] === '--help' ||
args[0] === '-h'
) {
printHelp(ctx, prog)
ctx.exitCode = 0
return
}
const sub = args[0]
const rest = args.slice(1)
if (sub === 'path') {
ctx.console.log(bareHolesailManagedStatePath(env))
ctx.exitCode = 0
return
}
const vfs = ctx.vfs
if (!vfs || typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function') {
ctx.console.error('holesail: ctx.vfs read/write unavailable')
ctx.exitCode = 1
return
}
if (!ctx.b4a || typeof ctx.b4a.from !== 'function') {
ctx.console.error('holesail: ctx.b4a.from unavailable')
ctx.exitCode = 1
return
}
try {
if (sub === 'list') {
let { path, state } = await holesailCliReadStateMergedForList(ctx, env)
const idsSorted = Object.keys(state.connections).sort()
let resynced = false
for (const id of idsSorted) {
if (!bareHolesailManagedRuntimeRunning(id)) continue
const raw0 = state.connections[id]
const norm0 = bareHolesailManagedNormalizeEntry(raw0)
if (norm0.ok && norm0.entry.server) {
const w = await bareHolesailManagedSyncPersistedServerKey(
ctx,
bareHolesailManagedEnvForConnectionId(env, id),
id
)
if (w) resynced = true
}
}
if (resynced) {
;({ path, state } = await holesailCliReadStateMergedForList(ctx, env))
}
const daemon = bareHolesailManagedServiceIsRunning()
ctx.console.log(`state: ${path}`)
ctx.console.log(`daemon: ${daemon ? 'managed bare-holesail running' : 'no managed daemon (live ops limited)'}`)
const ids = Object.keys(state.connections).sort()
if (ids.length === 0) {
ctx.console.log('(no connections)')
ctx.exitCode = 0
return
}
for (const id of ids) {
const raw = state.connections[id]
const norm = bareHolesailManagedNormalizeEntry(raw)
const live = bareHolesailManagedRuntimeRunning(id)
const runtimeUrl = live ? bareHolesailManagedRuntimeUrl(id).trim() : ''
if (norm.ok) {
ctx.console.log(holesailCliFormatListRow(id, norm.entry, live, runtimeUrl))
} else {
ctx.console.log(`${id}\t?\tenabled=?\tlive=${live}\tINVALID: ${norm.err}`)
}
}
ctx.exitCode = 0
return
}
if (sub === 'status') {
const { path, state } = await holesailCliReadStateMergedForList(ctx, env)
const ids = Object.keys(state.connections).sort()
let enabled = 0
let live = 0
let servers = 0
let clients = 0
for (const id of ids) {
const norm = bareHolesailManagedNormalizeEntry(state.connections[id])
if (!norm.ok) continue
if (norm.entry.enabled) enabled++
if (norm.entry.server) servers++
if (norm.entry.client) clients++
if (bareHolesailManagedRuntimeRunning(id)) live++
}
const daemon = bareHolesailManagedServiceIsRunning()
ctx.console.log(`state: ${path}`)
ctx.console.log(`daemon: ${daemon ? 'managed bare-holesail running' : 'no managed daemon (live ops limited)'}`)
ctx.console.log(
`connections: ${ids.length}\tenabled=${enabled}\tlive=${live}\tserver=${servers}\tclient=${clients}`
)
ctx.exitCode = 0
return
}
if (sub === 'show') {
const idOk = bareHolesailManagedValidateId(rest[0] || '')
if (!idOk.ok) {
ctx.console.error(`holesail show: ${idOk.err}`)
ctx.exitCode = 2
return
}
const envW = bareHolesailManagedEnvForConnectionId(env, idOk.id)
const { path, state } = await bareHolesailManagedReadState(ctx, envW)
const raw = state.connections[idOk.id]
if (!raw || typeof raw !== 'object') {
ctx.console.error(`holesail show: unknown ${idOk.id}`)
ctx.exitCode = 1
return
}
const norm = bareHolesailManagedNormalizeEntry(raw)
if (!norm.ok) {
ctx.console.error(`holesail show: ${norm.err}`)
ctx.exitCode = 1
return
}
const live = bareHolesailManagedRuntimeRunning(idOk.id)
const persistedUrl = String(norm.entry.key ?? '').trim()
const runtimeUrl = live ? bareHolesailManagedRuntimeUrl(idOk.id).trim() : ''
const url = runtimeUrl || persistedUrl
const seedSet = String(norm.entry.seed ?? '').trim() ? 'yes' : 'no'
const daemon = bareHolesailManagedServiceIsRunning()
ctx.console.log(`id\t${idOk.id}`)
ctx.console.log(`mode\t${norm.entry.server ? 'server' : 'client'}`)
ctx.console.log(`enabled\t${String(!!norm.entry.enabled)}`)
ctx.console.log(`live\t${String(!!live)}`)
if (norm.entry.port != null) ctx.console.log(`port\t${norm.entry.port}`)
if (norm.entry.host) ctx.console.log(`host\t${norm.entry.host}`)
if (norm.entry.udp != null) ctx.console.log(`udp\t${String(!!norm.entry.udp)}`)
if (norm.entry.secure != null) ctx.console.log(`secure\t${String(!!norm.entry.secure)}`)
if (norm.entry.log != null) ctx.console.log(`log\t${norm.entry.log}`)
ctx.console.log(`seed\t${seedSet}`)
if (url) ctx.console.log(`url\t${url}`)
ctx.console.log(`state\t${path}`)
ctx.console.log(`daemon\t${daemon ? 'running' : 'stopped'}`)
ctx.exitCode = 0
return
}
if (sub === 'add') {
const parsed = holesailCliParseAdd(rest)
if (!parsed.ok) {
ctx.console.error(parsed.err)
ctx.exitCode = 2
return
}
const envW = bareHolesailManagedEnvForConnectionId(env, parsed.id)
const { path, state } = await bareHolesailManagedReadState(ctx, envW)
if (state.connections[parsed.id]) {
ctx.console.error(`holesail add: ${parsed.id} already exists (use remove first)`)
ctx.exitCode = 1
return
}
state.connections[parsed.id] = parsed.entry
await bareHolesailManagedWriteState(ctx, envW, state)
ctx.console.log(`wrote ${path}`)
if (bareHolesailManagedServiceIsRunning()) {
try {
await bareHolesailManagedStartOne(ctx, envW, parsed.id)
ctx.console.log(`started ${parsed.id}`)
} catch (e) {
const msg = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
ctx.console.error(`holesail: persisted but start failed: ${msg}`)
ctx.exitCode = 1
return
}
}
ctx.exitCode = 0
return
}
if (sub === 'remove') {
const idOk = bareHolesailManagedValidateId(rest[0] || '')
if (!idOk.ok) {
ctx.console.error(`holesail remove: ${idOk.err}`)
ctx.exitCode = 2
return
}
const envW = bareHolesailManagedEnvForConnectionId(env, idOk.id)
const { path, state } = await bareHolesailManagedReadState(ctx, envW)
if (!state.connections[idOk.id]) {
ctx.console.error(`holesail remove: unknown ${idOk.id}`)
ctx.exitCode = 1
return
}
if (bareHolesailManagedRuntimeRunning(idOk.id)) {
await bareHolesailManagedStopOne(ctx, idOk.id)
}
delete state.connections[idOk.id]
await bareHolesailManagedWriteState(ctx, envW, state)
ctx.console.log(`removed ${idOk.id} (${path})`)
ctx.exitCode = 0
return
}
if (sub === 'start' || sub === 'stop' || sub === 'restart') {
const idOk = bareHolesailManagedValidateId(rest[0] || '')
if (!idOk.ok) {
ctx.console.error(`holesail ${sub}: ${idOk.err}`)
ctx.exitCode = 2
return
}
if (sub === 'restart' || sub === 'stop') {
if (bareHolesailManagedRuntimeRunning(idOk.id)) {
await bareHolesailManagedStopOne(ctx, idOk.id)
}
}
if (sub === 'stop') {
ctx.console.log(`stopped ${idOk.id} (still in state; will restart on boot if enabled)`)
ctx.exitCode = 0
return
}
await bareHolesailManagedStartOne(
ctx,
bareHolesailManagedEnvForConnectionId(env, idOk.id),
idOk.id
)
ctx.console.log(`started ${idOk.id}`)
ctx.exitCode = 0
return
}
if (sub === 'edit') {
const parsed = holesailCliParseEdit(rest)
if (!parsed.ok) {
ctx.console.error(parsed.err)
ctx.exitCode = 2
return
}
const envW = bareHolesailManagedEnvForConnectionId(env, parsed.id)
const { path, state } = await bareHolesailManagedReadState(ctx, envW)
const raw = state.connections[parsed.id]
if (!raw || typeof raw !== 'object') {
ctx.console.error(`holesail edit: unknown ${parsed.id}`)
ctx.exitCode = 1
return
}
const merged = { .../** @type {Record<string, unknown>} */ (raw), ...parsed.patch }
if (Object.prototype.hasOwnProperty.call(parsed.patch, 'host') && parsed.patch.host === '') {
delete merged.host
}
const norm = bareHolesailManagedNormalizeEntry(merged)
if (!norm.ok) {
ctx.console.error(`holesail edit: ${norm.err}`)
ctx.exitCode = 2
return
}
const prev = /** @type {Record<string, unknown>} */ (raw)
if (prev.seed) norm.entry.seed = prev.seed
if (prev.server) {
norm.entry.server = true
norm.entry.client = false
} else if (prev.client) {
norm.entry.client = true
norm.entry.server = false
}
if (prev.enabled !== undefined && parsed.patch.enabled === undefined) {
norm.entry.enabled = prev.enabled
}
state.connections[parsed.id] = norm.entry
await bareHolesailManagedWriteState(ctx, envW, state)
ctx.console.log(`wrote ${path}`)
const wasLive = bareHolesailManagedRuntimeRunning(parsed.id)
if (wasLive) {
await bareHolesailManagedStopOne(ctx, parsed.id)
if (norm.entry.enabled && bareHolesailManagedServiceIsRunning()) {
try {
await bareHolesailManagedStartOne(ctx, envW, parsed.id)
ctx.console.log(`restarted ${parsed.id}`)
} catch (e) {
const msg = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
ctx.console.error(`holesail: saved but restart failed: ${msg}`)
ctx.exitCode = 1
return
}
}
}
ctx.exitCode = 0
return
}
if (sub === 'enable' || sub === 'disable') {
const idOk = bareHolesailManagedValidateId(rest[0] || '')
if (!idOk.ok) {
ctx.console.error(`holesail ${sub}: ${idOk.err}`)
ctx.exitCode = 2
return
}
const envW = bareHolesailManagedEnvForConnectionId(env, idOk.id)
const { path, state } = await bareHolesailManagedReadState(ctx, envW)
const raw = state.connections[idOk.id]
if (!raw || typeof raw !== 'object') {
ctx.console.error(`holesail ${sub}: unknown ${idOk.id}`)
ctx.exitCode = 1
return
}
const o = /** @type {Record<string, unknown>} */ (raw)
o.enabled = sub === 'enable'
await bareHolesailManagedWriteState(ctx, envW, state)
ctx.console.log(`${sub}d ${idOk.id} (${path})`)
if (sub === 'disable' && bareHolesailManagedRuntimeRunning(idOk.id)) {
await bareHolesailManagedStopOne(ctx, idOk.id)
}
if (sub === 'enable' && bareHolesailManagedServiceIsRunning()) {
try {
await bareHolesailManagedStartOne(ctx, envW, idOk.id)
} catch (e) {
const msg = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
ctx.console.error(`holesail: enable saved but start failed: ${msg}`)
ctx.exitCode = 1
return
}
}
ctx.exitCode = 0
return
}
ctx.console.error(`holesail: unknown command ${sub} (try ${prog} help)`)
ctx.exitCode = 2
} catch (e) {
const msg = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
ctx.console.error(`holesail: ${msg}`)
ctx.exitCode = 1
}
}