Expose /proc/bare_os/metrics.prom (kernel counter OpenMetrics) and /proc/bare_os/protomux.json (alias registry snapshot). Extend VFS routing, readdir, pseudo watch, and bare_os_proc_index to schema 8; restore flat /proc entries for syscalls and process_table with sort order matching tests. Add ctx.bareOsAuditLogAppendBatch and bump BARE_OS_CTX_API_VERSION to 1.30.0 with d.ts and compatibility-matrix updates. Harden swarm peer ban backoff with jitter on the exponential window. Refresh conformance matrix, environment appendix, booter package doc, and handbook chapters for proc paths, param expansion V3, vault at rest, and baretop snapshot keys. Align baretop-snapshot with bareOsReadBareTopSnapshot.
2254 lines
67 KiB
Plaintext
2254 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'],
|
||
['processTable', '/proc/bare_os/process_table.json'],
|
||
['syscalls', '/proc/bare_os/syscalls.json'],
|
||
['metricsProm', '/proc/bare_os/metrics.prom'],
|
||
['protomuxWire', '/proc/bare_os/protomux.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)
|
||
}
|