Files
bare-operating-system/packages/bare-os-booter/lib/holesail-cli.js
T

283 lines
9.5 KiB
JavaScript

/**
* 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<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>} */ ({})
}
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<string, unknown> } | { 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<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 {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') {
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<string, unknown>} */ (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
}
}