(staged as /lib/init/init-main.js); point bundle-kernel-init and verify scripts at the new path. Wire curl, wget, openssl, ssh-keygen, and tar through coreutils and booter host delegates with booter-side CLI helpers; refresh related bins, bare manifest, shell completion, and man DB (kernel + seeder). Add booter support modules for ACL evaluation, audit chain, secret handles, peer admission, replication priority, process table, swarm lifecycle, boot-graph proc, metrics, monotonic time, protomux alias registry, and swarm peer policy; extend extension resolver, VFS, swarm connection managers, IPC, identity-account, and initd. Harden bare-os-bare-libs build on esbuild failure; add verify scripts for extension manifest schema and runtime incomplete markers; extend ctx API typings, gen-ctx-client-stub, and verify-ctx-dts. Update boot hook fragment, bundled init.js, handbook and reference docs (incl. kernel security and VFS path classes).
2250 lines
67 KiB
Plaintext
2250 lines
67 KiB
Plaintext
/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */
|
||
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
|
||
function bareStdin(ctx) {
|
||
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
||
}
|
||
|
||
/** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */
|
||
function bareFormatModeString(mode, type) {
|
||
const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-'
|
||
const perm = mode & 0o777
|
||
const r = (bit) => (perm & bit ? 'r' : '-')
|
||
const w = (bit) => (perm & bit ? 'w' : '-')
|
||
const x = (bit) => (perm & bit ? 'x' : '-')
|
||
return (
|
||
typeChar +
|
||
r(0o400) +
|
||
w(0o200) +
|
||
x(0o100) +
|
||
r(0o040) +
|
||
w(0o020) +
|
||
x(0o010) +
|
||
r(0o004) +
|
||
w(0o002) +
|
||
x(0o001)
|
||
)
|
||
}
|
||
|
||
/** @param {number} mtimeMs @param {number} [nowMs] */
|
||
function bareFormatLsMtime(mtimeMs, nowMs) {
|
||
const now = nowMs != null ? nowMs : Date.now()
|
||
const d = new Date(mtimeMs)
|
||
const months = [
|
||
'Jan',
|
||
'Feb',
|
||
'Mar',
|
||
'Apr',
|
||
'May',
|
||
'Jun',
|
||
'Jul',
|
||
'Aug',
|
||
'Sep',
|
||
'Oct',
|
||
'Nov',
|
||
'Dec'
|
||
]
|
||
const mon = months[d.getMonth()]
|
||
const day = String(d.getDate()).padStart(2, ' ')
|
||
const sixMo = 180 * 24 * 3600 * 1000
|
||
if (Math.abs(now - mtimeMs) > sixMo) {
|
||
const yr = String(d.getFullYear()).padStart(4, ' ')
|
||
return mon + ' ' + day + ' ' + yr
|
||
}
|
||
const hh = String(d.getHours()).padStart(2, '0')
|
||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||
return mon + ' ' + day + ' ' + hh + ':' + mm
|
||
}
|
||
|
||
/** @param {number} size */
|
||
function barePosixBlocks(size) {
|
||
return Math.ceil(Number(size) / 512) || 0
|
||
}
|
||
|
||
/**
|
||
* Raw stdout for NUL/binary when **`process.stdout.write`** is missing.
|
||
* If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it.
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string | Uint8Array} chunk
|
||
* @returns {boolean}
|
||
*/
|
||
function bareOsEmitRaw(ctx, chunk) {
|
||
if (typeof ctx.bareOsBinWrite === 'function') {
|
||
const b4 = ctx.b4a
|
||
const u8 =
|
||
typeof chunk === 'string'
|
||
? b4 && typeof b4.from === 'function'
|
||
? b4.from(chunk)
|
||
: new TextEncoder().encode(chunk)
|
||
: chunk
|
||
ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8))
|
||
return true
|
||
}
|
||
const w = globalThis.process?.stdout?.write
|
||
if (typeof w === 'function') {
|
||
w.call(globalThis.process.stdout, chunk)
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
/** ANSI helpers for /bin/edit (preamble; no import in src). */
|
||
|
||
const EDIT_ANSI_RESET = '\x1b[0m'
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} [ctx]
|
||
* @returns {import('stream').Writable | undefined}
|
||
*/
|
||
function bareEditResolveStdout(ctx) {
|
||
if (!ctx || typeof ctx !== 'object') return globalThis.process?.stdout
|
||
const c = /** @type {{ replStdout?: unknown, stdout?: unknown }} */ (ctx)
|
||
const out = c.replStdout || c.stdout || globalThis.process?.stdout
|
||
return /** @type {import('stream').Writable | undefined} */ (out)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} [ctx]
|
||
*/
|
||
function bareEditUseColor(ctx) {
|
||
const env =
|
||
ctx && typeof ctx === 'object' && ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
if (env.NO_COLOR != null && String(env.NO_COLOR) !== '') return false
|
||
const out = bareEditResolveStdout(ctx)
|
||
return Boolean(out && /** @type {{ isTTY?: boolean }} */ (out).isTTY)
|
||
}
|
||
|
||
/**
|
||
* @param {'keyword'|'string'|'comment'|'number'|'status'|'inverse'|'dim'} cls
|
||
* @param {boolean} on
|
||
*/
|
||
function bareEditSgr(cls, on) {
|
||
if (!on) return ''
|
||
switch (cls) {
|
||
case 'keyword':
|
||
return '\x1b[36m'
|
||
case 'string':
|
||
return '\x1b[32m'
|
||
case 'comment':
|
||
return '\x1b[90m'
|
||
case 'number':
|
||
return '\x1b[33m'
|
||
case 'status':
|
||
return '\x1b[44m\x1b[97m'
|
||
case 'inverse':
|
||
return '\x1b[7m'
|
||
case 'dim':
|
||
return '\x1b[2m'
|
||
default:
|
||
return ''
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string} fullLine
|
||
* @param {Array<{ start: number, end: number, cls: string }>} spans offsets into fullLine
|
||
* @param {number} visStart first column (0-based)
|
||
* @param {number} maxLen max code units to show
|
||
* @param {boolean} useColor
|
||
*/
|
||
function bareEditPaintLineWindow(fullLine, spans, visStart, maxLen, useColor) {
|
||
const slice = fullLine.slice(visStart, visStart + maxLen)
|
||
if (!useColor) return slice
|
||
const n = slice.length
|
||
const relSpans = spans
|
||
.map((s) => ({
|
||
start: Math.max(0, s.start - visStart),
|
||
end: Math.min(n, s.end - visStart),
|
||
cls: s.cls
|
||
}))
|
||
.filter((s) => s.end > 0 && s.start < n)
|
||
.sort((a, b) => a.start - b.start)
|
||
|
||
let out = ''
|
||
let pos = 0
|
||
for (const sp of relSpans) {
|
||
if (sp.start > pos) out += slice.slice(pos, sp.start)
|
||
out +=
|
||
bareEditSgr(/** @type {'keyword'} */ (sp.cls), true) +
|
||
slice.slice(sp.start, sp.end) +
|
||
EDIT_ANSI_RESET
|
||
pos = sp.end
|
||
}
|
||
if (pos < n) out += slice.slice(pos)
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* Move cursor (1-based row/col, DEC origin). Clamp to sane bounds for escape parsing.
|
||
* @param {number} row1
|
||
* @param {number} col1
|
||
*/
|
||
function bareEditCup(row1, col1) {
|
||
const r = Math.max(1, Math.min(Math.floor(row1), 9999))
|
||
const c = Math.max(1, Math.min(Math.floor(col1), 9999))
|
||
return '\x1b[' + r + ';' + c + 'H'
|
||
}
|
||
|
||
/** TTY key parsing for /bin/edit: consume one logical key from a mutable byte queue. */
|
||
|
||
/**
|
||
* @param {number} b1
|
||
*/
|
||
function bareEditUtf8TrailCount(b1) {
|
||
if (b1 >= 0xc0 && b1 < 0xe0) return 1
|
||
if (b1 >= 0xe0 && b1 < 0xf0) return 2
|
||
if (b1 >= 0xf0) return 3
|
||
return 0
|
||
}
|
||
|
||
/**
|
||
* @param {number[]} bytes
|
||
*/
|
||
function bareEditUtf8DecodeKey(bytes) {
|
||
try {
|
||
const u = new Uint8Array(bytes)
|
||
if (typeof TextDecoder !== 'undefined') {
|
||
return new TextDecoder('utf-8', { fatal: false }).decode(u)
|
||
}
|
||
} catch {
|
||
/* fall through */
|
||
}
|
||
let s = ''
|
||
for (const b of bytes) s += String.fromCharCode(b)
|
||
return s
|
||
}
|
||
|
||
/**
|
||
* @param {string} seq CSI payload after ESC [, including final byte (e.g. "A", "1;5A", "3~")
|
||
*/
|
||
function bareEditCsiToEvent(seq) {
|
||
if (seq === '3~') return { type: 'ctrl', code: 'delete' }
|
||
if (seq === '5~') return { type: 'nav', key: 'pageup' }
|
||
if (seq === '6~') return { type: 'nav', key: 'pagedown' }
|
||
const last = seq.charAt(seq.length - 1)
|
||
if (last === '~') {
|
||
if (seq === '1~' || seq === '7~') return { type: 'nav', key: 'home' }
|
||
if (seq === '4~' || seq === '8~') return { type: 'nav', key: 'end' }
|
||
return { type: 'unknown' }
|
||
}
|
||
if (last === 'A' || last === 'B' || last === 'C' || last === 'D') {
|
||
const map = { A: 'up', B: 'down', C: 'right', D: 'left' }
|
||
return { type: 'nav', key: map[last] }
|
||
}
|
||
if (last === 'H') return { type: 'nav', key: 'home' }
|
||
if (last === 'F') return { type: 'nav', key: 'end' }
|
||
return { type: 'unknown' }
|
||
}
|
||
|
||
/**
|
||
* @param {number} b3 byte after ESC O
|
||
*/
|
||
function bareEditSs3ToEvent(b3) {
|
||
if (b3 === 72) return { type: 'nav', key: 'home' }
|
||
if (b3 === 70) return { type: 'nav', key: 'end' }
|
||
if (b3 === 65) return { type: 'nav', key: 'up' }
|
||
if (b3 === 66) return { type: 'nav', key: 'down' }
|
||
if (b3 === 67) return { type: 'nav', key: 'right' }
|
||
if (b3 === 68) return { type: 'nav', key: 'left' }
|
||
return { type: 'unknown' }
|
||
}
|
||
|
||
/**
|
||
* @param {number[]} q mutable queue (front = index 0)
|
||
* @returns {Record<string, unknown> | null} null if more bytes needed
|
||
*/
|
||
function bareEditTryConsumeKey(q) {
|
||
if (!q.length) return null
|
||
const b1 = q[0]
|
||
if (b1 === 3) {
|
||
q.shift()
|
||
return { type: 'ctrl', code: 'interrupt' }
|
||
}
|
||
if (b1 === 8 || b1 === 127) {
|
||
q.shift()
|
||
return { type: 'ctrl', code: 'backspace' }
|
||
}
|
||
if (b1 === 13 || b1 === 10) {
|
||
q.shift()
|
||
return { type: 'key', ch: '\n' }
|
||
}
|
||
if (b1 === 9) {
|
||
q.shift()
|
||
return { type: 'key', ch: '\t' }
|
||
}
|
||
if (b1 === 27) {
|
||
if (q.length < 2) return null
|
||
const b2 = q[1]
|
||
if (b2 === 91) {
|
||
let i = 2
|
||
while (i < q.length) {
|
||
const b = q[i]
|
||
if (b >= 0x40 && b <= 0x7e) {
|
||
const seq = String.fromCharCode.apply(null, q.slice(2, i + 1))
|
||
q.splice(0, i + 1)
|
||
return bareEditCsiToEvent(seq)
|
||
}
|
||
i++
|
||
}
|
||
return null
|
||
}
|
||
if (b2 === 79) {
|
||
if (q.length < 3) return null
|
||
const b3 = q[2]
|
||
q.splice(0, 3)
|
||
return bareEditSs3ToEvent(b3)
|
||
}
|
||
q.splice(0, 2)
|
||
return { type: 'key', ch: String.fromCharCode(b2) }
|
||
}
|
||
if (b1 < 0x20) {
|
||
q.shift()
|
||
return { type: 'ctrl', code: b1 }
|
||
}
|
||
const need = bareEditUtf8TrailCount(b1)
|
||
if (q.length < 1 + need) return null
|
||
const chunk = q.splice(0, 1 + need)
|
||
return { type: 'key', ch: bareEditUtf8DecodeKey(chunk) }
|
||
}
|
||
|
||
/** Fetch /proc and ctx snapshots for /bin/baretop (preamble; no import). */
|
||
|
||
/**
|
||
* Keep in sync with `bareOsReadBareTopSnapshot` path list in packages/bare-os-booter/index.js
|
||
* @type {readonly [string, string][]}
|
||
*/
|
||
var BARE_TOP_SNAPSHOT_PROC_ENTRIES = [
|
||
['index', '/proc/bare_os/index.json'],
|
||
['version', '/proc/bare_os/version'],
|
||
['hostOs', '/proc/bare_os/host_os.json'],
|
||
['debug', '/proc/bare_os/debug.json'],
|
||
['sessionStats', '/proc/bare_os/session_stats'],
|
||
['quotas', '/proc/bare_os/quotas'],
|
||
['capabilitiesJson', '/proc/bare_os/capabilities.json'],
|
||
['capabilitiesNode', '/proc/bare_os/capabilities'],
|
||
['seedHandshake', '/proc/bare_os/seed_handshake'],
|
||
['bootstrap', '/proc/bare_os/bootstrap'],
|
||
['provenance', '/proc/bare_os/provenance'],
|
||
['snapshotHints', '/proc/bare_os/snapshot_hints.json'],
|
||
['stagingSlot', '/proc/bare_os/staging_slot'],
|
||
['peerHealth', '/proc/bare_os/peer_health'],
|
||
['pearIpc', '/proc/bare_os/pear_ipc.json'],
|
||
['pearIpcHealth', '/proc/bare_os/pear_ipc_health.json'],
|
||
['pearTrust', '/proc/bare_os/pear_trust.json'],
|
||
['replication', '/proc/bare_os/replication'],
|
||
['replicationBackpressure', '/proc/bare_os/replication_backpressure.json'],
|
||
['swarm', '/proc/bare_os/swarm'],
|
||
['syncWindow', '/proc/bare_os/sync_window.json'],
|
||
['hdmsHealth', '/proc/bare_os/hdms_health.json'],
|
||
['hdmsHints', '/proc/bare_os/hdms_hints.json'],
|
||
['dhtStatus', '/proc/bare_os/dht_status.json'],
|
||
['udxExtended', '/proc/bare_os/udx_extended.json'],
|
||
['ipcBackpressure', '/proc/bare_os/ipc_backpressure.json'],
|
||
['delegateRed', '/proc/bare_os/delegate_red.json'],
|
||
['gitDelegateStats', '/proc/bare_os/git_delegate_stats.json'],
|
||
['kernelProgram', '/proc/bare_os/kernel_program.json'],
|
||
['gitLfsPointerStats', '/proc/bare_os/git_lfs_pointer_stats.json'],
|
||
['workerBudget', '/proc/bare_os/worker_budget.json'],
|
||
['sandboxProfile', '/proc/bare_os/sandbox_profile.json'],
|
||
['rlimits', '/proc/bare_os/rlimits.json'],
|
||
['extensions', '/proc/bare_os/extensions.json'],
|
||
['initdDag', '/proc/bare_os/initd_dag.json'],
|
||
['bootGraph', '/proc/bare_os/boot_graph.json']
|
||
]
|
||
|
||
/** @param {unknown} buf @param {unknown} b4a @returns {string} */
|
||
function bareTopBufToString(buf, b4a) {
|
||
if (buf == null) return ''
|
||
if (typeof buf === 'string') return buf
|
||
try {
|
||
if (b4a && typeof b4a.toString === 'function') return b4a.toString(buf)
|
||
} catch {
|
||
/* fall through */
|
||
}
|
||
try {
|
||
return new TextDecoder().decode(
|
||
buf instanceof Uint8Array ? buf : new Uint8Array(/** @type {ArrayLike<number>} */ (buf))
|
||
)
|
||
} catch {
|
||
return ''
|
||
}
|
||
}
|
||
|
||
/** @param {string} s @returns {unknown} */
|
||
function bareTopJsonParse(s) {
|
||
const t = String(s || '').trim()
|
||
if (!t) return null
|
||
try {
|
||
return JSON.parse(t)
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {string} path
|
||
* @returns {Promise<string>}
|
||
*/
|
||
async function bareTopReadProc(ctx, path) {
|
||
const vfs = ctx.vfs
|
||
if (!vfs || typeof vfs.readFile !== 'function') return ''
|
||
try {
|
||
const buf = await vfs.readFile(path)
|
||
return bareTopBufToString(buf, ctx.b4a)
|
||
} catch {
|
||
return ''
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {readonly [string, string][]} entries
|
||
* @param {number} concurrency
|
||
* @returns {Promise<Record<string, string>>}
|
||
*/
|
||
async function bareTopReadProcBatch(ctx, entries, concurrency) {
|
||
/** @type {Record<string, string>} */
|
||
const out = {}
|
||
const conc = Math.max(1, Math.min(16, concurrency | 0) || 6)
|
||
for (let i = 0; i < entries.length; i += conc) {
|
||
const slice = entries.slice(i, i + conc)
|
||
await Promise.all(
|
||
slice.map(async ([key, path]) => {
|
||
out[key] = await bareTopReadProc(ctx, path)
|
||
})
|
||
)
|
||
}
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} o
|
||
* @param {number} maxChars
|
||
* @returns {string}
|
||
*/
|
||
function bareTopTruncateJsonPretty(o, maxChars) {
|
||
const raw = JSON.stringify(o, null, 2)
|
||
if (raw.length <= maxChars) return raw
|
||
return raw.slice(0, Math.max(0, maxChars - 24)) + '\n… (truncated) …\n'
|
||
}
|
||
|
||
/** @param {number} n @returns {string} */
|
||
function bareTopFormatBytes(n) {
|
||
const x = Number(n)
|
||
if (!Number.isFinite(x) || x < 0) return '—'
|
||
if (x < 1024) return String(Math.floor(x)) + ' B'
|
||
if (x < 1048576) return (x / 1024).toFixed(1) + ' KiB'
|
||
if (x < 1073741824) return (x / 1048576).toFixed(1) + ' MiB'
|
||
return (x / 1073741824).toFixed(2) + ' GiB'
|
||
}
|
||
|
||
/** @param {number} ms @returns {string} */
|
||
function bareTopFormatDuration(ms) {
|
||
const x = Number(ms)
|
||
if (!Number.isFinite(x) || x < 0) return '—'
|
||
if (x < 1000) return String(Math.floor(x)) + ' ms'
|
||
if (x < 60000) return (x / 1000).toFixed(1) + ' s'
|
||
if (x < 3600000) return Math.floor(x / 60000) + ' m'
|
||
return (x / 3600000).toFixed(1) + ' h'
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown> | null} m metrics live
|
||
* @param {number} peers
|
||
* @returns {number} 0–100
|
||
*/
|
||
function bareTopHealthScore(m, peers) {
|
||
let score = 72
|
||
if (m && typeof m.delegateInflight === 'object' && m.delegateInflight) {
|
||
const o = /** @type {Record<string, unknown>} */ (m.delegateInflight)
|
||
const n = Object.keys(o).length
|
||
score -= Math.min(25, n * 3)
|
||
}
|
||
if (peers < 0) score -= 5
|
||
if (score < 0) score = 0
|
||
if (score > 100) score = 100
|
||
return score
|
||
}
|
||
|
||
/**
|
||
* @param {number} prev
|
||
* @param {number} value
|
||
* @param {number} alpha 0–1
|
||
* @returns {number}
|
||
*/
|
||
function bareTopEma(prev, value, alpha) {
|
||
const a = Math.min(1, Math.max(0, alpha))
|
||
return prev * (1 - a) + value * a
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {Record<string, string>} files
|
||
* @param {string} key
|
||
* @returns {Record<string, unknown> | null}
|
||
*/
|
||
function bareTopParsedFile(files, key) {
|
||
const t = files[key]
|
||
if (!t) return null
|
||
const p = bareTopJsonParse(t)
|
||
return p && typeof p === 'object'
|
||
? /** @type {Record<string, unknown>} */ (p)
|
||
: null
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @returns {Promise<Record<string, unknown>>}
|
||
*/
|
||
/** One coalesced operator frame; keep VFS read fan-out bounded (batch size + concurrency). */
|
||
async function bareTopFetchSnapshot(ctx) {
|
||
const fetchStart = Date.now()
|
||
let readErr = null
|
||
const env =
|
||
ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
const concRaw = parseInt(env.BARE_TOP_FETCH_CONCURRENCY || '6', 10)
|
||
const concurrency = Number.isFinite(concRaw) ? concRaw : 6
|
||
|
||
/** @type {Record<string, string>} */
|
||
let fileTexts = {}
|
||
let fastAtMs = 0
|
||
|
||
try {
|
||
if (typeof ctx.bareOsReadBareTopSnapshot === 'function') {
|
||
const r = await ctx.bareOsReadBareTopSnapshot()
|
||
if (r && typeof r === 'object') {
|
||
const o = /** @type {Record<string, unknown>} */ (r)
|
||
const files = o.files
|
||
if (files && typeof files === 'object') {
|
||
for (const k of Object.keys(files)) {
|
||
const v = files[k]
|
||
if (typeof v === 'string') fileTexts[k] = v
|
||
}
|
||
}
|
||
const am = o.atMs
|
||
if (typeof am === 'number' && Number.isFinite(am)) fastAtMs = am
|
||
}
|
||
}
|
||
} catch (e) {
|
||
readErr = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||
}
|
||
|
||
if (Object.keys(fileTexts).length === 0) {
|
||
fileTexts = await bareTopReadProcBatch(ctx, BARE_TOP_SNAPSHOT_PROC_ENTRIES, concurrency)
|
||
}
|
||
|
||
const atMs = fastAtMs || Date.now()
|
||
|
||
/** @type {Record<string, unknown> | null} */
|
||
let metricsLive = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
let resources = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
let features = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
let netSummary = null
|
||
/** @type {unknown} */
|
||
let initdGraph = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
let fairnessSnapshot = null
|
||
/** @type {unknown} */
|
||
let subprocessBridge = null
|
||
/** @type {Record<string, unknown> | null} */
|
||
let hostStats = null
|
||
|
||
try {
|
||
if (typeof ctx.bareOsReadProcMetricsLive === 'function') {
|
||
const o = ctx.bareOsReadProcMetricsLive()
|
||
if (o && typeof o === 'object') metricsLive = /** @type {Record<string, unknown>} */ (o)
|
||
}
|
||
} catch (e) {
|
||
if (!readErr)
|
||
readErr = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
|
||
}
|
||
|
||
if (!metricsLive) {
|
||
const t = await bareTopReadProc(ctx, '/proc/bare_os/metrics_live.json')
|
||
const p = bareTopJsonParse(t)
|
||
if (p && typeof p === 'object') metricsLive = /** @type {Record<string, unknown>} */ (p)
|
||
}
|
||
|
||
try {
|
||
if (typeof ctx.bareOsGetResourceStatus === 'function') {
|
||
const o = ctx.bareOsGetResourceStatus()
|
||
if (o && typeof o === 'object') resources = /** @type {Record<string, unknown>} */ (o)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
|
||
if (!resources) {
|
||
const t = await bareTopReadProc(ctx, '/proc/bare_os_resources')
|
||
const p = bareTopJsonParse(t)
|
||
if (p && typeof p === 'object') resources = /** @type {Record<string, unknown>} */ (p)
|
||
}
|
||
|
||
let fp = bareTopJsonParse(await bareTopReadProc(ctx, '/proc/bare_os/features'))
|
||
if (!fp) fp = bareTopJsonParse(await bareTopReadProc(ctx, '/proc/bare_os_features'))
|
||
if (fp && typeof fp === 'object') features = /** @type {Record<string, unknown>} */ (fp)
|
||
|
||
const netT = await bareTopReadProc(ctx, '/proc/bare_os/net_summary.json')
|
||
const np = bareTopJsonParse(netT)
|
||
if (np && typeof np === 'object') netSummary = /** @type {Record<string, unknown>} */ (np)
|
||
|
||
const initT = await bareTopReadProc(ctx, '/proc/bare_os/initd_graph.json')
|
||
initdGraph = bareTopJsonParse(initT)
|
||
if (initdGraph === null) {
|
||
const alt = bareTopJsonParse(fileTexts.initdDag || '')
|
||
initdGraph = alt
|
||
}
|
||
|
||
try {
|
||
if (typeof ctx.bareOsReadDelegateFairnessSnapshot === 'function') {
|
||
const o = ctx.bareOsReadDelegateFairnessSnapshot()
|
||
if (o && typeof o === 'object')
|
||
fairnessSnapshot = /** @type {Record<string, unknown>} */ (o)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
|
||
try {
|
||
if (typeof ctx.bareOsReadSubprocessBridgeSnapshot === 'function') {
|
||
subprocessBridge = ctx.bareOsReadSubprocessBridgeSnapshot()
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
|
||
try {
|
||
const hs = ctx.bareOsHostStats
|
||
if (hs && typeof hs === 'object') hostStats = /** @type {Record<string, unknown>} */ (hs)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
|
||
const memRaw = await bareTopReadProc(ctx, '/proc/meminfo')
|
||
let meminfoLine = ''
|
||
for (const line of memRaw.split('\n')) {
|
||
const L = line.trim()
|
||
if (L.startsWith('MemTotal:') || L.startsWith('MemAvailable:')) {
|
||
meminfoLine = L
|
||
break
|
||
}
|
||
}
|
||
if (!meminfoLine) meminfoLine = memRaw.split('\n')[0]?.trim() || ''
|
||
|
||
const loadavgLine = (await bareTopReadProc(ctx, '/proc/loadavg')).split('\n')[0]?.trim() || ''
|
||
|
||
let cpuLine = ''
|
||
const cpuRaw = await bareTopReadProc(ctx, '/proc/cpuinfo')
|
||
for (const line of cpuRaw.split('\n')) {
|
||
if (line.startsWith('model name') || line.startsWith('Model')) {
|
||
cpuLine = line.replace(/^[^:]+:\s*/, '').trim().slice(0, 72)
|
||
break
|
||
}
|
||
}
|
||
|
||
const hostOs = bareTopParsedFile(fileTexts, 'hostOs')
|
||
const metaAt =
|
||
metricsLive && typeof metricsLive.atMs === 'number' ? metricsLive.atMs : atMs
|
||
|
||
/** @type {Record<string, Record<string, unknown> | null>} */
|
||
const extra = {}
|
||
for (const [key] of BARE_TOP_SNAPSHOT_PROC_ENTRIES) {
|
||
extra[key] = bareTopParsedFile(fileTexts, key)
|
||
}
|
||
|
||
const fetchWallMs = Date.now() - fetchStart
|
||
|
||
return {
|
||
metricsLive,
|
||
resources,
|
||
features,
|
||
initdGraph,
|
||
netSummary,
|
||
meminfoLine,
|
||
loadavgLine,
|
||
cpuLine,
|
||
hostOs,
|
||
atMs,
|
||
metaAtMs: metaAt,
|
||
readErr,
|
||
fileTexts,
|
||
extra,
|
||
fairnessSnapshot,
|
||
subprocessBridge,
|
||
hostStats,
|
||
fetchWallMs,
|
||
healthScore: bareTopHealthScore(
|
||
metricsLive,
|
||
Number(metricsLive && metricsLive.peers) || 0
|
||
)
|
||
}
|
||
}
|
||
|
||
/** Full-screen TUI for /bin/baretop — session / kernel dashboard (preamble; no import). */
|
||
|
||
/** User-visible strings (i18n-ready single object). */
|
||
var bareTopStrings = {
|
||
title: 'baretop',
|
||
helpKeys: 'Keys',
|
||
quitHint: '^C also quits',
|
||
noHtop: 'Bare OS has no per-PID process table; this dashboard shows session,',
|
||
noHtop2: ' pipeline, Hyperswarm peers, delegates, /proc/bare_os mirrors, and ctx snapshots.',
|
||
pressCloseHelp: 'Press any key to close help.',
|
||
activity: 'Activity (per refresh)',
|
||
session: 'Session',
|
||
delegates: 'Delegates',
|
||
pipeline: 'Pipeline limits',
|
||
exportOk: 'exported snapshot',
|
||
exportFail: 'export failed',
|
||
paused: 'PAUSED',
|
||
na: 'N/A'
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} chunk
|
||
* @returns {number[]}
|
||
*/
|
||
function bareTopChunkBytes(chunk) {
|
||
if (chunk == null) return []
|
||
if (typeof chunk === 'string') {
|
||
const out = []
|
||
for (let i = 0; i < chunk.length; i++) out.push(chunk.charCodeAt(i) & 0xff)
|
||
return out
|
||
}
|
||
const len = /** @type {{ length: number, [k: number]: number }} */ (chunk).length
|
||
const out = []
|
||
for (let i = 0; i < len; i++) out.push(Number(chunk[i]) & 0xff)
|
||
return out
|
||
}
|
||
|
||
/**
|
||
* Strip bracketed paste wrappers from raw bytes before key parse (best-effort).
|
||
* @param {number[]} q
|
||
*/
|
||
function bareTopStripBracketedPaste(q) {
|
||
const esc = 0x1b
|
||
let i = 0
|
||
while (i < q.length) {
|
||
if (q[i] !== esc || i + 1 >= q.length || q[i + 1] !== 0x5b) {
|
||
i++
|
||
continue
|
||
}
|
||
let j = i + 2
|
||
let acc = ''
|
||
while (j < q.length && q[j] !== 0x7e) {
|
||
acc += String.fromCharCode(q[j])
|
||
j++
|
||
}
|
||
if (j < q.length && acc.startsWith('200')) {
|
||
j++
|
||
let k = j
|
||
while (k < q.length) {
|
||
if (
|
||
k + 5 < q.length &&
|
||
q[k] === esc &&
|
||
q[k + 1] === 0x5b &&
|
||
q[k + 2] === 0x32 &&
|
||
q[k + 3] === 0x30 &&
|
||
q[k + 4] === 0x31 &&
|
||
q[k + 5] === 0x7e
|
||
) {
|
||
q.splice(i, k + 6 - i)
|
||
i = Math.max(0, i - 1)
|
||
break
|
||
}
|
||
k++
|
||
}
|
||
if (k >= q.length) break
|
||
continue
|
||
}
|
||
i++
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
* @param {import('stream').Writable} stdout
|
||
* @param {string} s
|
||
*/
|
||
function bareTopWrite(ctx, stdout, s) {
|
||
if (!stdout || typeof stdout.write !== 'function') return
|
||
try {
|
||
stdout.write(s)
|
||
} catch {
|
||
try {
|
||
ctx.console?.error?.('baretop: stdout write failed')
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {string} theme
|
||
* @param {boolean} useColor
|
||
*/
|
||
function bareTopTheme(theme, useColor, highContrast) {
|
||
if (!useColor)
|
||
return {
|
||
kw: '',
|
||
dim: '',
|
||
warn: '',
|
||
bad: '',
|
||
good: '',
|
||
barHi: '',
|
||
reset: ''
|
||
}
|
||
if (highContrast) {
|
||
return {
|
||
kw: '\x1b[1;36m',
|
||
dim: '\x1b[4m',
|
||
warn: '\x1b[1;33m',
|
||
bad: '\x1b[1;31m',
|
||
good: '\x1b[1;32m',
|
||
barHi: '\x1b[1;35m',
|
||
reset: '\x1b[0m'
|
||
}
|
||
}
|
||
if (theme === 'light') {
|
||
return {
|
||
kw: '\x1b[34m',
|
||
dim: '\x1b[90m',
|
||
warn: '\x1b[33m',
|
||
bad: '\x1b[31m',
|
||
good: '\x1b[32m',
|
||
barHi: '\x1b[35m',
|
||
reset: '\x1b[0m'
|
||
}
|
||
}
|
||
if (theme === 'none') {
|
||
return {
|
||
kw: '',
|
||
dim: '',
|
||
warn: '',
|
||
bad: '',
|
||
good: '',
|
||
barHi: '',
|
||
reset: ''
|
||
}
|
||
}
|
||
return {
|
||
kw: '\x1b[36m',
|
||
dim: '\x1b[2m',
|
||
warn: '\x1b[33m',
|
||
bad: '\x1b[31m',
|
||
good: '\x1b[32m',
|
||
barHi: '\x1b[35m',
|
||
reset: '\x1b[0m'
|
||
}
|
||
}
|
||
|
||
/** @param {number[]} q @param {number} max @param {{ vi: boolean }} o */
|
||
function bareTopDrainKeys(q, max, o) {
|
||
let n = 0
|
||
for (;;) {
|
||
if (n >= max) break
|
||
const ev = bareEditTryConsumeKey(q)
|
||
if (!ev) break
|
||
n++
|
||
if (ev.type === 'eof') return { type: 'quit' }
|
||
if (ev.type === 'ctrl' && ev.code === 'interrupt') return { type: 'quit' }
|
||
if (ev.type === 'ctrl' && ev.code === 'backspace')
|
||
return { type: 'filter_bs' }
|
||
if (ev.type === 'key' && ev.ch === '\n') return { type: 'filter_enter' }
|
||
if (ev.type === 'nav') {
|
||
if (ev.key === 'pageup') return { type: 'scroll', dir: -1 }
|
||
if (ev.key === 'pagedown') return { type: 'scroll', dir: 1 }
|
||
if (ev.key === 'home') return { type: 'scroll', dir: 'home' }
|
||
if (ev.key === 'end') return { type: 'scroll', dir: 'end' }
|
||
if (o.vi) {
|
||
if (ev.key === 'left') return { type: 'focus', dir: -1 }
|
||
if (ev.key === 'right') return { type: 'focus', dir: 1 }
|
||
if (ev.key === 'up') return { type: 'scroll', dir: -1 }
|
||
if (ev.key === 'down') return { type: 'scroll', dir: 1 }
|
||
}
|
||
}
|
||
if (ev.type === 'key' && ev.ch) {
|
||
const ch = ev.ch
|
||
if (ch === 'q' || ch === 'Q') return { type: 'quit' }
|
||
if (ch === 'r' || ch === 'R') return { type: 'refresh' }
|
||
if (ch === 'h' || ch === '?' || ch === 'H') return { type: 'help_open' }
|
||
if (ch === ' ') return { type: 'pause_toggle' }
|
||
if (ch === '.') return { type: 'step' }
|
||
if (ch === 'd' || ch === 'D') return { type: 'delta_toggle' }
|
||
if (ch === 'e' || ch === 'E') return { type: 'export' }
|
||
if (ch === 'f' || ch === 'F') return { type: 'fullscreen_toggle' }
|
||
if (ch === 't' || ch === 'T') return { type: 'clock_toggle' }
|
||
if (ch === '[') return { type: 'tab_prev' }
|
||
if (ch === ']') return { type: 'tab_next' }
|
||
if (ch === '/') return { type: 'filter_open' }
|
||
if (ch === 'n' && o.vi) return { type: 'filter_next' }
|
||
if (ch === 'N' && o.vi) return { type: 'filter_prev' }
|
||
if (ch >= '1' && ch <= '9') return { type: 'tab', n: ch.charCodeAt(0) - 0x31 }
|
||
if (ch === '0') return { type: 'tab', n: 9 }
|
||
if (ch.length === 1 && ch >= ' ' && ch.charCodeAt(0) > 32)
|
||
return { type: 'filter_char', ch }
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* @param {number[]} values
|
||
* @param {number} width
|
||
* @param {boolean} ascii
|
||
* @param {boolean} logScale
|
||
* @param {boolean} braille
|
||
*/
|
||
function bareTopSparkline(values, width, ascii, logScale, braille) {
|
||
if (width <= 0) return ''
|
||
const slice =
|
||
values.length > width ? values.slice(values.length - width) : values.slice()
|
||
if (slice.length === 0) return ' '.repeat(width)
|
||
const vmap = logScale
|
||
? slice.map((v) => Math.log1p(Math.max(0, v)))
|
||
: slice.slice()
|
||
let min = vmap[0]
|
||
let max = vmap[0]
|
||
for (let i = 1; i < vmap.length; i++) {
|
||
if (vmap[i] < min) min = vmap[i]
|
||
if (vmap[i] > max) max = vmap[i]
|
||
}
|
||
const span = max - min || 1
|
||
const uni = '\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588'
|
||
const br = '\u2800\u2840\u28c0\u28e0\u28f0\u28f4\u28f6\u28ff'
|
||
const asc = ' .:-=+*#'
|
||
let out = ''
|
||
const pad = width - slice.length
|
||
for (let i = 0; i < pad; i++) out += ascii ? ' ' : '\u2581'
|
||
for (let i = 0; i < vmap.length; i++) {
|
||
const v = vmap[i]
|
||
const t = (v - min) / span
|
||
let idx = Math.min(7, Math.floor(t * 8))
|
||
if (idx < 0) idx = 0
|
||
if (ascii) out += asc.charAt(idx)
|
||
else if (braille) out += br.charAt(idx)
|
||
else out += uni.charAt(idx)
|
||
}
|
||
return out.slice(0, width)
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} inflight
|
||
* @param {number} maxWidth
|
||
* @param {boolean} use256
|
||
* @returns {string}
|
||
*/
|
||
function bareTopDelegateHistogram(inflight, maxWidth, use256) {
|
||
if (!inflight || typeof inflight !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (inflight)
|
||
const keys = Object.keys(o).slice(0, 12)
|
||
if (!keys.length) return ''
|
||
let maxV = 1
|
||
for (const k of keys) {
|
||
const v = Number(o[k])
|
||
if (Number.isFinite(v) && v > maxV) maxV = v
|
||
}
|
||
let line = ''
|
||
const per = Math.max(4, Math.floor(maxWidth / Math.max(1, keys.length)) - 1)
|
||
for (const k of keys) {
|
||
const v = Math.min(maxV, Math.max(0, Number(o[k]) || 0))
|
||
const fill = Math.round((v / maxV) * per)
|
||
let bar = ''
|
||
for (let i = 0; i < per; i++) {
|
||
const on = i < fill
|
||
if (use256 && on) bar += '\x1b[38;5;39m\u2588\x1b[0m'
|
||
else if (on) bar += '\u2588'
|
||
else bar += '\u2581'
|
||
}
|
||
line += k.slice(0, 3) + bar + ' '
|
||
}
|
||
return line.slice(0, maxWidth)
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} pl
|
||
* @param {number} cols
|
||
* @param {{ dim: string, warn: string, reset: string }} pal
|
||
*/
|
||
function bareTopPipelineGauges(pl, cols, pal, useColor) {
|
||
if (!pl || typeof pl !== 'object') return ''
|
||
const o = /** @type {Record<string, unknown>} */ (pl)
|
||
const parts = []
|
||
for (const k of Object.keys(o).slice(0, 8)) {
|
||
const v = o[k]
|
||
if (typeof v === 'number' && Number.isFinite(v)) {
|
||
const w = Math.min(12, Math.max(4, Math.floor(cols / 10)))
|
||
const cap = v > 1000 ? v : 100
|
||
const pct = Math.min(100, Math.round((v / cap) * 100))
|
||
const fill = Math.round((pct / 100) * w)
|
||
let bar = ''
|
||
for (let i = 0; i < w; i++)
|
||
bar += i < fill ? '\u2588' : '\u2591'
|
||
const s =
|
||
k.slice(0, 10) +
|
||
' ' +
|
||
(useColor && pct > 85 ? pal.warn : pal.dim) +
|
||
bar +
|
||
pal.reset +
|
||
pct +
|
||
'%'
|
||
parts.push(s)
|
||
}
|
||
}
|
||
return parts.join(' ').slice(0, cols - 1)
|
||
}
|
||
|
||
/**
|
||
* Break one logical line into wrapped physical lines (word-aware when possible).
|
||
* @param {string} s
|
||
* @param {number} width
|
||
* @returns {string[]}
|
||
*/
|
||
function bareTopWrapLine(s, width) {
|
||
const t = String(s)
|
||
if (width < 8) return t ? [t] : ['']
|
||
if (t.length <= width) return [t]
|
||
const out = []
|
||
let i = 0
|
||
while (i < t.length) {
|
||
let take = Math.min(width, t.length - i)
|
||
if (i + take < t.length) {
|
||
const chunk = t.slice(i, i + take)
|
||
const sp = chunk.lastIndexOf(' ')
|
||
if (sp > (width >> 1)) take = sp
|
||
else {
|
||
const tab = chunk.lastIndexOf('\t')
|
||
if (tab > (width >> 1)) take = tab + 1
|
||
}
|
||
}
|
||
const piece = t.slice(i, i + take).replace(/\s+$/g, '')
|
||
if (piece.length) out.push(piece)
|
||
i += take
|
||
while (i < t.length && (t[i] === ' ' || t[i] === '\t')) i++
|
||
}
|
||
return out.length ? out : ['']
|
||
}
|
||
|
||
/**
|
||
* @param {string} k
|
||
*/
|
||
function bareTopHumanKey(k) {
|
||
return String(k || '')
|
||
.replace(/([A-Z])/g, ' $1')
|
||
.replace(/_/g, ' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
.toLowerCase()
|
||
.replace(/^\w/, (c) => c.toUpperCase())
|
||
}
|
||
|
||
/**
|
||
* Human-readable lines (no JSON).
|
||
* @param {string} prefix
|
||
* @param {unknown} v
|
||
* @param {number} depth
|
||
* @param {number} maxD
|
||
* @param {string[]} lines
|
||
* @param {number} maxKeys
|
||
*/
|
||
function bareTopFlatten(prefix, v, depth, maxD, lines, maxKeys) {
|
||
const pad = ' '.repeat(depth)
|
||
if (depth > maxD) {
|
||
lines.push(pad + (prefix ? bareTopHumanKey(prefix) + ': ' : '') + '\u2026')
|
||
return
|
||
}
|
||
if (v == null) {
|
||
lines.push(pad + (prefix ? bareTopHumanKey(prefix) + ': ' : '') + bareTopStrings.na)
|
||
return
|
||
}
|
||
const t = typeof v
|
||
if (t === 'string' || t === 'number' || t === 'boolean') {
|
||
let s = String(v)
|
||
if (t === 'string' && s.length > 800) s = s.slice(0, 797) + '\u2026'
|
||
lines.push(pad + (prefix ? bareTopHumanKey(prefix) + ': ' : '') + s)
|
||
return
|
||
}
|
||
if (Array.isArray(v)) {
|
||
const label = prefix ? bareTopHumanKey(prefix) + ': ' : ''
|
||
if (v.length === 0) {
|
||
lines.push(pad + label + '(empty)')
|
||
return
|
||
}
|
||
lines.push(pad + label + '(' + v.length + ' items)')
|
||
const lim = Math.min(v.length, 120)
|
||
for (let i = 0; i < lim; i++) {
|
||
const it = v[i]
|
||
if (it != null && typeof it === 'object' && !Array.isArray(it)) {
|
||
lines.push(pad + ' #' + i)
|
||
bareTopFlatten('', it, depth + 2, maxD, lines, maxKeys)
|
||
} else if (Array.isArray(it)) {
|
||
lines.push(pad + ' #' + i)
|
||
bareTopFlatten('', it, depth + 2, maxD, lines, maxKeys)
|
||
} else {
|
||
lines.push(
|
||
pad +
|
||
' #' +
|
||
i +
|
||
': ' +
|
||
(it == null ? bareTopStrings.na : String(it))
|
||
)
|
||
}
|
||
}
|
||
if (v.length > lim) lines.push(pad + ' \u2026 +' + (v.length - lim) + ' more')
|
||
return
|
||
}
|
||
if (t === 'object') {
|
||
const o = /** @type {Record<string, unknown>} */ (v)
|
||
const keys = Object.keys(o)
|
||
if (keys.length === 0) {
|
||
lines.push(pad + (prefix ? bareTopHumanKey(prefix) + ': ' : '') + '(empty)')
|
||
return
|
||
}
|
||
const slice = keys.slice(0, maxKeys)
|
||
if (prefix) {
|
||
lines.push(pad + bareTopHumanKey(prefix) + ':')
|
||
for (const k of slice)
|
||
bareTopFlatten(k, o[k], depth + 1, maxD, lines, maxKeys)
|
||
} else {
|
||
for (const k of slice) bareTopFlatten(k, o[k], depth, maxD, lines, maxKeys)
|
||
}
|
||
if (keys.length > maxKeys)
|
||
lines.push(pad + '\u2026 +' + (keys.length - maxKeys) + ' keys')
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown | null | undefined>} pack
|
||
* @param {number} maxDepth
|
||
* @returns {string[]}
|
||
*/
|
||
function bareTopLinesFromPack(pack, maxDepth) {
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
for (const key of Object.keys(pack)) {
|
||
const val = pack[key]
|
||
lines.push('')
|
||
lines.push('\u2500 ' + bareTopHumanKey(key) + ' \u2500')
|
||
if (val == null) lines.push(' ' + bareTopStrings.na)
|
||
else bareTopFlatten('', val, 1, maxDepth, lines, 220)
|
||
}
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* @param {string} title
|
||
* @param {unknown} o
|
||
* @param {number} maxDepth
|
||
* @returns {string[]}
|
||
*/
|
||
function bareTopLinesFromSingle(title, o, maxDepth) {
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
lines.push('')
|
||
lines.push('\u2500 ' + title + ' \u2500')
|
||
if (o == null) lines.push(' ' + bareTopStrings.na)
|
||
else bareTopFlatten('', o, 1, maxDepth, lines, 220)
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} graph
|
||
* @returns {string[]}
|
||
*/
|
||
function bareTopInitdGraphLines(graph) {
|
||
/** @type {string[]} */
|
||
const lines = []
|
||
if (graph == null) {
|
||
lines.push(bareTopStrings.na)
|
||
return lines
|
||
}
|
||
if (typeof graph === 'object' && graph && Array.isArray(graph.nodes)) {
|
||
lines.push('Units (' + graph.nodes.length + '):')
|
||
for (const n of graph.nodes) lines.push(' \u2022 ' + String(n))
|
||
return lines
|
||
}
|
||
bareTopFlatten('', graph, 0, 6, lines, 200)
|
||
return lines
|
||
}
|
||
|
||
/**
|
||
* @param {unknown} hints
|
||
* @returns {string}
|
||
*/
|
||
function bareTopHintsFootLine(hints) {
|
||
if (!hints || typeof hints !== 'object') return ''
|
||
/** @type {string[]} */
|
||
const parts = []
|
||
bareTopFlatten('', hints, 0, 2, parts, 20)
|
||
return parts.filter(Boolean).join(' \u00b7 ').slice(0, 380)
|
||
}
|
||
|
||
/**
|
||
* @param {Record<string, unknown>} ctx
|
||
*/
|
||
async function bareOsRunBareTopTui(ctx) {
|
||
const stdin = /** @type {import('stream').Readable | undefined} */ (
|
||
ctx.replStdin
|
||
)
|
||
const stdout = bareEditResolveStdout(ctx)
|
||
if (!stdin || !stdout) {
|
||
ctx.console.error('baretop: missing stdin/stdout')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
|
||
const useColor = bareEditUseColor(ctx)
|
||
const envTop =
|
||
ctx.env && typeof ctx.env === 'object'
|
||
? /** @type {Record<string, string>} */ (ctx.env)
|
||
: {}
|
||
const noAlt =
|
||
envTop.BARE_TOP_NO_ALTSCREEN != null &&
|
||
String(envTop.BARE_TOP_NO_ALTSCREEN) !== ''
|
||
const intervalRaw = parseInt(envTop.BARE_TOP_INTERVAL_MS || '1000', 10)
|
||
const intervalMs = Number.isFinite(intervalRaw)
|
||
? Math.min(10000, Math.max(250, intervalRaw))
|
||
: 1000
|
||
const uiMinRaw = parseInt(envTop.BARE_TOP_UI_MIN_MS || '0', 10)
|
||
const uiMinMs = Number.isFinite(uiMinRaw) ? Math.max(0, uiMinRaw) : 0
|
||
const emaRaw = parseFloat(envTop.BARE_TOP_EMA_ALPHA || '0')
|
||
const emaAlpha = Number.isFinite(emaRaw) ? Math.min(1, Math.max(0, emaRaw)) : 0
|
||
const theme = String(envTop.BARE_TOP_THEME || 'dark').toLowerCase()
|
||
const asciiUi =
|
||
envTop.BARE_TOP_ASCII_UI === '1' ||
|
||
(envTop.LANG && String(envTop.LANG).toLowerCase().includes('ascii'))
|
||
const asciiGraph = envTop.BARE_TOP_ASCII_GRAPH === '1' || asciiUi
|
||
const braille = envTop.BARE_TOP_BRAILLE_SPARK === '1'
|
||
const logSpark = envTop.BARE_TOP_LOG_SPARK === '1'
|
||
const use256 =
|
||
useColor &&
|
||
(String(envTop.COLORTERM || '').toLowerCase() === 'truecolor' ||
|
||
String(envTop.COLORTERM || '').includes('256') ||
|
||
envTop.BARE_TOP_COLOR256 === '1')
|
||
const viKeys =
|
||
envTop.BARE_TOP_VI_KEYS === '1' || envTop.BARE_TOP_VI_KEYS === 'true'
|
||
const highContrast =
|
||
envTop.BARE_TOP_HIGH_CONTRAST === '1' ||
|
||
envTop.BARE_TOP_HIGH_CONTRAST === 'true'
|
||
const layout = String(envTop.BARE_TOP_LAYOUT || 'stacked').toLowerCase()
|
||
const exportPath = String(
|
||
envTop.BARE_TOP_EXPORT_PATH || '/tmp/baretop-snapshot.json'
|
||
).trim()
|
||
const icons = envTop.BARE_TOP_ICONS === '1'
|
||
|
||
const pal = bareTopTheme(theme, useColor, highContrast)
|
||
|
||
const TAB_NAMES = [
|
||
'overview',
|
||
'initd',
|
||
'network',
|
||
'features',
|
||
'diagnostics',
|
||
'operator',
|
||
'pear',
|
||
'catalog',
|
||
'host',
|
||
'keys'
|
||
]
|
||
const NTABS = TAB_NAMES.length
|
||
|
||
/** @type {number[]} */
|
||
const keyq = []
|
||
function onData(chunk) {
|
||
keyq.push(...bareTopChunkBytes(chunk))
|
||
bareTopStripBracketedPaste(keyq)
|
||
}
|
||
stdin.on('data', onData)
|
||
|
||
let needsRedraw = true
|
||
function onResize() {
|
||
needsRedraw = true
|
||
}
|
||
if (typeof stdout.on === 'function') {
|
||
try {
|
||
stdout.on('resize', onResize)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
try {
|
||
if (typeof process !== 'undefined' && typeof process.on === 'function') {
|
||
process.on('SIGWINCH', onResize)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
|
||
function termDims() {
|
||
const env = envTop
|
||
const cols = /** @type {{ columns?: number }} */ (stdout).columns ||
|
||
parseInt(env.COLUMNS || '80', 10) ||
|
||
80
|
||
const rows = /** @type {{ rows?: number }} */ (stdout).rows ||
|
||
parseInt(env.LINES || '24', 10) ||
|
||
24
|
||
return { cols: Math.max(40, cols), rows: Math.max(12, rows) }
|
||
}
|
||
|
||
function headerLine(title) {
|
||
const bar =
|
||
(useColor ? bareEditSgr('status', true) : '') +
|
||
title +
|
||
(useColor ? EDIT_ANSI_RESET : '')
|
||
return bar
|
||
}
|
||
|
||
/** @type {number[]} */
|
||
const ringExecDelta = []
|
||
/** @type {number[]} */
|
||
const ringPipeDelta = []
|
||
/** @type {number[]} */
|
||
const ringWallDelta = []
|
||
/** @type {number[]} */
|
||
const ringPeers = []
|
||
const RING_CAP = 72
|
||
let prevExec = -1
|
||
let prevPipe = -1
|
||
let prevWall = -1
|
||
let emaExec = -1
|
||
let emaPipe = -1
|
||
let emaWall = -1
|
||
|
||
function pushRing(arr, v) {
|
||
arr.push(v)
|
||
while (arr.length > RING_CAP) arr.shift()
|
||
}
|
||
|
||
let tab = 0
|
||
let helpMode = false
|
||
let quit = false
|
||
/** @type {Awaited<ReturnType<typeof bareTopFetchSnapshot>> | null} */
|
||
let lastSnap = null
|
||
/** @type {Awaited<ReturnType<typeof bareTopFetchSnapshot>> | null} */
|
||
let prevSnap = null
|
||
let paused = false
|
||
let deltaMode = false
|
||
let fullscreenPanel = false
|
||
let utcClock = false
|
||
/** @type {number[]} */
|
||
const scrollRows = Array(NTABS).fill(0)
|
||
let filterOpen = false
|
||
/** @type {string} */
|
||
let filterLine = ''
|
||
/** @type {string} */
|
||
let filterActive = ''
|
||
let burstUntil = 0
|
||
let lastDrawAt = 0
|
||
let tipRot = 0
|
||
|
||
let suspended = false
|
||
let useAltScreen = false
|
||
/** @type {(() => void) | null} */
|
||
let hookOffSuspend = null
|
||
/** @type {(() => void) | null} */
|
||
let hookOffResume = null
|
||
|
||
try {
|
||
if (typeof ctx.suspendReplForSubprocess === 'function') {
|
||
ctx.suspendReplForSubprocess()
|
||
suspended = true
|
||
}
|
||
if (typeof ctx.bareOsRegisterSuspendHook === 'function') {
|
||
hookOffSuspend = ctx.bareOsRegisterSuspendHook(() => {
|
||
paused = true
|
||
})
|
||
}
|
||
if (typeof ctx.bareOsRegisterResumeHook === 'function') {
|
||
hookOffResume = ctx.bareOsRegisterResumeHook(() => {
|
||
paused = false
|
||
needsRedraw = true
|
||
})
|
||
}
|
||
if (typeof stdin.setRawMode === 'function') stdin.setRawMode(true)
|
||
if (typeof stdin.resume === 'function') stdin.resume()
|
||
|
||
if (!noAlt) {
|
||
bareTopWrite(ctx, stdout, '\x1b[?1049h')
|
||
useAltScreen = true
|
||
}
|
||
|
||
async function tick() {
|
||
lastSnap = await bareTopFetchSnapshot(ctx)
|
||
const m = lastSnap.metricsLive
|
||
const sess =
|
||
m && typeof m.session === 'object' && m.session
|
||
? /** @type {Record<string, unknown>} */ (m.session)
|
||
: {}
|
||
const ec = Number(sess.execLineCount) || 0
|
||
const pb = Number(sess.pipelineBytesTotal) || 0
|
||
const wm = Number(sess.execLineWallMsTotal) || 0
|
||
const peers = Number(m && m.peers) || 0
|
||
|
||
let dExec = 0
|
||
let dPipe = 0
|
||
let dWall = 0
|
||
if (prevExec >= 0) dExec = Math.max(0, ec - prevExec)
|
||
if (prevPipe >= 0) dPipe = Math.max(0, pb - prevPipe)
|
||
if (prevWall >= 0) dWall = Math.max(0, wm - prevWall)
|
||
prevExec = ec
|
||
prevPipe = pb
|
||
prevWall = wm
|
||
|
||
if (emaAlpha > 0 && emaExec >= 0) {
|
||
dExec = bareTopEma(emaExec, dExec, emaAlpha)
|
||
dPipe = bareTopEma(emaPipe, dPipe, emaAlpha)
|
||
dWall = bareTopEma(emaWall, dWall, emaAlpha)
|
||
}
|
||
emaExec = dExec
|
||
emaPipe = dPipe
|
||
emaWall = dWall
|
||
|
||
pushRing(ringExecDelta, dExec)
|
||
pushRing(ringPipeDelta, dPipe)
|
||
pushRing(ringWallDelta, dWall)
|
||
pushRing(ringPeers, peers)
|
||
}
|
||
|
||
/** @type {string} */
|
||
let out = ''
|
||
|
||
function draw() {
|
||
const now = Date.now()
|
||
if (uiMinMs > 0 && now - lastDrawAt < uiMinMs && !helpMode) return
|
||
lastDrawAt = now
|
||
const { cols, rows } = termDims()
|
||
const narrow = cols < 80
|
||
let scrollHint = ''
|
||
out = '\x1b[?25l\x1b[2J\x1b[H'
|
||
|
||
const clock =
|
||
utcClock && typeof Date.prototype.toISOString === 'function'
|
||
? new Date().toISOString().slice(11, 19) + 'Z'
|
||
: new Date().toTimeString().slice(0, 8)
|
||
|
||
const tabTitle = TAB_NAMES[tab] || 'tab'
|
||
const icon = icons ? '\u25cf ' : ''
|
||
let title =
|
||
icon +
|
||
bareTopStrings.title +
|
||
' ' +
|
||
(helpMode
|
||
? '— help '
|
||
: '— ' + tabTitle + ' ') +
|
||
'| ' +
|
||
clock +
|
||
' | ' +
|
||
intervalMs +
|
||
'ms'
|
||
if (paused) title += ' | ' + bareTopStrings.paused
|
||
if (deltaMode) title += ' | DELTA'
|
||
const pad = Math.max(0, cols - title.length)
|
||
out += headerLine(title + ' '.repeat(pad)) + '\r\n'
|
||
|
||
if (helpMode) {
|
||
out +=
|
||
'\r\n' +
|
||
(useColor ? pal.kw : '') +
|
||
bareTopStrings.helpKeys +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n' +
|
||
' q Quit\r\n' +
|
||
' r Refresh (burst poll)\r\n' +
|
||
' Space Pause / resume sampling\r\n' +
|
||
' . Step one sample when paused\r\n' +
|
||
' d Toggle delta overlay (overview)\r\n' +
|
||
' e Export snapshot JSON (see BARE_TOP_EXPORT_PATH)\r\n' +
|
||
' f Fullscreen focus current tab\r\n' +
|
||
' t Toggle UTC / local clock\r\n' +
|
||
' [ ] Prev / next tab\r\n' +
|
||
' 1-9,0 Jump tab (0 = last)\r\n' +
|
||
' PgUp/Dn Home/End Scroll lists (tabs 1–8)\r\n' +
|
||
' / Edit filter (initd names); Enter apply, BS erase\r\n' +
|
||
' h ? Help\r\n' +
|
||
'\r\n' +
|
||
(useColor ? pal.dim : '') +
|
||
bareTopStrings.noHtop +
|
||
'\r\n' +
|
||
bareTopStrings.noHtop2 +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n' +
|
||
'\r\n' +
|
||
'Pear IPC channel families: bare_os_*, pear:* (see booter pear_ipc registry).\r\n' +
|
||
'\r\n' +
|
||
bareTopStrings.pressCloseHelp +
|
||
'\r\n'
|
||
const used = 22
|
||
for (let r = used; r < rows; r++) {
|
||
out += bareEditCup(r, 1) + '\x1b[K'
|
||
}
|
||
out += bareEditCup(rows, 1) + '\x1b[K'
|
||
out +=
|
||
(useColor ? pal.dim : '') +
|
||
bareTopStrings.quitHint +
|
||
(useColor ? pal.reset : '')
|
||
out += '\x1b[?25h'
|
||
bareTopWrite(ctx, stdout, out)
|
||
return
|
||
}
|
||
|
||
const snap = lastSnap
|
||
const mem = snap ? snap.meminfoLine : ''
|
||
const load = snap ? snap.loadavgLine : ''
|
||
const cpu = snap ? snap.cpuLine : ''
|
||
const sparkW = Math.min(48, Math.max(8, cols - (narrow ? 22 : 28)))
|
||
|
||
const staleMs =
|
||
snap && snap.metaAtMs ? Math.max(0, Date.now() - snap.metaAtMs) : 0
|
||
const staleStr =
|
||
staleMs > 2000
|
||
? (useColor ? pal.warn : '') +
|
||
' staleness ' +
|
||
bareTopFormatDuration(staleMs) +
|
||
(useColor ? pal.reset : '')
|
||
: ''
|
||
|
||
{
|
||
const memRow =
|
||
(useColor ? pal.dim : '') +
|
||
(mem || '(no meminfo)') +
|
||
staleStr +
|
||
(useColor ? pal.reset : '')
|
||
const mw = bareTopWrapLine(memRow, cols - 2)
|
||
for (let mi = 0; mi < mw.length; mi++) {
|
||
out += mw[mi] + '\r\n\x1b[K'
|
||
}
|
||
}
|
||
out +=
|
||
(() => {
|
||
const row = (load ? load + ' ' : '') + (cpu || '')
|
||
const first = bareTopWrapLine(row, cols - 2)
|
||
let s = ''
|
||
for (let li = 0; li < first.length; li++) {
|
||
s += first[li] + '\r\n\x1b[K'
|
||
}
|
||
return s
|
||
})()
|
||
|
||
if (snap && snap.hostOs && typeof snap.hostOs === 'object') {
|
||
/** @type {string[]} */
|
||
const hl = []
|
||
bareTopFlatten('Host OS', snap.hostOs, 0, 4, hl, 32)
|
||
for (const ln of hl) {
|
||
for (const w of bareTopWrapLine(ln, cols - 2)) {
|
||
out +=
|
||
(useColor ? pal.dim : '') +
|
||
w +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n\x1b[K'
|
||
}
|
||
}
|
||
}
|
||
|
||
if (snap && snap.healthScore != null) {
|
||
out +=
|
||
(useColor ? pal.good : '') +
|
||
'health ' +
|
||
String(snap.healthScore) +
|
||
'/100' +
|
||
(useColor ? pal.reset : '') +
|
||
(snap.fetchWallMs != null
|
||
? (useColor ? pal.dim : '') +
|
||
' fetch ' +
|
||
String(snap.fetchWallMs) +
|
||
'ms' +
|
||
(useColor ? pal.reset : '')
|
||
: '') +
|
||
'\r\n\x1b[K'
|
||
}
|
||
|
||
const mainStart = 6
|
||
const mainEnd = fullscreenPanel ? rows - 1 : rows - 2
|
||
|
||
/**
|
||
* @param {string[]} rawLines
|
||
* @param {number} tabIdx
|
||
*/
|
||
function drawScrollableLines(rawLines, tabIdx) {
|
||
const wrapW = Math.max(16, cols - 2)
|
||
/** @type {string[]} */
|
||
const wrapped = []
|
||
for (const ln of rawLines) {
|
||
for (const w of bareTopWrapLine(ln, wrapW)) wrapped.push(w)
|
||
}
|
||
const vis = Math.max(1, mainEnd - mainStart)
|
||
const maxScroll = Math.max(0, wrapped.length - vis)
|
||
if (scrollRows[tabIdx] > maxScroll) scrollRows[tabIdx] = maxScroll
|
||
const s = scrollRows[tabIdx]
|
||
let r = mainStart
|
||
for (let i = s; i < wrapped.length && r < mainEnd; i++) {
|
||
const zebra = i % 2 === 1 ? pal.dim : ''
|
||
const rst = useColor ? pal.reset : ''
|
||
out +=
|
||
bareEditCup(r, 1) +
|
||
'\x1b[K' +
|
||
zebra +
|
||
wrapped[i].slice(0, cols - 1) +
|
||
rst +
|
||
'\r\n'
|
||
r++
|
||
}
|
||
if (wrapped.length > vis) {
|
||
scrollHint =
|
||
' | ' +
|
||
(s + 1) +
|
||
'\u2013' +
|
||
Math.min(wrapped.length, s + vis) +
|
||
'/' +
|
||
wrapped.length
|
||
}
|
||
}
|
||
|
||
const split =
|
||
layout === 'even' && !fullscreenPanel && cols >= 100 && tab === 0
|
||
|
||
if (tab === 0 && snap) {
|
||
const m = snap.metricsLive
|
||
const sess =
|
||
m && typeof m.session === 'object' && m.session
|
||
? /** @type {Record<string, unknown>} */ (m.session)
|
||
: {}
|
||
const ec = Number(sess.execLineCount) || 0
|
||
const pb = Number(sess.pipelineBytesTotal) || 0
|
||
const wm = Number(sess.execLineWallMsTotal) || 0
|
||
|
||
out +=
|
||
'\r\n' +
|
||
(useColor ? pal.kw : '') +
|
||
bareTopStrings.activity +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n'
|
||
out +=
|
||
' exec dlt ' +
|
||
bareTopSparkline(
|
||
ringExecDelta,
|
||
split ? Math.floor(sparkW / 2) : sparkW,
|
||
asciiGraph,
|
||
logSpark,
|
||
braille
|
||
) +
|
||
'\r\n'
|
||
out +=
|
||
' pipe dlt ' +
|
||
bareTopSparkline(
|
||
ringPipeDelta,
|
||
split ? Math.floor(sparkW / 2) : sparkW,
|
||
asciiGraph,
|
||
logSpark,
|
||
braille
|
||
) +
|
||
' ' +
|
||
bareTopFormatBytes(pb) +
|
||
'\r\n'
|
||
out +=
|
||
' wall dlt ' +
|
||
bareTopSparkline(
|
||
ringWallDelta,
|
||
split ? Math.floor(sparkW / 2) : sparkW,
|
||
asciiGraph,
|
||
logSpark,
|
||
braille
|
||
) +
|
||
' ' +
|
||
bareTopFormatDuration(wm) +
|
||
'\r\n'
|
||
out +=
|
||
' peers ' +
|
||
bareTopSparkline(
|
||
ringPeers,
|
||
split ? Math.floor(sparkW / 2) : sparkW,
|
||
asciiGraph,
|
||
logSpark,
|
||
braille
|
||
) +
|
||
' ' +
|
||
(ringPeers.length ? String(ringPeers[ringPeers.length - 1]) : '') +
|
||
'\r\n'
|
||
|
||
if (deltaMode && prevSnap && prevSnap.metricsLive) {
|
||
const ps = /** @type {Record<string, unknown>} */ (
|
||
prevSnap.metricsLive.session || {}
|
||
)
|
||
const pec = Number(ps.execLineCount) || 0
|
||
const ppb = Number(ps.pipelineBytesTotal) || 0
|
||
const pwm = Number(ps.execLineWallMsTotal) || 0
|
||
out +=
|
||
(useColor ? pal.dim : '') +
|
||
' d exec ' +
|
||
(ec - pec) +
|
||
' d pipe ' +
|
||
(pb - ppb) +
|
||
' d wall ' +
|
||
(wm - pwm) +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n\x1b[K'
|
||
}
|
||
|
||
out +=
|
||
'\r\n' +
|
||
(useColor ? pal.kw : '') +
|
||
bareTopStrings.session +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n'
|
||
out +=
|
||
' execLineCount=' +
|
||
String(sess.execLineCount ?? bareTopStrings.na) +
|
||
' pipelineBytesTotal=' +
|
||
String(sess.pipelineBytesTotal ?? bareTopStrings.na) +
|
||
'\r\n\x1b[K'
|
||
out +=
|
||
' execLineWallMsTotal=' +
|
||
String(sess.execLineWallMsTotal ?? bareTopStrings.na) +
|
||
'\r\n\x1b[K'
|
||
|
||
const pg = bareTopPipelineGauges(m && m.pipeline, cols, pal, useColor)
|
||
if (pg) {
|
||
out +=
|
||
'\r\n' +
|
||
(useColor ? pal.kw : '') +
|
||
bareTopStrings.pipeline +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n' +
|
||
pg +
|
||
'\r\n\x1b[K'
|
||
}
|
||
|
||
const delI = m && m.delegateInflight
|
||
out +=
|
||
'\r\n' +
|
||
(useColor ? pal.kw : '') +
|
||
bareTopStrings.delegates +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n'
|
||
const hist = bareTopDelegateHistogram(delI, cols - 4, use256)
|
||
if (hist) out += ' ' + hist + '\r\n\x1b[K'
|
||
if (delI != null && typeof delI === 'object') {
|
||
/** @type {string[]} */
|
||
const d1 = []
|
||
bareTopFlatten('Inflight', delI, 0, 4, d1, 48)
|
||
for (const ln of d1) {
|
||
for (const w of bareTopWrapLine(ln, cols - 2))
|
||
out += w + '\r\n\x1b[K'
|
||
}
|
||
}
|
||
if (m && m.delegateRateBuckets != null) {
|
||
/** @type {string[]} */
|
||
const d2 = []
|
||
bareTopFlatten('Rate buckets', m.delegateRateBuckets, 0, 4, d2, 48)
|
||
for (const ln of d2) {
|
||
for (const w of bareTopWrapLine(ln, cols - 2))
|
||
out += w + '\r\n\x1b[K'
|
||
}
|
||
}
|
||
|
||
if (snap.fairnessSnapshot) {
|
||
/** @type {string[]} */
|
||
const f1 = []
|
||
bareTopFlatten('Fairness', snap.fairnessSnapshot, 0, 4, f1, 48)
|
||
for (const ln of f1) {
|
||
for (const w of bareTopWrapLine(ln, cols - 2)) {
|
||
out +=
|
||
(useColor ? pal.dim : '') +
|
||
w +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n\x1b[K'
|
||
}
|
||
}
|
||
}
|
||
if (snap.subprocessBridge) {
|
||
/** @type {string[]} */
|
||
const b1 = []
|
||
bareTopFlatten('Subprocess bridge', snap.subprocessBridge, 0, 4, b1, 64)
|
||
for (const ln of b1) {
|
||
for (const w of bareTopWrapLine(ln, cols - 2)) {
|
||
out +=
|
||
(useColor ? pal.dim : '') +
|
||
w +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n\x1b[K'
|
||
}
|
||
}
|
||
}
|
||
if (snap.hostStats) {
|
||
/** @type {string[]} */
|
||
const h1 = []
|
||
bareTopFlatten('Host stats', snap.hostStats, 0, 4, h1, 48)
|
||
for (const ln of h1) {
|
||
for (const w of bareTopWrapLine(ln, cols - 2)) {
|
||
out +=
|
||
(useColor ? pal.dim : '') +
|
||
w +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n\x1b[K'
|
||
}
|
||
}
|
||
}
|
||
if (snap.resources) {
|
||
out +=
|
||
'\r\n' +
|
||
(useColor ? pal.kw : '') +
|
||
'resources' +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n'
|
||
/** @type {string[]} */
|
||
const r1 = []
|
||
bareTopFlatten('', snap.resources, 0, 4, r1, 80)
|
||
for (const ln of r1) {
|
||
for (const w of bareTopWrapLine(' ' + ln, cols - 2))
|
||
out += w + '\r\n\x1b[K'
|
||
}
|
||
}
|
||
|
||
if (snap.readErr) {
|
||
for (const w of bareTopWrapLine('warn: ' + snap.readErr, cols - 2)) {
|
||
out +=
|
||
(useColor ? pal.bad : '') +
|
||
w +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n\x1b[K'
|
||
}
|
||
}
|
||
} else if (tab === 1 && snap && snap.initdGraph) {
|
||
out +=
|
||
'\r\n' +
|
||
(useColor ? pal.kw : '') +
|
||
'Initd graph' +
|
||
(useColor ? pal.reset : '') +
|
||
(filterActive ? ' filter:' + filterActive : '') +
|
||
'\r\n'
|
||
const g = snap.initdGraph
|
||
/** @type {string[]} */
|
||
let initLines = []
|
||
if (typeof g === 'object' && g && Array.isArray(g.nodes)) {
|
||
const nodes = /** @type {string[]} */ (g.nodes)
|
||
const filt = filterActive.toLowerCase()
|
||
const list = filt
|
||
? nodes.filter((n) => String(n).toLowerCase().includes(filt))
|
||
: nodes
|
||
initLines.push('Units: ' + list.length + (filterActive ? ' (filtered)' : ''))
|
||
for (const n of list) initLines.push(' \u2022 ' + String(n))
|
||
} else {
|
||
initLines = bareTopInitdGraphLines(g)
|
||
}
|
||
drawScrollableLines(initLines, 1)
|
||
} else if (tab === 2 && snap) {
|
||
out +=
|
||
'\r\n' +
|
||
(useColor ? pal.kw : '') +
|
||
'Net summary' +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n'
|
||
drawScrollableLines(
|
||
bareTopLinesFromSingle('Network', snap.netSummary, 6),
|
||
2
|
||
)
|
||
} else if (tab === 3 && snap) {
|
||
out +=
|
||
'\r\n' +
|
||
(useColor ? pal.kw : '') +
|
||
'Features / capabilities' +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n'
|
||
drawScrollableLines(
|
||
bareTopLinesFromSingle('Features', snap.features, 6),
|
||
3
|
||
)
|
||
} else if (tab === 4 && snap) {
|
||
out +=
|
||
'\r\n' +
|
||
(useColor ? pal.kw : '') +
|
||
'Diagnostics (debug, delegate_red, ipc backpressure)' +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n'
|
||
const pack = {
|
||
debug: snap.extra.debug,
|
||
delegateRed: snap.extra.delegateRed,
|
||
ipcBackpressure: snap.extra.ipcBackpressure
|
||
}
|
||
drawScrollableLines(bareTopLinesFromPack(pack, 6), 4)
|
||
} else if (tab === 5 && snap) {
|
||
out +=
|
||
'\r\n' +
|
||
(useColor ? pal.kw : '') +
|
||
'Operator (replication, swarm, sync, staging)' +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n'
|
||
const pack = {
|
||
replication: snap.extra.replication,
|
||
replicationBackpressure: snap.extra.replicationBackpressure,
|
||
swarm: snap.extra.swarm,
|
||
syncWindow: snap.extra.syncWindow,
|
||
stagingSlot: snap.extra.stagingSlot,
|
||
snapshotHints: snap.extra.snapshotHints
|
||
}
|
||
drawScrollableLines(bareTopLinesFromPack(pack, 6), 5)
|
||
} else if (tab === 6 && snap) {
|
||
out +=
|
||
'\r\n' +
|
||
(useColor ? pal.kw : '') +
|
||
'Pear (ipc, health, trust, peer_health)' +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n'
|
||
const pack = {
|
||
pearIpc: snap.extra.pearIpc,
|
||
pearIpcHealth: snap.extra.pearIpcHealth,
|
||
pearTrust: snap.extra.pearTrust,
|
||
peerHealth: snap.extra.peerHealth
|
||
}
|
||
drawScrollableLines(bareTopLinesFromPack(pack, 6), 6)
|
||
} else if (tab === 7 && snap) {
|
||
out +=
|
||
'\r\n' +
|
||
(useColor ? pal.kw : '') +
|
||
'Catalog (index, version, bootstrap, provenance, quotas, rlimits, extensions)' +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n'
|
||
const pack = {
|
||
index: snap.extra.index,
|
||
version:
|
||
snap.fileTexts && snap.fileTexts.version
|
||
? bareTopJsonParse(snap.fileTexts.version)
|
||
: null,
|
||
bootstrap: snap.extra.bootstrap,
|
||
provenance: snap.extra.provenance,
|
||
quotas: snap.extra.quotas,
|
||
rlimits: snap.extra.rlimits,
|
||
extensions: snap.extra.extensions,
|
||
capabilitiesJson: snap.extra.capabilitiesJson,
|
||
capabilitiesNode: snap.extra.capabilitiesNode,
|
||
seedHandshake: snap.extra.seedHandshake
|
||
}
|
||
drawScrollableLines(bareTopLinesFromPack(pack, 6), 7)
|
||
} else if (tab === 8 && snap) {
|
||
out +=
|
||
'\r\n' +
|
||
(useColor ? pal.kw : '') +
|
||
'Host / workers (host_os, worker_budget, sandbox, git stats, HDMS, DHT)' +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n'
|
||
const pack = {
|
||
hostOs: snap.hostOs,
|
||
workerBudget: snap.extra.workerBudget,
|
||
sandboxProfile: snap.extra.sandboxProfile,
|
||
gitDelegateStats: snap.extra.gitDelegateStats,
|
||
gitLfsPointerStats: snap.extra.gitLfsPointerStats,
|
||
hdmsHealth: snap.extra.hdmsHealth,
|
||
hdmsHints: snap.extra.hdmsHints,
|
||
dhtStatus: snap.extra.dhtStatus,
|
||
udxExtended: snap.extra.udxExtended,
|
||
sessionStats: snap.extra.sessionStats
|
||
}
|
||
drawScrollableLines(bareTopLinesFromPack(pack, 6), 8)
|
||
} else if (tab === 9) {
|
||
out +=
|
||
'\r\n' +
|
||
(useColor ? pal.kw : '') +
|
||
'Key reference' +
|
||
(useColor ? pal.reset : '') +
|
||
'\r\n' +
|
||
' Same as h/? help — use tab 9 or press h.\r\n'
|
||
}
|
||
|
||
let foot =
|
||
(useColor ? pal.dim : '') +
|
||
'[ ] tabs PgUp/Dn scroll sp pause . step d delta e export f full t UTC / filter h help' +
|
||
(useColor ? pal.reset : '') +
|
||
scrollHint
|
||
if (filterOpen) {
|
||
foot =
|
||
(useColor ? pal.warn : '') +
|
||
'FILTER> ' +
|
||
filterLine +
|
||
'_' +
|
||
(useColor ? pal.reset : '')
|
||
} else {
|
||
const hints = snap && snap.extra.snapshotHints
|
||
const hintLine = bareTopHintsFootLine(hints)
|
||
if (hintLine) {
|
||
tipRot = (tipRot + 1) % Math.max(1, hintLine.length)
|
||
const rot = hintLine.slice(tipRot) + ' · ' + hintLine.slice(0, tipRot)
|
||
const room = Math.max(0, cols - foot.length - 4)
|
||
if (room > 12) foot += ' | ' + rot.slice(0, room)
|
||
}
|
||
}
|
||
out += bareEditCup(rows, 1) + '\x1b[K' + foot.slice(0, cols)
|
||
out += '\x1b[?25h'
|
||
bareTopWrite(ctx, stdout, out)
|
||
}
|
||
|
||
await tick()
|
||
draw()
|
||
|
||
while (!quit) {
|
||
if (helpMode) {
|
||
const evh = bareTopDrainKeys(keyq, 32, { vi: viKeys })
|
||
if (evh) {
|
||
if (evh.type === 'quit') {
|
||
quit = true
|
||
break
|
||
}
|
||
helpMode = false
|
||
draw()
|
||
}
|
||
await new Promise((r) => setTimeout(r, 50))
|
||
continue
|
||
}
|
||
|
||
if (filterOpen) {
|
||
const evf = bareTopDrainKeys(keyq, 64, { vi: viKeys })
|
||
if (evf) {
|
||
if (evf.type === 'quit') {
|
||
quit = true
|
||
break
|
||
}
|
||
if (evf.type === 'filter_bs') {
|
||
filterLine = filterLine.slice(0, -1)
|
||
draw()
|
||
continue
|
||
}
|
||
if (evf.type === 'filter_enter') {
|
||
filterActive = filterLine
|
||
filterOpen = false
|
||
scrollRows[tab] = 0
|
||
draw()
|
||
continue
|
||
}
|
||
if (evf.type === 'filter_char' && evf.ch) {
|
||
if (evf.ch.length === 1 && filterLine.length < 64)
|
||
filterLine += evf.ch
|
||
draw()
|
||
continue
|
||
}
|
||
}
|
||
await new Promise((r) => setTimeout(r, 50))
|
||
continue
|
||
}
|
||
|
||
const ev = bareTopDrainKeys(keyq, 32, { vi: viKeys })
|
||
if (ev) {
|
||
if (ev.type === 'quit') {
|
||
quit = true
|
||
break
|
||
}
|
||
if (ev.type === 'refresh') {
|
||
burstUntil = Date.now() + Math.min(5000, intervalMs * 3)
|
||
const beforeDelta = deltaMode ? lastSnap : null
|
||
await tick()
|
||
if (beforeDelta) prevSnap = beforeDelta
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'help_open') {
|
||
helpMode = true
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'pause_toggle') {
|
||
paused = !paused
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'step') {
|
||
if (paused) {
|
||
const beforeDelta = deltaMode ? lastSnap : null
|
||
await tick()
|
||
if (beforeDelta) prevSnap = beforeDelta
|
||
draw()
|
||
}
|
||
continue
|
||
}
|
||
if (ev.type === 'delta_toggle') {
|
||
deltaMode = !deltaMode
|
||
if (deltaMode) prevSnap = lastSnap
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'fullscreen_toggle') {
|
||
fullscreenPanel = !fullscreenPanel
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'clock_toggle') {
|
||
utcClock = !utcClock
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'tab_prev') {
|
||
tab = (tab + NTABS - 1) % NTABS
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'tab_next') {
|
||
tab = (tab + 1) % NTABS
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'tab') {
|
||
tab = Math.max(0, Math.min(NTABS - 1, ev.n))
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'scroll') {
|
||
const sr = scrollRows[tab] || 0
|
||
if (ev.dir === 'home') scrollRows[tab] = 0
|
||
else if (ev.dir === 'end') scrollRows[tab] = 99999
|
||
else if (typeof ev.dir === 'number')
|
||
scrollRows[tab] = Math.max(0, sr + ev.dir * 5)
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'filter_open') {
|
||
filterOpen = true
|
||
filterLine = filterActive
|
||
draw()
|
||
continue
|
||
}
|
||
if (ev.type === 'export') {
|
||
if (lastSnap && ctx.vfs && typeof ctx.vfs.writeFile === 'function') {
|
||
try {
|
||
const payload = JSON.stringify(lastSnap, null, 2)
|
||
await ctx.vfs.writeFile(exportPath, payload)
|
||
ctx.console.log(bareTopStrings.exportOk + ' ' + exportPath)
|
||
} catch (e) {
|
||
ctx.console.error(
|
||
bareTopStrings.exportFail +
|
||
': ' +
|
||
((e && /** @type {{ message?: string }} */ (e).message) ||
|
||
String(e))
|
||
)
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
}
|
||
|
||
if (needsRedraw) {
|
||
needsRedraw = false
|
||
draw()
|
||
}
|
||
|
||
const waitMs =
|
||
Date.now() < burstUntil ? Math.min(250, intervalMs) : intervalMs
|
||
await new Promise((r) => setTimeout(r, paused ? 200 : waitMs))
|
||
if (!paused) {
|
||
const beforeDelta = deltaMode ? lastSnap : null
|
||
await tick()
|
||
if (beforeDelta) prevSnap = beforeDelta
|
||
}
|
||
draw()
|
||
}
|
||
} finally {
|
||
try {
|
||
hookOffSuspend?.()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
hookOffResume?.()
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
stdin.removeListener('data', onData)
|
||
try {
|
||
if (typeof stdout.removeListener === 'function') {
|
||
stdout.removeListener('resize', onResize)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
if (typeof process !== 'undefined' && typeof process.off === 'function') {
|
||
process.off('SIGWINCH', onResize)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
if (useAltScreen) {
|
||
bareTopWrite(ctx, stdout, '\x1b[?1049l')
|
||
} else {
|
||
bareTopWrite(ctx, stdout, '\x1b[2J\x1b[H')
|
||
}
|
||
bareTopWrite(ctx, stdout, '\x1b[?25h\x1b[0m')
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
if (typeof stdin.setRawMode === 'function') stdin.setRawMode(false)
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
if (suspended && typeof ctx.resumeReplAfterSubprocess === 'function') {
|
||
ctx.resumeReplAfterSubprocess()
|
||
}
|
||
}
|
||
}
|
||
|
||
async function run(ctx, argv) {
|
||
const args = argv.slice(1)
|
||
if (args.includes('-h') || args.includes('--help')) {
|
||
ctx.console.log(
|
||
'usage: ' +
|
||
(argv[0] || 'baretop') +
|
||
'\n' +
|
||
'Full-screen session / kernel dashboard (htop-inspired; not a Linux process list).\n' +
|
||
'Shows metrics_live, peers, delegates, pipeline limits, many /proc/bare_os JSON mirrors,\n' +
|
||
'and optional ctx fast paths (bareOsReadBareTopSnapshot, fairness, subprocess bridge).\n' +
|
||
'Requires a TTY.\n' +
|
||
'\n' +
|
||
'Tabs: 1 overview 2 initd 3 network 4 features 5 diagnostics 6 operator\n' +
|
||
' 7 pear 8 catalog 9 keys / help | [ ] cycle tabs 0 = tab 10 (keys)\n' +
|
||
'\n' +
|
||
'Environment:\n' +
|
||
' BARE_TOP_INTERVAL_MS Refresh period (default 1000; clamped 250–10000)\n' +
|
||
' BARE_TOP_UI_MIN_MS Minimum milliseconds between full redraws (0 = off)\n' +
|
||
' BARE_TOP_FETCH_CONCURRENCY Parallel /proc reads per batch (default 6)\n' +
|
||
' BARE_TOP_NO_ALTSCREEN Any non-empty value skips alternate-screen mode\n' +
|
||
' BARE_TOP_ASCII_GRAPH Set to 1 for ASCII sparklines\n' +
|
||
' BARE_TOP_ASCII_UI Set to 1 to prefer ASCII box/spark fallbacks\n' +
|
||
' BARE_TOP_BRAILLE_SPARK Set to 1 for braille sparkline glyphs\n' +
|
||
' BARE_TOP_LOG_SPARK Set to 1 for log-scaled sparklines\n' +
|
||
' BARE_TOP_EMA_ALPHA 0–1 smoothing for delta samples (0 = off)\n' +
|
||
' BARE_TOP_THEME dark | light | none\n' +
|
||
' BARE_TOP_HIGH_CONTRAST Set to 1 for bold/underline emphasis\n' +
|
||
' BARE_TOP_COLOR256 Set to 1 to force 256-color bar hints\n' +
|
||
' BARE_TOP_VI_KEYS Set to 1 for hjkl navigation in lists\n' +
|
||
' BARE_TOP_LAYOUT stacked | even (wide overview layout hint)\n' +
|
||
' BARE_TOP_EXPORT_PATH JSON export target for the e key\n' +
|
||
' BARE_TOP_ICONS Set to 1 for bullet prefix in title\n' +
|
||
' NO_COLOR Disable ANSI colors\n' +
|
||
'\n' +
|
||
'Keys: q quit r refresh+burst sp pause . step d delta e export f fullscreen\n' +
|
||
' t UTC clock / filter initd [ ] tabs h/? help\n' +
|
||
'\n' +
|
||
'Contributors: /bin bundles cannot use node:crypto or node:module; use ctx.vfs and\n' +
|
||
'JSON.parse only. See holepunch-repos/docs/05-BARE-RUNTIME.md (Bare module stack).'
|
||
)
|
||
return
|
||
}
|
||
const stdin = ctx.replStdin
|
||
const stdout = ctx.replStdout || ctx.stdout
|
||
if (!stdin || !/** @type {{ isTTY?: boolean }} */ (stdin).isTTY) {
|
||
ctx.console.error('baretop: a terminal (TTY) is required')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
if (!stdout) {
|
||
ctx.console.error('baretop: missing stdout')
|
||
ctx.exitCode = 1
|
||
return
|
||
}
|
||
await bareOsRunBareTopTui(ctx)
|
||
}
|