/** * Drive-resident /bin/holesail → ctx.bareOsRunHolesailCli (booter). * Manages persisted tunnels under BARE_OS_HOLESAIL_STATE (default ~/.holesail/state.json). * Live start/stop uses the bare-holesail initd unit (managed mode is the stock default). */ import { bareHolesailManagedNormalizeEntry, bareHolesailManagedReadState, bareHolesailManagedRuntimeRunning, bareHolesailManagedRuntimeUrl, bareHolesailManagedServiceIsRunning, bareHolesailManagedStartOne, bareHolesailManagedStatePath, bareHolesailManagedStopOne, bareHolesailManagedValidateId, bareHolesailManagedWriteState } from './bare-holesail-managed.js' /** * @param {Record} ctx */ function ctxEnv(ctx) { return ctx.env && typeof ctx.env === 'object' ? /** @type {Record} */ (ctx.env) : /** @type {Record} */ ({}) } function printHelp(ctx, prog) { ctx.console.log( `Usage: ${prog} help|path|list ${prog} add ID --server|--client [--key KEY] [--port N] [--host H] [--udp] [--secure] [--no-secure] [--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. Upstream holesail is AGPL-3.0; see handbook/04-the-booter-runtime.md.` ) } /** * @param {string[]} flagArgs tokens after ID for "add" * @returns {{ ok: true, id: string, entry: Record } | { ok: false, err: string }} */ /** * 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} */ 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 {Record} 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') { const { path, state } = await bareHolesailManagedReadState(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 en = norm.ok ? String(!!norm.entry.enabled) : '?' const live = bareHolesailManagedRuntimeRunning(id) const url = live ? bareHolesailManagedRuntimeUrl(id) : '' const mode = norm.ok && norm.entry.server ? 'server' : norm.ok && norm.entry.client ? 'client' : '?' const err = norm.ok ? '' : ` INVALID: ${norm.err}` ctx.console.log( `${id}\t${mode}\tenabled=${en}\tlive=${live}${url ? `\turl=${url.slice(0, 80)}` : ''}${err}` ) } ctx.exitCode = 0 return } if (sub === 'add') { const parsed = holesailCliParseAdd(rest) if (!parsed.ok) { ctx.console.error(parsed.err) ctx.exitCode = 2 return } const { path, state } = await bareHolesailManagedReadState(ctx, env) 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, env, state) ctx.console.log(`wrote ${path}`) if (bareHolesailManagedServiceIsRunning()) { try { await bareHolesailManagedStartOne(ctx, env, 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 { path, state } = await bareHolesailManagedReadState(ctx, env) 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, env, 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, env, idOk.id) ctx.console.log(`started ${idOk.id}`) 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 { path, state } = await bareHolesailManagedReadState(ctx, env) 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} */ (raw) o.enabled = sub === 'enable' await bareHolesailManagedWriteState(ctx, env, 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, env, 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 } }